Merge pull request #2003 from AstrBotDevs/feat-webchat-select-provider
Feature: WebChat 增加可选择提供商和模型的功能
This commit is contained in:
@@ -24,6 +24,7 @@ from astrbot.core.provider.entities import (
|
||||
)
|
||||
from astrbot.core.star.star_handler import EventType
|
||||
from ..agent_runner.tool_loop_agent import ToolLoopAgent
|
||||
from astrbot.core.provider import Provider
|
||||
|
||||
|
||||
class LLMRequestSubStage(Stage):
|
||||
@@ -51,16 +52,27 @@ class LLMRequestSubStage(Stage):
|
||||
|
||||
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
def _select_provider(self, event: AstrMessageEvent) -> Provider | None:
|
||||
"""选择使用的 LLM 提供商"""
|
||||
sel_provider = event.get_extra("selected_provider")
|
||||
_ctx = self.ctx.plugin_manager.context
|
||||
if sel_provider and isinstance(sel_provider, str):
|
||||
provider = _ctx.get_provider_by_id(sel_provider)
|
||||
if not provider:
|
||||
logger.error(f"未找到指定的提供商: {sel_provider}。")
|
||||
return provider
|
||||
|
||||
return _ctx.get_using_provider(umo=event.unified_msg_origin)
|
||||
|
||||
async def process(
|
||||
self, event: AstrMessageEvent, _nested: bool = False
|
||||
) -> Union[None, AsyncGenerator[None, None]]:
|
||||
req: ProviderRequest = None
|
||||
req: ProviderRequest | None = None
|
||||
|
||||
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
|
||||
logger.debug("未启用 LLM 能力,跳过处理。")
|
||||
return
|
||||
umo = event.unified_msg_origin
|
||||
provider = self.ctx.plugin_manager.context.get_using_provider(umo=umo)
|
||||
provider = self._select_provider(event)
|
||||
if provider is None:
|
||||
return
|
||||
|
||||
@@ -75,6 +87,8 @@ class LLMRequestSubStage(Stage):
|
||||
|
||||
else:
|
||||
req = ProviderRequest(prompt="", image_urls=[])
|
||||
if sel_model := event.get_extra("selected_model"):
|
||||
req.model = sel_model
|
||||
if self.provider_wake_prefix:
|
||||
if not event.message_str.startswith(self.provider_wake_prefix):
|
||||
return
|
||||
@@ -168,7 +182,10 @@ class LLMRequestSubStage(Stage):
|
||||
if self.streaming_response:
|
||||
# 用来标记流式响应需要分节
|
||||
yield MessageChain(chain=[], type="break")
|
||||
if self.show_tool_use or event.get_platform_name() == "webchat":
|
||||
if (
|
||||
self.show_tool_use
|
||||
or event.get_platform_name() == "webchat"
|
||||
):
|
||||
resp.data["chain"].type = "tool_call"
|
||||
await event.send(resp.data["chain"])
|
||||
continue
|
||||
|
||||
@@ -164,7 +164,7 @@ class WakingCheckStage(Stage):
|
||||
"parsed_params"
|
||||
)
|
||||
|
||||
event.clear_extra()
|
||||
event._extras.pop("parsed_params", None)
|
||||
|
||||
event.set_extra("activated_handlers", activated_handlers)
|
||||
event.set_extra("handlers_parsed_params", handlers_parsed_params)
|
||||
|
||||
@@ -151,6 +151,10 @@ class WebChatAdapter(Platform):
|
||||
session_id=message.session_id,
|
||||
)
|
||||
|
||||
_, _, payload = message.raw_message # type: ignore
|
||||
message_event.set_extra("selected_provider", payload.get("selected_provider"))
|
||||
message_event.set_extra("selected_model", payload.get("selected_model"))
|
||||
|
||||
self.commit_event(message_event)
|
||||
|
||||
async def terminate(self):
|
||||
|
||||
@@ -110,6 +110,9 @@ class ProviderRequest:
|
||||
tool_calls_result: list[ToolCallsResult] | ToolCallsResult | None = None
|
||||
"""附加的上次请求后工具调用的结果。参考: https://platform.openai.com/docs/guides/function-calling#handling-function-calls"""
|
||||
|
||||
model: str | None = None
|
||||
"""模型名称,为 None 时使用提供商的默认模型"""
|
||||
|
||||
def __repr__(self):
|
||||
return f"ProviderRequest(prompt={self.prompt}, session_id={self.session_id}, image_urls={self.image_urls}, func_tool={self.func_tool}, contexts={self._print_friendly_context()}, system_prompt={self.system_prompt.strip()}, tool_calls_result={self.tool_calls_result})"
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ class Provider(AbstractProvider):
|
||||
contexts: list = None,
|
||||
system_prompt: str = None,
|
||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] = None,
|
||||
model: str | None = None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
||||
@@ -116,6 +117,7 @@ class Provider(AbstractProvider):
|
||||
contexts: list = None,
|
||||
system_prompt: str = None,
|
||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] = None,
|
||||
model: str | None = None,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。
|
||||
|
||||
@@ -235,6 +235,7 @@ class ProviderAnthropic(Provider):
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if contexts is None:
|
||||
@@ -259,7 +260,7 @@ class ProviderAnthropic(Provider):
|
||||
system_prompt, new_messages = self._prepare_payload(context_query)
|
||||
|
||||
model_config = self.provider_config.get("model_config", {})
|
||||
model_config["model"] = self.get_model()
|
||||
model_config["model"] = model or self.get_model()
|
||||
|
||||
payloads = {"messages": new_messages, **model_config}
|
||||
|
||||
@@ -285,6 +286,7 @@ class ProviderAnthropic(Provider):
|
||||
contexts=...,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
):
|
||||
if contexts is None:
|
||||
@@ -309,7 +311,7 @@ class ProviderAnthropic(Provider):
|
||||
system_prompt, new_messages = self._prepare_payload(context_query)
|
||||
|
||||
model_config = self.provider_config.get("model_config", {})
|
||||
model_config["model"] = self.get_model()
|
||||
model_config["model"] = model or self.get_model()
|
||||
|
||||
payloads = {"messages": new_messages, **model_config}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
func_tool: FuncCall = None,
|
||||
contexts: List = None,
|
||||
system_prompt: str = None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if contexts is None:
|
||||
@@ -163,6 +164,7 @@ class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
contexts=...,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
):
|
||||
# raise NotImplementedError("This method is not implemented yet.")
|
||||
|
||||
@@ -60,6 +60,8 @@ class ProviderDify(Provider):
|
||||
func_tool: FuncCall = None,
|
||||
contexts: List = None,
|
||||
system_prompt: str = None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if image_urls is None:
|
||||
@@ -84,11 +86,13 @@ class ProviderDify(Provider):
|
||||
f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。"
|
||||
)
|
||||
continue
|
||||
files_payload.append({
|
||||
"type": "image",
|
||||
"transfer_method": "local_file",
|
||||
"upload_file_id": file_response["id"],
|
||||
})
|
||||
files_payload.append(
|
||||
{
|
||||
"type": "image",
|
||||
"transfer_method": "local_file",
|
||||
"upload_file_id": file_response["id"],
|
||||
}
|
||||
)
|
||||
|
||||
# 获得会话变量
|
||||
payload_vars = self.variables.copy()
|
||||
@@ -195,6 +199,7 @@ class ProviderDify(Provider):
|
||||
contexts=...,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
):
|
||||
# raise NotImplementedError("This method is not implemented yet.")
|
||||
|
||||
@@ -259,10 +259,12 @@ class ProviderGoogleGenAI(Provider):
|
||||
contents.append(content_cls(parts=part))
|
||||
|
||||
gemini_contents: list[types.Content] = []
|
||||
native_tool_enabled = any([
|
||||
self.provider_config.get("gm_native_coderunner", False),
|
||||
self.provider_config.get("gm_native_search", False),
|
||||
])
|
||||
native_tool_enabled = any(
|
||||
[
|
||||
self.provider_config.get("gm_native_coderunner", False),
|
||||
self.provider_config.get("gm_native_search", False),
|
||||
]
|
||||
)
|
||||
for message in payloads["messages"]:
|
||||
role, content = message["role"], message.get("content")
|
||||
|
||||
@@ -505,6 +507,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if contexts is None:
|
||||
@@ -527,7 +530,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
context_query.extend(tcr.to_openai_messages())
|
||||
|
||||
model_config = self.provider_config.get("model_config", {})
|
||||
model_config["model"] = self.get_model()
|
||||
model_config["model"] = model or self.get_model()
|
||||
|
||||
payloads = {"messages": context_query, **model_config}
|
||||
|
||||
@@ -551,6 +554,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
if contexts is None:
|
||||
@@ -573,7 +577,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
context_query.extend(tcr.to_openai_messages())
|
||||
|
||||
model_config = self.provider_config.get("model_config", {})
|
||||
model_config["model"] = self.get_model()
|
||||
model_config["model"] = model or self.get_model()
|
||||
|
||||
payloads = {"messages": context_query, **model_config}
|
||||
|
||||
@@ -632,10 +636,12 @@ class ProviderGoogleGenAI(Provider):
|
||||
if not image_data:
|
||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||
continue
|
||||
user_content["content"].append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_data},
|
||||
})
|
||||
user_content["content"].append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_data},
|
||||
}
|
||||
)
|
||||
return user_content
|
||||
else:
|
||||
return {"role": "user", "content": text}
|
||||
|
||||
@@ -222,6 +222,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
contexts: list | None = None,
|
||||
system_prompt: str | None = None,
|
||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||
model: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple:
|
||||
"""准备聊天所需的有效载荷和上下文"""
|
||||
@@ -245,7 +246,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
context_query.extend(tcr.to_openai_messages())
|
||||
|
||||
model_config = self.provider_config.get("model_config", {})
|
||||
model_config["model"] = self.get_model()
|
||||
model_config["model"] = model or self.get_model()
|
||||
|
||||
payloads = {"messages": context_query, **model_config}
|
||||
|
||||
@@ -346,6 +347,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
payloads, context_query = await self._prepare_chat_payload(
|
||||
@@ -354,6 +356,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
contexts,
|
||||
system_prompt,
|
||||
tool_calls_result,
|
||||
model=model,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -413,6 +416,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
contexts=[],
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式对话,与服务商交互并逐步返回结果"""
|
||||
@@ -422,6 +426,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
contexts,
|
||||
system_prompt,
|
||||
tool_calls_result,
|
||||
model=model,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -525,10 +530,12 @@ class ProviderOpenAIOfficial(Provider):
|
||||
if not image_data:
|
||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||
continue
|
||||
user_content["content"].append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_data},
|
||||
})
|
||||
user_content["content"].append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_data},
|
||||
}
|
||||
)
|
||||
return user_content
|
||||
else:
|
||||
return {"role": "user", "content": text}
|
||||
|
||||
@@ -28,6 +28,7 @@ class ProviderZhipu(ProviderOpenAIOfficial):
|
||||
func_tool: FuncCall = None,
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if contexts is None:
|
||||
@@ -38,7 +39,7 @@ class ProviderZhipu(ProviderOpenAIOfficial):
|
||||
context_query = [*contexts, new_record]
|
||||
|
||||
model_cfgs: dict = self.provider_config.get("model_config", {})
|
||||
model = self.get_model()
|
||||
model = model or self.get_model()
|
||||
# glm-4v-flash 只支持一张图片
|
||||
if model.lower() == "glm-4v-flash" and image_urls and len(context_query) > 1:
|
||||
logger.debug("glm-4v-flash 只支持一张图片,将只保留最后一张图片")
|
||||
|
||||
@@ -120,6 +120,8 @@ class ChatRoute(Route):
|
||||
conversation_id = post_data["conversation_id"]
|
||||
image_url = post_data.get("image_url")
|
||||
audio_url = post_data.get("audio_url")
|
||||
selected_provider = post_data.get("selected_provider")
|
||||
selected_model = post_data.get("selected_model")
|
||||
if not message and not image_url and not audio_url:
|
||||
return (
|
||||
Response()
|
||||
@@ -202,6 +204,8 @@ class ChatRoute(Route):
|
||||
"message": message,
|
||||
"image_url": image_url, # list
|
||||
"audio_url": audio_url,
|
||||
"selected_provider": selected_provider,
|
||||
"selected_model": selected_model,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from astrbot.core.platform.register import platform_registry
|
||||
from astrbot.core.provider.register import provider_registry
|
||||
from astrbot.core.star.star import star_registry
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.provider import Provider
|
||||
import asyncio
|
||||
|
||||
|
||||
@@ -168,6 +169,7 @@ class ConfigRoute(Route):
|
||||
"/config/llmtools": ("GET", self.get_llm_tools),
|
||||
"/config/provider/check_status": ("GET", self.check_all_providers_status),
|
||||
"/config/provider/list": ("GET", self.get_provider_config_list),
|
||||
"/config/provider/model_list": ("GET", self.get_provider_model_list),
|
||||
"/config/provider/get_session_seperate": (
|
||||
"GET",
|
||||
lambda: Response()
|
||||
@@ -319,6 +321,28 @@ class ConfigRoute(Route):
|
||||
provider_list.append(provider)
|
||||
return Response().ok(provider_list).__dict__
|
||||
|
||||
async def get_provider_model_list(self):
|
||||
"""获取指定提供商的模型列表"""
|
||||
provider_id = request.args.get("provider_id", None)
|
||||
if not provider_id:
|
||||
return Response().error("缺少参数 provider_id").__dict__
|
||||
|
||||
prov_mgr = self.core_lifecycle.provider_manager
|
||||
provider: Provider | None = prov_mgr.inst_map.get(provider_id, None)
|
||||
if not provider:
|
||||
return Response().error(f"未找到 ID 为 {provider_id} 的提供商").__dict__
|
||||
|
||||
try:
|
||||
models = await provider.get_models()
|
||||
ret = {
|
||||
"models": models,
|
||||
"provider_id": provider_id,
|
||||
}
|
||||
return Response().ok(ret).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def post_astrbot_configs(self):
|
||||
post_configs = await request.json
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 选择提供商和模型按钮 -->
|
||||
<v-btn
|
||||
class="text-none"
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
size="small"
|
||||
v-if="selectedProviderId && selectedModelName"
|
||||
@click="showDialog = true">
|
||||
{{ selectedProviderId }} / {{ selectedModelName }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
size="small"
|
||||
v-else
|
||||
@click="showDialog = true">
|
||||
选择模型
|
||||
</v-btn>
|
||||
|
||||
<!-- 选择提供商和模型对话框 -->
|
||||
<v-dialog v-model="showDialog" max-width="800" persistent>
|
||||
<v-card style="padding: 8px;">
|
||||
<v-card-title class="dialog-title">
|
||||
<span>选择提供商和模型</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<div class="provider-model-container">
|
||||
<!-- 左侧提供商列表 -->
|
||||
<div class="provider-list-panel">
|
||||
<div class="panel-header">
|
||||
<h4>提供商</h4>
|
||||
</div>
|
||||
<v-list density="compact" nav class="provider-list">
|
||||
<v-list-item
|
||||
v-for="provider in providerConfigs"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
@click="selectProvider(provider)"
|
||||
:active="selectedProviderId === provider.id"
|
||||
rounded="lg"
|
||||
class="provider-item">
|
||||
<v-list-item-title>{{ provider.id }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="provider.api_base">{{ provider.api_base }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-if="providerConfigs.length === 0" class="empty-state">
|
||||
<v-icon icon="mdi-cloud-off-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="empty-text">暂无可用提供商</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧模型列表 -->
|
||||
<div class="model-list-panel">
|
||||
<div class="panel-header">
|
||||
<h4>模型</h4>
|
||||
<v-btn
|
||||
v-if="selectedProviderId"
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="refreshModels"
|
||||
:loading="loadingModels">
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-list density="compact" nav class="model-list" v-if="selectedProviderId">
|
||||
<v-list-item
|
||||
v-for="model in modelList"
|
||||
:key="model"
|
||||
:value="model"
|
||||
@click="selectModel(model)"
|
||||
:active="selectedModelName === model"
|
||||
rounded="lg"
|
||||
class="model-item">
|
||||
<v-list-item-title>{{ model }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="model.description">{{ model.description }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="empty-state">
|
||||
<v-icon icon="mdi-robot-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="empty-text">请先选择提供商</div>
|
||||
</div>
|
||||
<div v-if="selectedProviderId && modelList.length === 0 && !loadingModels" class="empty-state">
|
||||
<v-icon icon="mdi-robot-off-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="empty-text">该提供商暂无可用模型</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="closeDialog" color="grey-darken-1">取消</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="confirmSelection"
|
||||
color="primary"
|
||||
:disabled="!selectedProviderId || !selectedModelName">
|
||||
确认选择
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'ProviderModelSelector',
|
||||
props: {
|
||||
initialProvider: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialModel: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['selection-changed'],
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
providerConfigs: [],
|
||||
modelList: [],
|
||||
selectedProviderId: '',
|
||||
selectedModelName: '',
|
||||
loadingModels: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 从localStorage加载保存的选择
|
||||
this.loadFromStorage();
|
||||
// 获取提供商列表
|
||||
this.loadProviderConfigs();
|
||||
// 如果有保存的选择,加载对应的模型列表
|
||||
if (this.selectedProviderId) {
|
||||
this.getProviderModels(this.selectedProviderId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 从localStorage加载保存的选择
|
||||
loadFromStorage() {
|
||||
const savedProvider = localStorage.getItem('selectedProvider');
|
||||
const savedModel = localStorage.getItem('selectedModel');
|
||||
|
||||
if (savedProvider) {
|
||||
this.selectedProviderId = savedProvider;
|
||||
} else if (this.initialProvider) {
|
||||
this.selectedProviderId = this.initialProvider;
|
||||
}
|
||||
|
||||
if (savedModel) {
|
||||
this.selectedModelName = savedModel;
|
||||
} else if (this.initialModel) {
|
||||
this.selectedModelName = this.initialModel;
|
||||
}
|
||||
},
|
||||
|
||||
// 保存到localStorage
|
||||
saveToStorage() {
|
||||
if (this.selectedProviderId) {
|
||||
localStorage.setItem('selectedProvider', this.selectedProviderId);
|
||||
}
|
||||
if (this.selectedModelName) {
|
||||
localStorage.setItem('selectedModel', this.selectedModelName);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取提供商配置列表
|
||||
loadProviderConfigs() {
|
||||
axios.get('/api/config/provider/list', {
|
||||
params: {
|
||||
provider_type: 'chat_completion'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.status === 'ok') {
|
||||
this.providerConfigs = response.data.data || [];
|
||||
} else {
|
||||
console.error('获取聊天完成提供商列表失败:', response.data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取聊天完成提供商列表失败:', error);
|
||||
});
|
||||
},
|
||||
|
||||
// 获取指定提供商的模型列表
|
||||
getProviderModels(providerId) {
|
||||
this.loadingModels = true;
|
||||
axios.get('/api/config/provider/model_list', {
|
||||
params: {
|
||||
provider_id: providerId
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.status === 'ok') {
|
||||
this.modelList = response.data.data.models || [];
|
||||
} else {
|
||||
console.error('获取模型列表失败:', response.data.message);
|
||||
this.modelList = [];
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取模型列表失败:', error);
|
||||
this.modelList = [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingModels = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 选择提供商
|
||||
selectProvider(provider) {
|
||||
this.selectedProviderId = provider.id;
|
||||
this.selectedModelName = ''; // 清空已选择的模型
|
||||
this.modelList = []; // 清空模型列表
|
||||
this.getProviderModels(provider.id); // 获取该提供商的模型列表
|
||||
},
|
||||
|
||||
// 选择模型
|
||||
selectModel(model) {
|
||||
this.selectedModelName = model;
|
||||
},
|
||||
|
||||
// 刷新模型列表
|
||||
refreshModels() {
|
||||
if (this.selectedProviderId) {
|
||||
this.getProviderModels(this.selectedProviderId);
|
||||
}
|
||||
},
|
||||
|
||||
// 确认选择
|
||||
confirmSelection() {
|
||||
if (this.selectedProviderId && this.selectedModelName) {
|
||||
// 保存到localStorage
|
||||
this.saveToStorage();
|
||||
|
||||
// 触发事件通知父组件
|
||||
this.$emit('selection-changed', {
|
||||
providerId: this.selectedProviderId,
|
||||
modelName: this.selectedModelName
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭对话框
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
},
|
||||
|
||||
// 公开方法:获取当前选择
|
||||
getCurrentSelection() {
|
||||
return {
|
||||
providerId: this.selectedProviderId,
|
||||
modelName: this.selectedModelName
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 对话框标题样式 */
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 提供商和模型选择对话框样式 */
|
||||
.provider-model-container {
|
||||
display: flex;
|
||||
height: 500px;
|
||||
border: 1px solid var(--v-theme-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.provider-list-panel,
|
||||
.model-list-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--v-theme-surface);
|
||||
}
|
||||
|
||||
.provider-list-panel {
|
||||
border-right: 1px solid var(--v-theme-border);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
background-color: var(--v-theme-containerBg);
|
||||
}
|
||||
|
||||
.panel-header h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--v-theme-primaryText);
|
||||
}
|
||||
|
||||
.provider-list,
|
||||
.model-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.provider-item,
|
||||
.model-item {
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px !important;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.provider-item:hover,
|
||||
.model-item:hover {
|
||||
background-color: rgba(103, 58, 183, 0.05);
|
||||
}
|
||||
|
||||
.provider-item.v-list-item--active,
|
||||
.model-item.v-list-item--active {
|
||||
background-color: rgba(103, 58, 183, 0.1);
|
||||
color: var(--v-theme-secondary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
opacity: 0.6;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
</style>
|
||||
@@ -50,10 +50,12 @@
|
||||
|
||||
<template v-if="!sidebarCollapsed" v-slot:append>
|
||||
<div class="conversation-actions">
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text" class="edit-title-btn"
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text"
|
||||
class="edit-title-btn"
|
||||
@click.stop="showEditTitleDialog(item.cid, item.title)" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text" class="delete-conversation-btn"
|
||||
color="error" @click.stop="deleteConversation(item.cid)" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||
class="delete-conversation-btn" color="error"
|
||||
@click.stop="deleteConversation(item.cid)" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
@@ -100,8 +102,8 @@
|
||||
<!-- 主题切换按钮 -->
|
||||
<v-tooltip :text="isDark ? tm('modes.lightMode') : tm('modes.darkMode')" v-if="chatboxMode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon @click="toggleTheme" class="theme-toggle-icon" size="small" rounded="sm" style="margin-right: 8px;"
|
||||
variant="text">
|
||||
<v-btn v-bind="props" icon @click="toggleTheme" class="theme-toggle-icon"
|
||||
size="small" rounded="sm" style="margin-right: 8px;" variant="text">
|
||||
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -188,14 +190,23 @@
|
||||
<textarea id="input-field" v-model="prompt" @keydown="handleInputKeyDown"
|
||||
@click:clear="clearMessage" placeholder="Ask AstrBot..."
|
||||
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 12px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px;">
|
||||
<v-btn @click="sendMessage" icon="mdi-send" variant="text" color="deep-purple"
|
||||
:disabled="!prompt && stagedImagesName.length === 0 && !stagedAudioUrl"
|
||||
class="send-btn" size="small" />
|
||||
<v-btn @click="isRecording ? stopRecording() : startRecording()"
|
||||
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
|
||||
:color="isRecording ? 'error' : 'deep-purple'" class="record-btn" size="small" />
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 0px 8px;">
|
||||
<div style="display: flex; justify-content: flex-start; margin-top: 8px;">
|
||||
<!-- 选择提供商和模型 -->
|
||||
<ProviderModelSelector ref="providerModelSelector" />
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px;">
|
||||
<v-btn @click="sendMessage" icon="mdi-send" variant="text" color="deep-purple"
|
||||
:disabled="!prompt && stagedImagesName.length === 0 && !stagedAudioUrl"
|
||||
class="send-btn" size="small" />
|
||||
<v-btn @click="isRecording ? stopRecording() : startRecording()"
|
||||
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
|
||||
:color="isRecording ? 'error' : 'deep-purple'" class="record-btn"
|
||||
size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 附件预览区 -->
|
||||
@@ -246,12 +257,13 @@ import { ref } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import ProviderModelSelector from '@/components/chat/ProviderModelSelector.vue';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
highlight: function(code, lang) {
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
@@ -266,7 +278,8 @@ marked.setOptions({
|
||||
export default {
|
||||
name: 'ChatPage',
|
||||
components: {
|
||||
LanguageSwitcher
|
||||
LanguageSwitcher,
|
||||
ProviderModelSelector
|
||||
},
|
||||
props: {
|
||||
chatboxMode: {
|
||||
@@ -790,6 +803,11 @@ export default {
|
||||
|
||||
this.loadingChat = true
|
||||
|
||||
// 从ProviderModelSelector组件获取当前选择
|
||||
const selection = this.$refs.providerModelSelector?.getCurrentSelection();
|
||||
const selectedProviderId = selection?.providerId || '';
|
||||
const selectedModelName = selection?.modelName || '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat/send', {
|
||||
method: 'POST',
|
||||
@@ -801,7 +819,9 @@ export default {
|
||||
message: this.prompt.trim(), // 确保发送的消息已去除前后空格
|
||||
conversation_id: this.currCid,
|
||||
image_url: this.stagedImagesName,
|
||||
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : []
|
||||
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [],
|
||||
selected_provider: selectedProviderId,
|
||||
selected_model: selectedModelName
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user