Compare commits
73 Commits
feat/memor
...
v4.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2f036b27c | ||
|
|
4087bbb512 | ||
|
|
e1c728582d | ||
|
|
93c69a639a | ||
|
|
a7fdc98b29 | ||
|
|
85b7f104df | ||
|
|
d76d1bd7fe | ||
|
|
df4412aa80 | ||
|
|
ab2c94e19a | ||
|
|
37cc4e2121 | ||
|
|
60dfdd0a66 | ||
|
|
bb8b2cb194 | ||
|
|
4e29684aa3 | ||
|
|
0e17e3553d | ||
|
|
0a55060e89 | ||
|
|
77859c7daa | ||
|
|
ba39c393a0 | ||
|
|
6a50d316d9 | ||
|
|
88c1d77f0b | ||
|
|
758ce40cc1 | ||
|
|
3e7bb80492 | ||
|
|
75e95aa9ca | ||
|
|
a389842e25 | ||
|
|
0f6a3c3f5a | ||
|
|
133f27422d | ||
|
|
abc6deb244 | ||
|
|
06869b4597 | ||
|
|
d32cea9870 | ||
|
|
4b68100f16 | ||
|
|
5c5515d462 | ||
|
|
3932b8f982 | ||
|
|
82488ca900 | ||
|
|
29d9b9b2d6 | ||
|
|
02215e9b7b | ||
|
|
7160b7a18b | ||
|
|
ea8dac837a | ||
|
|
e2a7a028bd | ||
|
|
70db8d264b | ||
|
|
0518e6d487 | ||
|
|
39eb367866 | ||
|
|
f1d51a22ad | ||
|
|
77fb554e8f | ||
|
|
91f8a0ae09 | ||
|
|
370cda7cf0 | ||
|
|
66b3eed273 | ||
|
|
99b061a143 | ||
|
|
5f3c7ed673 | ||
|
|
a6dc458212 | ||
|
|
520f521887 | ||
|
|
01427d9969 | ||
|
|
34c03ce983 | ||
|
|
95e9da42d6 | ||
|
|
1338cab61b | ||
|
|
7ba98c1e91 | ||
|
|
9a5f507cbe | ||
|
|
d560671d1f | ||
|
|
82c9cf4db6 | ||
|
|
910ec6c695 | ||
|
|
766d6f2bec | ||
|
|
9f39140987 | ||
|
|
89716ef4da | ||
|
|
3c4ea5a339 | ||
|
|
601846a8c1 | ||
|
|
85d66c1056 | ||
|
|
b89d3f663c | ||
|
|
0260d430d1 | ||
|
|
2e608cdc09 | ||
|
|
234ce93dc1 | ||
|
|
2ada1deb9a | ||
|
|
788ceb9721 | ||
|
|
61a68477d0 | ||
|
|
e74f626383 | ||
|
|
ef99f64291 |
4
.github/workflows/auto_release.yml
vendored
4
.github/workflows/auto_release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Dashboard Build
|
||||
run: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
needs: build-and-publish-to-github-release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
2
.github/workflows/code-format.yml
vendored
2
.github/workflows/code-format.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/coverage_test.yml
vendored
2
.github/workflows/coverage_test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/dashboard_ci.yml
vendored
2
.github/workflows/dashboard_ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tag: true
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tag: true
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,6 +34,7 @@ dashboard/node_modules/
|
||||
dashboard/dist/
|
||||
package-lock.json
|
||||
package.json
|
||||
yarn.lock
|
||||
|
||||
# Operating System
|
||||
**/.DS_Store
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
||||
</div>
|
||||
|
||||
AstrBot 是一个开源的一站式 Agent 聊天机器人平台及开发框架。
|
||||
AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可无缝接入主流即时通讯软件,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建生产可用的 AI 应用。
|
||||
|
||||
## 主要功能
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.5.23"
|
||||
__version__ = "4.7.2"
|
||||
|
||||
@@ -345,9 +345,6 @@ class MCPClient:
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up resources including old exit stacks from reconnections"""
|
||||
# Set running_event first to unblock any waiting tasks
|
||||
self.running_event.set()
|
||||
|
||||
# Close current exit stack
|
||||
try:
|
||||
await self.exit_stack.aclose()
|
||||
@@ -359,6 +356,9 @@ class MCPClient:
|
||||
# Just clear the list to release references
|
||||
self._old_exit_stacks.clear()
|
||||
|
||||
# Set running_event first to unblock any waiting tasks
|
||||
self.running_event.set()
|
||||
|
||||
|
||||
class MCPTool(FunctionTool, Generic[TContext]):
|
||||
"""A function tool that calls an MCP service."""
|
||||
|
||||
@@ -153,6 +153,7 @@ class AssistantMessageSegment(Message):
|
||||
"""A message segment from the assistant."""
|
||||
|
||||
role: Literal["assistant"] = "assistant"
|
||||
content: str | list[ContentPart] | None = None
|
||||
tool_calls: list[ToolCall] | list[dict] | None = None
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ import abc
|
||||
import typing as T
|
||||
from enum import Enum, auto
|
||||
|
||||
from astrbot.core.provider import Provider
|
||||
from astrbot import logger
|
||||
from astrbot.core.provider.entities import LLMResponse
|
||||
|
||||
from ..hooks import BaseAgentRunHooks
|
||||
from ..response import AgentResponse
|
||||
from ..run_context import ContextWrapper, TContext
|
||||
from ..tool_executor import BaseFunctionToolExecutor
|
||||
|
||||
|
||||
class AgentState(Enum):
|
||||
@@ -24,9 +23,7 @@ class BaseAgentRunner(T.Generic[TContext]):
|
||||
@abc.abstractmethod
|
||||
async def reset(
|
||||
self,
|
||||
provider: Provider,
|
||||
run_context: ContextWrapper[TContext],
|
||||
tool_executor: BaseFunctionToolExecutor[TContext],
|
||||
agent_hooks: BaseAgentRunHooks[TContext],
|
||||
**kwargs: T.Any,
|
||||
) -> None:
|
||||
@@ -60,3 +57,9 @@ class BaseAgentRunner(T.Generic[TContext]):
|
||||
This method should be called after the agent is done.
|
||||
"""
|
||||
...
|
||||
|
||||
def _transition_state(self, new_state: AgentState) -> None:
|
||||
"""Transition the agent state."""
|
||||
if self._state != new_state:
|
||||
logger.debug(f"Agent state transition: {self._state} -> {new_state}")
|
||||
self._state = new_state
|
||||
|
||||
367
astrbot/core/agent/runners/coze/coze_agent_runner.py
Normal file
367
astrbot/core/agent/runners/coze/coze_agent_runner.py
Normal file
@@ -0,0 +1,367 @@
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
import typing as T
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot import logger
|
||||
from astrbot.core import sp
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.entities import (
|
||||
LLMResponse,
|
||||
ProviderRequest,
|
||||
)
|
||||
|
||||
from ...hooks import BaseAgentRunHooks
|
||||
from ...response import AgentResponseData
|
||||
from ...run_context import ContextWrapper, TContext
|
||||
from ..base import AgentResponse, AgentState, BaseAgentRunner
|
||||
from .coze_api_client import CozeAPIClient
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class CozeAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""Coze Agent Runner"""
|
||||
|
||||
@override
|
||||
async def reset(
|
||||
self,
|
||||
request: ProviderRequest,
|
||||
run_context: ContextWrapper[TContext],
|
||||
agent_hooks: BaseAgentRunHooks[TContext],
|
||||
provider_config: dict,
|
||||
**kwargs: T.Any,
|
||||
) -> None:
|
||||
self.req = request
|
||||
self.streaming = kwargs.get("streaming", False)
|
||||
self.final_llm_resp = None
|
||||
self._state = AgentState.IDLE
|
||||
self.agent_hooks = agent_hooks
|
||||
self.run_context = run_context
|
||||
|
||||
self.api_key = provider_config.get("coze_api_key", "")
|
||||
if not self.api_key:
|
||||
raise Exception("Coze API Key 不能为空。")
|
||||
self.bot_id = provider_config.get("bot_id", "")
|
||||
if not self.bot_id:
|
||||
raise Exception("Coze Bot ID 不能为空。")
|
||||
self.api_base: str = provider_config.get("coze_api_base", "https://api.coze.cn")
|
||||
|
||||
if not isinstance(self.api_base, str) or not self.api_base.startswith(
|
||||
("http://", "https://"),
|
||||
):
|
||||
raise Exception(
|
||||
"Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。",
|
||||
)
|
||||
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
self.auto_save_history = provider_config.get("auto_save_history", True)
|
||||
|
||||
# 创建 API 客户端
|
||||
self.api_client = CozeAPIClient(api_key=self.api_key, api_base=self.api_base)
|
||||
|
||||
# 会话相关缓存
|
||||
self.file_id_cache: dict[str, dict[str, str]] = {}
|
||||
|
||||
@override
|
||||
async def step(self):
|
||||
"""
|
||||
执行 Coze Agent 的一个步骤
|
||||
"""
|
||||
if not self.req:
|
||||
raise ValueError("Request is not set. Please call reset() first.")
|
||||
|
||||
if self._state == AgentState.IDLE:
|
||||
try:
|
||||
await self.agent_hooks.on_agent_begin(self.run_context)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
|
||||
|
||||
# 开始处理,转换到运行状态
|
||||
self._transition_state(AgentState.RUNNING)
|
||||
|
||||
try:
|
||||
# 执行 Coze 请求并处理结果
|
||||
async for response in self._execute_coze_request():
|
||||
yield response
|
||||
except Exception as e:
|
||||
logger.error(f"Coze 请求失败:{str(e)}")
|
||||
self._transition_state(AgentState.ERROR)
|
||||
self.final_llm_resp = LLMResponse(
|
||||
role="err", completion_text=f"Coze 请求失败:{str(e)}"
|
||||
)
|
||||
yield AgentResponse(
|
||||
type="err",
|
||||
data=AgentResponseData(
|
||||
chain=MessageChain().message(f"Coze 请求失败:{str(e)}")
|
||||
),
|
||||
)
|
||||
finally:
|
||||
await self.api_client.close()
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
while not self.done():
|
||||
async for resp in self.step():
|
||||
yield resp
|
||||
|
||||
async def _execute_coze_request(self):
|
||||
"""执行 Coze 请求的核心逻辑"""
|
||||
prompt = self.req.prompt or ""
|
||||
session_id = self.req.session_id or "unknown"
|
||||
image_urls = self.req.image_urls or []
|
||||
contexts = self.req.contexts or []
|
||||
system_prompt = self.req.system_prompt
|
||||
|
||||
# 用户ID参数
|
||||
user_id = session_id
|
||||
|
||||
# 获取或创建会话ID
|
||||
conversation_id = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=user_id,
|
||||
key="coze_conversation_id",
|
||||
default="",
|
||||
)
|
||||
|
||||
# 构建消息
|
||||
additional_messages = []
|
||||
|
||||
if system_prompt:
|
||||
if not self.auto_save_history or not conversation_id:
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_prompt,
|
||||
"content_type": "text",
|
||||
},
|
||||
)
|
||||
|
||||
# 处理历史上下文
|
||||
if not self.auto_save_history and contexts:
|
||||
for ctx in contexts:
|
||||
if isinstance(ctx, dict) and "role" in ctx and "content" in ctx:
|
||||
# 处理上下文中的图片
|
||||
content = ctx["content"]
|
||||
if isinstance(content, list):
|
||||
# 多模态内容,需要处理图片
|
||||
processed_content = []
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
if item.get("type") == "text":
|
||||
processed_content.append(item)
|
||||
elif item.get("type") == "image_url":
|
||||
# 处理图片上传
|
||||
try:
|
||||
image_data = item.get("image_url", {})
|
||||
url = image_data.get("url", "")
|
||||
if url:
|
||||
file_id = (
|
||||
await self._download_and_upload_image(
|
||||
url, session_id
|
||||
)
|
||||
)
|
||||
processed_content.append(
|
||||
{
|
||||
"type": "file",
|
||||
"file_id": file_id,
|
||||
"file_url": url,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"处理上下文图片失败: {e}")
|
||||
continue
|
||||
|
||||
if processed_content:
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": ctx["role"],
|
||||
"content": processed_content,
|
||||
"content_type": "object_string",
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 纯文本内容
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": ctx["role"],
|
||||
"content": content,
|
||||
"content_type": "text",
|
||||
}
|
||||
)
|
||||
|
||||
# 构建当前消息
|
||||
if prompt or image_urls:
|
||||
if image_urls:
|
||||
# 多模态
|
||||
object_string_content = []
|
||||
if prompt:
|
||||
object_string_content.append({"type": "text", "text": prompt})
|
||||
|
||||
for url in image_urls:
|
||||
# the url is a base64 string
|
||||
try:
|
||||
image_data = base64.b64decode(url)
|
||||
file_id = await self.api_client.upload_file(image_data)
|
||||
object_string_content.append(
|
||||
{
|
||||
"type": "image",
|
||||
"file_id": file_id,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"处理图片失败 {url}: {e}")
|
||||
continue
|
||||
|
||||
if object_string_content:
|
||||
content = json.dumps(object_string_content, ensure_ascii=False)
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
"content_type": "object_string",
|
||||
}
|
||||
)
|
||||
elif prompt:
|
||||
# 纯文本
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
"content_type": "text",
|
||||
},
|
||||
)
|
||||
|
||||
# 执行 Coze API 请求
|
||||
accumulated_content = ""
|
||||
message_started = False
|
||||
|
||||
async for chunk in self.api_client.chat_messages(
|
||||
bot_id=self.bot_id,
|
||||
user_id=user_id,
|
||||
additional_messages=additional_messages,
|
||||
conversation_id=conversation_id,
|
||||
auto_save_history=self.auto_save_history,
|
||||
stream=True,
|
||||
timeout=self.timeout,
|
||||
):
|
||||
event_type = chunk.get("event")
|
||||
data = chunk.get("data", {})
|
||||
|
||||
if event_type == "conversation.chat.created":
|
||||
if isinstance(data, dict) and "conversation_id" in data:
|
||||
await sp.put_async(
|
||||
scope="umo",
|
||||
scope_id=user_id,
|
||||
key="coze_conversation_id",
|
||||
value=data["conversation_id"],
|
||||
)
|
||||
|
||||
if event_type == "conversation.message.delta":
|
||||
# 增量消息
|
||||
content = data.get("content", "")
|
||||
if not content and "delta" in data:
|
||||
content = data["delta"].get("content", "")
|
||||
if not content and "text" in data:
|
||||
content = data.get("text", "")
|
||||
|
||||
if content:
|
||||
accumulated_content += content
|
||||
message_started = True
|
||||
|
||||
# 如果是流式响应,发送增量数据
|
||||
if self.streaming:
|
||||
yield AgentResponse(
|
||||
type="streaming_delta",
|
||||
data=AgentResponseData(
|
||||
chain=MessageChain().message(content)
|
||||
),
|
||||
)
|
||||
|
||||
elif event_type == "conversation.message.completed":
|
||||
# 消息完成
|
||||
logger.debug("Coze message completed")
|
||||
message_started = True
|
||||
|
||||
elif event_type == "conversation.chat.completed":
|
||||
# 对话完成
|
||||
logger.debug("Coze chat completed")
|
||||
break
|
||||
|
||||
elif event_type == "error":
|
||||
# 错误处理
|
||||
error_msg = data.get("msg", "未知错误")
|
||||
error_code = data.get("code", "UNKNOWN")
|
||||
logger.error(f"Coze 出现错误: {error_code} - {error_msg}")
|
||||
raise Exception(f"Coze 出现错误: {error_code} - {error_msg}")
|
||||
|
||||
if not message_started and not accumulated_content:
|
||||
logger.warning("Coze 未返回任何内容")
|
||||
accumulated_content = ""
|
||||
|
||||
# 创建最终响应
|
||||
chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
|
||||
self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
|
||||
self._transition_state(AgentState.DONE)
|
||||
|
||||
try:
|
||||
await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
||||
|
||||
# 返回最终结果
|
||||
yield AgentResponse(
|
||||
type="llm_result",
|
||||
data=AgentResponseData(chain=chain),
|
||||
)
|
||||
|
||||
async def _download_and_upload_image(
|
||||
self,
|
||||
image_url: str,
|
||||
session_id: str | None = None,
|
||||
) -> str:
|
||||
"""下载图片并上传到 Coze,返回 file_id"""
|
||||
import hashlib
|
||||
|
||||
# 计算哈希实现缓存
|
||||
cache_key = hashlib.md5(image_url.encode("utf-8")).hexdigest()
|
||||
|
||||
if session_id:
|
||||
if session_id not in self.file_id_cache:
|
||||
self.file_id_cache[session_id] = {}
|
||||
|
||||
if cache_key in self.file_id_cache[session_id]:
|
||||
file_id = self.file_id_cache[session_id][cache_key]
|
||||
logger.debug(f"[Coze] 使用缓存的 file_id: {file_id}")
|
||||
return file_id
|
||||
|
||||
try:
|
||||
image_data = await self.api_client.download_image(image_url)
|
||||
file_id = await self.api_client.upload_file(image_data)
|
||||
|
||||
if session_id:
|
||||
self.file_id_cache[session_id][cache_key] = file_id
|
||||
logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}")
|
||||
|
||||
return file_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理图片失败 {image_url}: {e!s}")
|
||||
raise Exception(f"处理图片失败: {e!s}")
|
||||
|
||||
@override
|
||||
def done(self) -> bool:
|
||||
"""检查 Agent 是否已完成工作"""
|
||||
return self._state in (AgentState.DONE, AgentState.ERROR)
|
||||
|
||||
@override
|
||||
def get_final_llm_resp(self) -> LLMResponse | None:
|
||||
return self.final_llm_resp
|
||||
403
astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py
Normal file
403
astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py
Normal file
@@ -0,0 +1,403 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import typing as T
|
||||
|
||||
from dashscope import Application
|
||||
from dashscope.app.application_response import ApplicationResponse
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.entities import (
|
||||
LLMResponse,
|
||||
ProviderRequest,
|
||||
)
|
||||
|
||||
from ...hooks import BaseAgentRunHooks
|
||||
from ...response import AgentResponseData
|
||||
from ...run_context import ContextWrapper, TContext
|
||||
from ..base import AgentResponse, AgentState, BaseAgentRunner
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class DashscopeAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""Dashscope Agent Runner"""
|
||||
|
||||
@override
|
||||
async def reset(
|
||||
self,
|
||||
request: ProviderRequest,
|
||||
run_context: ContextWrapper[TContext],
|
||||
agent_hooks: BaseAgentRunHooks[TContext],
|
||||
provider_config: dict,
|
||||
**kwargs: T.Any,
|
||||
) -> None:
|
||||
self.req = request
|
||||
self.streaming = kwargs.get("streaming", False)
|
||||
self.final_llm_resp = None
|
||||
self._state = AgentState.IDLE
|
||||
self.agent_hooks = agent_hooks
|
||||
self.run_context = run_context
|
||||
|
||||
self.api_key = provider_config.get("dashscope_api_key", "")
|
||||
if not self.api_key:
|
||||
raise Exception("阿里云百炼 API Key 不能为空。")
|
||||
self.app_id = provider_config.get("dashscope_app_id", "")
|
||||
if not self.app_id:
|
||||
raise Exception("阿里云百炼 APP ID 不能为空。")
|
||||
self.dashscope_app_type = provider_config.get("dashscope_app_type", "")
|
||||
if not self.dashscope_app_type:
|
||||
raise Exception("阿里云百炼 APP 类型不能为空。")
|
||||
|
||||
self.variables: dict = provider_config.get("variables", {}) or {}
|
||||
self.rag_options: dict = provider_config.get("rag_options", {})
|
||||
self.output_reference = self.rag_options.get("output_reference", False)
|
||||
self.rag_options = self.rag_options.copy()
|
||||
self.rag_options.pop("output_reference", None)
|
||||
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
|
||||
def has_rag_options(self):
|
||||
"""判断是否有 RAG 选项
|
||||
|
||||
Returns:
|
||||
bool: 是否有 RAG 选项
|
||||
|
||||
"""
|
||||
if self.rag_options and (
|
||||
len(self.rag_options.get("pipeline_ids", [])) > 0
|
||||
or len(self.rag_options.get("file_ids", [])) > 0
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@override
|
||||
async def step(self):
|
||||
"""
|
||||
执行 Dashscope Agent 的一个步骤
|
||||
"""
|
||||
if not self.req:
|
||||
raise ValueError("Request is not set. Please call reset() first.")
|
||||
|
||||
if self._state == AgentState.IDLE:
|
||||
try:
|
||||
await self.agent_hooks.on_agent_begin(self.run_context)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
|
||||
|
||||
# 开始处理,转换到运行状态
|
||||
self._transition_state(AgentState.RUNNING)
|
||||
|
||||
try:
|
||||
# 执行 Dashscope 请求并处理结果
|
||||
async for response in self._execute_dashscope_request():
|
||||
yield response
|
||||
except Exception as e:
|
||||
logger.error(f"阿里云百炼请求失败:{str(e)}")
|
||||
self._transition_state(AgentState.ERROR)
|
||||
self.final_llm_resp = LLMResponse(
|
||||
role="err", completion_text=f"阿里云百炼请求失败:{str(e)}"
|
||||
)
|
||||
yield AgentResponse(
|
||||
type="err",
|
||||
data=AgentResponseData(
|
||||
chain=MessageChain().message(f"阿里云百炼请求失败:{str(e)}")
|
||||
),
|
||||
)
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
while not self.done():
|
||||
async for resp in self.step():
|
||||
yield resp
|
||||
|
||||
def _consume_sync_generator(
|
||||
self, response: T.Any, response_queue: queue.Queue
|
||||
) -> None:
|
||||
"""在线程中消费同步generator,将结果放入队列
|
||||
|
||||
Args:
|
||||
response: 同步generator对象
|
||||
response_queue: 用于传递数据的队列
|
||||
|
||||
"""
|
||||
try:
|
||||
if self.streaming:
|
||||
for chunk in response:
|
||||
response_queue.put(("data", chunk))
|
||||
else:
|
||||
response_queue.put(("data", response))
|
||||
except Exception as e:
|
||||
response_queue.put(("error", e))
|
||||
finally:
|
||||
response_queue.put(("done", None))
|
||||
|
||||
async def _process_stream_chunk(
|
||||
self, chunk: ApplicationResponse, output_text: str
|
||||
) -> tuple[str, list | None, AgentResponse | None]:
|
||||
"""处理流式响应的单个chunk
|
||||
|
||||
Args:
|
||||
chunk: Dashscope响应chunk
|
||||
output_text: 当前累积的输出文本
|
||||
|
||||
Returns:
|
||||
(更新后的output_text, doc_references, AgentResponse或None)
|
||||
|
||||
"""
|
||||
logger.debug(f"dashscope stream chunk: {chunk}")
|
||||
|
||||
if chunk.status_code != 200:
|
||||
logger.error(
|
||||
f"阿里云百炼请求失败: request_id={chunk.request_id}, code={chunk.status_code}, message={chunk.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code",
|
||||
)
|
||||
self._transition_state(AgentState.ERROR)
|
||||
error_msg = (
|
||||
f"阿里云百炼请求失败: message={chunk.message} code={chunk.status_code}"
|
||||
)
|
||||
self.final_llm_resp = LLMResponse(
|
||||
role="err",
|
||||
result_chain=MessageChain().message(error_msg),
|
||||
)
|
||||
return (
|
||||
output_text,
|
||||
None,
|
||||
AgentResponse(
|
||||
type="err",
|
||||
data=AgentResponseData(chain=MessageChain().message(error_msg)),
|
||||
),
|
||||
)
|
||||
|
||||
chunk_text = chunk.output.get("text", "") or ""
|
||||
# RAG 引用脚标格式化
|
||||
chunk_text = re.sub(r"<ref>\[(\d+)\]</ref>", r"[\1]", chunk_text)
|
||||
|
||||
response = None
|
||||
if chunk_text:
|
||||
output_text += chunk_text
|
||||
response = AgentResponse(
|
||||
type="streaming_delta",
|
||||
data=AgentResponseData(chain=MessageChain().message(chunk_text)),
|
||||
)
|
||||
|
||||
# 获取文档引用
|
||||
doc_references = chunk.output.get("doc_references", None)
|
||||
|
||||
return output_text, doc_references, response
|
||||
|
||||
def _format_doc_references(self, doc_references: list) -> str:
|
||||
"""格式化文档引用为文本
|
||||
|
||||
Args:
|
||||
doc_references: 文档引用列表
|
||||
|
||||
Returns:
|
||||
格式化后的引用文本
|
||||
|
||||
"""
|
||||
ref_parts = []
|
||||
for ref in doc_references:
|
||||
ref_title = (
|
||||
ref.get("title", "") if ref.get("title") else ref.get("doc_name", "")
|
||||
)
|
||||
ref_parts.append(f"{ref['index_id']}. {ref_title}\n")
|
||||
ref_str = "".join(ref_parts)
|
||||
return f"\n\n回答来源:\n{ref_str}"
|
||||
|
||||
async def _build_request_payload(
|
||||
self, prompt: str, session_id: str, contexts: list, system_prompt: str
|
||||
) -> dict:
|
||||
"""构建请求payload
|
||||
|
||||
Args:
|
||||
prompt: 用户输入
|
||||
session_id: 会话ID
|
||||
contexts: 上下文列表
|
||||
system_prompt: 系统提示词
|
||||
|
||||
Returns:
|
||||
请求payload字典
|
||||
|
||||
"""
|
||||
conversation_id = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="dashscope_conversation_id",
|
||||
default="",
|
||||
)
|
||||
# 获得会话变量
|
||||
payload_vars = self.variables.copy()
|
||||
session_var = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_variables",
|
||||
default={},
|
||||
)
|
||||
payload_vars.update(session_var)
|
||||
|
||||
if (
|
||||
self.dashscope_app_type in ["agent", "dialog-workflow"]
|
||||
and not self.has_rag_options()
|
||||
):
|
||||
# 支持多轮对话的
|
||||
p = {
|
||||
"app_id": self.app_id,
|
||||
"api_key": self.api_key,
|
||||
"prompt": prompt,
|
||||
"biz_params": payload_vars or None,
|
||||
"stream": self.streaming,
|
||||
"incremental_output": True,
|
||||
}
|
||||
if conversation_id:
|
||||
p["session_id"] = conversation_id
|
||||
return p
|
||||
else:
|
||||
# 不支持多轮对话的
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"prompt": prompt,
|
||||
"api_key": self.api_key,
|
||||
"biz_params": payload_vars or None,
|
||||
"stream": self.streaming,
|
||||
"incremental_output": True,
|
||||
}
|
||||
if self.rag_options:
|
||||
payload["rag_options"] = self.rag_options
|
||||
return payload
|
||||
|
||||
async def _handle_streaming_response(
|
||||
self, response: T.Any, session_id: str
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
"""处理流式响应
|
||||
|
||||
Args:
|
||||
response: Dashscope 流式响应 generator
|
||||
|
||||
Yields:
|
||||
AgentResponse 对象
|
||||
|
||||
"""
|
||||
response_queue = queue.Queue()
|
||||
consumer_thread = threading.Thread(
|
||||
target=self._consume_sync_generator,
|
||||
args=(response, response_queue),
|
||||
daemon=True,
|
||||
)
|
||||
consumer_thread.start()
|
||||
|
||||
output_text = ""
|
||||
doc_references = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
item_type, item_data = await asyncio.get_event_loop().run_in_executor(
|
||||
None, response_queue.get, True, 1
|
||||
)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
if item_type == "done":
|
||||
break
|
||||
elif item_type == "error":
|
||||
raise item_data
|
||||
elif item_type == "data":
|
||||
chunk = item_data
|
||||
assert isinstance(chunk, ApplicationResponse)
|
||||
|
||||
(
|
||||
output_text,
|
||||
chunk_doc_refs,
|
||||
response,
|
||||
) = await self._process_stream_chunk(chunk, output_text)
|
||||
|
||||
if response:
|
||||
if response.type == "err":
|
||||
yield response
|
||||
return
|
||||
yield response
|
||||
|
||||
if chunk_doc_refs:
|
||||
doc_references = chunk_doc_refs
|
||||
|
||||
if chunk.output.session_id:
|
||||
await sp.put_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="dashscope_conversation_id",
|
||||
value=chunk.output.session_id,
|
||||
)
|
||||
|
||||
# 添加 RAG 引用
|
||||
if self.output_reference and doc_references:
|
||||
ref_text = self._format_doc_references(doc_references)
|
||||
output_text += ref_text
|
||||
|
||||
if self.streaming:
|
||||
yield AgentResponse(
|
||||
type="streaming_delta",
|
||||
data=AgentResponseData(chain=MessageChain().message(ref_text)),
|
||||
)
|
||||
|
||||
# 创建最终响应
|
||||
chain = MessageChain(chain=[Comp.Plain(output_text)])
|
||||
self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
|
||||
self._transition_state(AgentState.DONE)
|
||||
|
||||
try:
|
||||
await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
||||
|
||||
# 返回最终结果
|
||||
yield AgentResponse(
|
||||
type="llm_result",
|
||||
data=AgentResponseData(chain=chain),
|
||||
)
|
||||
|
||||
async def _execute_dashscope_request(self):
|
||||
"""执行 Dashscope 请求的核心逻辑"""
|
||||
prompt = self.req.prompt or ""
|
||||
session_id = self.req.session_id or "unknown"
|
||||
image_urls = self.req.image_urls or []
|
||||
contexts = self.req.contexts or []
|
||||
system_prompt = self.req.system_prompt
|
||||
|
||||
# 检查图片输入
|
||||
if image_urls:
|
||||
logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。")
|
||||
|
||||
# 构建请求payload
|
||||
payload = await self._build_request_payload(
|
||||
prompt, session_id, contexts, system_prompt
|
||||
)
|
||||
|
||||
if not self.streaming:
|
||||
payload["incremental_output"] = False
|
||||
|
||||
# 发起请求
|
||||
partial = functools.partial(Application.call, **payload)
|
||||
response = await asyncio.get_event_loop().run_in_executor(None, partial)
|
||||
|
||||
async for resp in self._handle_streaming_response(response, session_id):
|
||||
yield resp
|
||||
|
||||
@override
|
||||
def done(self) -> bool:
|
||||
"""检查 Agent 是否已完成工作"""
|
||||
return self._state in (AgentState.DONE, AgentState.ERROR)
|
||||
|
||||
@override
|
||||
def get_final_llm_resp(self) -> LLMResponse | None:
|
||||
return self.final_llm_resp
|
||||
336
astrbot/core/agent/runners/dify/dify_agent_runner.py
Normal file
336
astrbot/core/agent/runners/dify/dify_agent_runner.py
Normal file
@@ -0,0 +1,336 @@
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
import typing as T
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.entities import (
|
||||
LLMResponse,
|
||||
ProviderRequest,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
|
||||
from ...hooks import BaseAgentRunHooks
|
||||
from ...response import AgentResponseData
|
||||
from ...run_context import ContextWrapper, TContext
|
||||
from ..base import AgentResponse, AgentState, BaseAgentRunner
|
||||
from .dify_api_client import DifyAPIClient
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""Dify Agent Runner"""
|
||||
|
||||
@override
|
||||
async def reset(
|
||||
self,
|
||||
request: ProviderRequest,
|
||||
run_context: ContextWrapper[TContext],
|
||||
agent_hooks: BaseAgentRunHooks[TContext],
|
||||
provider_config: dict,
|
||||
**kwargs: T.Any,
|
||||
) -> None:
|
||||
self.req = request
|
||||
self.streaming = kwargs.get("streaming", False)
|
||||
self.final_llm_resp = None
|
||||
self._state = AgentState.IDLE
|
||||
self.agent_hooks = agent_hooks
|
||||
self.run_context = run_context
|
||||
|
||||
self.api_key = provider_config.get("dify_api_key", "")
|
||||
self.api_base = provider_config.get("dify_api_base", "https://api.dify.ai/v1")
|
||||
self.api_type = provider_config.get("dify_api_type", "chat")
|
||||
self.workflow_output_key = provider_config.get(
|
||||
"dify_workflow_output_key",
|
||||
"astrbot_wf_output",
|
||||
)
|
||||
self.dify_query_input_key = provider_config.get(
|
||||
"dify_query_input_key",
|
||||
"astrbot_text_query",
|
||||
)
|
||||
self.variables: dict = provider_config.get("variables", {}) or {}
|
||||
self.timeout = provider_config.get("timeout", 60)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
|
||||
self.api_client = DifyAPIClient(self.api_key, self.api_base)
|
||||
|
||||
@override
|
||||
async def step(self):
|
||||
"""
|
||||
执行 Dify Agent 的一个步骤
|
||||
"""
|
||||
if not self.req:
|
||||
raise ValueError("Request is not set. Please call reset() first.")
|
||||
|
||||
if self._state == AgentState.IDLE:
|
||||
try:
|
||||
await self.agent_hooks.on_agent_begin(self.run_context)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
|
||||
|
||||
# 开始处理,转换到运行状态
|
||||
self._transition_state(AgentState.RUNNING)
|
||||
|
||||
try:
|
||||
# 执行 Dify 请求并处理结果
|
||||
async for response in self._execute_dify_request():
|
||||
yield response
|
||||
except Exception as e:
|
||||
logger.error(f"Dify 请求失败:{str(e)}")
|
||||
self._transition_state(AgentState.ERROR)
|
||||
self.final_llm_resp = LLMResponse(
|
||||
role="err", completion_text=f"Dify 请求失败:{str(e)}"
|
||||
)
|
||||
yield AgentResponse(
|
||||
type="err",
|
||||
data=AgentResponseData(
|
||||
chain=MessageChain().message(f"Dify 请求失败:{str(e)}")
|
||||
),
|
||||
)
|
||||
finally:
|
||||
await self.api_client.close()
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
while not self.done():
|
||||
async for resp in self.step():
|
||||
yield resp
|
||||
|
||||
async def _execute_dify_request(self):
|
||||
"""执行 Dify 请求的核心逻辑"""
|
||||
prompt = self.req.prompt or ""
|
||||
session_id = self.req.session_id or "unknown"
|
||||
image_urls = self.req.image_urls or []
|
||||
system_prompt = self.req.system_prompt
|
||||
|
||||
conversation_id = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="dify_conversation_id",
|
||||
default="",
|
||||
)
|
||||
result = ""
|
||||
|
||||
# 处理图片上传
|
||||
files_payload = []
|
||||
for image_url in image_urls:
|
||||
# image_url is a base64 string
|
||||
try:
|
||||
image_data = base64.b64decode(image_url)
|
||||
file_response = await self.api_client.file_upload(
|
||||
file_data=image_data,
|
||||
user=session_id,
|
||||
mime_type="image/png",
|
||||
file_name="image.png",
|
||||
)
|
||||
logger.debug(f"Dify 上传图片响应:{file_response}")
|
||||
if "id" not in file_response:
|
||||
logger.warning(
|
||||
f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。"
|
||||
)
|
||||
continue
|
||||
files_payload.append(
|
||||
{
|
||||
"type": "image",
|
||||
"transfer_method": "local_file",
|
||||
"upload_file_id": file_response["id"],
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"上传图片失败:{e}")
|
||||
continue
|
||||
|
||||
# 获得会话变量
|
||||
payload_vars = self.variables.copy()
|
||||
# 动态变量
|
||||
session_var = await sp.get_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="session_variables",
|
||||
default={},
|
||||
)
|
||||
payload_vars.update(session_var)
|
||||
payload_vars["system_prompt"] = system_prompt
|
||||
|
||||
# 处理不同的 API 类型
|
||||
match self.api_type:
|
||||
case "chat" | "agent" | "chatflow":
|
||||
if not prompt:
|
||||
prompt = "请描述这张图片。"
|
||||
|
||||
async for chunk in self.api_client.chat_messages(
|
||||
inputs={
|
||||
**payload_vars,
|
||||
},
|
||||
query=prompt,
|
||||
user=session_id,
|
||||
conversation_id=conversation_id,
|
||||
files=files_payload,
|
||||
timeout=self.timeout,
|
||||
):
|
||||
logger.debug(f"dify resp chunk: {chunk}")
|
||||
if chunk["event"] == "message" or chunk["event"] == "agent_message":
|
||||
result += chunk["answer"]
|
||||
if not conversation_id:
|
||||
await sp.put_async(
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
key="dify_conversation_id",
|
||||
value=chunk["conversation_id"],
|
||||
)
|
||||
conversation_id = chunk["conversation_id"]
|
||||
|
||||
# 如果是流式响应,发送增量数据
|
||||
if self.streaming and chunk["answer"]:
|
||||
yield AgentResponse(
|
||||
type="streaming_delta",
|
||||
data=AgentResponseData(
|
||||
chain=MessageChain().message(chunk["answer"])
|
||||
),
|
||||
)
|
||||
elif chunk["event"] == "message_end":
|
||||
logger.debug("Dify message end")
|
||||
break
|
||||
elif chunk["event"] == "error":
|
||||
logger.error(f"Dify 出现错误:{chunk}")
|
||||
raise Exception(
|
||||
f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}"
|
||||
)
|
||||
|
||||
case "workflow":
|
||||
async for chunk in self.api_client.workflow_run(
|
||||
inputs={
|
||||
self.dify_query_input_key: prompt,
|
||||
"astrbot_session_id": session_id,
|
||||
**payload_vars,
|
||||
},
|
||||
user=session_id,
|
||||
files=files_payload,
|
||||
timeout=self.timeout,
|
||||
):
|
||||
logger.debug(f"dify workflow resp chunk: {chunk}")
|
||||
match chunk["event"]:
|
||||
case "workflow_started":
|
||||
logger.info(
|
||||
f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。"
|
||||
)
|
||||
case "node_finished":
|
||||
logger.debug(
|
||||
f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。"
|
||||
)
|
||||
case "text_chunk":
|
||||
if self.streaming and chunk["data"]["text"]:
|
||||
yield AgentResponse(
|
||||
type="streaming_delta",
|
||||
data=AgentResponseData(
|
||||
chain=MessageChain().message(
|
||||
chunk["data"]["text"]
|
||||
)
|
||||
),
|
||||
)
|
||||
case "workflow_finished":
|
||||
logger.info(
|
||||
f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束"
|
||||
)
|
||||
logger.debug(f"Dify 工作流结果:{chunk}")
|
||||
if chunk["data"]["error"]:
|
||||
logger.error(
|
||||
f"Dify 工作流出现错误:{chunk['data']['error']}"
|
||||
)
|
||||
raise Exception(
|
||||
f"Dify 工作流出现错误:{chunk['data']['error']}"
|
||||
)
|
||||
if self.workflow_output_key not in chunk["data"]["outputs"]:
|
||||
raise Exception(
|
||||
f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}"
|
||||
)
|
||||
result = chunk
|
||||
case _:
|
||||
raise Exception(f"未知的 Dify API 类型:{self.api_type}")
|
||||
|
||||
if not result:
|
||||
logger.warning("Dify 请求结果为空,请查看 Debug 日志。")
|
||||
|
||||
# 解析结果
|
||||
chain = await self.parse_dify_result(result)
|
||||
|
||||
# 创建最终响应
|
||||
self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
|
||||
self._transition_state(AgentState.DONE)
|
||||
|
||||
try:
|
||||
await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
||||
|
||||
# 返回最终结果
|
||||
yield AgentResponse(
|
||||
type="llm_result",
|
||||
data=AgentResponseData(chain=chain),
|
||||
)
|
||||
|
||||
async def parse_dify_result(self, chunk: dict | str) -> MessageChain:
|
||||
"""解析 Dify 的响应结果"""
|
||||
if isinstance(chunk, str):
|
||||
# Chat
|
||||
return MessageChain(chain=[Comp.Plain(chunk)])
|
||||
|
||||
async def parse_file(item: dict):
|
||||
match item["type"]:
|
||||
case "image":
|
||||
return Comp.Image(file=item["url"], url=item["url"])
|
||||
case "audio":
|
||||
# 仅支持 wav
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
path = os.path.join(temp_dir, f"{item['filename']}.wav")
|
||||
await download_file(item["url"], path)
|
||||
return Comp.Image(file=item["url"], url=item["url"])
|
||||
case "video":
|
||||
return Comp.Video(file=item["url"])
|
||||
case _:
|
||||
return Comp.File(name=item["filename"], file=item["url"])
|
||||
|
||||
output = chunk["data"]["outputs"][self.workflow_output_key]
|
||||
chains = []
|
||||
if isinstance(output, str):
|
||||
# 纯文本输出
|
||||
chains.append(Comp.Plain(output))
|
||||
elif isinstance(output, list):
|
||||
# 主要适配 Dify 的 HTTP 请求结点的多模态输出
|
||||
for item in output:
|
||||
# handle Array[File]
|
||||
if (
|
||||
not isinstance(item, dict)
|
||||
or item.get("dify_model_identity", "") != "__dify__file__"
|
||||
):
|
||||
chains.append(Comp.Plain(str(output)))
|
||||
break
|
||||
else:
|
||||
chains.append(Comp.Plain(str(output)))
|
||||
|
||||
# scan file
|
||||
files = chunk["data"].get("files", [])
|
||||
for item in files:
|
||||
comp = await parse_file(item)
|
||||
chains.append(comp)
|
||||
|
||||
return MessageChain(chain=chains)
|
||||
|
||||
@override
|
||||
def done(self) -> bool:
|
||||
"""检查 Agent 是否已完成工作"""
|
||||
return self._state in (AgentState.DONE, AgentState.ERROR)
|
||||
|
||||
@override
|
||||
def get_final_llm_resp(self) -> LLMResponse | None:
|
||||
return self.final_llm_resp
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponse, ClientSession
|
||||
from aiohttp import ClientResponse, ClientSession, FormData
|
||||
|
||||
from astrbot.core import logger
|
||||
|
||||
@@ -101,21 +101,59 @@ class DifyAPIClient:
|
||||
|
||||
async def file_upload(
|
||||
self,
|
||||
file_path: str,
|
||||
user: str,
|
||||
file_path: str | None = None,
|
||||
file_data: bytes | None = None,
|
||||
file_name: str | None = None,
|
||||
mime_type: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Upload a file to Dify. Must provide either file_path or file_data.
|
||||
|
||||
Args:
|
||||
user: The user ID.
|
||||
file_path: The path to the file to upload.
|
||||
file_data: The file data in bytes.
|
||||
file_name: Optional file name when using file_data.
|
||||
Returns:
|
||||
A dictionary containing the uploaded file information.
|
||||
"""
|
||||
url = f"{self.api_base}/files/upload"
|
||||
with open(file_path, "rb") as f:
|
||||
payload = {
|
||||
"user": user,
|
||||
"file": f,
|
||||
}
|
||||
async with self.session.post(
|
||||
url,
|
||||
data=payload,
|
||||
headers=self.headers,
|
||||
) as resp:
|
||||
return await resp.json() # {"id": "xxx", ...}
|
||||
|
||||
form = FormData()
|
||||
form.add_field("user", user)
|
||||
|
||||
if file_data is not None:
|
||||
# 使用 bytes 数据
|
||||
form.add_field(
|
||||
"file",
|
||||
file_data,
|
||||
filename=file_name or "uploaded_file",
|
||||
content_type=mime_type or "application/octet-stream",
|
||||
)
|
||||
elif file_path is not None:
|
||||
# 使用文件路径
|
||||
import os
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
form.add_field(
|
||||
"file",
|
||||
file_content,
|
||||
filename=os.path.basename(file_path),
|
||||
content_type=mime_type or "application/octet-stream",
|
||||
)
|
||||
else:
|
||||
raise ValueError("file_path 和 file_data 不能同时为 None")
|
||||
|
||||
async with self.session.post(
|
||||
url,
|
||||
data=form,
|
||||
headers=self.headers, # 不包含 Content-Type,让 aiohttp 自动设置
|
||||
) as resp:
|
||||
if resp.status != 200 and resp.status != 201:
|
||||
text = await resp.text()
|
||||
raise Exception(f"Dify 文件上传失败:{resp.status}. {text}")
|
||||
return await resp.json() # {"id": "xxx", ...}
|
||||
|
||||
async def close(self):
|
||||
await self.session.close()
|
||||
@@ -69,12 +69,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
)
|
||||
self.run_context.messages = messages
|
||||
|
||||
def _transition_state(self, new_state: AgentState) -> None:
|
||||
"""转换 Agent 状态"""
|
||||
if self._state != new_state:
|
||||
logger.debug(f"Agent state transition: {self._state} -> {new_state}")
|
||||
self._state = new_state
|
||||
|
||||
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
||||
"""Yields chunks *and* a final LLMResponse."""
|
||||
if self.streaming:
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.6.0"
|
||||
VERSION = "4.7.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -68,6 +68,10 @@ DEFAULT_CONFIG = {
|
||||
"dequeue_context_length": 1,
|
||||
"streaming_response": False,
|
||||
"show_tool_use_status": False,
|
||||
"agent_runner_type": "local",
|
||||
"dify_agent_runner_provider_id": "",
|
||||
"coze_agent_runner_provider_id": "",
|
||||
"dashscope_agent_runner_provider_id": "",
|
||||
"unsupported_streaming_strategy": "realtime_segmenting",
|
||||
"max_agent_step": 30,
|
||||
"tool_call_timeout": 60,
|
||||
@@ -86,6 +90,7 @@ DEFAULT_CONFIG = {
|
||||
"group_icl_enable": False,
|
||||
"group_message_max_cnt": 300,
|
||||
"image_caption": False,
|
||||
"image_caption_provider_id": "",
|
||||
"active_reply": {
|
||||
"enable": False,
|
||||
"method": "possibility_reply",
|
||||
@@ -141,7 +146,16 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
# 配置项的中文描述、值类型
|
||||
"""
|
||||
AstrBot v3 时代的配置元数据,目前仅承担以下功能:
|
||||
|
||||
1. 保存配置时,配置项的类型验证
|
||||
2. WebUI 展示提供商和平台适配器模版
|
||||
|
||||
WebUI 的配置文件在 `CONFIG_METADATA_3` 中。
|
||||
|
||||
未来将会逐步淘汰此配置元数据。
|
||||
"""
|
||||
CONFIG_METADATA_2 = {
|
||||
"platform_group": {
|
||||
"metadata": {
|
||||
@@ -634,7 +648,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"type": "int",
|
||||
"hint": "超过这个字数的消息不会被分段回复。默认为 150",
|
||||
"hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
|
||||
},
|
||||
"regex": {
|
||||
"type": "string",
|
||||
@@ -1011,7 +1025,7 @@ CONFIG_METADATA_2 = {
|
||||
"id": "dify_app_default",
|
||||
"provider": "dify",
|
||||
"type": "dify",
|
||||
"provider_type": "chat_completion",
|
||||
"provider_type": "agent_runner",
|
||||
"enable": True,
|
||||
"dify_api_type": "chat",
|
||||
"dify_api_key": "",
|
||||
@@ -1025,20 +1039,20 @@ CONFIG_METADATA_2 = {
|
||||
"Coze": {
|
||||
"id": "coze",
|
||||
"provider": "coze",
|
||||
"provider_type": "chat_completion",
|
||||
"provider_type": "agent_runner",
|
||||
"type": "coze",
|
||||
"enable": True,
|
||||
"coze_api_key": "",
|
||||
"bot_id": "",
|
||||
"coze_api_base": "https://api.coze.cn",
|
||||
"timeout": 60,
|
||||
"auto_save_history": True,
|
||||
# "auto_save_history": True,
|
||||
},
|
||||
"阿里云百炼应用": {
|
||||
"id": "dashscope",
|
||||
"provider": "dashscope",
|
||||
"type": "dashscope",
|
||||
"provider_type": "chat_completion",
|
||||
"provider_type": "agent_runner",
|
||||
"enable": True,
|
||||
"dashscope_app_type": "agent",
|
||||
"dashscope_api_key": "",
|
||||
@@ -1087,7 +1101,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "",
|
||||
"model": "whisper-1",
|
||||
},
|
||||
"Whisper(本地加载)": {
|
||||
"Whisper(Local)": {
|
||||
"hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"provider": "openai",
|
||||
"type": "openai_whisper_selfhost",
|
||||
@@ -1096,7 +1110,7 @@ CONFIG_METADATA_2 = {
|
||||
"id": "whisper_selfhost",
|
||||
"model": "tiny",
|
||||
},
|
||||
"SenseVoice(本地加载)": {
|
||||
"SenseVoice(Local)": {
|
||||
"hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"type": "sensevoice_stt_selfhost",
|
||||
"provider": "sensevoice",
|
||||
@@ -1131,7 +1145,7 @@ CONFIG_METADATA_2 = {
|
||||
"pitch": "+0Hz",
|
||||
"timeout": 20,
|
||||
},
|
||||
"GSV TTS(本地加载)": {
|
||||
"GSV TTS(Local)": {
|
||||
"id": "gsv_tts",
|
||||
"enable": False,
|
||||
"provider": "gpt_sovits",
|
||||
@@ -1308,6 +1322,19 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 20,
|
||||
"launch_model_if_not_running": False,
|
||||
},
|
||||
"阿里云百炼重排序": {
|
||||
"id": "bailian_rerank",
|
||||
"type": "bailian_rerank",
|
||||
"provider": "bailian",
|
||||
"provider_type": "rerank",
|
||||
"enable": True,
|
||||
"rerank_api_key": "",
|
||||
"rerank_api_base": "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank",
|
||||
"rerank_model": "qwen3-rerank",
|
||||
"timeout": 30,
|
||||
"return_documents": False,
|
||||
"instruct": "",
|
||||
},
|
||||
"Xinference STT": {
|
||||
"id": "xinference_stt",
|
||||
"type": "xinference_stt",
|
||||
@@ -1342,6 +1369,16 @@ CONFIG_METADATA_2 = {
|
||||
"description": "重排序模型名称",
|
||||
"type": "string",
|
||||
},
|
||||
"return_documents": {
|
||||
"description": "是否在排序结果中返回文档原文",
|
||||
"type": "bool",
|
||||
"hint": "默认值false,以减少网络传输开销。",
|
||||
},
|
||||
"instruct": {
|
||||
"description": "自定义排序任务类型说明",
|
||||
"type": "string",
|
||||
"hint": "仅在使用 qwen3-rerank 模型时生效。建议使用英文撰写。",
|
||||
},
|
||||
"launch_model_if_not_running": {
|
||||
"description": "模型未运行时自动启动",
|
||||
"type": "bool",
|
||||
@@ -1884,7 +1921,6 @@ CONFIG_METADATA_2 = {
|
||||
"enable": {
|
||||
"description": "启用",
|
||||
"type": "bool",
|
||||
"hint": "是否启用。",
|
||||
},
|
||||
"key": {
|
||||
"description": "API Key",
|
||||
@@ -2014,12 +2050,22 @@ CONFIG_METADATA_2 = {
|
||||
"unsupported_streaming_strategy": {
|
||||
"type": "string",
|
||||
},
|
||||
"agent_runner_type": {
|
||||
"type": "string",
|
||||
},
|
||||
"dify_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"dashscope_agent_runner_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "工具调用轮数上限",
|
||||
"type": "int",
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "工具调用超时时间(秒)",
|
||||
"type": "int",
|
||||
},
|
||||
},
|
||||
@@ -2064,6 +2110,9 @@ CONFIG_METADATA_2 = {
|
||||
"image_caption": {
|
||||
"type": "bool",
|
||||
},
|
||||
"image_caption_provider_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"image_caption_prompt": {
|
||||
"type": "string",
|
||||
},
|
||||
@@ -2153,34 +2202,87 @@ CONFIG_METADATA_2 = {
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
v4.7.0 之后,name, description, hint 等字段已经实现 i18n 国际化。国际化资源文件位于:
|
||||
|
||||
- dashboard/src/i18n/locales/en-US/features/config-metadata.json
|
||||
- dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
|
||||
|
||||
如果在此文件中添加了新的配置字段,请务必同步更新上述两个国际化资源文件。
|
||||
"""
|
||||
CONFIG_METADATA_3 = {
|
||||
"ai_group": {
|
||||
"name": "AI 配置",
|
||||
"metadata": {
|
||||
"ai": {
|
||||
"description": "模型",
|
||||
"agent_runner": {
|
||||
"description": "Agent 执行方式",
|
||||
"hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify 或 Coze 等第三方 Agent 执行器,不需要修改此节。",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.enable": {
|
||||
"description": "启用大语言模型聊天",
|
||||
"description": "启用",
|
||||
"type": "bool",
|
||||
"hint": "AI 对话总开关",
|
||||
},
|
||||
"provider_settings.agent_runner_type": {
|
||||
"description": "执行器",
|
||||
"type": "string",
|
||||
"options": ["local", "dify", "coze", "dashscope"],
|
||||
"labels": ["内置 Agent", "Dify", "Coze", "阿里云百炼应用"],
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:coze",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "coze",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.dify_agent_runner_provider_id": {
|
||||
"description": "Dify Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:dify",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "dify",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.dashscope_agent_runner_provider_id": {
|
||||
"description": "阿里云百炼应用 Agent 执行器提供商 ID",
|
||||
"type": "string",
|
||||
"_special": "select_agent_runner_provider:dashscope",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "dashscope",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ai": {
|
||||
"description": "模型",
|
||||
"hint": "当使用非内置 Agent 执行器时,默认聊天模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.default_provider_id": {
|
||||
"description": "默认聊天模型",
|
||||
"type": "string",
|
||||
"_special": "select_provider",
|
||||
"hint": "留空时使用第一个模型。",
|
||||
"hint": "留空时使用第一个模型",
|
||||
},
|
||||
"provider_settings.default_image_caption_provider_id": {
|
||||
"description": "默认图片转述模型",
|
||||
"type": "string",
|
||||
"_special": "select_provider",
|
||||
"hint": "留空代表不使用。可用于不支持视觉模态的聊天模型。",
|
||||
"hint": "留空代表不使用,可用于非多模态模型",
|
||||
},
|
||||
"provider_stt_settings.enable": {
|
||||
"description": "启用语音转文本",
|
||||
"type": "bool",
|
||||
"hint": "STT 总开关。",
|
||||
"hint": "STT 总开关",
|
||||
},
|
||||
"provider_stt_settings.provider_id": {
|
||||
"description": "默认语音转文本模型",
|
||||
@@ -2194,12 +2296,11 @@ CONFIG_METADATA_3 = {
|
||||
"provider_tts_settings.enable": {
|
||||
"description": "启用文本转语音",
|
||||
"type": "bool",
|
||||
"hint": "TTS 总开关。当关闭时,会话启用 TTS 也不会生效。",
|
||||
"hint": "TTS 总开关",
|
||||
},
|
||||
"provider_tts_settings.provider_id": {
|
||||
"description": "默认文本转语音模型",
|
||||
"type": "string",
|
||||
"hint": "用户也可使用 /provider 单独选择会话的 TTS 模型。",
|
||||
"_special": "select_provider_tts",
|
||||
"condition": {
|
||||
"provider_tts_settings.enable": True,
|
||||
@@ -2210,6 +2311,9 @@ CONFIG_METADATA_3 = {
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"persona": {
|
||||
"description": "人格",
|
||||
@@ -2221,6 +2325,10 @@ CONFIG_METADATA_3 = {
|
||||
"_special": "select_persona",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"knowledgebase": {
|
||||
"description": "知识库",
|
||||
@@ -2249,6 +2357,10 @@ CONFIG_METADATA_3 = {
|
||||
"hint": "启用后,知识库检索将作为 LLM Tool,由模型自主决定何时调用知识库进行查询。需要模型支持函数调用能力。",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"websearch": {
|
||||
"description": "网页搜索",
|
||||
@@ -2285,6 +2397,10 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"others": {
|
||||
"description": "其他配置",
|
||||
@@ -2293,34 +2409,51 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.display_reasoning_text": {
|
||||
"description": "显示思考内容",
|
||||
"type": "bool",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.identifier": {
|
||||
"description": "用户识别",
|
||||
"type": "bool",
|
||||
"hint": "启用后,会在提示词前包含用户 ID 信息。",
|
||||
},
|
||||
"provider_settings.group_name_display": {
|
||||
"description": "显示群名称",
|
||||
"type": "bool",
|
||||
"hint": "启用后,在支持的平台(aiocqhttp)上会在 prompt 中包含群名称信息。",
|
||||
"hint": "启用后,在支持的平台(OneBot v11)上会在提示词前包含群名称信息。",
|
||||
},
|
||||
"provider_settings.datetime_system_prompt": {
|
||||
"description": "现实世界时间感知",
|
||||
"type": "bool",
|
||||
"hint": "启用后,会在系统提示词中附带当前时间信息。",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.show_tool_use_status": {
|
||||
"description": "输出函数调用状态",
|
||||
"type": "bool",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.max_agent_step": {
|
||||
"description": "工具调用轮数上限",
|
||||
"type": "int",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.tool_call_timeout": {
|
||||
"description": "工具调用超时时间(秒)",
|
||||
"type": "int",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.streaming_response": {
|
||||
"description": "流式回复",
|
||||
"description": "流式输出",
|
||||
"type": "bool",
|
||||
},
|
||||
"provider_settings.unsupported_streaming_strategy": {
|
||||
@@ -2336,17 +2469,23 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.max_context_length": {
|
||||
"description": "最多携带对话轮数",
|
||||
"type": "int",
|
||||
"hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条。-1 为不限制。",
|
||||
"hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条,-1 为不限制",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.dequeue_context_length": {
|
||||
"description": "丢弃对话轮数",
|
||||
"type": "int",
|
||||
"hint": "超出最多携带对话轮数时, 一次丢弃的聊天轮数。",
|
||||
"hint": "超出最多携带对话轮数时, 一次丢弃的聊天轮数",
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
},
|
||||
},
|
||||
"provider_settings.wake_prefix": {
|
||||
"description": "LLM 聊天额外唤醒前缀 ",
|
||||
"type": "string",
|
||||
"hint": "如果唤醒前缀为 `/`, 额外聊天唤醒前缀为 `chat`,则需要 `/chat` 才会触发 LLM 请求。默认为空。",
|
||||
"hint": "如果唤醒前缀为 /, 额外聊天唤醒前缀为 chat,则需要 /chat 才会触发 LLM 请求",
|
||||
},
|
||||
"provider_settings.prompt_prefix": {
|
||||
"description": "用户提示词",
|
||||
@@ -2358,6 +2497,9 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2647,7 +2789,16 @@ CONFIG_METADATA_3 = {
|
||||
"provider_ltm_settings.image_caption": {
|
||||
"description": "自动理解图片",
|
||||
"type": "bool",
|
||||
"hint": "需要设置默认图片转述模型。",
|
||||
"hint": "需要设置群聊图片转述模型。",
|
||||
},
|
||||
"provider_ltm_settings.image_caption_provider_id": {
|
||||
"description": "群聊图片转述模型",
|
||||
"type": "string",
|
||||
"_special": "select_provider",
|
||||
"hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。",
|
||||
"condition": {
|
||||
"provider_ltm_settings.image_caption": True,
|
||||
},
|
||||
},
|
||||
"provider_ltm_settings.active_reply.enable": {
|
||||
"description": "主动回复",
|
||||
|
||||
110
astrbot/core/config/i18n_utils.py
Normal file
110
astrbot/core/config/i18n_utils.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
配置元数据国际化工具
|
||||
|
||||
提供配置元数据的国际化键转换功能
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ConfigMetadataI18n:
|
||||
"""配置元数据国际化转换器"""
|
||||
|
||||
@staticmethod
|
||||
def _get_i18n_key(group: str, section: str, field: str, attr: str) -> str:
|
||||
"""
|
||||
生成国际化键
|
||||
|
||||
Args:
|
||||
group: 配置组,如 'ai_group', 'platform_group'
|
||||
section: 配置节,如 'agent_runner', 'general'
|
||||
field: 字段名,如 'enable', 'default_provider'
|
||||
attr: 属性类型,如 'description', 'hint', 'labels'
|
||||
|
||||
Returns:
|
||||
国际化键,格式如: 'ai_group.agent_runner.enable.description'
|
||||
"""
|
||||
if field:
|
||||
return f"{group}.{section}.{field}.{attr}"
|
||||
else:
|
||||
return f"{group}.{section}.{attr}"
|
||||
|
||||
@staticmethod
|
||||
def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
将配置元数据转换为使用国际化键
|
||||
|
||||
Args:
|
||||
metadata: 原始配置元数据字典
|
||||
|
||||
Returns:
|
||||
使用国际化键的配置元数据字典
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for group_key, group_data in metadata.items():
|
||||
group_result = {
|
||||
"name": f"{group_key}.name",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
for section_key, section_data in group_data.get("metadata", {}).items():
|
||||
section_result = {
|
||||
"description": f"{group_key}.{section_key}.description",
|
||||
"type": section_data.get("type"),
|
||||
}
|
||||
|
||||
# 复制其他属性
|
||||
for key in ["items", "condition", "_special", "invisible"]:
|
||||
if key in section_data:
|
||||
section_result[key] = section_data[key]
|
||||
|
||||
# 处理 hint
|
||||
if "hint" in section_data:
|
||||
section_result["hint"] = f"{group_key}.{section_key}.hint"
|
||||
|
||||
# 处理 items 中的字段
|
||||
if "items" in section_data and isinstance(section_data["items"], dict):
|
||||
items_result = {}
|
||||
for field_key, field_data in section_data["items"].items():
|
||||
# 处理嵌套的点号字段名(如 provider_settings.enable)
|
||||
field_name = field_key
|
||||
|
||||
field_result = {}
|
||||
|
||||
# 复制基本属性
|
||||
for attr in [
|
||||
"type",
|
||||
"condition",
|
||||
"_special",
|
||||
"invisible",
|
||||
"options",
|
||||
]:
|
||||
if attr in field_data:
|
||||
field_result[attr] = field_data[attr]
|
||||
|
||||
# 转换文本属性为国际化键
|
||||
if "description" in field_data:
|
||||
field_result["description"] = (
|
||||
f"{group_key}.{section_key}.{field_name}.description"
|
||||
)
|
||||
|
||||
if "hint" in field_data:
|
||||
field_result["hint"] = (
|
||||
f"{group_key}.{section_key}.{field_name}.hint"
|
||||
)
|
||||
|
||||
if "labels" in field_data:
|
||||
field_result["labels"] = (
|
||||
f"{group_key}.{section_key}.{field_name}.labels"
|
||||
)
|
||||
|
||||
items_result[field_key] = field_result
|
||||
|
||||
section_result["items"] = items_result
|
||||
|
||||
group_result["metadata"][section_key] = section_result
|
||||
|
||||
result[group_key] = group_result
|
||||
|
||||
return result
|
||||
@@ -16,13 +16,12 @@ import time
|
||||
import traceback
|
||||
from asyncio import Queue
|
||||
|
||||
from astrbot.core import LogBroker, logger, sp
|
||||
from astrbot.api import logger, sp
|
||||
from astrbot.core import LogBroker
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.conversation_mgr import ConversationManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_session
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
from astrbot.core.persona_mgr import PersonaManager
|
||||
from astrbot.core.pipeline.scheduler import PipelineContext, PipelineScheduler
|
||||
@@ -34,6 +33,7 @@ from astrbot.core.star.context import Context
|
||||
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
|
||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core.utils.migra_helper import migra
|
||||
|
||||
from . import astrbot_config, html_renderer
|
||||
from .event_bus import EventBus
|
||||
@@ -97,18 +97,16 @@ class AstrBotCoreLifecycle:
|
||||
sp=sp,
|
||||
)
|
||||
|
||||
# 4.5 to 4.6 migration for umop_config_router
|
||||
# apply migration
|
||||
try:
|
||||
await migrate_45_to_46(self.astrbot_config_mgr, self.umop_config_router)
|
||||
await migra(
|
||||
self.db,
|
||||
self.astrbot_config_mgr,
|
||||
self.umop_config_router,
|
||||
self.astrbot_config_mgr,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration from version 4.5 to 4.6 failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migration for webchat session
|
||||
try:
|
||||
await migrate_webchat_session(self.db)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for webchat session failed: {e!s}")
|
||||
logger.error(f"AstrBot migration failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 初始化事件队列
|
||||
|
||||
@@ -25,7 +25,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase):
|
||||
"""
|
||||
# 检查是否已经完成迁移
|
||||
migration_done = await db_helper.get_preference(
|
||||
"global", "global", "migration_done_webchat_session"
|
||||
"global", "global", "migration_done_webchat_session_1"
|
||||
)
|
||||
if migration_done:
|
||||
return
|
||||
@@ -43,7 +43,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase):
|
||||
func.max(PlatformMessageHistory.updated_at).label("latest"),
|
||||
)
|
||||
.where(col(PlatformMessageHistory.platform_id) == "webchat")
|
||||
.where(col(PlatformMessageHistory.sender_id) == "astrbot")
|
||||
.where(col(PlatformMessageHistory.sender_id) != "bot")
|
||||
.group_by(col(PlatformMessageHistory.user_id))
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase):
|
||||
if not webchat_users:
|
||||
logger.info("没有找到需要迁移的 WebChat 数据")
|
||||
await sp.put_async(
|
||||
"global", "global", "migration_done_webchat_session", True
|
||||
"global", "global", "migration_done_webchat_session_1", True
|
||||
)
|
||||
return
|
||||
|
||||
@@ -124,7 +124,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase):
|
||||
logger.info("没有新会话需要迁移")
|
||||
|
||||
# 标记迁移完成
|
||||
await sp.put_async("global", "global", "migration_done_webchat_session", True)
|
||||
await sp.put_async("global", "global", "migration_done_webchat_session_1", True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移过程中发生错误: {e}", exc_info=True)
|
||||
|
||||
@@ -173,7 +173,7 @@ class PlatformSession(SQLModel, table=True):
|
||||
max_length=100,
|
||||
nullable=False,
|
||||
unique=True,
|
||||
default_factory=lambda: f"webchat_{uuid.uuid4()}",
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
)
|
||||
platform_id: str = Field(default="webchat", nullable=False)
|
||||
"""Platform identifier (e.g., 'webchat', 'qq', 'discord')"""
|
||||
|
||||
@@ -794,7 +794,7 @@ class SQLiteDatabase(BaseDatabase):
|
||||
|
||||
await session.execute(
|
||||
update(PlatformSession)
|
||||
.where(col(PlatformSession.session_id == session_id))
|
||||
.where(col(PlatformSession.session_id) == session_id)
|
||||
.values(**values),
|
||||
)
|
||||
|
||||
@@ -805,6 +805,6 @@ class SQLiteDatabase(BaseDatabase):
|
||||
async with session.begin():
|
||||
await session.execute(
|
||||
delete(PlatformSession).where(
|
||||
col(PlatformSession.session_id == session_id),
|
||||
col(PlatformSession.session_id) == session_id,
|
||||
),
|
||||
)
|
||||
|
||||
48
astrbot/core/pipeline/process_stage/method/agent_request.py
Normal file
48
astrbot/core/pipeline/process_stage/method/agent_request.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
|
||||
from ...context import PipelineContext
|
||||
from ..stage import Stage
|
||||
from .agent_sub_stages.internal import InternalAgentSubStage
|
||||
from .agent_sub_stages.third_party import ThirdPartyAgentSubStage
|
||||
|
||||
|
||||
class AgentRequestSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.config = ctx.astrbot_config
|
||||
|
||||
self.bot_wake_prefixs: list[str] = self.config["wake_prefix"]
|
||||
self.prov_wake_prefix: str = self.config["provider_settings"]["wake_prefix"]
|
||||
for bwp in self.bot_wake_prefixs:
|
||||
if self.prov_wake_prefix.startswith(bwp):
|
||||
logger.info(
|
||||
f"识别 LLM 聊天额外唤醒前缀 {self.prov_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。",
|
||||
)
|
||||
self.prov_wake_prefix = self.prov_wake_prefix[len(bwp) :]
|
||||
|
||||
agent_runner_type = self.config["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type == "local":
|
||||
self.agent_sub_stage = InternalAgentSubStage()
|
||||
else:
|
||||
self.agent_sub_stage = ThirdPartyAgentSubStage()
|
||||
await self.agent_sub_stage.initialize(ctx)
|
||||
|
||||
async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]:
|
||||
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
|
||||
logger.debug(
|
||||
"This pipeline does not enable AI capability, skip processing."
|
||||
)
|
||||
return
|
||||
|
||||
if not SessionServiceManager.should_process_llm_request(event):
|
||||
logger.debug(
|
||||
f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing."
|
||||
)
|
||||
return
|
||||
|
||||
async for resp in self.agent_sub_stage.process(event, self.prov_wake_prefix):
|
||||
yield resp
|
||||
@@ -21,27 +21,24 @@ from astrbot.core.provider.entities import (
|
||||
LLMResponse,
|
||||
ProviderRequest,
|
||||
)
|
||||
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
from astrbot.core.star.star_handler import EventType, star_map
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
from astrbot.core.utils.session_lock import session_lock_manager
|
||||
|
||||
from ....astr_agent_context import AgentContextWrapper
|
||||
from ....astr_agent_hooks import MAIN_AGENT_HOOKS
|
||||
from ....astr_agent_run_util import AgentRunner, run_agent
|
||||
from ....astr_agent_tool_exec import FunctionToolExecutor
|
||||
from ...context import PipelineContext, call_event_hook
|
||||
from ..stage import Stage
|
||||
from ..utils import KNOWLEDGE_BASE_QUERY_TOOL, retrieve_knowledge_base
|
||||
from .....astr_agent_context import AgentContextWrapper
|
||||
from .....astr_agent_hooks import MAIN_AGENT_HOOKS
|
||||
from .....astr_agent_run_util import AgentRunner, run_agent
|
||||
from .....astr_agent_tool_exec import FunctionToolExecutor
|
||||
from ....context import PipelineContext, call_event_hook
|
||||
from ...stage import Stage
|
||||
from ...utils import KNOWLEDGE_BASE_QUERY_TOOL, retrieve_knowledge_base
|
||||
|
||||
|
||||
class LLMRequestSubStage(Stage):
|
||||
class InternalAgentSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
conf = ctx.astrbot_config
|
||||
settings = conf["provider_settings"]
|
||||
self.bot_wake_prefixs: list[str] = conf["wake_prefix"] # list
|
||||
self.provider_wake_prefix: str = settings["wake_prefix"] # str
|
||||
self.max_context_length = settings["max_context_length"] # int
|
||||
self.dequeue_context_length: int = min(
|
||||
max(1, settings["dequeue_context_length"]),
|
||||
@@ -59,13 +56,6 @@ class LLMRequestSubStage(Stage):
|
||||
self.show_reasoning = settings.get("display_reasoning_text", False)
|
||||
self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
|
||||
|
||||
for bwp in self.bot_wake_prefixs:
|
||||
if self.provider_wake_prefix.startswith(bwp):
|
||||
logger.info(
|
||||
f"识别 LLM 聊天额外唤醒前缀 {self.provider_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。",
|
||||
)
|
||||
self.provider_wake_prefix = self.provider_wake_prefix[len(bwp) :]
|
||||
|
||||
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
def _select_provider(self, event: AstrMessageEvent):
|
||||
@@ -304,21 +294,10 @@ class LLMRequestSubStage(Stage):
|
||||
return fixed_messages
|
||||
|
||||
async def process(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
_nested: bool = False,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
self, event: AstrMessageEvent, provider_wake_prefix: str
|
||||
) -> AsyncGenerator[None, None]:
|
||||
req: ProviderRequest | None = None
|
||||
|
||||
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
|
||||
logger.debug("未启用 LLM 能力,跳过处理。")
|
||||
return
|
||||
|
||||
# 检查会话级别的LLM启停状态
|
||||
if not SessionServiceManager.should_process_llm_request(event):
|
||||
logger.debug(f"会话 {event.unified_msg_origin} 禁用了 LLM,跳过处理。")
|
||||
return
|
||||
|
||||
provider = self._select_provider(event)
|
||||
if provider is None:
|
||||
return
|
||||
@@ -348,12 +327,12 @@ class LLMRequestSubStage(Stage):
|
||||
req.image_urls = []
|
||||
if sel_model := event.get_extra("selected_model"):
|
||||
req.model = sel_model
|
||||
if self.provider_wake_prefix and not event.message_str.startswith(
|
||||
self.provider_wake_prefix
|
||||
if provider_wake_prefix and not event.message_str.startswith(
|
||||
provider_wake_prefix
|
||||
):
|
||||
return
|
||||
|
||||
req.prompt = event.message_str[len(self.provider_wake_prefix) :]
|
||||
req.prompt = event.message_str[len(provider_wake_prefix) :]
|
||||
# func_tool selection 现在已经转移到 packages/astrbot 插件中进行选择。
|
||||
# req.func_tool = self.ctx.plugin_manager.context.get_llm_tool_manager()
|
||||
for comp in event.message_obj.message:
|
||||
@@ -0,0 +1,205 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from astrbot.core import astrbot_config, logger
|
||||
from astrbot.core.agent.runners.coze.coze_agent_runner import CozeAgentRunner
|
||||
from astrbot.core.agent.runners.dashscope.dashscope_agent_runner import (
|
||||
DashscopeAgentRunner,
|
||||
)
|
||||
from astrbot.core.agent.runners.dify.dify_agent_runner import DifyAgentRunner
|
||||
from astrbot.core.message.components import Image
|
||||
from astrbot.core.message.message_event_result import (
|
||||
MessageChain,
|
||||
MessageEventResult,
|
||||
ResultContentType,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot.core.agent.runners.base import BaseAgentRunner
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.provider.entities import (
|
||||
ProviderRequest,
|
||||
)
|
||||
from astrbot.core.star.star_handler import EventType
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
|
||||
from .....astr_agent_context import AgentContextWrapper, AstrAgentContext
|
||||
from .....astr_agent_hooks import MAIN_AGENT_HOOKS
|
||||
from ....context import PipelineContext, call_event_hook
|
||||
from ...stage import Stage
|
||||
|
||||
AGENT_RUNNER_TYPE_KEY = {
|
||||
"dify": "dify_agent_runner_provider_id",
|
||||
"coze": "coze_agent_runner_provider_id",
|
||||
"dashscope": "dashscope_agent_runner_provider_id",
|
||||
}
|
||||
|
||||
|
||||
async def run_third_party_agent(
|
||||
runner: "BaseAgentRunner",
|
||||
stream_to_general: bool = False,
|
||||
) -> AsyncGenerator[MessageChain | None, None]:
|
||||
"""
|
||||
运行第三方 agent runner 并转换响应格式
|
||||
类似于 run_agent 函数,但专门处理第三方 agent runner
|
||||
"""
|
||||
try:
|
||||
async for resp in runner.step_until_done(max_step=30): # type: ignore[misc]
|
||||
if resp.type == "streaming_delta":
|
||||
if stream_to_general:
|
||||
continue
|
||||
yield resp.data["chain"]
|
||||
elif resp.type == "llm_result":
|
||||
if stream_to_general:
|
||||
yield resp.data["chain"]
|
||||
except Exception as e:
|
||||
logger.error(f"Third party agent runner error: {e}")
|
||||
err_msg = (
|
||||
f"\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n"
|
||||
f"错误信息: {e!s}\n\n请在控制台查看和分享错误详情。\n"
|
||||
)
|
||||
yield MessageChain().message(err_msg)
|
||||
|
||||
|
||||
class ThirdPartyAgentSubStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
self.conf = ctx.astrbot_config
|
||||
self.runner_type = self.conf["provider_settings"]["agent_runner_type"]
|
||||
self.prov_id = self.conf["provider_settings"].get(
|
||||
AGENT_RUNNER_TYPE_KEY.get(self.runner_type, ""),
|
||||
"",
|
||||
)
|
||||
settings = ctx.astrbot_config["provider_settings"]
|
||||
self.streaming_response: bool = settings["streaming_response"]
|
||||
self.unsupported_streaming_strategy: str = settings[
|
||||
"unsupported_streaming_strategy"
|
||||
]
|
||||
|
||||
async def process(
|
||||
self, event: AstrMessageEvent, provider_wake_prefix: str
|
||||
) -> AsyncGenerator[None, None]:
|
||||
req: ProviderRequest | None = None
|
||||
|
||||
if provider_wake_prefix and not event.message_str.startswith(
|
||||
provider_wake_prefix
|
||||
):
|
||||
return
|
||||
|
||||
self.prov_cfg: dict = next(
|
||||
(p for p in astrbot_config["provider"] if p["id"] == self.prov_id),
|
||||
{},
|
||||
)
|
||||
if not self.prov_id:
|
||||
logger.error("没有填写 Agent Runner 提供商 ID,请前往配置页面配置。")
|
||||
return
|
||||
if not self.prov_cfg:
|
||||
logger.error(
|
||||
f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。"
|
||||
)
|
||||
return
|
||||
|
||||
# make provider request
|
||||
req = ProviderRequest()
|
||||
req.session_id = event.unified_msg_origin
|
||||
req.prompt = event.message_str[len(provider_wake_prefix) :]
|
||||
for comp in event.message_obj.message:
|
||||
if isinstance(comp, Image):
|
||||
image_path = await comp.convert_to_base64()
|
||||
req.image_urls.append(image_path)
|
||||
|
||||
if not req.prompt and not req.image_urls:
|
||||
return
|
||||
|
||||
# call event hook
|
||||
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
|
||||
return
|
||||
|
||||
if self.runner_type == "dify":
|
||||
runner = DifyAgentRunner[AstrAgentContext]()
|
||||
elif self.runner_type == "coze":
|
||||
runner = CozeAgentRunner[AstrAgentContext]()
|
||||
elif self.runner_type == "dashscope":
|
||||
runner = DashscopeAgentRunner[AstrAgentContext]()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported third party agent runner type: {self.runner_type}",
|
||||
)
|
||||
|
||||
astr_agent_ctx = AstrAgentContext(
|
||||
context=self.ctx.plugin_manager.context,
|
||||
event=event,
|
||||
)
|
||||
|
||||
streaming_response = self.streaming_response
|
||||
if (enable_streaming := event.get_extra("enable_streaming")) is not None:
|
||||
streaming_response = bool(enable_streaming)
|
||||
|
||||
stream_to_general = (
|
||||
self.unsupported_streaming_strategy == "turn_off"
|
||||
and not event.platform_meta.support_streaming_message
|
||||
)
|
||||
|
||||
await runner.reset(
|
||||
request=req,
|
||||
run_context=AgentContextWrapper(
|
||||
context=astr_agent_ctx,
|
||||
tool_call_timeout=60,
|
||||
),
|
||||
agent_hooks=MAIN_AGENT_HOOKS,
|
||||
provider_config=self.prov_cfg,
|
||||
streaming=streaming_response,
|
||||
)
|
||||
|
||||
if streaming_response and not stream_to_general:
|
||||
# 流式响应
|
||||
event.set_result(
|
||||
MessageEventResult()
|
||||
.set_result_content_type(ResultContentType.STREAMING_RESULT)
|
||||
.set_async_stream(
|
||||
run_third_party_agent(
|
||||
runner,
|
||||
stream_to_general=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
yield
|
||||
if runner.done():
|
||||
final_resp = runner.get_final_llm_resp()
|
||||
if final_resp and final_resp.result_chain:
|
||||
event.set_result(
|
||||
MessageEventResult(
|
||||
chain=final_resp.result_chain.chain or [],
|
||||
result_content_type=ResultContentType.STREAMING_FINISH,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# 非流式响应或转换为普通响应
|
||||
async for _ in run_third_party_agent(
|
||||
runner,
|
||||
stream_to_general=stream_to_general,
|
||||
):
|
||||
yield
|
||||
|
||||
final_resp = runner.get_final_llm_resp()
|
||||
|
||||
if not final_resp or not final_resp.result_chain:
|
||||
logger.warning("Agent Runner 未返回最终结果。")
|
||||
return
|
||||
|
||||
event.set_result(
|
||||
MessageEventResult(
|
||||
chain=final_resp.result_chain.chain or [],
|
||||
result_content_type=ResultContentType.LLM_RESULT,
|
||||
),
|
||||
)
|
||||
yield
|
||||
|
||||
asyncio.create_task(
|
||||
Metric.upload(
|
||||
llm_tick=1,
|
||||
model_name=self.runner_type,
|
||||
provider_type=self.runner_type,
|
||||
),
|
||||
)
|
||||
@@ -24,7 +24,7 @@ class StarRequestSubStage(Stage):
|
||||
async def process(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
) -> AsyncGenerator[None, None]:
|
||||
activated_handlers: list[StarHandlerMetadata] = event.get_extra(
|
||||
"activated_handlers",
|
||||
)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.provider.entities import ProviderRequest
|
||||
from astrbot.core.star.star_handler import StarHandlerMetadata
|
||||
|
||||
from ..context import PipelineContext
|
||||
from ..stage import Stage, register_stage
|
||||
from .method.llm_request import LLMRequestSubStage
|
||||
from .method.agent_request import AgentRequestSubStage
|
||||
from .method.star_request import StarRequestSubStage
|
||||
|
||||
|
||||
@@ -17,9 +16,12 @@ class ProcessStage(Stage):
|
||||
self.ctx = ctx
|
||||
self.config = ctx.astrbot_config
|
||||
self.plugin_manager = ctx.plugin_manager
|
||||
self.llm_request_sub_stage = LLMRequestSubStage()
|
||||
await self.llm_request_sub_stage.initialize(ctx)
|
||||
|
||||
# initialize agent sub stage
|
||||
self.agent_sub_stage = AgentRequestSubStage()
|
||||
await self.agent_sub_stage.initialize(ctx)
|
||||
|
||||
# initialize star request sub stage
|
||||
self.star_request_sub_stage = StarRequestSubStage()
|
||||
await self.star_request_sub_stage.initialize(ctx)
|
||||
|
||||
@@ -39,7 +41,7 @@ class ProcessStage(Stage):
|
||||
# Handler 的 LLM 请求
|
||||
event.set_extra("provider_request", resp)
|
||||
_t = False
|
||||
async for _ in self.llm_request_sub_stage.process(event):
|
||||
async for _ in self.agent_sub_stage.process(event):
|
||||
_t = True
|
||||
yield
|
||||
if not _t:
|
||||
@@ -60,12 +62,5 @@ class ProcessStage(Stage):
|
||||
if (
|
||||
event.get_result() and not event.get_result().is_stopped()
|
||||
) or not event.get_result():
|
||||
# 事件没有终止传播
|
||||
provider = self.ctx.plugin_manager.context.get_using_provider()
|
||||
|
||||
if not provider:
|
||||
logger.info("未找到可用的 LLM 提供商,请先前往配置服务提供商。")
|
||||
return
|
||||
|
||||
async for _ in self.llm_request_sub_stage.process(event):
|
||||
async for _ in self.agent_sub_stage.process(event):
|
||||
yield
|
||||
|
||||
@@ -161,11 +161,21 @@ class ResultDecorateStage(Stage):
|
||||
# 不分段回复
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
split_response = re.findall(
|
||||
self.regex,
|
||||
comp.text,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
try:
|
||||
split_response = re.findall(
|
||||
self.regex,
|
||||
comp.text,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
except re.error:
|
||||
logger.error(
|
||||
f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
|
||||
)
|
||||
split_response = re.findall(
|
||||
r".*?[。?!~…]+|.+$",
|
||||
comp.text,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
if not split_response:
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core import astrbot_config, logger, sp
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
|
||||
@@ -24,6 +24,7 @@ class ProviderManager:
|
||||
db_helper: BaseDatabase,
|
||||
persona_mgr: PersonaManager,
|
||||
):
|
||||
self.reload_lock = asyncio.Lock()
|
||||
self.persona_mgr = persona_mgr
|
||||
self.acm = acm
|
||||
config = acm.confs["default"]
|
||||
@@ -226,6 +227,9 @@ class ProviderManager:
|
||||
|
||||
async def load_provider(self, provider_config: dict):
|
||||
if not provider_config["enable"]:
|
||||
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
|
||||
return
|
||||
if provider_config.get("provider_type", "") == "agent_runner":
|
||||
return
|
||||
|
||||
logger.info(
|
||||
@@ -247,14 +251,6 @@ class ProviderManager:
|
||||
from .sources.anthropic_source import (
|
||||
ProviderAnthropic as ProviderAnthropic,
|
||||
)
|
||||
case "dify":
|
||||
from .sources.dify_source import ProviderDify as ProviderDify
|
||||
case "coze":
|
||||
from .sources.coze_source import ProviderCoze as ProviderCoze
|
||||
case "dashscope":
|
||||
from .sources.dashscope_source import (
|
||||
ProviderDashscope as ProviderDashscope,
|
||||
)
|
||||
case "googlegenai_chat_completion":
|
||||
from .sources.gemini_source import (
|
||||
ProviderGoogleGenAI as ProviderGoogleGenAI,
|
||||
@@ -331,6 +327,10 @@ class ProviderManager:
|
||||
from .sources.xinference_rerank_source import (
|
||||
XinferenceRerankProvider as XinferenceRerankProvider,
|
||||
)
|
||||
case "bailian_rerank":
|
||||
from .sources.bailian_rerank_source import (
|
||||
BailianRerankProvider as BailianRerankProvider,
|
||||
)
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.critical(
|
||||
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。",
|
||||
@@ -436,40 +436,46 @@ class ProviderManager:
|
||||
)
|
||||
|
||||
async def reload(self, provider_config: dict):
|
||||
await self.terminate_provider(provider_config["id"])
|
||||
if provider_config["enable"]:
|
||||
await self.load_provider(provider_config)
|
||||
async with self.reload_lock:
|
||||
await self.terminate_provider(provider_config["id"])
|
||||
if provider_config["enable"]:
|
||||
await self.load_provider(provider_config)
|
||||
|
||||
# 和配置文件保持同步
|
||||
config_ids = [provider["id"] for provider in self.providers_config]
|
||||
logger.debug(f"providers in user's config: {config_ids}")
|
||||
for key in list(self.inst_map.keys()):
|
||||
if key not in config_ids:
|
||||
await self.terminate_provider(key)
|
||||
# 和配置文件保持同步
|
||||
self.providers_config = astrbot_config["provider"]
|
||||
config_ids = [provider["id"] for provider in self.providers_config]
|
||||
logger.info(f"providers in user's config: {config_ids}")
|
||||
for key in list(self.inst_map.keys()):
|
||||
if key not in config_ids:
|
||||
await self.terminate_provider(key)
|
||||
|
||||
if len(self.provider_insts) == 0:
|
||||
self.curr_provider_inst = None
|
||||
elif self.curr_provider_inst is None and len(self.provider_insts) > 0:
|
||||
self.curr_provider_inst = self.provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。",
|
||||
)
|
||||
if len(self.provider_insts) == 0:
|
||||
self.curr_provider_inst = None
|
||||
elif self.curr_provider_inst is None and len(self.provider_insts) > 0:
|
||||
self.curr_provider_inst = self.provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。",
|
||||
)
|
||||
|
||||
if len(self.stt_provider_insts) == 0:
|
||||
self.curr_stt_provider_inst = None
|
||||
elif self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0:
|
||||
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。",
|
||||
)
|
||||
if len(self.stt_provider_insts) == 0:
|
||||
self.curr_stt_provider_inst = None
|
||||
elif (
|
||||
self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0
|
||||
):
|
||||
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。",
|
||||
)
|
||||
|
||||
if len(self.tts_provider_insts) == 0:
|
||||
self.curr_tts_provider_inst = None
|
||||
elif self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0:
|
||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。",
|
||||
)
|
||||
if len(self.tts_provider_insts) == 0:
|
||||
self.curr_tts_provider_inst = None
|
||||
elif (
|
||||
self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0
|
||||
):
|
||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。",
|
||||
)
|
||||
|
||||
def get_insts(self):
|
||||
return self.provider_insts
|
||||
|
||||
@@ -290,7 +290,7 @@ class ProviderAnthropic(Provider):
|
||||
try:
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
except Exception as e:
|
||||
logger.error(f"发生了错误。Provider 配置如下: {model_config}")
|
||||
# logger.error(f"发生了错误。Provider 配置如下: {model_config}")
|
||||
raise e
|
||||
|
||||
return llm_response
|
||||
|
||||
236
astrbot/core/provider/sources/bailian_rerank_source.py
Normal file
236
astrbot/core/provider/sources/bailian_rerank_source.py
Normal file
@@ -0,0 +1,236 @@
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
from ..entities import ProviderType, RerankResult
|
||||
from ..provider import RerankProvider
|
||||
from ..register import register_provider_adapter
|
||||
|
||||
|
||||
class BailianRerankError(Exception):
|
||||
"""百炼重排序服务异常基类"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BailianAPIError(BailianRerankError):
|
||||
"""百炼API返回错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BailianNetworkError(BailianRerankError):
|
||||
"""百炼网络请求错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"bailian_rerank", "阿里云百炼文本排序适配器", provider_type=ProviderType.RERANK
|
||||
)
|
||||
class BailianRerankProvider(RerankProvider):
|
||||
"""阿里云百炼文本重排序适配器."""
|
||||
|
||||
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
self.provider_config = provider_config
|
||||
self.provider_settings = provider_settings
|
||||
|
||||
# API配置
|
||||
self.api_key = provider_config.get("rerank_api_key") or os.getenv(
|
||||
"DASHSCOPE_API_KEY", ""
|
||||
)
|
||||
if not self.api_key:
|
||||
raise ValueError("阿里云百炼 API Key 不能为空。")
|
||||
|
||||
self.model = provider_config.get("rerank_model", "qwen3-rerank")
|
||||
self.timeout = provider_config.get("timeout", 30)
|
||||
self.return_documents = provider_config.get("return_documents", False)
|
||||
self.instruct = provider_config.get("instruct", "")
|
||||
|
||||
self.base_url = provider_config.get(
|
||||
"rerank_api_base",
|
||||
"https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank",
|
||||
)
|
||||
|
||||
# 设置HTTP客户端
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
self.client = aiohttp.ClientSession(
|
||||
headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout)
|
||||
)
|
||||
|
||||
# 设置模型名称
|
||||
self.set_model(self.model)
|
||||
|
||||
logger.info(f"AstrBot 百炼 Rerank 初始化完成。模型: {self.model}")
|
||||
|
||||
def _build_payload(
|
||||
self, query: str, documents: list[str], top_n: int | None
|
||||
) -> dict:
|
||||
"""构建请求载荷
|
||||
|
||||
Args:
|
||||
query: 查询文本
|
||||
documents: 文档列表
|
||||
top_n: 返回前N个结果,如果为None则返回所有结果
|
||||
|
||||
Returns:
|
||||
请求载荷字典
|
||||
"""
|
||||
base = {"model": self.model, "input": {"query": query, "documents": documents}}
|
||||
|
||||
params = {
|
||||
k: v
|
||||
for k, v in [
|
||||
("top_n", top_n if top_n is not None and top_n > 0 else None),
|
||||
("return_documents", True if self.return_documents else None),
|
||||
(
|
||||
"instruct",
|
||||
self.instruct
|
||||
if self.instruct and self.model == "qwen3-rerank"
|
||||
else None,
|
||||
),
|
||||
]
|
||||
if v is not None
|
||||
}
|
||||
|
||||
if params:
|
||||
base["parameters"] = params
|
||||
|
||||
return base
|
||||
|
||||
def _parse_results(self, data: dict) -> list[RerankResult]:
|
||||
"""解析API响应结果
|
||||
|
||||
Args:
|
||||
data: API响应数据
|
||||
|
||||
Returns:
|
||||
重排序结果列表
|
||||
|
||||
Raises:
|
||||
BailianAPIError: API返回错误
|
||||
KeyError: 结果缺少必要字段
|
||||
"""
|
||||
# 检查响应状态
|
||||
if data.get("code", "200") != "200":
|
||||
raise BailianAPIError(
|
||||
f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}"
|
||||
)
|
||||
|
||||
results = data.get("output", {}).get("results", [])
|
||||
if not results:
|
||||
logger.warning(f"百炼 Rerank 返回空结果: {data}")
|
||||
return []
|
||||
|
||||
# 转换为RerankResult对象,使用.get()避免KeyError
|
||||
rerank_results = []
|
||||
for idx, result in enumerate(results):
|
||||
try:
|
||||
index = result.get("index", idx)
|
||||
relevance_score = result.get("relevance_score", 0.0)
|
||||
|
||||
if relevance_score is None:
|
||||
logger.warning(f"结果 {idx} 缺少 relevance_score,使用默认值 0.0")
|
||||
relevance_score = 0.0
|
||||
|
||||
rerank_result = RerankResult(
|
||||
index=index, relevance_score=relevance_score
|
||||
)
|
||||
rerank_results.append(rerank_result)
|
||||
except Exception as e:
|
||||
logger.warning(f"解析结果 {idx} 时出错: {e}, result={result}")
|
||||
continue
|
||||
|
||||
return rerank_results
|
||||
|
||||
def _log_usage(self, data: dict) -> None:
|
||||
"""记录使用量信息
|
||||
|
||||
Args:
|
||||
data: API响应数据
|
||||
"""
|
||||
tokens = data.get("usage", {}).get("total_tokens", 0)
|
||||
if tokens > 0:
|
||||
logger.debug(f"百炼 Rerank 消耗 Token: {tokens}")
|
||||
|
||||
async def rerank(
|
||||
self,
|
||||
query: str,
|
||||
documents: list[str],
|
||||
top_n: int | None = None,
|
||||
) -> list[RerankResult]:
|
||||
"""
|
||||
对文档进行重排序
|
||||
|
||||
Args:
|
||||
query: 查询文本
|
||||
documents: 待排序的文档列表
|
||||
top_n: 返回前N个结果,如果为None则使用配置中的默认值
|
||||
|
||||
Returns:
|
||||
重排序结果列表
|
||||
"""
|
||||
if not documents:
|
||||
logger.warning("文档列表为空,返回空结果")
|
||||
return []
|
||||
|
||||
if not query.strip():
|
||||
logger.warning("查询文本为空,返回空结果")
|
||||
return []
|
||||
|
||||
# 检查限制
|
||||
if len(documents) > 500:
|
||||
logger.warning(
|
||||
f"文档数量({len(documents)})超过限制(500),将截断前500个文档"
|
||||
)
|
||||
documents = documents[:500]
|
||||
|
||||
try:
|
||||
# 构建请求载荷,如果top_n为None则返回所有重排序结果
|
||||
payload = self._build_payload(query, documents, top_n)
|
||||
|
||||
logger.debug(
|
||||
f"百炼 Rerank 请求: query='{query[:50]}...', 文档数量={len(documents)}"
|
||||
)
|
||||
|
||||
# 发送请求
|
||||
async with self.client.post(self.base_url, json=payload) as response:
|
||||
response.raise_for_status()
|
||||
response_data = await response.json()
|
||||
|
||||
# 解析结果并记录使用量
|
||||
results = self._parse_results(response_data)
|
||||
self._log_usage(response_data)
|
||||
|
||||
logger.debug(f"百炼 Rerank 成功返回 {len(results)} 个结果")
|
||||
|
||||
return results
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
error_msg = f"网络请求失败: {e}"
|
||||
logger.error(f"百炼 Rerank 网络请求失败: {e}")
|
||||
raise BailianNetworkError(error_msg) from e
|
||||
except BailianRerankError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"重排序失败: {e}"
|
||||
logger.error(f"百炼 Rerank 处理失败: {e}")
|
||||
raise BailianRerankError(error_msg) from e
|
||||
|
||||
async def terminate(self) -> None:
|
||||
"""关闭HTTP客户端会话."""
|
||||
if self.client:
|
||||
logger.info("关闭 百炼 Rerank 客户端会话")
|
||||
try:
|
||||
await self.client.close()
|
||||
except Exception as e:
|
||||
logger.error(f"关闭 百炼 Rerank 客户端时出错: {e}")
|
||||
finally:
|
||||
self.client = None
|
||||
@@ -1,650 +0,0 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot import logger
|
||||
from astrbot.api.provider import Provider
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.entities import LLMResponse
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .coze_api_client import CozeAPIClient
|
||||
|
||||
|
||||
@register_provider_adapter("coze", "Coze (扣子) 智能体适配器")
|
||||
class ProviderCoze(Provider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config,
|
||||
provider_settings,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
provider_config,
|
||||
provider_settings,
|
||||
)
|
||||
self.api_key = provider_config.get("coze_api_key", "")
|
||||
if not self.api_key:
|
||||
raise Exception("Coze API Key 不能为空。")
|
||||
self.bot_id = provider_config.get("bot_id", "")
|
||||
if not self.bot_id:
|
||||
raise Exception("Coze Bot ID 不能为空。")
|
||||
self.api_base: str = provider_config.get("coze_api_base", "https://api.coze.cn")
|
||||
|
||||
if not isinstance(self.api_base, str) or not self.api_base.startswith(
|
||||
("http://", "https://"),
|
||||
):
|
||||
raise Exception(
|
||||
"Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。",
|
||||
)
|
||||
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
self.auto_save_history = provider_config.get("auto_save_history", True)
|
||||
self.conversation_ids: dict[str, str] = {}
|
||||
self.file_id_cache: dict[str, dict[str, str]] = {}
|
||||
|
||||
# 创建 API 客户端
|
||||
self.api_client = CozeAPIClient(api_key=self.api_key, api_base=self.api_base)
|
||||
|
||||
def _generate_cache_key(self, data: str, is_base64: bool = False) -> str:
|
||||
"""生成统一的缓存键
|
||||
|
||||
Args:
|
||||
data: 图片数据或路径
|
||||
is_base64: 是否是 base64 数据
|
||||
|
||||
Returns:
|
||||
str: 缓存键
|
||||
|
||||
"""
|
||||
try:
|
||||
if is_base64 and data.startswith("data:image/"):
|
||||
try:
|
||||
header, encoded = data.split(",", 1)
|
||||
image_bytes = base64.b64decode(encoded)
|
||||
cache_key = hashlib.md5(image_bytes).hexdigest()
|
||||
return cache_key
|
||||
except Exception:
|
||||
cache_key = hashlib.md5(encoded.encode("utf-8")).hexdigest()
|
||||
return cache_key
|
||||
elif data.startswith(("http://", "https://")):
|
||||
# URL图片,使用URL作为缓存键
|
||||
cache_key = hashlib.md5(data.encode("utf-8")).hexdigest()
|
||||
return cache_key
|
||||
else:
|
||||
clean_path = (
|
||||
data.split("_")[0]
|
||||
if "_" in data and len(data.split("_")) >= 3
|
||||
else data
|
||||
)
|
||||
|
||||
if os.path.exists(clean_path):
|
||||
with open(clean_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
cache_key = hashlib.md5(file_content).hexdigest()
|
||||
return cache_key
|
||||
cache_key = hashlib.md5(clean_path.encode("utf-8")).hexdigest()
|
||||
return cache_key
|
||||
|
||||
except Exception as e:
|
||||
cache_key = hashlib.md5(data.encode("utf-8")).hexdigest()
|
||||
logger.debug(f"[Coze] 异常文件缓存键: {cache_key}, error={e}")
|
||||
return cache_key
|
||||
|
||||
async def _upload_file(
|
||||
self,
|
||||
file_data: bytes,
|
||||
session_id: str | None = None,
|
||||
cache_key: str | None = None,
|
||||
) -> str:
|
||||
"""上传文件到 Coze 并返回 file_id"""
|
||||
# 使用 API 客户端上传文件
|
||||
file_id = await self.api_client.upload_file(file_data)
|
||||
|
||||
# 缓存 file_id
|
||||
if session_id and cache_key:
|
||||
if session_id not in self.file_id_cache:
|
||||
self.file_id_cache[session_id] = {}
|
||||
self.file_id_cache[session_id][cache_key] = file_id
|
||||
logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}")
|
||||
|
||||
return file_id
|
||||
|
||||
async def _download_and_upload_image(
|
||||
self,
|
||||
image_url: str,
|
||||
session_id: str | None = None,
|
||||
) -> str:
|
||||
"""下载图片并上传到 Coze,返回 file_id"""
|
||||
# 计算哈希实现缓存
|
||||
cache_key = self._generate_cache_key(image_url) if session_id else None
|
||||
|
||||
if session_id and cache_key:
|
||||
if session_id not in self.file_id_cache:
|
||||
self.file_id_cache[session_id] = {}
|
||||
|
||||
if cache_key in self.file_id_cache[session_id]:
|
||||
file_id = self.file_id_cache[session_id][cache_key]
|
||||
return file_id
|
||||
|
||||
try:
|
||||
image_data = await self.api_client.download_image(image_url)
|
||||
|
||||
file_id = await self._upload_file(image_data, session_id, cache_key)
|
||||
|
||||
if session_id and cache_key:
|
||||
self.file_id_cache[session_id][cache_key] = file_id
|
||||
|
||||
return file_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理图片失败 {image_url}: {e!s}")
|
||||
raise Exception(f"处理图片失败: {e!s}")
|
||||
|
||||
async def _process_context_images(
|
||||
self,
|
||||
content: str | list,
|
||||
session_id: str,
|
||||
) -> str:
|
||||
"""处理上下文中的图片内容,将 base64 图片上传并替换为 file_id"""
|
||||
try:
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
|
||||
processed_content = []
|
||||
if session_id not in self.file_id_cache:
|
||||
self.file_id_cache[session_id] = {}
|
||||
|
||||
for item in content:
|
||||
if not isinstance(item, dict):
|
||||
processed_content.append(item)
|
||||
continue
|
||||
if item.get("type") == "text":
|
||||
processed_content.append(item)
|
||||
elif item.get("type") == "image_url":
|
||||
# 处理图片逻辑
|
||||
if "file_id" in item:
|
||||
# 已经有 file_id
|
||||
logger.debug(f"[Coze] 图片已有file_id: {item['file_id']}")
|
||||
processed_content.append(item)
|
||||
else:
|
||||
# 获取图片数据
|
||||
image_data = ""
|
||||
if "image_url" in item and isinstance(item["image_url"], dict):
|
||||
image_data = item["image_url"].get("url", "")
|
||||
elif "data" in item:
|
||||
image_data = item.get("data", "")
|
||||
elif "url" in item:
|
||||
image_data = item.get("url", "")
|
||||
|
||||
if not image_data:
|
||||
continue
|
||||
# 计算哈希用于缓存
|
||||
cache_key = self._generate_cache_key(
|
||||
image_data,
|
||||
is_base64=image_data.startswith("data:image/"),
|
||||
)
|
||||
|
||||
# 检查缓存
|
||||
if cache_key in self.file_id_cache[session_id]:
|
||||
file_id = self.file_id_cache[session_id][cache_key]
|
||||
processed_content.append(
|
||||
{"type": "image", "file_id": file_id},
|
||||
)
|
||||
else:
|
||||
# 上传图片并缓存
|
||||
if image_data.startswith("data:image/"):
|
||||
# base64 处理
|
||||
_, encoded = image_data.split(",", 1)
|
||||
image_bytes = base64.b64decode(encoded)
|
||||
file_id = await self._upload_file(
|
||||
image_bytes,
|
||||
session_id,
|
||||
cache_key,
|
||||
)
|
||||
elif image_data.startswith(("http://", "https://")):
|
||||
# URL 图片
|
||||
file_id = await self._download_and_upload_image(
|
||||
image_data,
|
||||
session_id,
|
||||
)
|
||||
# 为URL图片也添加缓存
|
||||
self.file_id_cache[session_id][cache_key] = file_id
|
||||
elif os.path.exists(image_data):
|
||||
# 本地文件
|
||||
with open(image_data, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
file_id = await self._upload_file(
|
||||
image_bytes,
|
||||
session_id,
|
||||
cache_key,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"无法处理的图片格式: {image_data[:50]}...",
|
||||
)
|
||||
continue
|
||||
|
||||
processed_content.append(
|
||||
{"type": "image", "file_id": file_id},
|
||||
)
|
||||
|
||||
result = json.dumps(processed_content, ensure_ascii=False)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"处理上下文图片失败: {e!s}")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
return json.dumps(content, ensure_ascii=False)
|
||||
|
||||
async def text_chat(
|
||||
self,
|
||||
prompt: str,
|
||||
session_id=None,
|
||||
image_urls=None,
|
||||
func_tool=None,
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
"""文本对话, 内部使用流式接口实现非流式
|
||||
|
||||
Args:
|
||||
prompt (str): 用户提示词
|
||||
session_id (str): 会话ID
|
||||
image_urls (List[str]): 图片URL列表
|
||||
func_tool (FuncCall): 函数调用工具(不支持)
|
||||
contexts (List): 上下文列表
|
||||
system_prompt (str): 系统提示语
|
||||
tool_calls_result (ToolCallsResult | List[ToolCallsResult]): 工具调用结果(不支持)
|
||||
model (str): 模型名称(不支持)
|
||||
|
||||
Returns:
|
||||
LLMResponse: LLM响应对象
|
||||
|
||||
"""
|
||||
accumulated_content = ""
|
||||
final_response = None
|
||||
|
||||
async for llm_response in self.text_chat_stream(
|
||||
prompt=prompt,
|
||||
session_id=session_id,
|
||||
image_urls=image_urls,
|
||||
func_tool=func_tool,
|
||||
contexts=contexts,
|
||||
system_prompt=system_prompt,
|
||||
tool_calls_result=tool_calls_result,
|
||||
model=model,
|
||||
**kwargs,
|
||||
):
|
||||
if llm_response.is_chunk:
|
||||
if llm_response.completion_text:
|
||||
accumulated_content += llm_response.completion_text
|
||||
else:
|
||||
final_response = llm_response
|
||||
|
||||
if final_response:
|
||||
return final_response
|
||||
|
||||
if accumulated_content:
|
||||
chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
|
||||
return LLMResponse(role="assistant", result_chain=chain)
|
||||
return LLMResponse(role="assistant", completion_text="")
|
||||
|
||||
async def text_chat_stream(
|
||||
self,
|
||||
prompt: str,
|
||||
session_id=None,
|
||||
image_urls=None,
|
||||
func_tool=None,
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式对话接口"""
|
||||
# 用户ID参数(参考文档, 可以自定义)
|
||||
user_id = session_id or kwargs.get("user", "default_user")
|
||||
|
||||
# 获取或创建会话ID
|
||||
conversation_id = self.conversation_ids.get(user_id)
|
||||
|
||||
# 构建消息
|
||||
additional_messages = []
|
||||
|
||||
if system_prompt:
|
||||
if not self.auto_save_history or not conversation_id:
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_prompt,
|
||||
"content_type": "text",
|
||||
},
|
||||
)
|
||||
|
||||
contexts = self._ensure_message_to_dicts(contexts)
|
||||
if not self.auto_save_history and contexts:
|
||||
# 如果关闭了自动保存历史,传入上下文
|
||||
for ctx in contexts:
|
||||
if isinstance(ctx, dict) and "role" in ctx and "content" in ctx:
|
||||
content = ctx["content"]
|
||||
content_type = ctx.get("content_type", "text")
|
||||
|
||||
# 处理可能包含图片的上下文
|
||||
if (
|
||||
content_type == "object_string"
|
||||
or (isinstance(content, str) and content.startswith("["))
|
||||
or (
|
||||
isinstance(content, list)
|
||||
and any(
|
||||
isinstance(item, dict)
|
||||
and item.get("type") == "image_url"
|
||||
for item in content
|
||||
)
|
||||
)
|
||||
):
|
||||
processed_content = await self._process_context_images(
|
||||
content,
|
||||
user_id,
|
||||
)
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": ctx["role"],
|
||||
"content": processed_content,
|
||||
"content_type": "object_string",
|
||||
},
|
||||
)
|
||||
else:
|
||||
# 纯文本
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": ctx["role"],
|
||||
"content": (
|
||||
content
|
||||
if isinstance(content, str)
|
||||
else json.dumps(content, ensure_ascii=False)
|
||||
),
|
||||
"content_type": "text",
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.info(f"[Coze] 跳过格式不正确的上下文: {ctx}")
|
||||
|
||||
if prompt or image_urls:
|
||||
if image_urls:
|
||||
# 多模态
|
||||
object_string_content = []
|
||||
if prompt:
|
||||
object_string_content.append({"type": "text", "text": prompt})
|
||||
|
||||
for url in image_urls:
|
||||
try:
|
||||
if url.startswith(("http://", "https://")):
|
||||
# 网络图片
|
||||
file_id = await self._download_and_upload_image(
|
||||
url,
|
||||
user_id,
|
||||
)
|
||||
else:
|
||||
# 本地文件或 base64
|
||||
if url.startswith("data:image/"):
|
||||
# base64
|
||||
_, encoded = url.split(",", 1)
|
||||
image_data = base64.b64decode(encoded)
|
||||
cache_key = self._generate_cache_key(
|
||||
url,
|
||||
is_base64=True,
|
||||
)
|
||||
file_id = await self._upload_file(
|
||||
image_data,
|
||||
user_id,
|
||||
cache_key,
|
||||
)
|
||||
# 本地文件
|
||||
elif os.path.exists(url):
|
||||
with open(url, "rb") as f:
|
||||
image_data = f.read()
|
||||
# 用文件路径和修改时间来缓存
|
||||
file_stat = os.stat(url)
|
||||
cache_key = self._generate_cache_key(
|
||||
f"{url}_{file_stat.st_mtime}_{file_stat.st_size}",
|
||||
is_base64=False,
|
||||
)
|
||||
file_id = await self._upload_file(
|
||||
image_data,
|
||||
user_id,
|
||||
cache_key,
|
||||
)
|
||||
else:
|
||||
logger.warning(f"图片文件不存在: {url}")
|
||||
continue
|
||||
|
||||
object_string_content.append(
|
||||
{
|
||||
"type": "image",
|
||||
"file_id": file_id,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"处理图片失败 {url}: {e!s}")
|
||||
continue
|
||||
|
||||
if object_string_content:
|
||||
content = json.dumps(object_string_content, ensure_ascii=False)
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
"content_type": "object_string",
|
||||
},
|
||||
)
|
||||
# 纯文本
|
||||
elif prompt:
|
||||
additional_messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
"content_type": "text",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
accumulated_content = ""
|
||||
message_started = False
|
||||
|
||||
async for chunk in self.api_client.chat_messages(
|
||||
bot_id=self.bot_id,
|
||||
user_id=user_id,
|
||||
additional_messages=additional_messages,
|
||||
conversation_id=conversation_id,
|
||||
auto_save_history=self.auto_save_history,
|
||||
stream=True,
|
||||
timeout=self.timeout,
|
||||
):
|
||||
event_type = chunk.get("event")
|
||||
data = chunk.get("data", {})
|
||||
|
||||
if event_type == "conversation.chat.created":
|
||||
if isinstance(data, dict) and "conversation_id" in data:
|
||||
self.conversation_ids[user_id] = data["conversation_id"]
|
||||
|
||||
elif event_type == "conversation.message.delta":
|
||||
if isinstance(data, dict):
|
||||
content = data.get("content", "")
|
||||
if not content and "delta" in data:
|
||||
content = data["delta"].get("content", "")
|
||||
if not content and "text" in data:
|
||||
content = data.get("text", "")
|
||||
|
||||
if content:
|
||||
message_started = True
|
||||
accumulated_content += content
|
||||
yield LLMResponse(
|
||||
role="assistant",
|
||||
completion_text=content,
|
||||
is_chunk=True,
|
||||
)
|
||||
|
||||
elif event_type == "conversation.message.completed":
|
||||
if isinstance(data, dict):
|
||||
msg_type = data.get("type")
|
||||
if msg_type == "answer" and data.get("role") == "assistant":
|
||||
final_content = data.get("content", "")
|
||||
if not accumulated_content and final_content:
|
||||
chain = MessageChain(chain=[Comp.Plain(final_content)])
|
||||
yield LLMResponse(
|
||||
role="assistant",
|
||||
result_chain=chain,
|
||||
is_chunk=False,
|
||||
)
|
||||
|
||||
elif event_type == "conversation.chat.completed":
|
||||
if accumulated_content:
|
||||
chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
|
||||
yield LLMResponse(
|
||||
role="assistant",
|
||||
result_chain=chain,
|
||||
is_chunk=False,
|
||||
)
|
||||
break
|
||||
|
||||
elif event_type == "done":
|
||||
break
|
||||
|
||||
elif event_type == "error":
|
||||
error_msg = (
|
||||
data.get("message", "未知错误")
|
||||
if isinstance(data, dict)
|
||||
else str(data)
|
||||
)
|
||||
logger.error(f"Coze 流式响应错误: {error_msg}")
|
||||
yield LLMResponse(
|
||||
role="err",
|
||||
completion_text=f"Coze 错误: {error_msg}",
|
||||
is_chunk=False,
|
||||
)
|
||||
break
|
||||
|
||||
if not message_started and not accumulated_content:
|
||||
yield LLMResponse(
|
||||
role="assistant",
|
||||
completion_text="LLM 未响应任何内容。",
|
||||
is_chunk=False,
|
||||
)
|
||||
elif message_started and accumulated_content:
|
||||
chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
|
||||
yield LLMResponse(
|
||||
role="assistant",
|
||||
result_chain=chain,
|
||||
is_chunk=False,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Coze 流式请求失败: {e!s}")
|
||||
yield LLMResponse(
|
||||
role="err",
|
||||
completion_text=f"Coze 流式请求失败: {e!s}",
|
||||
is_chunk=False,
|
||||
)
|
||||
|
||||
async def forget(self, session_id: str):
|
||||
"""清空指定会话的上下文"""
|
||||
user_id = session_id
|
||||
conversation_id = self.conversation_ids.get(user_id)
|
||||
|
||||
if user_id in self.file_id_cache:
|
||||
self.file_id_cache.pop(user_id, None)
|
||||
|
||||
if not conversation_id:
|
||||
return True
|
||||
|
||||
try:
|
||||
response = await self.api_client.clear_context(conversation_id)
|
||||
|
||||
if "code" in response and response["code"] == 0:
|
||||
self.conversation_ids.pop(user_id, None)
|
||||
return True
|
||||
logger.warning(f"清空 Coze 会话上下文失败: {response}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清空 Coze 会话失败: {e!s}")
|
||||
return False
|
||||
|
||||
async def get_current_key(self):
|
||||
"""获取当前API Key"""
|
||||
return self.api_key
|
||||
|
||||
async def set_key(self, key: str):
|
||||
"""设置新的API Key"""
|
||||
raise NotImplementedError("Coze 适配器不支持设置 API Key。")
|
||||
|
||||
async def get_models(self):
|
||||
"""获取可用模型列表"""
|
||||
return [f"bot_{self.bot_id}"]
|
||||
|
||||
def get_model(self):
|
||||
"""获取当前模型"""
|
||||
return f"bot_{self.bot_id}"
|
||||
|
||||
def set_model(self, model: str):
|
||||
"""设置模型(在Coze中是Bot ID)"""
|
||||
if model.startswith("bot_"):
|
||||
self.bot_id = model[4:]
|
||||
else:
|
||||
self.bot_id = model
|
||||
|
||||
async def get_human_readable_context(
|
||||
self,
|
||||
session_id: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
):
|
||||
"""获取人类可读的上下文历史"""
|
||||
user_id = session_id
|
||||
conversation_id = self.conversation_ids.get(user_id)
|
||||
|
||||
if not conversation_id:
|
||||
return []
|
||||
|
||||
try:
|
||||
data = await self.api_client.get_message_list(
|
||||
conversation_id=conversation_id,
|
||||
order="desc",
|
||||
limit=page_size,
|
||||
offset=(page - 1) * page_size,
|
||||
)
|
||||
|
||||
if data.get("code") != 0:
|
||||
logger.warning(f"获取 Coze 消息历史失败: {data}")
|
||||
return []
|
||||
|
||||
messages = data.get("data", {}).get("messages", [])
|
||||
|
||||
readable_history = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
msg_type = msg.get("type", "")
|
||||
|
||||
if role == "user":
|
||||
readable_history.append(f"用户: {content}")
|
||||
elif role == "assistant" and msg_type == "answer":
|
||||
readable_history.append(f"助手: {content}")
|
||||
|
||||
return readable_history
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取 Coze 消息历史失败: {e!s}")
|
||||
return []
|
||||
|
||||
async def terminate(self):
|
||||
"""清理资源"""
|
||||
await self.api_client.close()
|
||||
@@ -1,207 +0,0 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import re
|
||||
|
||||
from dashscope import Application
|
||||
from dashscope.app.application_response import ApplicationResponse
|
||||
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
|
||||
from .. import Provider
|
||||
from ..entities import LLMResponse
|
||||
from ..register import register_provider_adapter
|
||||
from .openai_source import ProviderOpenAIOfficial
|
||||
|
||||
|
||||
@register_provider_adapter("dashscope", "Dashscope APP 适配器。")
|
||||
class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config: dict,
|
||||
provider_settings: dict,
|
||||
) -> None:
|
||||
Provider.__init__(
|
||||
self,
|
||||
provider_config,
|
||||
provider_settings,
|
||||
)
|
||||
self.api_key = provider_config.get("dashscope_api_key", "")
|
||||
if not self.api_key:
|
||||
raise Exception("阿里云百炼 API Key 不能为空。")
|
||||
self.app_id = provider_config.get("dashscope_app_id", "")
|
||||
if not self.app_id:
|
||||
raise Exception("阿里云百炼 APP ID 不能为空。")
|
||||
self.dashscope_app_type = provider_config.get("dashscope_app_type", "")
|
||||
if not self.dashscope_app_type:
|
||||
raise Exception("阿里云百炼 APP 类型不能为空。")
|
||||
self.model_name = "dashscope"
|
||||
self.variables: dict = provider_config.get("variables", {})
|
||||
self.rag_options: dict = provider_config.get("rag_options", {})
|
||||
self.output_reference = self.rag_options.get("output_reference", False)
|
||||
self.rag_options = self.rag_options.copy()
|
||||
self.rag_options.pop("output_reference", None)
|
||||
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
|
||||
def has_rag_options(self):
|
||||
"""判断是否有 RAG 选项
|
||||
|
||||
Returns:
|
||||
bool: 是否有 RAG 选项
|
||||
|
||||
"""
|
||||
if self.rag_options and (
|
||||
len(self.rag_options.get("pipeline_ids", [])) > 0
|
||||
or len(self.rag_options.get("file_ids", [])) > 0
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def text_chat(
|
||||
self,
|
||||
prompt: str,
|
||||
session_id=None,
|
||||
image_urls=None,
|
||||
func_tool=None,
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if image_urls is None:
|
||||
image_urls = []
|
||||
if contexts is None:
|
||||
contexts = []
|
||||
# 获得会话变量
|
||||
payload_vars = self.variables.copy()
|
||||
# 动态变量
|
||||
session_var = await sp.session_get(session_id, "session_variables", default={})
|
||||
payload_vars.update(session_var)
|
||||
|
||||
if (
|
||||
self.dashscope_app_type in ["agent", "dialog-workflow"]
|
||||
and not self.has_rag_options()
|
||||
):
|
||||
# 支持多轮对话的
|
||||
new_record = {"role": "user", "content": prompt}
|
||||
if image_urls:
|
||||
logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。")
|
||||
contexts_no_img = await self._remove_image_from_context(contexts)
|
||||
context_query = [*contexts_no_img, new_record]
|
||||
if system_prompt:
|
||||
context_query.insert(0, {"role": "system", "content": system_prompt})
|
||||
for part in context_query:
|
||||
if "_no_save" in part:
|
||||
del part["_no_save"]
|
||||
# 调用阿里云百炼 API
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"api_key": self.api_key,
|
||||
"messages": context_query,
|
||||
"biz_params": payload_vars or None,
|
||||
}
|
||||
partial = functools.partial(
|
||||
Application.call,
|
||||
**payload,
|
||||
)
|
||||
response = await asyncio.get_event_loop().run_in_executor(None, partial)
|
||||
else:
|
||||
# 不支持多轮对话的
|
||||
# 调用阿里云百炼 API
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"prompt": prompt,
|
||||
"api_key": self.api_key,
|
||||
"biz_params": payload_vars or None,
|
||||
}
|
||||
if self.rag_options:
|
||||
payload["rag_options"] = self.rag_options
|
||||
partial = functools.partial(
|
||||
Application.call,
|
||||
**payload,
|
||||
)
|
||||
response = await asyncio.get_event_loop().run_in_executor(None, partial)
|
||||
|
||||
assert isinstance(response, ApplicationResponse)
|
||||
|
||||
logger.debug(f"dashscope resp: {response}")
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(
|
||||
f"阿里云百炼请求失败: request_id={response.request_id}, code={response.status_code}, message={response.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code",
|
||||
)
|
||||
return LLMResponse(
|
||||
role="err",
|
||||
result_chain=MessageChain().message(
|
||||
f"阿里云百炼请求失败: message={response.message} code={response.status_code}",
|
||||
),
|
||||
)
|
||||
|
||||
output_text = response.output.get("text", "") or ""
|
||||
# RAG 引用脚标格式化
|
||||
output_text = re.sub(r"<ref>\[(\d+)\]</ref>", r"[\1]", output_text)
|
||||
if self.output_reference and response.output.get("doc_references", None):
|
||||
ref_parts = []
|
||||
for ref in response.output.get("doc_references", []) or []:
|
||||
ref_title = (
|
||||
ref.get("title", "")
|
||||
if ref.get("title")
|
||||
else ref.get("doc_name", "")
|
||||
)
|
||||
ref_parts.append(f"{ref['index_id']}. {ref_title}\n")
|
||||
ref_str = "".join(ref_parts)
|
||||
output_text += f"\n\n回答来源:\n{ref_str}"
|
||||
|
||||
llm_response = LLMResponse("assistant")
|
||||
llm_response.result_chain = MessageChain().message(output_text)
|
||||
|
||||
return llm_response
|
||||
|
||||
async def text_chat_stream(
|
||||
self,
|
||||
prompt,
|
||||
session_id=None,
|
||||
image_urls=...,
|
||||
func_tool=None,
|
||||
contexts=...,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
):
|
||||
# raise NotImplementedError("This method is not implemented yet.")
|
||||
# 调用 text_chat 模拟流式
|
||||
llm_response = await self.text_chat(
|
||||
prompt=prompt,
|
||||
session_id=session_id,
|
||||
image_urls=image_urls,
|
||||
func_tool=func_tool,
|
||||
contexts=contexts,
|
||||
system_prompt=system_prompt,
|
||||
tool_calls_result=tool_calls_result,
|
||||
)
|
||||
llm_response.is_chunk = True
|
||||
yield llm_response
|
||||
llm_response.is_chunk = False
|
||||
yield llm_response
|
||||
|
||||
async def forget(self, session_id):
|
||||
return True
|
||||
|
||||
async def get_current_key(self):
|
||||
return self.api_key
|
||||
|
||||
async def set_key(self, key):
|
||||
raise Exception("阿里云百炼 适配器不支持设置 API Key。")
|
||||
|
||||
async def get_models(self):
|
||||
return [self.get_model()]
|
||||
|
||||
async def get_human_readable_context(self, session_id, page, page_size):
|
||||
raise Exception("暂不支持获得 阿里云百炼 的历史消息记录。")
|
||||
|
||||
async def terminate(self):
|
||||
pass
|
||||
@@ -1,285 +0,0 @@
|
||||
import os
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.dify_api_client import DifyAPIClient
|
||||
from astrbot.core.utils.io import download_file, download_image_by_url
|
||||
|
||||
from .. import Provider
|
||||
from ..entities import LLMResponse
|
||||
from ..register import register_provider_adapter
|
||||
|
||||
|
||||
@register_provider_adapter("dify", "Dify APP 适配器。")
|
||||
class ProviderDify(Provider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config,
|
||||
provider_settings,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
provider_config,
|
||||
provider_settings,
|
||||
)
|
||||
self.api_key = provider_config.get("dify_api_key", "")
|
||||
if not self.api_key:
|
||||
raise Exception("Dify API Key 不能为空。")
|
||||
api_base = provider_config.get("dify_api_base", "https://api.dify.ai/v1")
|
||||
self.api_type = provider_config.get("dify_api_type", "")
|
||||
if not self.api_type:
|
||||
raise Exception("Dify API 类型不能为空。")
|
||||
self.model_name = "dify"
|
||||
self.workflow_output_key = provider_config.get(
|
||||
"dify_workflow_output_key",
|
||||
"astrbot_wf_output",
|
||||
)
|
||||
self.dify_query_input_key = provider_config.get(
|
||||
"dify_query_input_key",
|
||||
"astrbot_text_query",
|
||||
)
|
||||
if not self.dify_query_input_key:
|
||||
self.dify_query_input_key = "astrbot_text_query"
|
||||
if not self.workflow_output_key:
|
||||
self.workflow_output_key = "astrbot_wf_output"
|
||||
self.variables: dict = provider_config.get("variables", {})
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
self.conversation_ids = {}
|
||||
"""记录当前 session id 的对话 ID"""
|
||||
|
||||
self.api_client = DifyAPIClient(self.api_key, api_base)
|
||||
|
||||
async def text_chat(
|
||||
self,
|
||||
prompt: str,
|
||||
session_id=None,
|
||||
image_urls=None,
|
||||
func_tool=None,
|
||||
contexts=None,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
if image_urls is None:
|
||||
image_urls = []
|
||||
result = ""
|
||||
session_id = session_id or kwargs.get("user") or "unknown" # 1734
|
||||
conversation_id = self.conversation_ids.get(session_id, "")
|
||||
|
||||
files_payload = []
|
||||
for image_url in image_urls:
|
||||
image_path = (
|
||||
await download_image_by_url(image_url)
|
||||
if image_url.startswith("http")
|
||||
else image_url
|
||||
)
|
||||
file_response = await self.api_client.file_upload(
|
||||
image_path,
|
||||
user=session_id,
|
||||
)
|
||||
logger.debug(f"Dify 上传图片响应:{file_response}")
|
||||
if "id" not in file_response:
|
||||
logger.warning(
|
||||
f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。",
|
||||
)
|
||||
continue
|
||||
files_payload.append(
|
||||
{
|
||||
"type": "image",
|
||||
"transfer_method": "local_file",
|
||||
"upload_file_id": file_response["id"],
|
||||
},
|
||||
)
|
||||
|
||||
# 获得会话变量
|
||||
payload_vars = self.variables.copy()
|
||||
# 动态变量
|
||||
session_var = await sp.session_get(session_id, "session_variables", default={})
|
||||
payload_vars.update(session_var)
|
||||
payload_vars["system_prompt"] = system_prompt
|
||||
|
||||
try:
|
||||
match self.api_type:
|
||||
case "chat" | "agent" | "chatflow":
|
||||
if not prompt:
|
||||
prompt = "请描述这张图片。"
|
||||
|
||||
async for chunk in self.api_client.chat_messages(
|
||||
inputs={
|
||||
**payload_vars,
|
||||
},
|
||||
query=prompt,
|
||||
user=session_id,
|
||||
conversation_id=conversation_id,
|
||||
files=files_payload,
|
||||
timeout=self.timeout,
|
||||
):
|
||||
logger.debug(f"dify resp chunk: {chunk}")
|
||||
if (
|
||||
chunk["event"] == "message"
|
||||
or chunk["event"] == "agent_message"
|
||||
):
|
||||
result += chunk["answer"]
|
||||
if not conversation_id:
|
||||
self.conversation_ids[session_id] = chunk[
|
||||
"conversation_id"
|
||||
]
|
||||
conversation_id = chunk["conversation_id"]
|
||||
elif chunk["event"] == "message_end":
|
||||
logger.debug("Dify message end")
|
||||
break
|
||||
elif chunk["event"] == "error":
|
||||
logger.error(f"Dify 出现错误:{chunk}")
|
||||
raise Exception(
|
||||
f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}",
|
||||
)
|
||||
|
||||
case "workflow":
|
||||
async for chunk in self.api_client.workflow_run(
|
||||
inputs={
|
||||
self.dify_query_input_key: prompt,
|
||||
"astrbot_session_id": session_id,
|
||||
**payload_vars,
|
||||
},
|
||||
user=session_id,
|
||||
files=files_payload,
|
||||
timeout=self.timeout,
|
||||
):
|
||||
match chunk["event"]:
|
||||
case "workflow_started":
|
||||
logger.info(
|
||||
f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。",
|
||||
)
|
||||
case "node_finished":
|
||||
logger.debug(
|
||||
f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。",
|
||||
)
|
||||
case "workflow_finished":
|
||||
logger.info(
|
||||
f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束",
|
||||
)
|
||||
logger.debug(f"Dify 工作流结果:{chunk}")
|
||||
if chunk["data"]["error"]:
|
||||
logger.error(
|
||||
f"Dify 工作流出现错误:{chunk['data']['error']}",
|
||||
)
|
||||
raise Exception(
|
||||
f"Dify 工作流出现错误:{chunk['data']['error']}",
|
||||
)
|
||||
if (
|
||||
self.workflow_output_key
|
||||
not in chunk["data"]["outputs"]
|
||||
):
|
||||
raise Exception(
|
||||
f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}",
|
||||
)
|
||||
result = chunk
|
||||
case _:
|
||||
raise Exception(f"未知的 Dify API 类型:{self.api_type}")
|
||||
except Exception as e:
|
||||
logger.error(f"Dify 请求失败:{e!s}")
|
||||
return LLMResponse(role="err", completion_text=f"Dify 请求失败:{e!s}")
|
||||
|
||||
if not result:
|
||||
logger.warning("Dify 请求结果为空,请查看 Debug 日志。")
|
||||
|
||||
chain = await self.parse_dify_result(result)
|
||||
|
||||
return LLMResponse(role="assistant", result_chain=chain)
|
||||
|
||||
async def text_chat_stream(
|
||||
self,
|
||||
prompt,
|
||||
session_id=None,
|
||||
image_urls=...,
|
||||
func_tool=None,
|
||||
contexts=...,
|
||||
system_prompt=None,
|
||||
tool_calls_result=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
):
|
||||
# raise NotImplementedError("This method is not implemented yet.")
|
||||
# 调用 text_chat 模拟流式
|
||||
llm_response = await self.text_chat(
|
||||
prompt=prompt,
|
||||
session_id=session_id,
|
||||
image_urls=image_urls,
|
||||
func_tool=func_tool,
|
||||
contexts=contexts,
|
||||
system_prompt=system_prompt,
|
||||
tool_calls_result=tool_calls_result,
|
||||
)
|
||||
llm_response.is_chunk = True
|
||||
yield llm_response
|
||||
llm_response.is_chunk = False
|
||||
yield llm_response
|
||||
|
||||
async def parse_dify_result(self, chunk: dict | str) -> MessageChain:
|
||||
if isinstance(chunk, str):
|
||||
# Chat
|
||||
return MessageChain(chain=[Comp.Plain(chunk)])
|
||||
|
||||
async def parse_file(item: dict):
|
||||
match item["type"]:
|
||||
case "image":
|
||||
return Comp.Image(file=item["url"], url=item["url"])
|
||||
case "audio":
|
||||
# 仅支持 wav
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
path = os.path.join(temp_dir, f"{item['filename']}.wav")
|
||||
await download_file(item["url"], path)
|
||||
return Comp.Image(file=item["url"], url=item["url"])
|
||||
case "video":
|
||||
return Comp.Video(file=item["url"])
|
||||
case _:
|
||||
return Comp.File(name=item["filename"], file=item["url"])
|
||||
|
||||
output = chunk["data"]["outputs"][self.workflow_output_key]
|
||||
chains = []
|
||||
if isinstance(output, str):
|
||||
# 纯文本输出
|
||||
chains.append(Comp.Plain(output))
|
||||
elif isinstance(output, list):
|
||||
# 主要适配 Dify 的 HTTP 请求结点的多模态输出
|
||||
for item in output:
|
||||
# handle Array[File]
|
||||
if (
|
||||
not isinstance(item, dict)
|
||||
or item.get("dify_model_identity", "") != "__dify__file__"
|
||||
):
|
||||
chains.append(Comp.Plain(str(output)))
|
||||
break
|
||||
else:
|
||||
chains.append(Comp.Plain(str(output)))
|
||||
|
||||
# scan file
|
||||
files = chunk["data"].get("files", [])
|
||||
for item in files:
|
||||
comp = await parse_file(item)
|
||||
chains.append(comp)
|
||||
|
||||
return MessageChain(chain=chains)
|
||||
|
||||
async def forget(self, session_id):
|
||||
self.conversation_ids[session_id] = ""
|
||||
return True
|
||||
|
||||
async def get_current_key(self):
|
||||
return self.api_key
|
||||
|
||||
async def set_key(self, key):
|
||||
raise Exception("Dify 适配器不支持设置 API Key。")
|
||||
|
||||
async def get_models(self):
|
||||
return [self.get_model()]
|
||||
|
||||
async def get_human_readable_context(self, session_id, page, page_size):
|
||||
raise Exception("暂不支持获得 Dify 的历史消息记录。")
|
||||
|
||||
async def terminate(self):
|
||||
await self.api_client.close()
|
||||
@@ -111,9 +111,9 @@ class ProviderGoogleGenAI(Provider):
|
||||
f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...",
|
||||
)
|
||||
raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
|
||||
logger.error(
|
||||
f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}",
|
||||
)
|
||||
# logger.error(
|
||||
# f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}",
|
||||
# )
|
||||
raise e
|
||||
|
||||
async def _prepare_query_config(
|
||||
|
||||
@@ -433,7 +433,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
)
|
||||
payloads.pop("tools", None)
|
||||
return False, chosen_key, available_api_keys, payloads, context_query, None
|
||||
logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}")
|
||||
# logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}")
|
||||
|
||||
if "tool" in str(e).lower() and "support" in str(e).lower():
|
||||
logger.error("疑似该模型不支持函数调用工具调用。请输入 /tool off_all")
|
||||
|
||||
@@ -171,110 +171,3 @@ class SessionServiceManager:
|
||||
|
||||
# 如果没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_session_status(session_id: str, enabled: bool) -> None:
|
||||
"""设置会话的整体启停状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
enabled: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
session_config = (
|
||||
sp.get("session_service_config", {}, scope="umo", scope_id=session_id) or {}
|
||||
)
|
||||
session_config["session_enabled"] = enabled
|
||||
sp.put(
|
||||
"session_service_config",
|
||||
session_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的整体状态已更新为: {'启用' if enabled else '禁用'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def should_process_session_request(event: AstrMessageEvent) -> bool:
|
||||
"""检查是否应该处理会话请求(会话整体启停检查)
|
||||
|
||||
Args:
|
||||
event: 消息事件
|
||||
|
||||
Returns:
|
||||
bool: True表示应该处理,False表示跳过
|
||||
|
||||
"""
|
||||
session_id = event.unified_msg_origin
|
||||
return SessionServiceManager.is_session_enabled(session_id)
|
||||
|
||||
# =============================================================================
|
||||
# 会话命名相关方法
|
||||
# =============================================================================
|
||||
|
||||
@staticmethod
|
||||
def get_session_custom_name(session_id: str) -> str | None:
|
||||
"""获取会话的自定义名称
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
str: 自定义名称,如果没有设置则返回None
|
||||
|
||||
"""
|
||||
session_services = sp.get(
|
||||
"session_service_config",
|
||||
{},
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
return session_services.get("custom_name")
|
||||
|
||||
@staticmethod
|
||||
def set_session_custom_name(session_id: str, custom_name: str) -> None:
|
||||
"""设置会话的自定义名称
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
custom_name: 自定义名称,可以为空字符串来清除名称
|
||||
|
||||
"""
|
||||
session_config = (
|
||||
sp.get("session_service_config", {}, scope="umo", scope_id=session_id) or {}
|
||||
)
|
||||
if custom_name and custom_name.strip():
|
||||
session_config["custom_name"] = custom_name.strip()
|
||||
else:
|
||||
# 如果传入空名称,则删除自定义名称
|
||||
session_config.pop("custom_name", None)
|
||||
sp.put(
|
||||
"session_service_config",
|
||||
session_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的自定义名称已更新为: {custom_name.strip() if custom_name and custom_name.strip() else '已清除'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_session_display_name(session_id: str) -> str:
|
||||
"""获取会话的显示名称(优先显示自定义名称,否则显示原始session_id的最后一段)
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
str: 显示名称
|
||||
|
||||
"""
|
||||
custom_name = SessionServiceManager.get_session_custom_name(session_id)
|
||||
if custom_name:
|
||||
return custom_name
|
||||
|
||||
# 如果没有自定义名称,返回session_id的最后一段
|
||||
return session_id.split(":")[2] if session_id.count(":") >= 2 else session_id
|
||||
|
||||
@@ -42,87 +42,6 @@ class SessionPluginManager:
|
||||
# 如果都没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_plugin_status_for_session(
|
||||
session_id: str,
|
||||
plugin_name: str,
|
||||
enabled: bool,
|
||||
) -> None:
|
||||
"""设置插件在指定会话中的启停状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
plugin_name: 插件名称
|
||||
enabled: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
# 获取当前配置
|
||||
session_plugin_config = sp.get(
|
||||
"session_plugin_config",
|
||||
{},
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
if session_id not in session_plugin_config:
|
||||
session_plugin_config[session_id] = {
|
||||
"enabled_plugins": [],
|
||||
"disabled_plugins": [],
|
||||
}
|
||||
|
||||
session_config = session_plugin_config[session_id]
|
||||
enabled_plugins = session_config.get("enabled_plugins", [])
|
||||
disabled_plugins = session_config.get("disabled_plugins", [])
|
||||
|
||||
if enabled:
|
||||
# 启用插件
|
||||
if plugin_name in disabled_plugins:
|
||||
disabled_plugins.remove(plugin_name)
|
||||
if plugin_name not in enabled_plugins:
|
||||
enabled_plugins.append(plugin_name)
|
||||
else:
|
||||
# 禁用插件
|
||||
if plugin_name in enabled_plugins:
|
||||
enabled_plugins.remove(plugin_name)
|
||||
if plugin_name not in disabled_plugins:
|
||||
disabled_plugins.append(plugin_name)
|
||||
|
||||
# 保存配置
|
||||
session_config["enabled_plugins"] = enabled_plugins
|
||||
session_config["disabled_plugins"] = disabled_plugins
|
||||
session_plugin_config[session_id] = session_config
|
||||
sp.put(
|
||||
"session_plugin_config",
|
||||
session_plugin_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的插件 {plugin_name} 状态已更新为: {'启用' if enabled else '禁用'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_session_plugin_config(session_id: str) -> dict[str, list[str]]:
|
||||
"""获取指定会话的插件配置
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: 包含enabled_plugins和disabled_plugins的字典
|
||||
|
||||
"""
|
||||
session_plugin_config = sp.get(
|
||||
"session_plugin_config",
|
||||
{},
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
return session_plugin_config.get(
|
||||
session_id,
|
||||
{"enabled_plugins": [], "disabled_plugins": []},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def filter_handlers_by_session(event: AstrMessageEvent, handlers: list) -> list:
|
||||
"""根据会话配置过滤处理器列表
|
||||
|
||||
@@ -85,3 +85,22 @@ class UmopConfigRouter:
|
||||
|
||||
self.umop_to_conf_id[umo] = conf_id
|
||||
await self.sp.global_put("umop_config_routing", self.umop_to_conf_id)
|
||||
|
||||
async def delete_route(self, umo: str):
|
||||
"""删除一条路由
|
||||
|
||||
Args:
|
||||
umo (str): 需要删除的 UMO 字符串
|
||||
|
||||
Raises:
|
||||
ValueError: 当 umo 格式不正确时抛出
|
||||
"""
|
||||
|
||||
if not isinstance(umo, str) or len(umo.split(":")) != 3:
|
||||
raise ValueError(
|
||||
"umop must be a string in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all",
|
||||
)
|
||||
|
||||
if umo in self.umop_to_conf_id:
|
||||
del self.umop_to_conf_id[umo]
|
||||
await self.sp.global_put("umop_config_routing", self.umop_to_conf_id)
|
||||
|
||||
73
astrbot/core/utils/migra_helper.py
Normal file
73
astrbot/core/utils/migra_helper.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import traceback
|
||||
|
||||
from astrbot.core import astrbot_config, logger
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfig, AstrBotConfigManager
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_session
|
||||
|
||||
|
||||
def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
|
||||
"""
|
||||
Migra agent runner configs from provider configs.
|
||||
"""
|
||||
try:
|
||||
default_prov_id = conf["provider_settings"]["default_provider_id"]
|
||||
if default_prov_id in ids_map:
|
||||
conf["provider_settings"]["default_provider_id"] = ""
|
||||
p = ids_map[default_prov_id]
|
||||
if p["type"] == "dify":
|
||||
conf["provider_settings"]["dify_agent_runner_provider_id"] = p["id"]
|
||||
conf["provider_settings"]["agent_runner_type"] = "dify"
|
||||
elif p["type"] == "coze":
|
||||
conf["provider_settings"]["coze_agent_runner_provider_id"] = p["id"]
|
||||
conf["provider_settings"]["agent_runner_type"] = "coze"
|
||||
elif p["type"] == "dashscope":
|
||||
conf["provider_settings"]["dashscope_agent_runner_provider_id"] = p[
|
||||
"id"
|
||||
]
|
||||
conf["provider_settings"]["agent_runner_type"] = "dashscope"
|
||||
conf.save_config()
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for third party agent runner configs failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
async def migra(
|
||||
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
||||
) -> None:
|
||||
"""
|
||||
Stores the migration logic here.
|
||||
btw, i really don't like migration :(
|
||||
"""
|
||||
# 4.5 to 4.6 migration for umop_config_router
|
||||
try:
|
||||
await migrate_45_to_46(astrbot_config_mgr, umop_config_router)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration from version 4.5 to 4.6 failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migration for webchat session
|
||||
try:
|
||||
await migrate_webchat_session(db)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration for webchat session failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# migra third party agent runner configs
|
||||
_c = False
|
||||
providers = astrbot_config["provider"]
|
||||
ids_map = {}
|
||||
for prov in providers:
|
||||
type_ = prov.get("type")
|
||||
if type_ in ["dify", "coze", "dashscope"]:
|
||||
prov["provider_type"] = "agent_runner"
|
||||
ids_map[prov["id"]] = {
|
||||
"type": type_,
|
||||
"id": prov["id"],
|
||||
}
|
||||
_c = True
|
||||
if _c:
|
||||
astrbot_config.save_config()
|
||||
|
||||
for conf in acm.confs.values():
|
||||
_migra_agent_runner_configs(conf, ids_map)
|
||||
@@ -40,9 +40,6 @@ class SharedPreferences:
|
||||
else:
|
||||
ret = default
|
||||
return ret
|
||||
raise ValueError(
|
||||
"scope_id and key cannot be None when getting a specific preference.",
|
||||
)
|
||||
|
||||
async def range_get_async(
|
||||
self,
|
||||
@@ -56,30 +53,6 @@ class SharedPreferences:
|
||||
ret = await self.db_helper.get_preferences(scope, scope_id, key)
|
||||
return ret
|
||||
|
||||
@overload
|
||||
async def session_get(
|
||||
self,
|
||||
umo: None,
|
||||
key: str,
|
||||
default: Any = None,
|
||||
) -> list[Preference]: ...
|
||||
|
||||
@overload
|
||||
async def session_get(
|
||||
self,
|
||||
umo: str,
|
||||
key: None,
|
||||
default: Any = None,
|
||||
) -> list[Preference]: ...
|
||||
|
||||
@overload
|
||||
async def session_get(
|
||||
self,
|
||||
umo: None,
|
||||
key: None,
|
||||
default: Any = None,
|
||||
) -> list[Preference]: ...
|
||||
|
||||
async def session_get(
|
||||
self,
|
||||
umo: str | None,
|
||||
@@ -88,7 +61,7 @@ class SharedPreferences:
|
||||
) -> _VT | list[Preference]:
|
||||
"""获取会话范围的偏好设置
|
||||
|
||||
Note: 当 scope_id 或者 key 为 None,时,返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。
|
||||
Note: 当 umo 或者 key 为 None,时,返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。
|
||||
"""
|
||||
if umo is None or key is None:
|
||||
return await self.range_get_async("umo", umo, key)
|
||||
|
||||
@@ -56,6 +56,7 @@ class ChatRoute(Route):
|
||||
self.conv_mgr = core_lifecycle.conversation_manager
|
||||
self.platform_history_mgr = core_lifecycle.platform_message_history_manager
|
||||
self.db = db
|
||||
self.umop_config_router = core_lifecycle.umop_config_router
|
||||
|
||||
self.running_convs: dict[str, bool] = {}
|
||||
|
||||
@@ -266,7 +267,8 @@ class ChatRoute(Route):
|
||||
return Response().error("Permission denied").__dict__
|
||||
|
||||
# 删除该会话下的所有对话
|
||||
unified_msg_origin = f"{session.platform_id}:FriendMessage:{session.platform_id}!{username}!{session_id}"
|
||||
message_type = "GroupMessage" if session.is_group else "FriendMessage"
|
||||
unified_msg_origin = f"{session.platform_id}:{message_type}:{session.platform_id}!{username}!{session_id}"
|
||||
await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin)
|
||||
|
||||
# 删除消息历史
|
||||
@@ -276,6 +278,16 @@ class ChatRoute(Route):
|
||||
offset_sec=99999999,
|
||||
)
|
||||
|
||||
# 删除与会话关联的配置路由
|
||||
try:
|
||||
await self.umop_config_router.delete_route(unified_msg_origin)
|
||||
except ValueError as exc:
|
||||
logger.warning(
|
||||
"Failed to delete UMO route %s during session cleanup: %s",
|
||||
unified_msg_origin,
|
||||
exc,
|
||||
)
|
||||
|
||||
# 清理队列(仅对 webchat)
|
||||
if session.platform_id == "webchat":
|
||||
webchat_queue_mgr.remove_queues(session_id)
|
||||
|
||||
@@ -14,6 +14,7 @@ from astrbot.core.config.default import (
|
||||
DEFAULT_CONFIG,
|
||||
DEFAULT_VALUE_MAP,
|
||||
)
|
||||
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.platform.register import platform_cls_map, platform_registry
|
||||
from astrbot.core.provider import Provider
|
||||
@@ -133,7 +134,9 @@ def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False)
|
||||
is_core,
|
||||
)
|
||||
else:
|
||||
errors, post_config = validate_config(post_config, config.schema, is_core)
|
||||
errors, post_config = validate_config(
|
||||
post_config, getattr(config, "schema", {}), is_core
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
logger.warning(f"验证配置时出现异常: {e}")
|
||||
@@ -247,11 +250,8 @@ class ConfigRoute(Route):
|
||||
|
||||
async def get_default_config(self):
|
||||
"""获取默认配置文件"""
|
||||
return (
|
||||
Response()
|
||||
.ok({"config": DEFAULT_CONFIG, "metadata": CONFIG_METADATA_3})
|
||||
.__dict__
|
||||
)
|
||||
metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3)
|
||||
return Response().ok({"config": DEFAULT_CONFIG, "metadata": metadata}).__dict__
|
||||
|
||||
async def get_abconf_list(self):
|
||||
"""获取所有 AstrBot 配置文件的列表"""
|
||||
@@ -282,17 +282,15 @@ class ConfigRoute(Route):
|
||||
try:
|
||||
if system_config:
|
||||
abconf = self.acm.confs["default"]
|
||||
return (
|
||||
Response()
|
||||
.ok({"config": abconf, "metadata": CONFIG_METADATA_3_SYSTEM})
|
||||
.__dict__
|
||||
metadata = ConfigMetadataI18n.convert_to_i18n_keys(
|
||||
CONFIG_METADATA_3_SYSTEM
|
||||
)
|
||||
return Response().ok({"config": abconf, "metadata": metadata}).__dict__
|
||||
if abconf_id is None:
|
||||
raise ValueError("abconf_id cannot be None")
|
||||
abconf = self.acm.confs[abconf_id]
|
||||
return (
|
||||
Response()
|
||||
.ok({"config": abconf, "metadata": CONFIG_METADATA_3})
|
||||
.__dict__
|
||||
)
|
||||
metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3)
|
||||
return Response().ok({"config": abconf, "metadata": metadata}).__dict__
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
@@ -598,9 +596,15 @@ class ConfigRoute(Route):
|
||||
return Response().error("缺少参数 provider_id").__dict__
|
||||
|
||||
prov_mgr = self.core_lifecycle.provider_manager
|
||||
provider: Provider | None = prov_mgr.inst_map.get(provider_id, None)
|
||||
provider = prov_mgr.inst_map.get(provider_id, None)
|
||||
if not provider:
|
||||
return Response().error(f"未找到 ID 为 {provider_id} 的提供商").__dict__
|
||||
if not isinstance(provider, Provider):
|
||||
return (
|
||||
Response()
|
||||
.error(f"提供商 {provider_id} 类型不支持获取模型列表")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
try:
|
||||
models = await provider.get_models()
|
||||
|
||||
@@ -60,10 +60,6 @@ class KnowledgeBaseRoute(Route):
|
||||
# "/kb/media/delete": ("POST", self.delete_media),
|
||||
# 检索
|
||||
"/kb/retrieve": ("POST", self.retrieve),
|
||||
# 会话知识库配置
|
||||
"/kb/session/config/get": ("GET", self.get_session_kb_config),
|
||||
"/kb/session/config/set": ("POST", self.set_session_kb_config),
|
||||
"/kb/session/config/delete": ("POST", self.delete_session_kb_config),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
@@ -920,158 +916,6 @@ class KnowledgeBaseRoute(Route):
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"检索失败: {e!s}").__dict__
|
||||
|
||||
# ===== 会话知识库配置 API =====
|
||||
|
||||
async def get_session_kb_config(self):
|
||||
"""获取会话的知识库配置
|
||||
|
||||
Query 参数:
|
||||
- session_id: 会话 ID (必填)
|
||||
|
||||
返回:
|
||||
- kb_ids: 知识库 ID 列表
|
||||
- top_k: 返回结果数量
|
||||
- enable_rerank: 是否启用重排序
|
||||
"""
|
||||
try:
|
||||
from astrbot.core import sp
|
||||
|
||||
session_id = request.args.get("session_id")
|
||||
|
||||
if not session_id:
|
||||
return Response().error("缺少参数 session_id").__dict__
|
||||
|
||||
# 从 SharedPreferences 获取配置
|
||||
config = await sp.session_get(session_id, "kb_config", default={})
|
||||
|
||||
logger.debug(f"[KB配置] 读取到配置: session_id={session_id}")
|
||||
|
||||
# 如果没有配置,返回默认值
|
||||
if not config:
|
||||
config = {"kb_ids": [], "top_k": 5, "enable_rerank": True}
|
||||
|
||||
return Response().ok(config).__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KB配置] 获取配置时出错: {e}", exc_info=True)
|
||||
return Response().error(f"获取会话知识库配置失败: {e!s}").__dict__
|
||||
|
||||
async def set_session_kb_config(self):
|
||||
"""设置会话的知识库配置
|
||||
|
||||
Body:
|
||||
- scope: 配置范围 (目前只支持 "session")
|
||||
- scope_id: 会话 ID (必填)
|
||||
- kb_ids: 知识库 ID 列表 (必填)
|
||||
- top_k: 返回结果数量 (可选, 默认 5)
|
||||
- enable_rerank: 是否启用重排序 (可选, 默认 true)
|
||||
"""
|
||||
try:
|
||||
from astrbot.core import sp
|
||||
|
||||
data = await request.json
|
||||
|
||||
scope = data.get("scope")
|
||||
scope_id = data.get("scope_id")
|
||||
kb_ids = data.get("kb_ids", [])
|
||||
top_k = data.get("top_k", 5)
|
||||
enable_rerank = data.get("enable_rerank", True)
|
||||
|
||||
# 验证参数
|
||||
if scope != "session":
|
||||
return Response().error("目前仅支持 session 范围的配置").__dict__
|
||||
|
||||
if not scope_id:
|
||||
return Response().error("缺少参数 scope_id").__dict__
|
||||
|
||||
if not isinstance(kb_ids, list):
|
||||
return Response().error("kb_ids 必须是列表").__dict__
|
||||
|
||||
# 验证知识库是否存在
|
||||
kb_mgr = self._get_kb_manager()
|
||||
invalid_ids = []
|
||||
valid_ids = []
|
||||
for kb_id in kb_ids:
|
||||
kb_helper = await kb_mgr.get_kb(kb_id)
|
||||
if kb_helper:
|
||||
valid_ids.append(kb_id)
|
||||
else:
|
||||
invalid_ids.append(kb_id)
|
||||
logger.warning(f"[KB配置] 知识库不存在: {kb_id}")
|
||||
|
||||
if invalid_ids:
|
||||
logger.warning(f"[KB配置] 以下知识库ID无效: {invalid_ids}")
|
||||
|
||||
# 允许保存空列表,表示明确不使用任何知识库
|
||||
if kb_ids and not valid_ids:
|
||||
# 只有当用户提供了 kb_ids 但全部无效时才报错
|
||||
return Response().error(f"所有提供的知识库ID都无效: {kb_ids}").__dict__
|
||||
|
||||
# 如果 kb_ids 为空列表,表示用户想清空配置
|
||||
if not kb_ids:
|
||||
valid_ids = []
|
||||
|
||||
# 构建配置对象(只保存有效的ID)
|
||||
config = {
|
||||
"kb_ids": valid_ids,
|
||||
"top_k": top_k,
|
||||
"enable_rerank": enable_rerank,
|
||||
}
|
||||
|
||||
# 保存到 SharedPreferences
|
||||
await sp.session_put(scope_id, "kb_config", config)
|
||||
|
||||
# 立即验证是否保存成功
|
||||
verify_config = await sp.session_get(scope_id, "kb_config", default={})
|
||||
|
||||
if verify_config == config:
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{"valid_ids": valid_ids, "invalid_ids": invalid_ids},
|
||||
"保存知识库配置成功",
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
logger.error("[KB配置] 配置保存失败,验证不匹配")
|
||||
return Response().error("配置保存失败").__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KB配置] 设置配置时出错: {e}", exc_info=True)
|
||||
return Response().error(f"设置会话知识库配置失败: {e!s}").__dict__
|
||||
|
||||
async def delete_session_kb_config(self):
|
||||
"""删除会话的知识库配置
|
||||
|
||||
Body:
|
||||
- scope: 配置范围 (目前只支持 "session")
|
||||
- scope_id: 会话 ID (必填)
|
||||
"""
|
||||
try:
|
||||
from astrbot.core import sp
|
||||
|
||||
data = await request.json
|
||||
|
||||
scope = data.get("scope")
|
||||
scope_id = data.get("scope_id")
|
||||
|
||||
# 验证参数
|
||||
if scope != "session":
|
||||
return Response().error("目前仅支持 session 范围的配置").__dict__
|
||||
|
||||
if not scope_id:
|
||||
return Response().error("缺少参数 scope_id").__dict__
|
||||
|
||||
# 从 SharedPreferences 删除配置
|
||||
await sp.session_remove(scope_id, "kb_config")
|
||||
|
||||
return Response().ok(message="删除知识库配置成功").__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除会话知识库配置失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"删除会话知识库配置失败: {e!s}").__dict__
|
||||
|
||||
async def upload_document_from_url(self):
|
||||
"""从 URL 上传文档
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
29
changelogs/v4.6.1.md
Normal file
29
changelogs/v4.6.1.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## What's Changed
|
||||
|
||||
**hot fix of v4.6.0**
|
||||
|
||||
fix(core.db): 修复升级后 webchat 相关对话数据未正确迁移的问题 ([#3745](https://github.com/AstrBotDevs/AstrBot/issues/3745))
|
||||
|
||||
---
|
||||
|
||||
1. 新增: 支持 gemini-3 系列的 thought signature ([#3698](https://github.com/AstrBotDevs/AstrBot/issues/3698))
|
||||
2. 新增: 支持知识库的 Agentic 检索功能 ([#3667](https://github.com/AstrBotDevs/AstrBot/issues/3667))
|
||||
3. 新增: 为知识库添加 URL 文档解析器 ([#3622](https://github.com/AstrBotDevs/AstrBot/issues/3622))
|
||||
4. 修复(core.platform): 修复启用多个企业微信智能机器人适配器时消息混乱的问题 ([#3693](https://github.com/AstrBotDevs/AstrBot/issues/3693))
|
||||
5. 修复: MCP Server 连接成功一段时间后,调用 mcp 工具时可能出现 `anyio.ClosedResourceError` 错误 ([#3700](https://github.com/AstrBotDevs/AstrBot/issues/3700))
|
||||
6. 新增(chat): 重构聊天组件结构并添加新功能 ([#3701](https://github.com/AstrBotDevs/AstrBot/issues/3701))
|
||||
7. 修复(dashboard.i18n): 完善缺失的英文国际化键值 ([#3699](https://github.com/AstrBotDevs/AstrBot/issues/3699))
|
||||
8. 重构: 实现 WebChat 会话管理及从版本 4.6 迁移到 4.7
|
||||
9. 持续集成(docker-build): 每日构建 Nightly 版本 Docker 镜像 ([#3120](https://github.com/AstrBotDevs/AstrBot/issues/3120))
|
||||
|
||||
---
|
||||
|
||||
1. feat: add supports for gemini-3 series thought signature ([#3698](https://github.com/AstrBotDevs/AstrBot/issues/3698))
|
||||
2. feat: supports knowledge base agentic search ([#3667](https://github.com/AstrBotDevs/AstrBot/issues/3667))
|
||||
3. feat: Add URL document parser for knowledge base ([#3622](https://github.com/AstrBotDevs/AstrBot/issues/3622))
|
||||
4. fix(core.platform): fix message mix-up issue when enabling multiple WeCom AI Bot adapters ([#3693](https://github.com/AstrBotDevs/AstrBot/issues/3693))
|
||||
5. fix: fix `anyio.ClosedResourceError` that may occur when calling mcp tools after a period of successful connection to MCP Server ([#3700](https://github.com/AstrBotDevs/AstrBot/issues/3700))
|
||||
6. feat(chat): refactor chat component structure and add new features ([#3701](https://github.com/AstrBotDevs/AstrBot/issues/3701))
|
||||
7. fix(dashboard.i18n): complete the missing i18n keys for en([#3699](https://github.com/AstrBotDevs/AstrBot/issues/3699))
|
||||
8. refactor: Implement WebChat session management and migration from version 4.6 to 4.7
|
||||
9. ci(docker-build): build nightly image everyday ([#3120](https://github.com/AstrBotDevs/AstrBot/issues/3120))
|
||||
18
changelogs/v4.7.0.md
Normal file
18
changelogs/v4.7.0.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## What's Changed
|
||||
|
||||
重构:
|
||||
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界
|
||||
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
|
||||
|
||||
优化:
|
||||
- Dify、阿里云百炼应用支持流式输出
|
||||
- 防止分段回复正则表达式解析错误导致消息不发送
|
||||
- 群聊上下文感知记录 At 信息
|
||||
- 优化模型提供商页面的测试提供商功能
|
||||
|
||||
新增:
|
||||
- 支持在配置文件页面快速测试对话
|
||||
- 为配置文件配置项内容添加国际化支持
|
||||
|
||||
修复:
|
||||
- 在更新 MCP Server 配置后,MCP 无法正常重启的问题
|
||||
22
changelogs/v4.7.1.md
Normal file
22
changelogs/v4.7.1.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复了自定义规则页面无法设置插件和知识库的规则的问题
|
||||
|
||||
---
|
||||
|
||||
重构:
|
||||
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界。详见:[Agent 执行器](https://docs.astrbot.app/use/agent-runner.html)
|
||||
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
|
||||
|
||||
优化:
|
||||
- Dify、阿里云百炼应用支持流式输出
|
||||
- 防止分段回复正则表达式解析错误导致消息不发送
|
||||
- 群聊上下文感知记录 At 信息
|
||||
- 优化模型提供商页面的测试提供商功能
|
||||
|
||||
新增:
|
||||
- 支持在配置文件页面快速测试对话
|
||||
- 为配置文件配置项内容添加国际化支持
|
||||
|
||||
修复:
|
||||
- 在更新 MCP Server 配置后,MCP 无法正常重启的问题
|
||||
25
changelogs/v4.7.2.md
Normal file
25
changelogs/v4.7.2.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复使用非默认配置文件情况下时,第三方 Agent Runner (Dify、Coze、阿里云百炼应用等)无法正常工作的问题
|
||||
2. 修复当“聊天模型”未设置,并且模型提供商中仅有 Agent Runner 时,无法正常使用 Agent Runner 的问题
|
||||
3. 修复部分情况下报错 `pydantic_core._pydantic_core.ValidationError: 1 validation error for Message content` 的问题
|
||||
4. 新增群聊模式下的专用图片转述模型配置 ([#3822](https://github.com/AstrBotDevs/AstrBot/issues/3822))
|
||||
|
||||
---
|
||||
|
||||
重构:
|
||||
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界。详见:[Agent 执行器](https://docs.astrbot.app/use/agent-runner.html)
|
||||
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
|
||||
|
||||
优化:
|
||||
- Dify、阿里云百炼应用支持流式输出
|
||||
- 防止分段回复正则表达式解析错误导致消息不发送
|
||||
- 群聊上下文感知记录 At 信息
|
||||
- 优化模型提供商页面的测试提供商功能
|
||||
|
||||
新增:
|
||||
- 支持在配置文件页面快速测试对话
|
||||
- 为配置文件配置项内容添加国际化支持
|
||||
|
||||
修复:
|
||||
- 在更新 MCP Server 配置后,MCP 无法正常重启的问题
|
||||
BIN
dashboard/src/assets/images/plugin_icon.png
Normal file
BIN
dashboard/src/assets/images/plugin_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -84,9 +84,11 @@
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:disabled="isStreaming || isConvRunning"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
|
||||
@@ -11,7 +11,14 @@
|
||||
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 8px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0px 12px;">
|
||||
<div style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px;">
|
||||
<ProviderModelSelector ref="providerModelSelectorRef" />
|
||||
<ConfigSelector
|
||||
:session-id="sessionId || null"
|
||||
:platform-id="sessionPlatformId"
|
||||
:is-group="sessionIsGroup"
|
||||
:initial-config-id="props.configId"
|
||||
@config-changed="handleConfigChange"
|
||||
/>
|
||||
<ProviderModelSelector v-if="showProviderSelector" ref="providerModelSelectorRef" />
|
||||
|
||||
<v-tooltip :text="enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled')" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
@@ -58,9 +65,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import ProviderModelSelector from './ProviderModelSelector.vue';
|
||||
import ConfigSelector from './ConfigSelector.vue';
|
||||
import type { Session } from '@/composables/useSessions';
|
||||
|
||||
interface Props {
|
||||
prompt: string;
|
||||
@@ -69,9 +78,16 @@ interface Props {
|
||||
disabled: boolean;
|
||||
enableStreaming: boolean;
|
||||
isRecording: boolean;
|
||||
sessionId?: string | null;
|
||||
currentSession?: Session | null;
|
||||
configId?: string | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
sessionId: null,
|
||||
currentSession: null,
|
||||
configId: null
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:prompt': [value: string];
|
||||
@@ -90,12 +106,16 @@ const { tm } = useModuleI18n('features/chat');
|
||||
const inputField = ref<HTMLTextAreaElement | null>(null);
|
||||
const imageInputRef = ref<HTMLInputElement | null>(null);
|
||||
const providerModelSelectorRef = ref<InstanceType<typeof ProviderModelSelector> | null>(null);
|
||||
const showProviderSelector = ref(true);
|
||||
|
||||
const localPrompt = computed({
|
||||
get: () => props.prompt,
|
||||
set: (value) => emit('update:prompt', value)
|
||||
});
|
||||
|
||||
const sessionPlatformId = computed(() => props.currentSession?.platform_id || 'webchat');
|
||||
const sessionIsGroup = computed(() => Boolean(props.currentSession?.is_group));
|
||||
|
||||
const canSend = computed(() => {
|
||||
return (props.prompt && props.prompt.trim()) || props.stagedImagesUrl.length > 0 || props.stagedAudioUrl;
|
||||
});
|
||||
@@ -168,7 +188,16 @@ function handleRecordClick() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfigChange(payload: { configId: string; agentRunnerType: string }) {
|
||||
const runnerType = (payload.agentRunnerType || '').toLowerCase();
|
||||
const isInternal = runnerType === 'internal' || runnerType === 'local';
|
||||
showProviderSelector.value = isInternal;
|
||||
}
|
||||
|
||||
function getCurrentSelection() {
|
||||
if (!showProviderSelector.value) {
|
||||
return null;
|
||||
}
|
||||
return providerModelSelectorRef.value?.getCurrentSelection();
|
||||
}
|
||||
|
||||
|
||||
313
dashboard/src/components/chat/ConfigSelector.vue
Normal file
313
dashboard/src/components/chat/ConfigSelector.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip text="选择用于当前会话的配置文件" location="top">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-chip
|
||||
v-bind="tooltipProps"
|
||||
class="text-none config-chip"
|
||||
variant="tonal"
|
||||
size="x-small"
|
||||
rounded="lg"
|
||||
@click="openDialog"
|
||||
:disabled="loadingConfigs || saving"
|
||||
>
|
||||
<v-icon start size="14">mdi-cog</v-icon>
|
||||
{{ selectedConfigLabel }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="480" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<span>选择配置文件</span>
|
||||
<v-btn icon variant="text" @click="closeDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-if="loadingConfigs" class="text-center py-6">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<v-list v-else class="config-list" density="comfortable">
|
||||
<v-list-item
|
||||
v-for="config in configOptions"
|
||||
:key="config.id"
|
||||
:active="tempSelectedConfig === config.id"
|
||||
rounded="lg"
|
||||
variant="text"
|
||||
@click="tempSelectedConfig = config.id"
|
||||
>
|
||||
<v-list-item-title>{{ config.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey">
|
||||
{{ config.id }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-icon v-if="tempSelectedConfig === config.id" color="primary">mdi-check</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<div v-if="configOptions.length === 0" class="text-center text-body-2 text-medium-emphasis">
|
||||
暂无可选配置,请先在配置页创建。
|
||||
</div>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="closeDialog">取消</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection"
|
||||
:disabled="!tempSelectedConfig"
|
||||
:loading="saving"
|
||||
>
|
||||
应用
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useToast } from '@/utils/toast';
|
||||
|
||||
interface ConfigInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ConfigChangedPayload {
|
||||
configId: string;
|
||||
agentRunnerType: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'chat.selectedConfigId';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
sessionId?: string | null;
|
||||
platformId?: string;
|
||||
isGroup?: boolean;
|
||||
initialConfigId?: string | null;
|
||||
}>(), {
|
||||
sessionId: null,
|
||||
platformId: 'webchat',
|
||||
isGroup: false,
|
||||
initialConfigId: null
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
|
||||
|
||||
const configOptions = ref<ConfigInfo[]>([]);
|
||||
const loadingConfigs = ref(false);
|
||||
const dialog = ref(false);
|
||||
const tempSelectedConfig = ref('');
|
||||
const selectedConfigId = ref('default');
|
||||
const agentRunnerType = ref('local');
|
||||
const saving = ref(false);
|
||||
const pendingSync = ref(false);
|
||||
const routingEntries = ref<Array<{ pattern: string; confId: string }>>([]);
|
||||
const configCache = ref<Record<string, string>>({});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const normalizedSessionId = computed(() => {
|
||||
const id = props.sessionId?.trim();
|
||||
return id ? id : null;
|
||||
});
|
||||
|
||||
const hasActiveSession = computed(() => !!normalizedSessionId.value);
|
||||
|
||||
const messageType = computed(() => (props.isGroup ? 'GroupMessage' : 'FriendMessage'));
|
||||
|
||||
const username = computed(() => localStorage.getItem('user') || 'guest');
|
||||
|
||||
const sessionKey = computed(() => {
|
||||
if (!normalizedSessionId.value) {
|
||||
return null;
|
||||
}
|
||||
return `${props.platformId}!${username.value}!${normalizedSessionId.value}`;
|
||||
});
|
||||
|
||||
const targetUmo = computed(() => {
|
||||
if (!sessionKey.value) {
|
||||
return null;
|
||||
}
|
||||
return `${props.platformId}:${messageType.value}:${sessionKey.value}`;
|
||||
});
|
||||
|
||||
const selectedConfigLabel = computed(() => {
|
||||
const target = configOptions.value.find((item) => item.id === selectedConfigId.value);
|
||||
return target?.name || selectedConfigId.value || 'default';
|
||||
});
|
||||
|
||||
function openDialog() {
|
||||
tempSelectedConfig.value = selectedConfigId.value;
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
async function fetchConfigList() {
|
||||
loadingConfigs.value = true;
|
||||
try {
|
||||
const res = await axios.get('/api/config/abconfs');
|
||||
configOptions.value = res.data.data?.info_list || [];
|
||||
} catch (error) {
|
||||
console.error('加载配置文件列表失败', error);
|
||||
configOptions.value = [];
|
||||
} finally {
|
||||
loadingConfigs.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRoutingEntries() {
|
||||
try {
|
||||
const res = await axios.get('/api/config/umo_abconf_routes');
|
||||
const routing = res.data.data?.routing || {};
|
||||
routingEntries.value = Object.entries(routing).map(([pattern, confId]) => ({
|
||||
pattern,
|
||||
confId: confId as string
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取配置路由失败', error);
|
||||
routingEntries.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function matchesPattern(pattern: string, target: string): boolean {
|
||||
const parts = pattern.split(':');
|
||||
const targetParts = target.split(':');
|
||||
if (parts.length !== 3 || targetParts.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
return parts.every((part, index) => part === '' || part === '*' || part === targetParts[index]);
|
||||
}
|
||||
|
||||
function resolveConfigId(umo: string | null): string {
|
||||
if (!umo) {
|
||||
return 'default';
|
||||
}
|
||||
for (const entry of routingEntries.value) {
|
||||
if (matchesPattern(entry.pattern, umo)) {
|
||||
return entry.confId;
|
||||
}
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
async function getAgentRunnerType(confId: string): Promise<string> {
|
||||
if (configCache.value[confId]) {
|
||||
return configCache.value[confId];
|
||||
}
|
||||
try {
|
||||
const res = await axios.get('/api/config/abconf', {
|
||||
params: { id: confId }
|
||||
});
|
||||
const type = res.data.data?.config?.provider_settings?.agent_runner_type || 'local';
|
||||
configCache.value[confId] = type;
|
||||
return type;
|
||||
} catch (error) {
|
||||
console.error('获取配置文件详情失败', error);
|
||||
return 'local';
|
||||
}
|
||||
}
|
||||
|
||||
async function setSelection(confId: string) {
|
||||
const normalized = confId || 'default';
|
||||
selectedConfigId.value = normalized;
|
||||
const runnerType = await getAgentRunnerType(normalized);
|
||||
agentRunnerType.value = runnerType;
|
||||
emit('config-changed', {
|
||||
configId: normalized,
|
||||
agentRunnerType: runnerType
|
||||
});
|
||||
}
|
||||
|
||||
async function applySelectionToBackend(confId: string): Promise<boolean> {
|
||||
if (!targetUmo.value) {
|
||||
pendingSync.value = true;
|
||||
return true;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await axios.post('/api/config/umo_abconf_route/update', {
|
||||
umo: targetUmo.value,
|
||||
conf_id: confId
|
||||
});
|
||||
const filtered = routingEntries.value.filter((entry) => entry.pattern !== targetUmo.value);
|
||||
filtered.push({ pattern: targetUmo.value, confId });
|
||||
routingEntries.value = filtered;
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as any;
|
||||
console.error('更新配置文件失败', err);
|
||||
toast.error(err?.response?.data?.message || '配置文件应用失败');
|
||||
return false;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSelection() {
|
||||
if (!tempSelectedConfig.value) {
|
||||
return;
|
||||
}
|
||||
const previousId = selectedConfigId.value;
|
||||
await setSelection(tempSelectedConfig.value);
|
||||
localStorage.setItem(STORAGE_KEY, tempSelectedConfig.value);
|
||||
const applied = await applySelectionToBackend(tempSelectedConfig.value);
|
||||
if (!applied) {
|
||||
localStorage.setItem(STORAGE_KEY, previousId);
|
||||
await setSelection(previousId);
|
||||
}
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
async function syncSelectionForSession() {
|
||||
if (!targetUmo.value) {
|
||||
pendingSync.value = true;
|
||||
return;
|
||||
}
|
||||
if (pendingSync.value) {
|
||||
pendingSync.value = false;
|
||||
await applySelectionToBackend(selectedConfigId.value);
|
||||
return;
|
||||
}
|
||||
await fetchRoutingEntries();
|
||||
const resolved = resolveConfigId(targetUmo.value);
|
||||
await setSelection(resolved);
|
||||
localStorage.setItem(STORAGE_KEY, resolved);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.sessionId, props.platformId, props.isGroup],
|
||||
async () => {
|
||||
await syncSelectionForSession();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchConfigList();
|
||||
const stored = props.initialConfigId || localStorage.getItem(STORAGE_KEY) || 'default';
|
||||
selectedConfigId.value = stored;
|
||||
await setSelection(stored);
|
||||
await syncSelectionForSession();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-chip {
|
||||
cursor: pointer;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.config-list {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -64,7 +64,7 @@
|
||||
@click.stop="$emit('editTitle', item.session_id, item.display_name)" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||
class="delete-conversation-btn" color="error"
|
||||
@click.stop="$emit('deleteConversation', item.session_id)" />
|
||||
@click.stop="handleDeleteConversation(item)" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { Session } from '@/composables/useSessions';
|
||||
|
||||
interface Props {
|
||||
@@ -109,7 +109,6 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { tm } = useModuleI18n('features/chat');
|
||||
const { t } = useI18n();
|
||||
|
||||
const sidebarCollapsed = ref(true);
|
||||
const sidebarHovered = ref(false);
|
||||
@@ -159,6 +158,14 @@ function handleSidebarMouseLeave() {
|
||||
}
|
||||
sidebarHoverExpanded.value = false;
|
||||
}
|
||||
|
||||
function handleDeleteConversation(session: Session) {
|
||||
const sessionTitle = session.display_name || tm('conversation.newConversation');
|
||||
const message = tm('conversation.confirmDelete', { name: sessionTitle });
|
||||
if (window.confirm(message)) {
|
||||
emit('deleteConversation', session.session_id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -549,7 +549,7 @@ export default {
|
||||
}
|
||||
|
||||
.bot-embedded-image {
|
||||
max-width: 80%;
|
||||
max-width: 40%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
@@ -558,10 +558,6 @@ export default {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bot-embedded-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.embedded-audio {
|
||||
width: 300px;
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- 选择提供商和模型按钮 -->
|
||||
<v-chip class="text-none" variant="tonal" size="x-small"
|
||||
v-if="selectedProviderId && selectedModelName" @click="openDialog">
|
||||
<v-icon start size="14">mdi-creation</v-icon>
|
||||
{{ selectedProviderId }} / {{ selectedModelName }}
|
||||
</v-chip>
|
||||
<v-chip variant="tonal" rounded="xl" size="x-small" v-else @click="openDialog">
|
||||
|
||||
319
dashboard/src/components/chat/StandaloneChat.vue
Normal file
319
dashboard/src/components/chat/StandaloneChat.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<v-card class="standalone-chat-card" elevation="0" rounded="0">
|
||||
<v-card-text class="standalone-chat-container">
|
||||
<div class="chat-layout">
|
||||
<!-- 聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
<MessageList v-if="messages && messages.length > 0" :messages="messages" :isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning" @openImagePreview="openImagePreview"
|
||||
ref="messageList" />
|
||||
<div class="welcome-container fade-in" v-else>
|
||||
<div class="welcome-title">
|
||||
<span>Hello, I'm</span>
|
||||
<span class="bot-name">AstrBot ⭐</span>
|
||||
</div>
|
||||
<p class="text-caption text-medium-emphasis mt-2">
|
||||
测试配置: {{ configId || 'default' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:config-id="configId"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
<v-card class="image-preview-card" elevation="8">
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t('core.common.imagePreview') }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img :src="previewImageUrl" class="preview-image-large" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
import MessageList from '@/components/chat/MessageList.vue';
|
||||
import ChatInput from '@/components/chat/ChatInput.vue';
|
||||
import { useMessages } from '@/composables/useMessages';
|
||||
import { useMediaHandling } from '@/composables/useMediaHandling';
|
||||
import { useRecording } from '@/composables/useRecording';
|
||||
import { useToast } from '@/utils/toast';
|
||||
|
||||
interface Props {
|
||||
configId?: string | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
configId: null
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { error: showError } = useToast();
|
||||
|
||||
// UI 状态
|
||||
const imagePreviewDialog = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
|
||||
// 会话管理(不使用 useSessions 避免路由跳转)
|
||||
const currSessionId = ref('');
|
||||
const getCurrentSession = computed(() => null); // 独立测试模式不需要会话信息
|
||||
|
||||
async function newSession() {
|
||||
try {
|
||||
const response = await axios.get('/api/chat/new_session');
|
||||
const sessionId = response.data.data.session_id;
|
||||
currSessionId.value = sessionId;
|
||||
return sessionId;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSessionTitle(sessionId: string, title: string) {
|
||||
// 独立模式不需要更新会话标题
|
||||
}
|
||||
|
||||
function getSessions() {
|
||||
// 独立模式不需要加载会话列表
|
||||
}
|
||||
|
||||
const {
|
||||
stagedImagesName,
|
||||
stagedImagesUrl,
|
||||
stagedAudioUrl,
|
||||
getMediaFile,
|
||||
processAndUploadImage,
|
||||
handlePaste,
|
||||
removeImage,
|
||||
removeAudio,
|
||||
clearStaged,
|
||||
cleanupMediaCache
|
||||
} = useMediaHandling();
|
||||
|
||||
const { isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording();
|
||||
|
||||
const {
|
||||
messages,
|
||||
isStreaming,
|
||||
isConvRunning,
|
||||
enableStreaming,
|
||||
getSessionMessages: getSessionMsg,
|
||||
sendMessage: sendMsg,
|
||||
toggleStreaming
|
||||
} = useMessages(currSessionId, getMediaFile, updateSessionTitle, getSessions);
|
||||
|
||||
// 组件引用
|
||||
const messageList = ref<InstanceType<typeof MessageList> | null>(null);
|
||||
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
|
||||
|
||||
// 输入状态
|
||||
const prompt = ref('');
|
||||
|
||||
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
|
||||
|
||||
function openImagePreview(imageUrl: string) {
|
||||
previewImageUrl.value = imageUrl;
|
||||
imagePreviewDialog.value = true;
|
||||
}
|
||||
|
||||
async function handleStartRecording() {
|
||||
await startRec();
|
||||
}
|
||||
|
||||
async function handleStopRecording() {
|
||||
const audioFilename = await stopRec();
|
||||
stagedAudioUrl.value = audioFilename;
|
||||
}
|
||||
|
||||
async function handleFileSelect(files: FileList) {
|
||||
for (const file of files) {
|
||||
await processAndUploadImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!prompt.value.trim() && stagedImagesName.value.length === 0 && !stagedAudioUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!currSessionId.value) {
|
||||
await newSession();
|
||||
}
|
||||
|
||||
const promptToSend = prompt.value.trim();
|
||||
const imageNamesToSend = [...stagedImagesName.value];
|
||||
const audioNameToSend = stagedAudioUrl.value;
|
||||
|
||||
// 清空输入和附件
|
||||
prompt.value = '';
|
||||
clearStaged();
|
||||
|
||||
// 获取选择的提供商和模型
|
||||
const selection = chatInputRef.value?.getCurrentSelection();
|
||||
const selectedProviderId = selection?.providerId || '';
|
||||
const selectedModelName = selection?.modelName || '';
|
||||
|
||||
await sendMsg(
|
||||
promptToSend,
|
||||
imageNamesToSend,
|
||||
audioNameToSend,
|
||||
selectedProviderId,
|
||||
selectedModelName
|
||||
);
|
||||
|
||||
// 滚动到底部
|
||||
nextTick(() => {
|
||||
messageList.value?.scrollToBottom();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err);
|
||||
showError(t('features.chat.errors.sendMessageFailed'));
|
||||
// 恢复输入内容,让用户可以重试
|
||||
// 注意:附件已经上传到服务器,所以不恢复附件
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 独立模式在挂载时创建新会话
|
||||
try {
|
||||
await newSession();
|
||||
} catch (err) {
|
||||
console.error('Failed to create initial session:', err);
|
||||
showError(t('features.chat.errors.createSessionFailed'));
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupMediaCache();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.standalone-chat-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.standalone-chat-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-layout {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-content-panel {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conversation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
padding-left: 16px;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
width: 100%;
|
||||
padding-right: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-header-info h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.conversation-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
color: var(--v-theme-secondary);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.preview-image-large {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
|
||||
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
|
||||
style="font-weight: 1000; font-size: 15px">
|
||||
{{ metadata[key]['name'] }}
|
||||
{{ tm(metadata[key]['name']) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
|
||||
@@ -59,7 +59,17 @@ export default {
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/config');
|
||||
const { tm: tmConfig } = useModuleI18n('features/config');
|
||||
const { tm: tmMetadata } = useModuleI18n('features/config-metadata');
|
||||
|
||||
const tm = (key) => {
|
||||
const metadataResult = tmMetadata(key);
|
||||
if (!metadataResult.startsWith('[MISSING:') && !metadataResult.startsWith('[INVALID:')) {
|
||||
return metadataResult;
|
||||
}
|
||||
return tmConfig(key);
|
||||
};
|
||||
|
||||
return {
|
||||
tm
|
||||
};
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.basic') }}
|
||||
</v-tab>
|
||||
<v-tab value="agent_runner" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-cogs</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.agentRunner') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.speechToText') }}
|
||||
@@ -27,7 +31,7 @@
|
||||
|
||||
<v-window v-model="activeProviderTab" class="mt-4">
|
||||
<v-window-item
|
||||
v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
|
||||
v-for="tabType in ['chat_completion', 'agent_runner', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
|
||||
:key="tabType" :value="tabType">
|
||||
<v-row class="mt-1">
|
||||
<v-col v-for="(template, name) in getTemplatesByType(tabType)" :key="name" cols="12" sm="6"
|
||||
@@ -36,7 +40,7 @@
|
||||
@click="selectProviderTemplate(name)">
|
||||
<div class="provider-card-content">
|
||||
<div class="provider-card-text">
|
||||
<v-card-title class="provider-card-title">接入 {{ name }}</v-card-title>
|
||||
<v-card-title class="provider-card-title">{{ name }}</v-card-title>
|
||||
<v-card-text
|
||||
class="text-caption text-medium-emphasis provider-card-description">
|
||||
{{ getProviderDescription(template, name) }}
|
||||
@@ -54,7 +58,7 @@
|
||||
</v-col>
|
||||
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('dialogs.addProvider.noTemplates', { type: getTabTypeName(tabType) }) }}
|
||||
{{ tm('dialogs.addProvider.noTemplates') }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -104,19 +108,6 @@ export default {
|
||||
this.$emit('update:show', value);
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译消息的计算属性
|
||||
messages() {
|
||||
return {
|
||||
tabTypes: {
|
||||
'chat_completion': this.tm('providers.tabs.chatCompletion'),
|
||||
'speech_to_text': this.tm('providers.tabs.speechToText'),
|
||||
'text_to_speech': this.tm('providers.tabs.textToSpeech'),
|
||||
'embedding': this.tm('providers.tabs.embedding'),
|
||||
'rerank': this.tm('providers.tabs.rerank')
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
@@ -140,11 +131,6 @@ export default {
|
||||
// 从工具函数导入
|
||||
getProviderIcon,
|
||||
|
||||
// 获取Tab类型的中文名称
|
||||
getTabTypeName(tabType) {
|
||||
return this.messages.tabTypes[tabType] || tabType;
|
||||
},
|
||||
|
||||
// 获取提供商简介
|
||||
getProviderDescription(template, name) {
|
||||
return getProviderDescription(template, name, this.tm);
|
||||
|
||||
@@ -8,7 +8,7 @@ import PersonaSelector from './PersonaSelector.vue'
|
||||
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
|
||||
import PluginSetSelector from './PluginSetSelector.vue'
|
||||
import T2ITemplateEditor from './T2ITemplateEditor.vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
@@ -27,6 +27,34 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const { tm } = useModuleI18n('features/config-metadata')
|
||||
|
||||
// 翻译器函数 - 如果是国际化键则翻译,否则原样返回
|
||||
const translateIfKey = (value) => {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return tm(value)
|
||||
}
|
||||
|
||||
// 处理labels翻译 - labels可以是数组或国际化键
|
||||
const getTranslatedLabels = (itemMeta) => {
|
||||
if (!itemMeta?.labels) return null
|
||||
|
||||
// 如果labels是字符串(国际化键)
|
||||
if (typeof itemMeta.labels === 'string') {
|
||||
const translatedLabels = tm(itemMeta.labels)
|
||||
// 如果翻译成功且是数组,返回翻译结果
|
||||
if (Array.isArray(translatedLabels)) {
|
||||
return translatedLabels
|
||||
}
|
||||
}
|
||||
|
||||
// 如果labels是数组,直接返回
|
||||
if (Array.isArray(itemMeta.labels)) {
|
||||
return itemMeta.labels
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const dialog = ref(false)
|
||||
const currentEditingKey = ref('')
|
||||
@@ -101,6 +129,21 @@ function shouldShowItem(itemMeta, itemKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查最外层的 object 是否应该显示
|
||||
function shouldShowSection() {
|
||||
const sectionMeta = props.metadata[props.metadataKey]
|
||||
if (!sectionMeta?.condition) {
|
||||
return true
|
||||
}
|
||||
for (const [conditionKey, expectedValue] of Object.entries(sectionMeta.condition)) {
|
||||
const actualValue = getValueBySelector(props.iterable, conditionKey)
|
||||
if (actualValue !== expectedValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
@@ -114,19 +157,40 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function parseSpecialValue(value) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return { name: '', subtype: '' }
|
||||
}
|
||||
const [name, ...rest] = value.split(':')
|
||||
return {
|
||||
name,
|
||||
subtype: rest.join(':') || ''
|
||||
}
|
||||
}
|
||||
|
||||
function getSpecialName(value) {
|
||||
return parseSpecialValue(value).name
|
||||
}
|
||||
|
||||
function getSpecialSubtype(value) {
|
||||
return parseSpecialValue(value).subtype
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<v-card style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));" rounded="md" variant="outlined">
|
||||
<v-card v-if="shouldShowSection()" style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));"
|
||||
rounded="md" variant="outlined">
|
||||
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'" style="padding-bottom: 8px;">
|
||||
<v-list-item-title class="config-title">
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
{{ translateIfKey(metadata[metadataKey]?.description) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
{{ translateIfKey(metadata[metadataKey]?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-card-text>
|
||||
|
||||
@@ -140,13 +204,13 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ itemMeta?.description || itemKey }}
|
||||
{{ translateIfKey(itemMeta?.description) || itemKey }}
|
||||
<span class="property-key">({{ itemKey }})</span>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint">‼️</span>
|
||||
{{ itemMeta?.hint }}
|
||||
{{ translateIfKey(itemMeta?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
@@ -154,7 +218,12 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<div class="w-100" v-if="!itemMeta?._special">
|
||||
<!-- Select input for JSON selector -->
|
||||
<v-select v-if="itemMeta?.options" v-model="createSelectorModel(itemKey).value"
|
||||
:items="itemMeta?.labels ? itemMeta.options.map((value, index) => ({ title: itemMeta.labels[index] || value, value: value })) : itemMeta.options"
|
||||
:items="(() => {
|
||||
const labels = getTranslatedLabels(itemMeta);
|
||||
return labels
|
||||
? itemMeta.options.map((value, index) => ({ title: labels[index] || value, value: value }))
|
||||
: itemMeta.options;
|
||||
})()"
|
||||
:disabled="itemMeta?.readonly" density="compact" variant="outlined"
|
||||
class="config-field" hide-details></v-select>
|
||||
|
||||
@@ -187,22 +256,16 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
<!-- Boolean switch for JSON selector -->
|
||||
<v-switch v-else-if="itemMeta?.type === 'bool'" v-model="createSelectorModel(itemKey).value"
|
||||
color="primary" inset density="compact" hide-details style="display: flex; justify-content: end;"></v-switch>
|
||||
color="primary" inset density="compact" hide-details
|
||||
style="display: flex; justify-content: end;"></v-switch>
|
||||
|
||||
<!-- List item for JSON selector -->
|
||||
<ListConfigItem
|
||||
v-else-if="itemMeta?.type === 'list'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="修改"
|
||||
class="config-field"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
@@ -211,50 +274,36 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<div v-else-if="itemMeta?._special === 'select_provider'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
/>
|
||||
<ProviderSelector v-model="createSelectorModel(itemKey).value" :provider-type="'chat_completion'" />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_stt'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'speech_to_text'"
|
||||
/>
|
||||
<ProviderSelector v-model="createSelectorModel(itemKey).value" :provider-type="'speech_to_text'" />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_tts'">
|
||||
<ProviderSelector v-model="createSelectorModel(itemKey).value" :provider-type="'text_to_speech'" />
|
||||
</div>
|
||||
<div v-else-if="getSpecialName(itemMeta?._special) === 'select_agent_runner_provider'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'text_to_speech'"
|
||||
:provider-type="'agent_runner'"
|
||||
:provider-subtype="getSpecialSubtype(itemMeta?._special)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'provider_pool'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
button-text="选择提供商池..."
|
||||
/>
|
||||
<ProviderSelector v-model="createSelectorModel(itemKey).value" :provider-type="'chat_completion'"
|
||||
button-text="选择提供商池..." />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_persona'">
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
<PersonaSelector v-model="createSelectorModel(itemKey).value" />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'persona_pool'">
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="选择人格池..."
|
||||
/>
|
||||
<PersonaSelector v-model="createSelectorModel(itemKey).value" button-text="选择人格池..." />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_knowledgebase'">
|
||||
<KnowledgeBaseSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
<KnowledgeBaseSelector v-model="createSelectorModel(itemKey).value" />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_plugin_set'">
|
||||
<PluginSetSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
<PluginSetSelector v-model="createSelectorModel(itemKey).value" />
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 't2i_template'">
|
||||
<T2ITemplateEditor />
|
||||
@@ -263,21 +312,17 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-row>
|
||||
|
||||
<!-- Plugin Set Selector 全宽显示区域 -->
|
||||
<v-row v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'" class="plugin-set-display-row">
|
||||
<v-row v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'"
|
||||
class="plugin-set-display-row">
|
||||
<v-col cols="12" class="plugin-set-display">
|
||||
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0" class="selected-plugins-full-width">
|
||||
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
|
||||
class="selected-plugins-full-width">
|
||||
<div class="plugins-header">
|
||||
<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"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-chip v-for="plugin in (createSelectorModel(itemKey).value || [])" :key="plugin" size="small" label
|
||||
color="primary" variant="outlined">
|
||||
{{ plugin === '*' ? '所有插件' : plugin }}
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -285,7 +330,8 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-divider class="config-divider" v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
|
||||
<v-divider class="config-divider"
|
||||
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useCommonStore } from '@/stores/common';
|
||||
<!-- 添加筛选级别控件 -->
|
||||
<div class="filter-controls mb-2" v-if="showLevelBtns">
|
||||
<v-chip-group v-model="selectedLevels" column multiple>
|
||||
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter
|
||||
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'">
|
||||
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small"
|
||||
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'" class="font-weight-medium">
|
||||
{{ level }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
@@ -168,6 +168,7 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div style="flex: 1; min-width: 0; overflow: hidden;">
|
||||
<span v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
|
||||
style="color: rgb(var(--v-theme-primaryText));">
|
||||
未选择
|
||||
{{ tm('knowledgeBaseSelector.notSelected') }}
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap gap-1">
|
||||
<v-chip
|
||||
@@ -28,7 +28,7 @@
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
选择知识库
|
||||
{{ tm('knowledgeBaseSelector.dialogTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
@@ -50,9 +50,9 @@
|
||||
</template>
|
||||
<v-list-item-title>{{ kb.kb_name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ kb.description || '无描述' }}
|
||||
<span v-if="kb.doc_count !== undefined"> - {{ kb.doc_count }} 个文档</span>
|
||||
<span v-if="kb.chunk_count !== undefined"> - {{ kb.chunk_count }} 个块</span>
|
||||
{{ kb.description || tm('knowledgeBaseSelector.noDescription') }}
|
||||
<span v-if="kb.doc_count !== undefined"> - {{ tm('knowledgeBaseSelector.documentCount', { count: kb.doc_count }) }}</span>
|
||||
<span v-if="kb.chunk_count !== undefined"> - {{ tm('knowledgeBaseSelector.chunkCount', { count: kb.chunk_count }) }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
@@ -68,9 +68,9 @@
|
||||
<!-- 当没有知识库时显示创建提示 -->
|
||||
<div v-if="knowledgeBaseList.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-database-off</v-icon>
|
||||
<p class="text-grey mt-4 mb-4">暂无知识库</p>
|
||||
<p class="text-grey mt-4 mb-4">{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}</p>
|
||||
<v-btn color="primary" variant="tonal" @click="goToKnowledgeBasePage">
|
||||
创建知识库
|
||||
{{ tm('knowledgeBaseSelector.createKnowledgeBase') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
@@ -78,14 +78,14 @@
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<div v-if="selectedKnowledgeBases.length > 0" class="text-caption text-grey">
|
||||
已选择 {{ selectedKnowledgeBases.length }} 个知识库
|
||||
{{ tm('knowledgeBaseSelector.selectedCount', { count: selectedKnowledgeBases.length }) }}
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ tm('knowledgeBaseSelector.cancelSelection') }}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection">
|
||||
确认选择
|
||||
{{ tm('knowledgeBaseSelector.confirmSelection') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -96,6 +96,7 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -110,6 +111,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const router = useRouter()
|
||||
const { tm } = useModuleI18n('core.shared')
|
||||
|
||||
const dialog = ref(false)
|
||||
const knowledgeBaseList = ref([])
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
未启用任何插件
|
||||
{{ tm('pluginSetSelector.notSelected') }}
|
||||
</span>
|
||||
<span v-else-if="isAllPlugins" style="color: rgb(var(--v-theme-primaryText));">
|
||||
启用所有插件 (*)
|
||||
{{ tm('pluginSetSelector.allPlugins') }}
|
||||
</span>
|
||||
<span v-else style="color: rgb(var(--v-theme-primaryText));">
|
||||
已选择 {{ modelValue.length }} 个插件
|
||||
{{ tm('pluginSetSelector.selectedCount', { count: modelValue.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
@@ -23,7 +23,7 @@
|
||||
<v-dialog v-model="dialog" max-width="700px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
选择插件集合
|
||||
{{ tm('pluginSetSelector.dialogTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
@@ -34,17 +34,17 @@
|
||||
<v-radio-group v-model="selectionMode" class="mb-4" hide-details>
|
||||
<v-radio
|
||||
value="all"
|
||||
label="启用所有插件"
|
||||
:label="tm('pluginSetSelector.enableAll')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
<v-radio
|
||||
value="none"
|
||||
label="不启用任何插件"
|
||||
:label="tm('pluginSetSelector.enableNone')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
<v-radio
|
||||
value="custom"
|
||||
label="自定义选择"
|
||||
:label="tm('pluginSetSelector.customSelect')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
</v-radio-group>
|
||||
@@ -68,21 +68,21 @@
|
||||
|
||||
<v-list-item-title>{{ plugin.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ plugin.desc || '无描述' }}
|
||||
{{ plugin.desc || tm('pluginSetSelector.noDescription') }}
|
||||
<v-chip v-if="!plugin.activated" size="x-small" color="grey" class="ml-1">
|
||||
未激活
|
||||
{{ tm('pluginSetSelector.notActivated') }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<div class="pl-8 pt-2">
|
||||
<small>*不显示系统插件和已经在插件页禁用的插件。</small>
|
||||
<small>{{ tm('pluginSetSelector.note') }}</small>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-puzzle-outline</v-icon>
|
||||
<p class="text-grey mt-4">暂无可用的插件</p>
|
||||
<p class="text-grey mt-4">{{ tm('pluginSetSelector.noPlugins') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,11 +90,11 @@
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ tm('pluginSetSelector.cancelSelection') }}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection">
|
||||
确认选择
|
||||
{{ tm('pluginSetSelector.confirmSelection') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -104,6 +104,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -121,6 +122,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { tm } = useModuleI18n('core.shared')
|
||||
|
||||
const dialog = ref(false)
|
||||
const pluginList = ref([])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
|
||||
未选择
|
||||
{{ tm('providerSelector.notSelected') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ modelValue }}
|
||||
@@ -15,7 +15,7 @@
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
选择提供商
|
||||
{{ tm('providerSelector.dialogTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
@@ -30,8 +30,8 @@
|
||||
:active="selectedProvider === ''"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
<v-list-item-title>不选择</v-list-item-title>
|
||||
<v-list-item-subtitle>清除当前选择</v-list-item-subtitle>
|
||||
<v-list-item-title>{{ tm('providerSelector.clearSelection') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ tm('providerSelector.clearSelectionSubtitle') }}</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="selectedProvider === ''" color="primary">mdi-check-circle</v-icon>
|
||||
@@ -50,7 +50,7 @@
|
||||
class="ma-1">
|
||||
<v-list-item-title>{{ provider.id }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ provider.type || provider.provider_type || '未知类型' }}
|
||||
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
|
||||
<span v-if="provider.model_config?.model">- {{ provider.model_config.model }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<div v-else-if="!loading && providerList.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">暂无可用的提供商</p>
|
||||
<p class="text-grey mt-4">{{ tm('providerSelector.noProviders') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
@@ -70,11 +70,11 @@
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ tm('providerSelector.cancelSelection') }}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection">
|
||||
确认选择
|
||||
{{ tm('providerSelector.confirmSelection') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -84,6 +84,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -94,6 +95,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'chat_completion'
|
||||
},
|
||||
providerSubtype: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '选择提供商...'
|
||||
@@ -101,6 +106,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { tm } = useModuleI18n('core.shared')
|
||||
|
||||
const dialog = ref(false)
|
||||
const providerList = ref([])
|
||||
@@ -127,7 +133,10 @@ async function loadProviders() {
|
||||
}
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
providerList.value = response.data.data || []
|
||||
const providers = response.data.data || []
|
||||
providerList.value = props.providerSubtype
|
||||
? providers.filter((provider) => matchesProviderSubtype(provider, props.providerSubtype))
|
||||
: providers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提供商列表失败:', error)
|
||||
@@ -137,6 +146,17 @@ async function loadProviders() {
|
||||
}
|
||||
}
|
||||
|
||||
function matchesProviderSubtype(provider, subtype) {
|
||||
if (!subtype) {
|
||||
return true
|
||||
}
|
||||
const normalized = String(subtype).toLowerCase()
|
||||
const candidates = [provider.type, provider.provider, provider.id]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value).toLowerCase())
|
||||
return candidates.includes(normalized)
|
||||
}
|
||||
|
||||
function selectProvider(provider) {
|
||||
selectedProvider.value = provider.id
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import { useRouter } from 'vue-router';
|
||||
|
||||
export interface Session {
|
||||
session_id: string;
|
||||
display_name: string;
|
||||
display_name: string | null;
|
||||
updated_at: string;
|
||||
platform_id: string;
|
||||
creator: string;
|
||||
is_group: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function useSessions(chatboxMode: boolean = false) {
|
||||
|
||||
@@ -33,6 +33,7 @@ export class I18nLoader {
|
||||
{ name: 'core/status', path: 'core/status.json' },
|
||||
{ name: 'core/navigation', path: 'core/navigation.json' },
|
||||
{ name: 'core/header', path: 'core/header.json' },
|
||||
{ name: 'core/shared', path: 'core/shared.json' },
|
||||
|
||||
// 功能模块
|
||||
{ name: 'features/chat', path: 'features/chat.json' },
|
||||
@@ -43,6 +44,7 @@ export class I18nLoader {
|
||||
{ name: 'features/provider', path: 'features/provider.json' },
|
||||
{ name: 'features/platform', path: 'features/platform.json' },
|
||||
{ name: 'features/config', path: 'features/config.json' },
|
||||
{ name: 'features/config-metadata', path: 'features/config-metadata.json' },
|
||||
{ name: 'features/console', path: 'features/console.json' },
|
||||
{ name: 'features/about', path: 'features/about.json' },
|
||||
{ name: 'features/settings', path: 'features/settings.json' },
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"delete": "Delete",
|
||||
"copy": "Copy",
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"noData": "No data available"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"chat": "Chat",
|
||||
"extension": "Extensions",
|
||||
"conversation": "Conversations",
|
||||
"sessionManagement": "Session Management",
|
||||
"sessionManagement": "Custom Rules",
|
||||
"console": "Console",
|
||||
"alkaid": "Alkaid Lab",
|
||||
"knowledgeBase": "Knowledge Base",
|
||||
|
||||
45
dashboard/src/i18n/locales/en-US/core/shared.json
Normal file
45
dashboard/src/i18n/locales/en-US/core/shared.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"knowledgeBaseSelector": {
|
||||
"notSelected": "Not selected",
|
||||
"buttonText": "Select Knowledge Base...",
|
||||
"dialogTitle": "Select Knowledge Base",
|
||||
"loading": "Loading...",
|
||||
"noKnowledgeBases": "No knowledge bases available",
|
||||
"createKnowledgeBase": "Create Knowledge Base",
|
||||
"selectedCount": "{count} knowledge base(s) selected",
|
||||
"confirmSelection": "Confirm Selection",
|
||||
"cancelSelection": "Cancel",
|
||||
"noDescription": "No description",
|
||||
"documentCount": "{count} document(s)",
|
||||
"chunkCount": "{count} chunk(s)"
|
||||
},
|
||||
"pluginSetSelector": {
|
||||
"notSelected": "No plugins enabled",
|
||||
"allPlugins": "All plugins enabled (*)",
|
||||
"selectedCount": "{count} plugin(s) selected",
|
||||
"buttonText": "Select Plugin Set...",
|
||||
"dialogTitle": "Select Plugin Set",
|
||||
"loading": "Loading...",
|
||||
"enableAll": "Enable all plugins",
|
||||
"enableNone": "Disable all plugins",
|
||||
"customSelect": "Custom selection",
|
||||
"noPlugins": "No plugins available",
|
||||
"confirmSelection": "Confirm Selection",
|
||||
"cancelSelection": "Cancel",
|
||||
"noDescription": "No description",
|
||||
"notActivated": "Not activated",
|
||||
"note": "*System plugins and disabled plugins are not shown."
|
||||
},
|
||||
"providerSelector": {
|
||||
"notSelected": "Not selected",
|
||||
"buttonText": "Select Provider...",
|
||||
"dialogTitle": "Select Provider",
|
||||
"loading": "Loading...",
|
||||
"noProviders": "No providers available",
|
||||
"confirmSelection": "Confirm Selection",
|
||||
"cancelSelection": "Cancel",
|
||||
"clearSelection": "None",
|
||||
"clearSelectionSubtitle": "Clear current selection",
|
||||
"unknownType": "Unknown type"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@
|
||||
"editDisplayName": "Edit Session Name",
|
||||
"displayName": "Session Name",
|
||||
"displayNameUpdated": "Session name updated",
|
||||
"displayNameUpdateFailed": "Failed to update session name"
|
||||
"displayNameUpdateFailed": "Failed to update session name",
|
||||
"confirmDelete": "Are you sure you want to delete \"{name}\"? This action cannot be undone."
|
||||
},
|
||||
"modes": {
|
||||
"darkMode": "Switch to Dark Mode",
|
||||
@@ -84,5 +85,9 @@
|
||||
"reconnected": "Chat connection re-established",
|
||||
"failed": "Connection failed, please refresh the page"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"sendMessageFailed": "Failed to send message, please try again",
|
||||
"createSessionFailed": "Failed to create session, please refresh the page"
|
||||
}
|
||||
}
|
||||
456
dashboard/src/i18n/locales/en-US/features/config-metadata.json
Normal file
456
dashboard/src/i18n/locales/en-US/features/config-metadata.json
Normal file
@@ -0,0 +1,456 @@
|
||||
{
|
||||
"ai_group": {
|
||||
"name": "AI",
|
||||
"agent_runner": {
|
||||
"description": "Agent Runner",
|
||||
"hint": "Select the runner for AI conversations. Defaults to AstrBot's built-in Agent runner, which supports knowledge base, persona, and tool calling features. You don't need to modify this section unless you plan to integrate third-party Agent runners like Dify or Coze.",
|
||||
"provider_settings": {
|
||||
"enable": {
|
||||
"description": "Enable",
|
||||
"hint": "Master switch for AI conversations"
|
||||
},
|
||||
"agent_runner_type": {
|
||||
"description": "Runner",
|
||||
"labels": ["Built-in Agent", "Dify", "Coze", "Alibaba Cloud Bailian Application"]
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent Runner Provider ID"
|
||||
},
|
||||
"dify_agent_runner_provider_id": {
|
||||
"description": "Dify Agent Runner Provider ID"
|
||||
},
|
||||
"dashscope_agent_runner_provider_id": {
|
||||
"description": "Alibaba Cloud Bailian Application Agent Runner Provider ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ai": {
|
||||
"description": "Model",
|
||||
"hint": "When using non-built-in Agent runners, the default chat model and default image caption model may not take effect, but some plugins rely on these settings to invoke AI capabilities.",
|
||||
"provider_settings": {
|
||||
"default_provider_id": {
|
||||
"description": "Default Chat Model",
|
||||
"hint": "Uses the first model when left empty"
|
||||
},
|
||||
"default_image_caption_provider_id": {
|
||||
"description": "Default Image Caption Model",
|
||||
"hint": "Leave empty to disable; useful for non-multimodal models"
|
||||
},
|
||||
"image_caption_prompt": {
|
||||
"description": "Image Caption Prompt"
|
||||
}
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": {
|
||||
"description": "Enable Speech-to-Text",
|
||||
"hint": "Master switch for STT"
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "Default Speech-to-Text Model",
|
||||
"hint": "Users can also select session-specific STT models using the /provider command."
|
||||
}
|
||||
},
|
||||
"provider_tts_settings": {
|
||||
"enable": {
|
||||
"description": "Enable Text-to-Speech",
|
||||
"hint": "Master switch for TTS"
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "Default Text-to-Speech Model"
|
||||
}
|
||||
}
|
||||
},
|
||||
"persona": {
|
||||
"description": "Persona",
|
||||
"provider_settings": {
|
||||
"default_personality": {
|
||||
"description": "Default Persona"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgebase": {
|
||||
"description": "Knowledge Base",
|
||||
"kb_names": {
|
||||
"description": "Knowledge Base List",
|
||||
"hint": "Supports multiple selections"
|
||||
},
|
||||
"kb_fusion_top_k": {
|
||||
"description": "Fusion Search Results Count",
|
||||
"hint": "Number of results returned after fusing search results from multiple knowledge bases"
|
||||
},
|
||||
"kb_final_top_k": {
|
||||
"description": "Final Results Count",
|
||||
"hint": "Number of results retrieved from the knowledge base. Higher values may provide more relevant information but could also introduce noise. Adjust based on actual needs"
|
||||
},
|
||||
"kb_agentic_mode": {
|
||||
"description": "Agentic Knowledge Base Retrieval",
|
||||
"hint": "When enabled, knowledge base retrieval becomes an LLM Tool, allowing the model to autonomously decide when to query the knowledge base. Requires the model to support function calling."
|
||||
}
|
||||
},
|
||||
"websearch": {
|
||||
"description": "Web Search",
|
||||
"provider_settings": {
|
||||
"web_search": {
|
||||
"description": "Enable Web Search"
|
||||
},
|
||||
"websearch_provider": {
|
||||
"description": "Web Search Provider"
|
||||
},
|
||||
"websearch_tavily_key": {
|
||||
"description": "Tavily API Key",
|
||||
"hint": "Multiple keys can be added for rotation."
|
||||
},
|
||||
"websearch_baidu_app_builder_key": {
|
||||
"description": "Baidu Qianfan Smart Cloud APP Builder API Key",
|
||||
"hint": "Reference: https://console.bce.baidu.com/iam/#/iam/apikey/list"
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "Display Source Citations"
|
||||
}
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "Other Settings",
|
||||
"provider_settings": {
|
||||
"display_reasoning_text": {
|
||||
"description": "Display Reasoning Content"
|
||||
},
|
||||
"identifier": {
|
||||
"description": "User Identification",
|
||||
"hint": "When enabled, user ID information will be included in the prompt."
|
||||
},
|
||||
"group_name_display": {
|
||||
"description": "Display Group Name",
|
||||
"hint": "When enabled, group name information will be included in the prompt on supported platforms (OneBot v11)."
|
||||
},
|
||||
"datetime_system_prompt": {
|
||||
"description": "Real-world Time Awareness",
|
||||
"hint": "When enabled, current time information will be appended to the system prompt."
|
||||
},
|
||||
"show_tool_use_status": {
|
||||
"description": "Output Function Call Status"
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "Maximum Tool Call Rounds"
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "Tool Call Timeout (seconds)"
|
||||
},
|
||||
"streaming_response": {
|
||||
"description": "Streaming Output"
|
||||
},
|
||||
"unsupported_streaming_strategy": {
|
||||
"description": "Platforms Without Streaming Support",
|
||||
"hint": "Select the handling method for platforms that don't support streaming responses. Real-time segmented reply sends content immediately when the system detects segment points like punctuation during streaming reception",
|
||||
"labels": ["Real-time Segmented Reply", "Disable Streaming Response"]
|
||||
},
|
||||
"max_context_length": {
|
||||
"description": "Maximum Conversation Rounds",
|
||||
"hint": "Discards the oldest parts when this count is exceeded. One conversation round counts as 1, -1 means unlimited"
|
||||
},
|
||||
"dequeue_context_length": {
|
||||
"description": "Dequeue Conversation Rounds",
|
||||
"hint": "Number of conversation rounds to discard at once when maximum context length is exceeded"
|
||||
},
|
||||
"wake_prefix": {
|
||||
"description": "Additional LLM Chat Wake Prefix",
|
||||
"hint": "If the wake prefix is / and the additional chat wake prefix is chat, then /chat is required to trigger LLM requests"
|
||||
},
|
||||
"prompt_prefix": {
|
||||
"description": "User Prompt",
|
||||
"hint": "You can use {{prompt}} as a placeholder for user input. If no placeholder is provided, it will be added before the user input."
|
||||
}
|
||||
},
|
||||
"provider_tts_settings": {
|
||||
"dual_output": {
|
||||
"description": "Output Both Voice and Text When TTS is Enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"platform_group": {
|
||||
"name": "Platform",
|
||||
"general": {
|
||||
"description": "General",
|
||||
"admins_id": {
|
||||
"description": "Administrator IDs"
|
||||
},
|
||||
"platform_settings": {
|
||||
"unique_session": {
|
||||
"description": "Isolate Sessions",
|
||||
"hint": "When enabled, group members have independent contexts."
|
||||
},
|
||||
"friend_message_needs_wake_prefix": {
|
||||
"description": "Private Messages Require Wake Word"
|
||||
},
|
||||
"reply_prefix": {
|
||||
"description": "Reply Text Prefix"
|
||||
},
|
||||
"reply_with_mention": {
|
||||
"description": "Mention Sender in Reply"
|
||||
},
|
||||
"reply_with_quote": {
|
||||
"description": "Quote Sender's Message in Reply"
|
||||
},
|
||||
"forward_threshold": {
|
||||
"description": "Forward Message Word Count Threshold"
|
||||
},
|
||||
"empty_mention_waiting": {
|
||||
"description": "Trigger Waiting on Mention-only Messages"
|
||||
}
|
||||
},
|
||||
"wake_prefix": {
|
||||
"description": "Wake Word"
|
||||
}
|
||||
},
|
||||
"whitelist": {
|
||||
"description": "Whitelist",
|
||||
"platform_settings": {
|
||||
"enable_id_white_list": {
|
||||
"description": "Enable Whitelist",
|
||||
"hint": "When enabled, only sessions in the whitelist will be responded to."
|
||||
},
|
||||
"id_whitelist": {
|
||||
"description": "Whitelist ID List",
|
||||
"hint": "Use /sid to get IDs."
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"description": "Output Logs",
|
||||
"hint": "When enabled, INFO level logs will be output when a message doesn't pass the whitelist."
|
||||
},
|
||||
"wl_ignore_admin_on_group": {
|
||||
"description": "Administrator Group Messages Bypass ID Whitelist"
|
||||
},
|
||||
"wl_ignore_admin_on_friend": {
|
||||
"description": "Administrator Private Messages Bypass ID Whitelist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
"description": "Rate Limiting",
|
||||
"platform_settings": {
|
||||
"rate_limit": {
|
||||
"time": {
|
||||
"description": "Message Rate Limit Time (seconds)"
|
||||
},
|
||||
"count": {
|
||||
"description": "Message Rate Limit Count"
|
||||
},
|
||||
"strategy": {
|
||||
"description": "Rate Limit Strategy"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content_safety": {
|
||||
"description": "Content Safety",
|
||||
"content_safety": {
|
||||
"also_use_in_response": {
|
||||
"description": "Also Check Model Response Content"
|
||||
},
|
||||
"baidu_aip": {
|
||||
"enable": {
|
||||
"description": "Use Baidu Content Safety Moderation",
|
||||
"hint": "You need to manually install the baidu-aip library."
|
||||
},
|
||||
"app_id": {
|
||||
"description": "App ID"
|
||||
},
|
||||
"api_key": {
|
||||
"description": "API Key"
|
||||
},
|
||||
"secret_key": {
|
||||
"description": "Secret Key"
|
||||
}
|
||||
},
|
||||
"internal_keywords": {
|
||||
"enable": {
|
||||
"description": "Keyword Check"
|
||||
},
|
||||
"extra_keywords": {
|
||||
"description": "Additional Keywords",
|
||||
"hint": "Additional keyword blocklist, supports regular expressions."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"t2i": {
|
||||
"description": "Text-to-Image",
|
||||
"t2i": {
|
||||
"description": "Text-to-Image Output"
|
||||
},
|
||||
"t2i_word_threshold": {
|
||||
"description": "Text-to-Image Word Count Threshold"
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "Other Settings",
|
||||
"platform_settings": {
|
||||
"ignore_bot_self_message": {
|
||||
"description": "Ignore Bot's Own Messages"
|
||||
},
|
||||
"ignore_at_all": {
|
||||
"description": "Ignore @All Events"
|
||||
},
|
||||
"no_permission_reply": {
|
||||
"description": "Reply When User Has Insufficient Permissions"
|
||||
}
|
||||
},
|
||||
"platform_specific": {
|
||||
"lark": {
|
||||
"pre_ack_emoji": {
|
||||
"enable": {
|
||||
"description": "[Lark] Enable Pre-acknowledgment Emoji"
|
||||
},
|
||||
"emojis": {
|
||||
"description": "Emoji List (Lark Emoji Enum Names)",
|
||||
"hint": "Emoji enum names reference: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce"
|
||||
}
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"pre_ack_emoji": {
|
||||
"enable": {
|
||||
"description": "[Telegram] Enable Pre-acknowledgment Emoji"
|
||||
},
|
||||
"emojis": {
|
||||
"description": "Emoji List (Unicode)",
|
||||
"hint": "Telegram only supports a fixed reaction set, reference: https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugin_group": {
|
||||
"name": "Plugin",
|
||||
"plugin": {
|
||||
"description": "Plugins",
|
||||
"plugin_set": {
|
||||
"description": "Available Plugins",
|
||||
"hint": "All non-disabled plugins are enabled by default. If a plugin is disabled on the plugins page, selections here will not take effect."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ext_group": {
|
||||
"name": "Ext.",
|
||||
"segmented_reply": {
|
||||
"description": "Segmented Reply",
|
||||
"platform_settings": {
|
||||
"segmented_reply": {
|
||||
"enable": {
|
||||
"description": "Enable Segmented Reply"
|
||||
},
|
||||
"only_llm_result": {
|
||||
"description": "Segment Only LLM Results"
|
||||
},
|
||||
"interval_method": {
|
||||
"description": "Interval Method"
|
||||
},
|
||||
"interval": {
|
||||
"description": "Random Interval Time",
|
||||
"hint": "Format: minimum,maximum (e.g., 1.5,3.5)"
|
||||
},
|
||||
"log_base": {
|
||||
"description": "Logarithm Base",
|
||||
"hint": "Base for logarithmic intervals, defaults to 2.0. Value range: 1.0-10.0."
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"description": "Segmented Reply Word Count Threshold"
|
||||
},
|
||||
"regex": {
|
||||
"description": "Segmentation Regular Expression"
|
||||
},
|
||||
"content_cleanup_rule": {
|
||||
"description": "Content Filtering Regular Expression",
|
||||
"hint": "Remove specified content from segmented content. For example, `[。?!]` will remove all periods, question marks, and exclamation marks."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ltm": {
|
||||
"description": "Group Chat Context Awareness (formerly Chat Memory Enhancement)",
|
||||
"provider_ltm_settings": {
|
||||
"group_icl_enable": {
|
||||
"description": "Enable Group Chat Context Awareness"
|
||||
},
|
||||
"group_message_max_cnt": {
|
||||
"description": "Maximum Message Count"
|
||||
},
|
||||
"image_caption": {
|
||||
"description": "Auto-understand Images",
|
||||
"hint": "Requires setting a group chat image caption model."
|
||||
},
|
||||
"image_caption_provider_id": {
|
||||
"description": "Group Chat Image Caption Model",
|
||||
"hint": "Used for image understanding in group chat context awareness, configured separately from the default image caption model."
|
||||
},
|
||||
"active_reply": {
|
||||
"enable": {
|
||||
"description": "Active Reply"
|
||||
},
|
||||
"method": {
|
||||
"description": "Active Reply Method"
|
||||
},
|
||||
"possibility_reply": {
|
||||
"description": "Reply Probability",
|
||||
"hint": "Value between 0.0-1.0"
|
||||
},
|
||||
"whitelist": {
|
||||
"description": "Active Reply Whitelist",
|
||||
"hint": "Whitelist filtering is disabled when empty. Use /sid to get IDs."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_group": {
|
||||
"name": "System",
|
||||
"system": {
|
||||
"description": "System Settings",
|
||||
"t2i_strategy": {
|
||||
"description": "Text-to-Image Strategy",
|
||||
"hint": "Text-to-image strategy. `remote` uses a remote HTML-based rendering service, `local` uses PIL for local rendering. When using local, place a TTF font named 'font.ttf' in the data/ directory to customize the font."
|
||||
},
|
||||
"t2i_endpoint": {
|
||||
"description": "Text-to-Image Service API Endpoint",
|
||||
"hint": "Uses AstrBot API service when empty"
|
||||
},
|
||||
"t2i_template": {
|
||||
"description": "Text-to-Image Custom Template",
|
||||
"hint": "When enabled, you can customize HTML templates for text-to-image rendering."
|
||||
},
|
||||
"t2i_active_template": {
|
||||
"description": "Currently Active Text-to-Image Rendering Template",
|
||||
"hint": "This value is maintained by the text-to-image template management page."
|
||||
},
|
||||
"log_level": {
|
||||
"description": "Console Log Level",
|
||||
"hint": "Log level for console output."
|
||||
},
|
||||
"pip_install_arg": {
|
||||
"description": "Additional pip Installation Arguments",
|
||||
"hint": "When installing plugin dependencies, Python's pip tool will be used. Additional arguments can be provided here, such as `--break-system-package`."
|
||||
},
|
||||
"pypi_index_url": {
|
||||
"description": "PyPI Repository URL",
|
||||
"hint": "PyPI repository URL for installing Python dependencies. Defaults to https://mirrors.aliyun.com/pypi/simple/"
|
||||
},
|
||||
"callback_api_base": {
|
||||
"description": "Externally Accessible Callback API Address",
|
||||
"hint": "External services may access AstrBot's backend through callback links generated by AstrBot (such as file download links). Since AstrBot cannot automatically determine the externally accessible host address in the deployment environment, this configuration item is needed to explicitly specify how external services should access AstrBot's address. Examples: http://localhost:6185, https://example.com, etc."
|
||||
},
|
||||
"timezone": {
|
||||
"description": "Timezone",
|
||||
"hint": "Timezone setting. Please enter an IANA timezone name, such as Asia/Shanghai. Uses system default timezone when empty. For all timezones, see: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab"
|
||||
},
|
||||
"http_proxy": {
|
||||
"description": "HTTP Proxy",
|
||||
"hint": "When enabled, proxy will be set by adding environment variables. Format: `http://ip:port`"
|
||||
},
|
||||
"no_proxy": {
|
||||
"description": "Direct Connection Address List"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,5 +62,33 @@
|
||||
"allowedHosts": "Allowed Hosts",
|
||||
"rateLimit": "Rate Limit",
|
||||
"encryption": "Encryption Settings"
|
||||
},
|
||||
"configSelection": {
|
||||
"selectConfig": "Select Configuration",
|
||||
"normalConfig": "Basic",
|
||||
"systemConfig": "System"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "Configuration Management",
|
||||
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
|
||||
"newConfig": "New Configuration",
|
||||
"editConfig": "Edit Configuration",
|
||||
"manageConfigs": "Manage Configurations...",
|
||||
"configName": "Name",
|
||||
"fillConfigName": "Enter configuration name",
|
||||
"confirmDelete": "Are you sure you want to delete the configuration \"{name}\"? This action cannot be undone.",
|
||||
"pleaseEnterName": "Please enter a configuration name",
|
||||
"createFailed": "Failed to create new configuration",
|
||||
"deleteFailed": "Failed to delete configuration",
|
||||
"updateFailed": "Failed to update configuration"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"update": "Update"
|
||||
},
|
||||
"codeEditor": {
|
||||
"title": "Edit Configuration File"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"tabs": {
|
||||
"all": "All",
|
||||
"chatCompletion": "Chat Completion",
|
||||
"agentRunner": "Agent Runner",
|
||||
"speechToText": "Speech to Text",
|
||||
"textToSpeech": "Text to Speech",
|
||||
"embedding": "Embedding",
|
||||
@@ -44,12 +45,13 @@
|
||||
"title": "Service Provider",
|
||||
"tabs": {
|
||||
"basic": "Basic",
|
||||
"agentRunner": "Agent Runner",
|
||||
"speechToText": "Speech to Text",
|
||||
"textToSpeech": "Text to Speech",
|
||||
"embedding": "Embedding",
|
||||
"rerank": "Rerank"
|
||||
},
|
||||
"noTemplates": "No {type} type provider templates available"
|
||||
"noTemplates": "No this type provider templates available"
|
||||
},
|
||||
"config": {
|
||||
"addTitle": "Add",
|
||||
|
||||
@@ -1,124 +1,110 @@
|
||||
{
|
||||
"title": "Session Management",
|
||||
"subtitle": "Manage active sessions and configurations",
|
||||
"title": "Custom Rules",
|
||||
"subtitle": "Set custom rules for specific sessions, which take priority over global settings",
|
||||
"buttons": {
|
||||
"refresh": "Refresh",
|
||||
"edit": "Edit",
|
||||
"apply": "Apply Batch Settings",
|
||||
"editName": "Edit Session Name",
|
||||
"editRule": "Edit Rules",
|
||||
"deleteAllRules": "Delete All Rules",
|
||||
"addRule": "Add Rule",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
"delete": "Delete",
|
||||
"clear": "Clear",
|
||||
"next": "Next",
|
||||
"editCustomName": "Edit Note",
|
||||
"batchDelete": "Batch Delete"
|
||||
},
|
||||
"sessions": {
|
||||
"activeSessions": "Active Sessions",
|
||||
"sessionCount": "sessions",
|
||||
"noActiveSessions": "No active sessions",
|
||||
"noActiveSessionsDesc": "Sessions will appear here when users interact with the bot"
|
||||
"customRules": {
|
||||
"title": "Custom Rules",
|
||||
"rulesCount": "rules",
|
||||
"hasRules": "Configured",
|
||||
"noRules": "No Custom Rules",
|
||||
"noRulesDesc": "Click 'Add Rule' to configure custom rules for specific sessions",
|
||||
"serviceConfig": "Service Config",
|
||||
"pluginConfig": "Plugin Config",
|
||||
"kbConfig": "Knowledge Base",
|
||||
"providerConfig": "Provider Config",
|
||||
"configured": "Configured",
|
||||
"noCustomName": "No note set"
|
||||
},
|
||||
"quickEditName": {
|
||||
"title": "Edit Note"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search sessions...",
|
||||
"platformFilter": "Platform Filter"
|
||||
"placeholder": "Search sessions..."
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"sessionStatus": "Session Status",
|
||||
"sessionInfo": "Session Info",
|
||||
"persona": "Persona",
|
||||
"chatProvider": "Chat Provider",
|
||||
"sttProvider": "STT Provider",
|
||||
"ttsProvider": "TTS Provider",
|
||||
"llmStatus": "LLM Status",
|
||||
"ttsStatus": "TTS Status",
|
||||
"knowledgeBase": "Knowledge Base",
|
||||
"pluginManagement": "Plugin Management",
|
||||
"umoInfo": "Unified Message Origin",
|
||||
"rulesOverview": "Rules Overview",
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"persona": {
|
||||
"none": "No Persona"
|
||||
"none": "Follow Config"
|
||||
},
|
||||
"batchOperations": {
|
||||
"title": "Batch Operations",
|
||||
"setPersona": "Batch Set Persona",
|
||||
"setChatProvider": "Batch Set Chat Provider",
|
||||
"setSttProvider": "Batch Set STT Provider",
|
||||
"setTtsProvider": "Batch Set TTS Provider",
|
||||
"setLlmStatus": "Batch Set LLM Status",
|
||||
"setTtsStatus": "Batch Set TTS Status",
|
||||
"noSttProvider": "No STT Provider Available",
|
||||
"noTtsProvider": "No TTS Provider Available"
|
||||
"provider": {
|
||||
"followConfig": "Follow Config"
|
||||
},
|
||||
"pluginManagement": {
|
||||
"title": "Plugin Management",
|
||||
"noPlugins": "No available plugins",
|
||||
"noPluginsDesc": "Currently no active plugins",
|
||||
"loading": "Loading plugin list...",
|
||||
"author": "Author"
|
||||
"addRule": {
|
||||
"title": "Add Custom Rule",
|
||||
"description": "Select a session (UMO) to configure custom rules. Custom rules take priority over global settings.",
|
||||
"selectUmo": "Select Session",
|
||||
"noUmos": "No sessions available"
|
||||
},
|
||||
"nameEditor": {
|
||||
"title": "Edit Session Name",
|
||||
"customName": "Custom Name",
|
||||
"placeholder": "Enter custom session name (leave empty to use original name)",
|
||||
"originalName": "Original Name",
|
||||
"fullSessionId": "Full Session ID",
|
||||
"hint": "Custom names help you easily identify sessions. The small information icon (!) will show the actual UMO when hovering."
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge Base Configuration",
|
||||
"configure": "Configure",
|
||||
"selectKB": "Select Knowledge Bases",
|
||||
"selectMultiple": "You can select multiple knowledge bases",
|
||||
"noKBAvailable": "No knowledge bases available",
|
||||
"noKBDesc": "No knowledge bases have been created yet",
|
||||
"createKB": "Create Knowledge Base",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"topK": "Result Count",
|
||||
"topKHint": "Number of results to retrieve from knowledge base",
|
||||
"enableRerank": "Enable Reranking",
|
||||
"enableRerankHint": "Use reranking model to improve retrieval quality",
|
||||
"clearConfig": "Clear Configuration",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"loading": "Loading knowledge base configuration...",
|
||||
"description": "Configure knowledge bases for this session. The session will use configured knowledge bases to enhance conversation context.",
|
||||
"saveSuccess": "Knowledge base configuration saved successfully",
|
||||
"saveFailed": "Failed to save knowledge base configuration",
|
||||
"loadFailed": "Failed to load knowledge base configuration",
|
||||
"clearSuccess": "Knowledge base configuration cleared",
|
||||
"clearFailed": "Failed to clear knowledge base configuration",
|
||||
"clearConfirm": "Are you sure you want to clear the knowledge base configuration for this session?"
|
||||
},
|
||||
"list": {
|
||||
"documents": "documents"
|
||||
"ruleEditor": {
|
||||
"title": "Edit Custom Rules",
|
||||
"description": "Configure custom rules for this session. These rules take priority over global settings.",
|
||||
"serviceConfig": {
|
||||
"title": "Service Configuration",
|
||||
"sessionEnabled": "Enable Session",
|
||||
"llmEnabled": "Enable LLM",
|
||||
"ttsEnabled": "Enable TTS",
|
||||
"customName": "Custom Name"
|
||||
},
|
||||
"providerConfig": {
|
||||
"title": "Provider Configuration",
|
||||
"chatProvider": "Chat Provider",
|
||||
"sttProvider": "STT Provider",
|
||||
"ttsProvider": "TTS Provider"
|
||||
},
|
||||
"personaConfig": {
|
||||
"title": "Persona Configuration",
|
||||
"selectPersona": "Select Persona",
|
||||
"hint": "Persona settings affect the conversation style and behavior of the LLM"
|
||||
},
|
||||
"pluginConfig": {
|
||||
"title": "Plugin Configuration",
|
||||
"disabledPlugins": "Disabled Plugins",
|
||||
"hint": "Select plugins to disable for this session. Unselected plugins will remain enabled."
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "Knowledge Base Configuration",
|
||||
"selectKbs": "Select Knowledge Bases",
|
||||
"topK": "Top K Results",
|
||||
"enableRerank": "Enable Reranking"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"message": "Are you sure you want to delete session {sessionName}?",
|
||||
"warning": "This action will permanently delete all chat history and preference settings for this session (except for data linked via plugins), and this cannot be undone. Continue?"
|
||||
"title": "Confirm Delete",
|
||||
"message": "Are you sure you want to delete all custom rules for this session? Global settings will be used after deletion."
|
||||
},
|
||||
"batchDeleteConfirm": {
|
||||
"title": "Confirm Batch Delete",
|
||||
"message": "Are you sure you want to delete {count} selected rules? Global settings will be used after deletion."
|
||||
},
|
||||
"messages": {
|
||||
"refreshSuccess": "Session list refreshed",
|
||||
"personaUpdateSuccess": "Persona updated successfully",
|
||||
"personaUpdateError": "Failed to update persona",
|
||||
"providerUpdateSuccess": "Provider updated successfully",
|
||||
"providerUpdateError": "Failed to update provider",
|
||||
"sessionStatusSuccess": "Session {status}",
|
||||
"llmStatusSuccess": "LLM {status}",
|
||||
"ttsStatusSuccess": "TTS {status}",
|
||||
"statusUpdateError": "Failed to update status",
|
||||
"loadSessionsError": "Failed to load session list",
|
||||
"batchUpdateSuccess": "Successfully batch updated {count} settings",
|
||||
"batchUpdatePartial": "Batch update completed, {success} successful, {error} failed",
|
||||
"loadPluginsError": "Failed to load plugin list",
|
||||
"pluginStatusSuccess": "Plugin {name} {status}",
|
||||
"pluginStatusError": "Failed to update plugin status",
|
||||
"nameUpdateSuccess": "Session name updated successfully",
|
||||
"nameUpdateError": "Failed to update session name",
|
||||
"deleteSuccess": "Session deleted successfully",
|
||||
"deleteError": "Failed to delete session"
|
||||
"refreshSuccess": "Data refreshed",
|
||||
"loadError": "Failed to load data",
|
||||
"saveSuccess": "Saved successfully",
|
||||
"saveError": "Failed to save",
|
||||
"clearSuccess": "Cleared successfully",
|
||||
"clearError": "Failed to clear",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"deleteError": "Failed to delete",
|
||||
"noChanges": "No changes to save",
|
||||
"batchDeleteSuccess": "Batch delete successful",
|
||||
"batchDeleteError": "Batch delete failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"config": "配置文件",
|
||||
"chat": "聊天",
|
||||
"conversation": "对话数据",
|
||||
"sessionManagement": "会话管理",
|
||||
"sessionManagement": "自定义规则",
|
||||
"console": "控制台",
|
||||
"alkaid": "Alkaid",
|
||||
"knowledgeBase": "知识库",
|
||||
|
||||
45
dashboard/src/i18n/locales/zh-CN/core/shared.json
Normal file
45
dashboard/src/i18n/locales/zh-CN/core/shared.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"knowledgeBaseSelector": {
|
||||
"notSelected": "未选择",
|
||||
"buttonText": "选择知识库...",
|
||||
"dialogTitle": "选择知识库",
|
||||
"loading": "加载中...",
|
||||
"noKnowledgeBases": "暂无知识库",
|
||||
"createKnowledgeBase": "创建知识库",
|
||||
"selectedCount": "已选择 {count} 个知识库",
|
||||
"confirmSelection": "确认选择",
|
||||
"cancelSelection": "取消",
|
||||
"noDescription": "无描述",
|
||||
"documentCount": "{count} 个文档",
|
||||
"chunkCount": "{count} 个块"
|
||||
},
|
||||
"pluginSetSelector": {
|
||||
"notSelected": "未启用任何插件",
|
||||
"allPlugins": "启用所有插件 (*)",
|
||||
"selectedCount": "已选择 {count} 个插件",
|
||||
"buttonText": "选择插件集合...",
|
||||
"dialogTitle": "选择插件集合",
|
||||
"loading": "加载中...",
|
||||
"enableAll": "启用所有插件",
|
||||
"enableNone": "不启用任何插件",
|
||||
"customSelect": "自定义选择",
|
||||
"noPlugins": "暂无可用的插件",
|
||||
"confirmSelection": "确认选择",
|
||||
"cancelSelection": "取消",
|
||||
"noDescription": "无描述",
|
||||
"notActivated": "未激活",
|
||||
"note": "*不显示系统插件和已经在插件页禁用的插件。"
|
||||
},
|
||||
"providerSelector": {
|
||||
"notSelected": "未选择",
|
||||
"buttonText": "选择提供商...",
|
||||
"dialogTitle": "选择提供商",
|
||||
"loading": "加载中...",
|
||||
"noProviders": "暂无可用的提供商",
|
||||
"confirmSelection": "确认选择",
|
||||
"cancelSelection": "取消",
|
||||
"clearSelection": "不选择",
|
||||
"clearSelectionSubtitle": "清除当前选择",
|
||||
"unknownType": "未知类型"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@
|
||||
"editDisplayName": "编辑会话名称",
|
||||
"displayName": "会话名称",
|
||||
"displayNameUpdated": "会话名称已更新",
|
||||
"displayNameUpdateFailed": "更新会话名称失败"
|
||||
"displayNameUpdateFailed": "更新会话名称失败",
|
||||
"confirmDelete": "确定要删除“{name}”吗?此操作无法撤销。"
|
||||
},
|
||||
"modes": {
|
||||
"darkMode": "切换到夜间模式",
|
||||
@@ -84,5 +85,9 @@
|
||||
"reconnected": "聊天连接已重新建立",
|
||||
"failed": "连接失败,请刷新页面重试"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"sendMessageFailed": "发送消息失败,请重试",
|
||||
"createSessionFailed": "创建会话失败,请刷新页面重试"
|
||||
}
|
||||
}
|
||||
456
dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
Normal file
456
dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
Normal file
@@ -0,0 +1,456 @@
|
||||
{
|
||||
"ai_group": {
|
||||
"name": "AI 配置",
|
||||
"agent_runner": {
|
||||
"description": "Agent 执行方式",
|
||||
"hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify 或 Coze 等第三方 Agent 执行器,不需要修改此节。",
|
||||
"provider_settings": {
|
||||
"enable": {
|
||||
"description": "启用",
|
||||
"hint": "AI 对话总开关"
|
||||
},
|
||||
"agent_runner_type": {
|
||||
"description": "执行器",
|
||||
"labels": ["内置 Agent", "Dify", "Coze", "阿里云百炼应用"]
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent 执行器提供商 ID"
|
||||
},
|
||||
"dify_agent_runner_provider_id": {
|
||||
"description": "Dify Agent 执行器提供商 ID"
|
||||
},
|
||||
"dashscope_agent_runner_provider_id": {
|
||||
"description": "阿里云百炼应用 Agent 执行器提供商 ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ai": {
|
||||
"description": "模型",
|
||||
"hint": "当使用非内置 Agent 执行器时,默认聊天模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
|
||||
"provider_settings": {
|
||||
"default_provider_id": {
|
||||
"description": "默认聊天模型",
|
||||
"hint": "留空时使用第一个模型"
|
||||
},
|
||||
"default_image_caption_provider_id": {
|
||||
"description": "默认图片转述模型",
|
||||
"hint": "留空代表不使用,可用于非多模态模型"
|
||||
},
|
||||
"image_caption_prompt": {
|
||||
"description": "图片转述提示词"
|
||||
}
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": {
|
||||
"description": "启用语音转文本",
|
||||
"hint": "STT 总开关"
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "默认语音转文本模型",
|
||||
"hint": "用户也可使用 /provider 指令单独选择会话的 STT 模型。"
|
||||
}
|
||||
},
|
||||
"provider_tts_settings": {
|
||||
"enable": {
|
||||
"description": "启用文本转语音",
|
||||
"hint": "TTS 总开关"
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "默认文本转语音模型"
|
||||
}
|
||||
}
|
||||
},
|
||||
"persona": {
|
||||
"description": "人格",
|
||||
"provider_settings": {
|
||||
"default_personality": {
|
||||
"description": "默认采用的人格"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgebase": {
|
||||
"description": "知识库",
|
||||
"kb_names": {
|
||||
"description": "知识库列表",
|
||||
"hint": "支持多选"
|
||||
},
|
||||
"kb_fusion_top_k": {
|
||||
"description": "融合检索结果数",
|
||||
"hint": "多个知识库检索结果融合后的返回结果数量"
|
||||
},
|
||||
"kb_final_top_k": {
|
||||
"description": "最终返回结果数",
|
||||
"hint": "从知识库中检索到的结果数量,越大可能获得越多相关信息,但也可能引入噪音。建议根据实际需求调整"
|
||||
},
|
||||
"kb_agentic_mode": {
|
||||
"description": "Agentic 知识库检索",
|
||||
"hint": "启用后,知识库检索将作为 LLM Tool,由模型自主决定何时调用知识库进行查询。需要模型支持函数调用能力。"
|
||||
}
|
||||
},
|
||||
"websearch": {
|
||||
"description": "网页搜索",
|
||||
"provider_settings": {
|
||||
"web_search": {
|
||||
"description": "启用网页搜索"
|
||||
},
|
||||
"websearch_provider": {
|
||||
"description": "网页搜索提供商"
|
||||
},
|
||||
"websearch_tavily_key": {
|
||||
"description": "Tavily API Key",
|
||||
"hint": "可添加多个 Key 进行轮询。"
|
||||
},
|
||||
"websearch_baidu_app_builder_key": {
|
||||
"description": "百度千帆智能云 APP Builder API Key",
|
||||
"hint": "参考:https://console.bce.baidu.com/iam/#/iam/apikey/list"
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "显示来源引用"
|
||||
}
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "其他配置",
|
||||
"provider_settings": {
|
||||
"display_reasoning_text": {
|
||||
"description": "显示思考内容"
|
||||
},
|
||||
"identifier": {
|
||||
"description": "用户识别",
|
||||
"hint": "启用后,会在提示词前包含用户 ID 信息。"
|
||||
},
|
||||
"group_name_display": {
|
||||
"description": "显示群名称",
|
||||
"hint": "启用后,在支持的平台(OneBot v11)上会在提示词前包含群名称信息。"
|
||||
},
|
||||
"datetime_system_prompt": {
|
||||
"description": "现实世界时间感知",
|
||||
"hint": "启用后,会在系统提示词中附带当前时间信息。"
|
||||
},
|
||||
"show_tool_use_status": {
|
||||
"description": "输出函数调用状态"
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "工具调用轮数上限"
|
||||
},
|
||||
"tool_call_timeout": {
|
||||
"description": "工具调用超时时间(秒)"
|
||||
},
|
||||
"streaming_response": {
|
||||
"description": "流式输出"
|
||||
},
|
||||
"unsupported_streaming_strategy": {
|
||||
"description": "不支持流式回复的平台",
|
||||
"hint": "选择在不支持流式回复的平台上的处理方式。实时分段回复会在系统接收流式响应检测到诸如标点符号等分段点时,立即发送当前已接收的内容",
|
||||
"labels": ["实时分段回复", "关闭流式回复"]
|
||||
},
|
||||
"max_context_length": {
|
||||
"description": "最多携带对话轮数",
|
||||
"hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条,-1 为不限制"
|
||||
},
|
||||
"dequeue_context_length": {
|
||||
"description": "丢弃对话轮数",
|
||||
"hint": "超出最多携带对话轮数时, 一次丢弃的聊天轮数"
|
||||
},
|
||||
"wake_prefix": {
|
||||
"description": "LLM 聊天额外唤醒前缀",
|
||||
"hint": "如果唤醒前缀为 /, 额外聊天唤醒前缀为 chat,则需要 /chat 才会触发 LLM 请求"
|
||||
},
|
||||
"prompt_prefix": {
|
||||
"description": "用户提示词",
|
||||
"hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。"
|
||||
}
|
||||
},
|
||||
"provider_tts_settings": {
|
||||
"dual_output": {
|
||||
"description": "开启 TTS 时同时输出语音和文字内容"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"platform_group": {
|
||||
"name": "平台配置",
|
||||
"general": {
|
||||
"description": "基本",
|
||||
"admins_id": {
|
||||
"description": "管理员 ID"
|
||||
},
|
||||
"platform_settings": {
|
||||
"unique_session": {
|
||||
"description": "隔离会话",
|
||||
"hint": "启用后,群成员的上下文独立。"
|
||||
},
|
||||
"friend_message_needs_wake_prefix": {
|
||||
"description": "私聊消息需要唤醒词"
|
||||
},
|
||||
"reply_prefix": {
|
||||
"description": "回复时的文本前缀"
|
||||
},
|
||||
"reply_with_mention": {
|
||||
"description": "回复时 @ 发送人"
|
||||
},
|
||||
"reply_with_quote": {
|
||||
"description": "回复时引用发送人消息"
|
||||
},
|
||||
"forward_threshold": {
|
||||
"description": "转发消息的字数阈值"
|
||||
},
|
||||
"empty_mention_waiting": {
|
||||
"description": "只 @ 机器人是否触发等待"
|
||||
}
|
||||
},
|
||||
"wake_prefix": {
|
||||
"description": "唤醒词"
|
||||
}
|
||||
},
|
||||
"whitelist": {
|
||||
"description": "白名单",
|
||||
"platform_settings": {
|
||||
"enable_id_white_list": {
|
||||
"description": "启用白名单",
|
||||
"hint": "启用后,只有在白名单内的会话会被响应。"
|
||||
},
|
||||
"id_whitelist": {
|
||||
"description": "白名单 ID 列表",
|
||||
"hint": "使用 /sid 获取 ID。"
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"description": "输出日志",
|
||||
"hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。"
|
||||
},
|
||||
"wl_ignore_admin_on_group": {
|
||||
"description": "管理员群组消息无视 ID 白名单"
|
||||
},
|
||||
"wl_ignore_admin_on_friend": {
|
||||
"description": "管理员私聊消息无视 ID 白名单"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
"description": "速率限制",
|
||||
"platform_settings": {
|
||||
"rate_limit": {
|
||||
"time": {
|
||||
"description": "消息速率限制时间(秒)"
|
||||
},
|
||||
"count": {
|
||||
"description": "消息速率限制计数"
|
||||
},
|
||||
"strategy": {
|
||||
"description": "速率限制策略"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content_safety": {
|
||||
"description": "内容安全",
|
||||
"content_safety": {
|
||||
"also_use_in_response": {
|
||||
"description": "同时检查模型的响应内容"
|
||||
},
|
||||
"baidu_aip": {
|
||||
"enable": {
|
||||
"description": "使用百度内容安全审核",
|
||||
"hint": "您需要手动安装 baidu-aip 库。"
|
||||
},
|
||||
"app_id": {
|
||||
"description": "App ID"
|
||||
},
|
||||
"api_key": {
|
||||
"description": "API Key"
|
||||
},
|
||||
"secret_key": {
|
||||
"description": "Secret Key"
|
||||
}
|
||||
},
|
||||
"internal_keywords": {
|
||||
"enable": {
|
||||
"description": "关键词检查"
|
||||
},
|
||||
"extra_keywords": {
|
||||
"description": "额外关键词",
|
||||
"hint": "额外的屏蔽关键词列表,支持正则表达式。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"t2i": {
|
||||
"description": "文本转图像",
|
||||
"t2i": {
|
||||
"description": "文本转图像输出"
|
||||
},
|
||||
"t2i_word_threshold": {
|
||||
"description": "文本转图像字数阈值"
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "其他配置",
|
||||
"platform_settings": {
|
||||
"ignore_bot_self_message": {
|
||||
"description": "是否忽略机器人自身的消息"
|
||||
},
|
||||
"ignore_at_all": {
|
||||
"description": "是否忽略 @ 全体成员事件"
|
||||
},
|
||||
"no_permission_reply": {
|
||||
"description": "用户权限不足时是否回复"
|
||||
}
|
||||
},
|
||||
"platform_specific": {
|
||||
"lark": {
|
||||
"pre_ack_emoji": {
|
||||
"enable": {
|
||||
"description": "[飞书] 启用预回应表情"
|
||||
},
|
||||
"emojis": {
|
||||
"description": "表情列表(飞书表情枚举名)",
|
||||
"hint": "表情枚举名参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce"
|
||||
}
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"pre_ack_emoji": {
|
||||
"enable": {
|
||||
"description": "[Telegram] 启用预回应表情"
|
||||
},
|
||||
"emojis": {
|
||||
"description": "表情列表(Unicode)",
|
||||
"hint": "Telegram 仅支持固定反应集合,参考:https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugin_group": {
|
||||
"name": "插件配置",
|
||||
"plugin": {
|
||||
"description": "插件",
|
||||
"plugin_set": {
|
||||
"description": "可用插件",
|
||||
"hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ext_group": {
|
||||
"name": "扩展功能",
|
||||
"segmented_reply": {
|
||||
"description": "分段回复",
|
||||
"platform_settings": {
|
||||
"segmented_reply": {
|
||||
"enable": {
|
||||
"description": "启用分段回复"
|
||||
},
|
||||
"only_llm_result": {
|
||||
"description": "仅对 LLM 结果分段"
|
||||
},
|
||||
"interval_method": {
|
||||
"description": "间隔方法"
|
||||
},
|
||||
"interval": {
|
||||
"description": "随机间隔时间",
|
||||
"hint": "格式:最小值,最大值(如:1.5,3.5)"
|
||||
},
|
||||
"log_base": {
|
||||
"description": "对数底数",
|
||||
"hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。"
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"description": "分段回复字数阈值"
|
||||
},
|
||||
"regex": {
|
||||
"description": "分段正则表达式"
|
||||
},
|
||||
"content_cleanup_rule": {
|
||||
"description": "内容过滤正则表达式",
|
||||
"hint": "移除分段后内容中的指定内容。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ltm": {
|
||||
"description": "群聊上下文感知(原聊天记忆增强)",
|
||||
"provider_ltm_settings": {
|
||||
"group_icl_enable": {
|
||||
"description": "启用群聊上下文感知"
|
||||
},
|
||||
"group_message_max_cnt": {
|
||||
"description": "最大消息数量"
|
||||
},
|
||||
"image_caption": {
|
||||
"description": "自动理解图片",
|
||||
"hint": "需要设置群聊图片转述模型。"
|
||||
},
|
||||
"image_caption_provider_id": {
|
||||
"description": "群聊图片转述模型",
|
||||
"hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。"
|
||||
},
|
||||
"active_reply": {
|
||||
"enable": {
|
||||
"description": "主动回复"
|
||||
},
|
||||
"method": {
|
||||
"description": "主动回复方法"
|
||||
},
|
||||
"possibility_reply": {
|
||||
"description": "回复概率",
|
||||
"hint": "0.0-1.0 之间的数值"
|
||||
},
|
||||
"whitelist": {
|
||||
"description": "主动回复白名单",
|
||||
"hint": "为空时不启用白名单过滤。使用 /sid 获取 ID。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_group": {
|
||||
"name": "系统配置",
|
||||
"system": {
|
||||
"description": "系统配置",
|
||||
"t2i_strategy": {
|
||||
"description": "文本转图像策略",
|
||||
"hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。"
|
||||
},
|
||||
"t2i_endpoint": {
|
||||
"description": "文本转图像服务 API 地址",
|
||||
"hint": "为空时使用 AstrBot API 服务"
|
||||
},
|
||||
"t2i_template": {
|
||||
"description": "文本转图像自定义模版",
|
||||
"hint": "启用后可自定义 HTML 模板用于文转图渲染。"
|
||||
},
|
||||
"t2i_active_template": {
|
||||
"description": "当前应用的文转图渲染模板",
|
||||
"hint": "此处的值由文转图模板管理页面进行维护。"
|
||||
},
|
||||
"log_level": {
|
||||
"description": "控制台日志级别",
|
||||
"hint": "控制台输出日志的级别。"
|
||||
},
|
||||
"pip_install_arg": {
|
||||
"description": "pip 安装额外参数",
|
||||
"hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。"
|
||||
},
|
||||
"pypi_index_url": {
|
||||
"description": "PyPI 软件仓库地址",
|
||||
"hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/"
|
||||
},
|
||||
"callback_api_base": {
|
||||
"description": "对外可达的回调接口地址",
|
||||
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定外部服务如何访问 AstrBot 的地址。如 http://localhost:6185,https://example.com 等。"
|
||||
},
|
||||
"timezone": {
|
||||
"description": "时区",
|
||||
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab"
|
||||
},
|
||||
"http_proxy": {
|
||||
"description": "HTTP 代理",
|
||||
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`"
|
||||
},
|
||||
"no_proxy": {
|
||||
"description": "直连地址列表"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,5 +62,32 @@
|
||||
"allowedHosts": "允许的主机",
|
||||
"rateLimit": "频率限制",
|
||||
"encryption": "加密设置"
|
||||
},
|
||||
"configSelection": {
|
||||
"selectConfig": "选择配置文件",
|
||||
"normalConfig": "普通",
|
||||
"systemConfig": "系统"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "配置文件管理",
|
||||
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
|
||||
"newConfig": "新建配置文件",
|
||||
"editConfig": "编辑配置文件",
|
||||
"manageConfigs": "管理配置文件...",
|
||||
"configName": "名称",
|
||||
"fillConfigName": "填写配置文件名称",
|
||||
"confirmDelete": "确定要删除配置文件 \"{name}\" 吗?此操作不可恢复。",
|
||||
"pleaseEnterName": "请填写配置名称",
|
||||
"createFailed": "新配置文件创建失败",
|
||||
"deleteFailed": "删除配置文件失败",
|
||||
"updateFailed": "更新配置文件失败"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "取消",
|
||||
"create": "创建",
|
||||
"update": "更新"
|
||||
},
|
||||
"codeEditor": {
|
||||
"title": "编辑配置文件"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
"providerType": "提供商类型",
|
||||
"tabs": {
|
||||
"all": "全部",
|
||||
"chatCompletion": "基本对话",
|
||||
"chatCompletion": "对话",
|
||||
"agentRunner": "Agent 执行器",
|
||||
"speechToText": "语音转文字",
|
||||
"textToSpeech": "文字转语音",
|
||||
"embedding": "嵌入(Embedding)",
|
||||
@@ -44,13 +45,14 @@
|
||||
"addProvider": {
|
||||
"title": "模型提供商",
|
||||
"tabs": {
|
||||
"basic": "基本",
|
||||
"basic": "对话",
|
||||
"agentRunner": "Agent 执行器",
|
||||
"speechToText": "语音转文字",
|
||||
"textToSpeech": "文字转语音",
|
||||
"embedding": "嵌入(Embedding)",
|
||||
"rerank": "重排序(Rerank)"
|
||||
},
|
||||
"noTemplates": "暂无{type}类型的提供商模板"
|
||||
"noTemplates": "暂无该类型的提供商模板"
|
||||
},
|
||||
"config": {
|
||||
"addTitle": "新增",
|
||||
|
||||
@@ -1,124 +1,110 @@
|
||||
{
|
||||
"title": "会话管理",
|
||||
"subtitle": "管理活跃会话和配置",
|
||||
"title": "自定义规则",
|
||||
"subtitle": "为特定会话设置自定义规则,优先级高于全局配置",
|
||||
"buttons": {
|
||||
"refresh": "刷新",
|
||||
"edit": "编辑",
|
||||
"apply": "应用批量设置",
|
||||
"editName": "备注",
|
||||
"editRule": "编辑规则",
|
||||
"deleteAllRules": "删除所有规则",
|
||||
"addRule": "添加规则",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
"delete": "删除",
|
||||
"clear": "清除",
|
||||
"next": "下一步",
|
||||
"editCustomName": "编辑备注",
|
||||
"batchDelete": "批量删除"
|
||||
},
|
||||
"sessions": {
|
||||
"activeSessions": "活跃会话",
|
||||
"sessionCount": "个会话",
|
||||
"noActiveSessions": "暂无活跃会话",
|
||||
"noActiveSessionsDesc": "当有用户与机器人交互时,会话将会显示在这里"
|
||||
"customRules": {
|
||||
"title": "自定义规则",
|
||||
"rulesCount": "条规则",
|
||||
"hasRules": "已配置",
|
||||
"noRules": "暂无自定义规则",
|
||||
"noRulesDesc": "点击「添加规则」为特定会话配置自定义规则",
|
||||
"serviceConfig": "服务配置",
|
||||
"pluginConfig": "插件配置",
|
||||
"kbConfig": "知识库配置",
|
||||
"providerConfig": "模型配置",
|
||||
"configured": "已配置",
|
||||
"noCustomName": "未设置备注"
|
||||
},
|
||||
"quickEditName": {
|
||||
"title": "编辑备注名"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索会话...",
|
||||
"platformFilter": "平台筛选"
|
||||
"placeholder": "搜索会话..."
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"sessionStatus": "会话状态",
|
||||
"sessionInfo": "消息会话来源",
|
||||
"persona": "人格",
|
||||
"chatProvider": "聊天模型",
|
||||
"sttProvider": "语音识别模型",
|
||||
"ttsProvider": "语音合成模型",
|
||||
"llmStatus": "启用 LLM",
|
||||
"ttsStatus": "启用 TTS",
|
||||
"knowledgeBase": "知识库配置",
|
||||
"pluginManagement": "插件管理",
|
||||
"umoInfo": "消息会话来源",
|
||||
"rulesOverview": "规则概览",
|
||||
"actions": "操作"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用"
|
||||
},
|
||||
"persona": {
|
||||
"none": "无人格"
|
||||
"none": "跟随配置文件"
|
||||
},
|
||||
"batchOperations": {
|
||||
"title": "批量操作",
|
||||
"setPersona": "批量设置人格",
|
||||
"setChatProvider": "批量设置 Chat Provider",
|
||||
"setSttProvider": "批量设置 STT Provider",
|
||||
"setTtsProvider": "批量设置 TTS Provider",
|
||||
"setLlmStatus": "批量设置 LLM 状态",
|
||||
"setTtsStatus": "批量设置 TTS 状态",
|
||||
"noSttProvider": "暂无可用 STT Provider",
|
||||
"noTtsProvider": "暂无可用 TTS Provider"
|
||||
"provider": {
|
||||
"followConfig": "跟随配置文件"
|
||||
},
|
||||
"pluginManagement": {
|
||||
"title": "插件管理",
|
||||
"noPlugins": "暂无可用插件",
|
||||
"noPluginsDesc": "目前没有激活的插件",
|
||||
"loading": "加载插件列表中...",
|
||||
"author": "作者"
|
||||
"addRule": {
|
||||
"title": "添加自定义规则",
|
||||
"description": "选择一个消息会话来源 (UMO) 来配置自定义规则。自定义规则的优先级高于该来源所属的配置文件中的全局规则。可以使用 /sid 指令获取该来源的 UMO 信息。",
|
||||
"selectUmo": "选择会话",
|
||||
"noUmos": "暂无可用会话"
|
||||
},
|
||||
"nameEditor": {
|
||||
"title": "编辑会话名称",
|
||||
"customName": "自定义名称",
|
||||
"placeholder": "输入自定义会话名称(留空则使用原始名称)",
|
||||
"originalName": "原始名称",
|
||||
"fullSessionId": "完整会话ID",
|
||||
"hint": "自定义名称帮助您轻松识别会话。当设置了自定义名称时,会显示一个小感叹号标识(!),鼠标悬停时会显示实际的UMO。"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "知识库配置",
|
||||
"configure": "配置",
|
||||
"selectKB": "选择知识库",
|
||||
"selectMultiple": "可以选择多个知识库",
|
||||
"noKBAvailable": "暂无可用的知识库",
|
||||
"noKBDesc": "目前没有创建任何知识库",
|
||||
"createKB": "创建知识库",
|
||||
"advancedSettings": "高级配置",
|
||||
"topK": "返回结果数量",
|
||||
"topKHint": "从知识库检索的结果数量",
|
||||
"enableRerank": "启用重排序",
|
||||
"enableRerankHint": "使用重排序模型提高检索质量",
|
||||
"clearConfig": "清除配置",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"loading": "加载知识库配置中...",
|
||||
"description": "为此会话配置使用的知识库。会话将使用配置的知识库来增强对话上下文。",
|
||||
"saveSuccess": "知识库配置保存成功",
|
||||
"saveFailed": "保存知识库配置失败",
|
||||
"loadFailed": "加载知识库配置失败",
|
||||
"clearSuccess": "知识库配置已清除",
|
||||
"clearFailed": "清除知识库配置失败",
|
||||
"clearConfirm": "确定要清除此会话的知识库配置吗?"
|
||||
},
|
||||
"list": {
|
||||
"documents": "篇文档"
|
||||
"ruleEditor": {
|
||||
"title": "编辑自定义规则",
|
||||
"description": "为此会话配置自定义规则,这些规则将优先于全局配置生效。",
|
||||
"serviceConfig": {
|
||||
"title": "服务配置",
|
||||
"sessionEnabled": "启用该消息会话来源的消息处理",
|
||||
"llmEnabled": "启用 LLM",
|
||||
"ttsEnabled": "启用 TTS",
|
||||
"customName": "消息会话来源备注名称"
|
||||
},
|
||||
"providerConfig": {
|
||||
"title": "模型配置",
|
||||
"chatProvider": "聊天模型",
|
||||
"sttProvider": "语音识别模型",
|
||||
"ttsProvider": "语音合成模型"
|
||||
},
|
||||
"personaConfig": {
|
||||
"title": "人格配置",
|
||||
"selectPersona": "选择人格",
|
||||
"hint": "应用人格配置后,将会强制该来源的所有对话使用该人格。"
|
||||
},
|
||||
"pluginConfig": {
|
||||
"title": "插件配置",
|
||||
"disabledPlugins": "禁用的插件",
|
||||
"hint": "选择要在此会话中禁用的插件。未选择的插件将保持启用状态。"
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "知识库配置",
|
||||
"selectKbs": "选择知识库",
|
||||
"topK": "返回结果数量 (Top K)",
|
||||
"enableRerank": "启用重排序"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"message": "确定要删除会话 {sessionName} 吗?",
|
||||
"warning": "此操作将永久删除本次会话的「全部对话记录」与「偏好设置」(插件对会话的关联数据除外),且无法恢复。确认继续?"
|
||||
"title": "确认删除",
|
||||
"message": "确定要删除此会话的所有自定义规则吗?删除后将恢复使用全局配置。"
|
||||
},
|
||||
"batchDeleteConfirm": {
|
||||
"title": "确认批量删除",
|
||||
"message": "确定要删除选中的 {count} 条规则吗?删除后将恢复使用全局配置。"
|
||||
},
|
||||
"messages": {
|
||||
"refreshSuccess": "会话列表已刷新",
|
||||
"personaUpdateSuccess": "人格更新成功",
|
||||
"personaUpdateError": "人格更新失败",
|
||||
"providerUpdateSuccess": "Provider 更新成功",
|
||||
"providerUpdateError": "Provider 更新失败",
|
||||
"sessionStatusSuccess": "会话 {status}",
|
||||
"llmStatusSuccess": "LLM {status}",
|
||||
"ttsStatusSuccess": "TTS {status}",
|
||||
"statusUpdateError": "状态更新失败",
|
||||
"loadSessionsError": "加载会话列表失败",
|
||||
"batchUpdateSuccess": "成功批量更新 {count} 项设置",
|
||||
"batchUpdatePartial": "批量更新完成,{success} 项成功,{error} 项失败",
|
||||
"loadPluginsError": "加载插件列表失败",
|
||||
"pluginStatusSuccess": "插件 {name} {status}",
|
||||
"pluginStatusError": "插件状态更新失败",
|
||||
"nameUpdateSuccess": "会话名称更新成功",
|
||||
"nameUpdateError": "会话名称更新失败",
|
||||
"deleteSuccess": "会话删除成功",
|
||||
"deleteError": "会话删除失败"
|
||||
"refreshSuccess": "数据已刷新",
|
||||
"loadError": "加载数据失败",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveError": "保存失败",
|
||||
"clearSuccess": "已清除",
|
||||
"clearError": "清除失败",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteError": "删除失败",
|
||||
"noChanges": "没有需要保存的更改",
|
||||
"batchDeleteSuccess": "批量删除成功",
|
||||
"batchDeleteError": "批量删除失败"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import zhCNActions from './locales/zh-CN/core/actions.json';
|
||||
import zhCNStatus from './locales/zh-CN/core/status.json';
|
||||
import zhCNNavigation from './locales/zh-CN/core/navigation.json';
|
||||
import zhCNHeader from './locales/zh-CN/core/header.json';
|
||||
import zhCNShared from './locales/zh-CN/core/shared.json';
|
||||
|
||||
import zhCNChat from './locales/zh-CN/features/chat.json';
|
||||
import zhCNExtension from './locales/zh-CN/features/extension.json';
|
||||
@@ -16,6 +17,7 @@ import zhCNToolUse from './locales/zh-CN/features/tool-use.json';
|
||||
import zhCNProvider from './locales/zh-CN/features/provider.json';
|
||||
import zhCNPlatform from './locales/zh-CN/features/platform.json';
|
||||
import zhCNConfig from './locales/zh-CN/features/config.json';
|
||||
import zhCNConfigMetadata from './locales/zh-CN/features/config-metadata.json';
|
||||
import zhCNConsole from './locales/zh-CN/features/console.json';
|
||||
import zhCNAbout from './locales/zh-CN/features/about.json';
|
||||
import zhCNSettings from './locales/zh-CN/features/settings.json';
|
||||
@@ -41,6 +43,7 @@ import enUSActions from './locales/en-US/core/actions.json';
|
||||
import enUSStatus from './locales/en-US/core/status.json';
|
||||
import enUSNavigation from './locales/en-US/core/navigation.json';
|
||||
import enUSHeader from './locales/en-US/core/header.json';
|
||||
import enUSShared from './locales/en-US/core/shared.json';
|
||||
|
||||
import enUSChat from './locales/en-US/features/chat.json';
|
||||
import enUSExtension from './locales/en-US/features/extension.json';
|
||||
@@ -50,6 +53,7 @@ import enUSToolUse from './locales/en-US/features/tool-use.json';
|
||||
import enUSProvider from './locales/en-US/features/provider.json';
|
||||
import enUSPlatform from './locales/en-US/features/platform.json';
|
||||
import enUSConfig from './locales/en-US/features/config.json';
|
||||
import enUSConfigMetadata from './locales/en-US/features/config-metadata.json';
|
||||
import enUSConsole from './locales/en-US/features/console.json';
|
||||
import enUSAbout from './locales/en-US/features/about.json';
|
||||
import enUSSettings from './locales/en-US/features/settings.json';
|
||||
@@ -77,7 +81,8 @@ export const translations = {
|
||||
actions: zhCNActions,
|
||||
status: zhCNStatus,
|
||||
navigation: zhCNNavigation,
|
||||
header: zhCNHeader
|
||||
header: zhCNHeader,
|
||||
shared: zhCNShared
|
||||
},
|
||||
features: {
|
||||
chat: zhCNChat,
|
||||
@@ -88,6 +93,7 @@ export const translations = {
|
||||
provider: zhCNProvider,
|
||||
platform: zhCNPlatform,
|
||||
config: zhCNConfig,
|
||||
'config-metadata': zhCNConfigMetadata,
|
||||
console: zhCNConsole,
|
||||
about: zhCNAbout,
|
||||
settings: zhCNSettings,
|
||||
@@ -119,7 +125,8 @@ export const translations = {
|
||||
actions: enUSActions,
|
||||
status: enUSStatus,
|
||||
navigation: enUSNavigation,
|
||||
header: enUSHeader
|
||||
header: enUSHeader,
|
||||
shared: enUSShared
|
||||
},
|
||||
features: {
|
||||
chat: enUSChat,
|
||||
@@ -130,6 +137,7 @@ export const translations = {
|
||||
provider: enUSProvider,
|
||||
platform: enUSPlatform,
|
||||
config: enUSConfig,
|
||||
'config-metadata': enUSConfigMetadata,
|
||||
console: enUSConsole,
|
||||
about: enUSAbout,
|
||||
settings: enUSSettings,
|
||||
|
||||
@@ -69,7 +69,7 @@ const sidebarItem: menu[] = [
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.sessionManagement',
|
||||
icon: 'mdi-account-group',
|
||||
icon: 'mdi-pencil-ruler',
|
||||
to: '/session-management'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
style="margin-bottom: 16px; align-items: center; gap: 12px; justify-content: space-between; width: 100%;">
|
||||
<div class="d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
v-if="!isSystemConfig" item-value="id" label="选择配置文件" hide-details density="compact" rounded="md"
|
||||
v-if="!isSystemConfig" item-value="id" :label="tm('configSelection.selectConfig')" hide-details density="compact" rounded="md"
|
||||
variant="outlined" @update:model-value="onConfigSelect">
|
||||
</v-select>
|
||||
<a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a>
|
||||
@@ -19,10 +19,10 @@
|
||||
<v-btn-toggle v-model="configType" mandatory color="primary" variant="outlined" density="comfortable"
|
||||
rounded="md" @update:model-value="onConfigTypeToggle">
|
||||
<v-btn value="normal" prepend-icon="mdi-cog" size="large">
|
||||
普通
|
||||
{{ tm('configSelection.normalConfig') }}
|
||||
</v-btn>
|
||||
<v-btn value="system" prepend-icon="mdi-cog-outline" size="large">
|
||||
系统
|
||||
{{ tm('configSelection.systemConfig') }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
@@ -45,6 +45,15 @@
|
||||
@click="configToString(); codeEditorDialog = true">
|
||||
</v-btn>
|
||||
|
||||
<v-tooltip text="测试当前配置" location="left" v-if="!isSystemConfig">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-chat-processing" size="x-large"
|
||||
style="position: fixed; right: 52px; bottom: 196px;" color="secondary"
|
||||
@click="openTestChat">
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
|
||||
@@ -59,7 +68,7 @@
|
||||
<v-btn icon @click="codeEditorDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>编辑配置文件</v-toolbar-title>
|
||||
<v-toolbar-title>{{ tm('codeEditor.title') }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items style="display: flex; align-items: center;">
|
||||
<v-btn style="margin-left: 16px;" size="small" @click="configToString()">{{
|
||||
@@ -81,15 +90,15 @@
|
||||
<v-dialog v-model="configManageDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<span class="text-h4">配置文件管理</span>
|
||||
<span class="text-h4">{{ tm('configManagement.title') }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="configManageDialog = false"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<small>AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。</small>
|
||||
<small>{{ tm('configManagement.description') }}</small>
|
||||
<div class="mt-6 mb-4">
|
||||
<v-btn prepend-icon="mdi-plus" @click="startCreateConfig" variant="tonal" color="primary">
|
||||
新建配置文件
|
||||
{{ tm('configManagement.newConfig') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -111,18 +120,18 @@
|
||||
<v-divider v-if="showConfigForm" class="my-6"></v-divider>
|
||||
|
||||
<div v-if="showConfigForm">
|
||||
<h3 class="mb-4">{{ isEditingConfig ? '编辑配置文件' : '新建配置文件' }}</h3>
|
||||
<h3 class="mb-4">{{ isEditingConfig ? tm('configManagement.editConfig') : tm('configManagement.newConfig') }}</h3>
|
||||
|
||||
<h4>名称</h4>
|
||||
<h4>{{ tm('configManagement.configName') }}</h4>
|
||||
|
||||
<v-text-field v-model="configFormData.name" label="填写配置文件名称" variant="outlined" class="mt-4 mb-4"
|
||||
<v-text-field v-model="configFormData.name" :label="tm('configManagement.fillConfigName')" variant="outlined" class="mt-4 mb-4"
|
||||
hide-details></v-text-field>
|
||||
|
||||
<div class="d-flex justify-end mt-4" style="gap: 8px;">
|
||||
<v-btn variant="text" @click="cancelConfigForm">取消</v-btn>
|
||||
<v-btn variant="text" @click="cancelConfigForm">{{ tm('buttons.cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="saveConfigForm"
|
||||
:disabled="!configFormData.name">
|
||||
{{ isEditingConfig ? '更新' : '创建' }}
|
||||
{{ isEditingConfig ? tm('buttons.update') : tm('buttons.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,6 +144,34 @@
|
||||
</v-snackbar>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
|
||||
<!-- 测试聊天抽屉 -->
|
||||
<v-overlay
|
||||
v-model="testChatDrawer"
|
||||
class="test-chat-overlay"
|
||||
location="right"
|
||||
transition="slide-x-reverse-transition"
|
||||
:scrim="true"
|
||||
@click:outside="closeTestChat"
|
||||
>
|
||||
<v-card class="test-chat-card" elevation="12">
|
||||
<div class="test-chat-header">
|
||||
<div>
|
||||
<span class="text-h6">测试配置</span>
|
||||
<div v-if="selectedConfigInfo.name" class="text-caption text-grey">
|
||||
{{ selectedConfigInfo.name }} ({{ testConfigId }})
|
||||
</div>
|
||||
</div>
|
||||
<v-btn icon variant="text" @click="closeTestChat">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<div class="test-chat-content">
|
||||
<StandaloneChat v-if="testChatDrawer" :configId="testConfigId" />
|
||||
</div>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -142,6 +179,7 @@
|
||||
import axios from 'axios';
|
||||
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
@@ -150,7 +188,8 @@ export default {
|
||||
components: {
|
||||
AstrBotCoreConfigWrapper,
|
||||
VueMonacoEditor,
|
||||
WaitingForRestart
|
||||
WaitingForRestart,
|
||||
StandaloneChat
|
||||
},
|
||||
props: {
|
||||
initialConfigId: {
|
||||
@@ -188,7 +227,7 @@ export default {
|
||||
const items = [...this.configInfoList];
|
||||
items.push({
|
||||
id: '_%manage%_',
|
||||
name: '管理配置文件...',
|
||||
name: this.tm('configManagement.manageConfigs'),
|
||||
umop: []
|
||||
});
|
||||
return items;
|
||||
@@ -238,6 +277,10 @@ export default {
|
||||
name: '',
|
||||
},
|
||||
editingConfigId: null,
|
||||
|
||||
// 测试聊天
|
||||
testChatDrawer: false,
|
||||
testConfigId: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -367,7 +410,7 @@ export default {
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.save_message = "新配置文件创建失败";
|
||||
this.save_message = this.tm('configManagement.createFailed');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
@@ -410,7 +453,7 @@ export default {
|
||||
},
|
||||
saveConfigForm() {
|
||||
if (!this.configFormData.name) {
|
||||
this.save_message = "请填写配置名称";
|
||||
this.save_message = this.tm('configManagement.pleaseEnterName');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
return;
|
||||
@@ -423,7 +466,7 @@ export default {
|
||||
}
|
||||
},
|
||||
confirmDeleteConfig(config) {
|
||||
if (confirm(`确定要删除配置文件 "${config.name}" 吗?此操作不可恢复。`)) {
|
||||
if (confirm(this.tm('configManagement.confirmDelete').replace('{name}', config.name))) {
|
||||
this.deleteConfig(config.id);
|
||||
}
|
||||
},
|
||||
@@ -445,7 +488,7 @@ export default {
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.save_message = "删除配置文件失败";
|
||||
this.save_message = this.tm('configManagement.deleteFailed');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
@@ -468,7 +511,7 @@ export default {
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.save_message = "更新配置文件失败";
|
||||
this.save_message = this.tm('configManagement.updateFailed');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
@@ -506,6 +549,20 @@ export default {
|
||||
this.getConfigInfoList("default");
|
||||
}
|
||||
}
|
||||
},
|
||||
openTestChat() {
|
||||
if (!this.selectedConfigID) {
|
||||
this.save_message = "请先选择一个配置文件";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "warning";
|
||||
return;
|
||||
}
|
||||
this.testConfigId = this.selectedConfigID;
|
||||
this.testChatDrawer = true;
|
||||
},
|
||||
closeTestChat() {
|
||||
this.testChatDrawer = false;
|
||||
this.testConfigId = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -565,4 +622,32 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 测试聊天抽屉样式 */
|
||||
.test-chat-overlay {
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.test-chat-card {
|
||||
width: clamp(320px, 50vw, 720px);
|
||||
height: calc(100vh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.test-chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 12px 20px;
|
||||
}
|
||||
|
||||
.test-chat-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@ import axios from 'axios';
|
||||
import { pinyin } from 'pinyin-pro';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import defaultPluginIcon from '@/assets/images/plugin_icon.png';
|
||||
|
||||
import { ref, computed, onMounted, reactive, inject, watch } from 'vue';
|
||||
|
||||
@@ -939,7 +940,7 @@ watch(marketSearch, (newVal) => {
|
||||
|
||||
<v-row style="min-height: 26rem;">
|
||||
<v-col v-for="plugin in paginatedPlugins" :key="plugin.name" cols="12" md="6" lg="4">
|
||||
<v-card class="rounded-lg d-flex flex-column" elevation="0"
|
||||
<v-card class="rounded-lg d-flex flex-column plugin-card" elevation="0"
|
||||
style=" height: 12rem; position: relative;">
|
||||
|
||||
<!-- 推荐标记 -->
|
||||
@@ -950,8 +951,8 @@ watch(marketSearch, (newVal) => {
|
||||
|
||||
<v-card-text
|
||||
style="padding: 12px; padding-bottom: 8px; display: flex; gap: 12px; width: 100%; flex: 1; overflow: hidden;">
|
||||
<div v-if="plugin?.logo" style="flex-shrink: 0;">
|
||||
<img :src="plugin.logo" :alt="plugin.name"
|
||||
<div style="flex-shrink: 0;">
|
||||
<img :src="plugin?.logo || defaultPluginIcon" :alt="plugin.name"
|
||||
style="height: 75px; width: 75px; border-radius: 8px; object-fit: cover;" />
|
||||
</div>
|
||||
|
||||
@@ -986,8 +987,7 @@ watch(marketSearch, (newVal) => {
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="text-caption"
|
||||
style="overflow: scroll; color: rgba(var(--v-theme-on-surface), 0.6); line-height: 1.3; margin-bottom: 6px; flex: 1;">
|
||||
<div class="text-caption plugin-description">
|
||||
{{ plugin.desc }}
|
||||
</div>
|
||||
|
||||
@@ -1246,4 +1246,36 @@ watch(marketSearch, (newVal) => {
|
||||
border-radius: 5px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 6px;
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.plugin-card:hover .plugin-description {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(var(--v-theme-primary-rgb), 0.4);
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(var(--v-theme-primary-rgb), 0.6);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 日志部分 -->
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card elevation="0" class="mt-4 mb-10">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon class="me-2">mdi-console-line</v-icon>
|
||||
<span class="text-h4">{{ tm('logs.title') }}</span>
|
||||
@@ -233,5 +233,6 @@ export default {
|
||||
.platform-page {
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('providers.tabs.chatCompletion') }}
|
||||
</v-tab>
|
||||
<v-tab value="agent_runner" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('providers.tabs.agentRunner') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('providers.tabs.speechToText') }}
|
||||
@@ -48,87 +52,103 @@
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-row v-if="filteredProviders.length === 0">
|
||||
<v-col cols="12" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ getEmptyText() }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="activeProviderTypeTab === 'all'">
|
||||
<v-row v-if="groupedProviders.length === 0">
|
||||
<v-col cols="12" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ getEmptyText() }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-else>
|
||||
<div v-for="group in groupedProviders" :key="group.typeKey" class="mb-8">
|
||||
<h1 class="text-h3 font-weight-bold mb-4">{{ group.label }}</h1>
|
||||
<v-row>
|
||||
<v-col v-for="(provider, index) in group.items" :key="`${group.typeKey}-${index}`" cols="12" md="6"
|
||||
lg="4" xl="3">
|
||||
<item-card :item="provider" title-field="id" enabled-field="enable"
|
||||
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
|
||||
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
|
||||
@copy="copyProvider" :show-copy-button="true">
|
||||
<template #item-details="{ item }">
|
||||
<!-- 测试状态 chip -->
|
||||
<v-tooltip v-if="getProviderStatus(item.id)" location="top" max-width="300">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip v-bind="props" :color="getStatusColor(getProviderStatus(item.id).status)" size="small">
|
||||
<v-icon start size="small">
|
||||
{{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
|
||||
getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
|
||||
'mdi-clock-outline' }}
|
||||
</v-icon>
|
||||
{{ getStatusText(getProviderStatus(item.id).status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-if="getProviderStatus(item.id).status === 'unavailable'">
|
||||
{{ getProviderStatus(item.id).error }}
|
||||
</span>
|
||||
<span v-else>{{ getStatusText(getProviderStatus(item.id).status) }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
|
||||
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
|
||||
{{ tm('availability.test') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:details="{ item }">
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-row v-if="filteredProviders.length === 0">
|
||||
<v-col cols="12" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ getEmptyText() }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col v-for="(provider, index) in filteredProviders" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card :item="provider" title-field="id" enabled-field="enable"
|
||||
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
|
||||
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
|
||||
@copy="copyProvider" :show-copy-button="true">
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(provider, index) in filteredProviders" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card :item="provider" title-field="id" enabled-field="enable"
|
||||
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
|
||||
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
|
||||
@copy="copyProvider" :show-copy-button="true">
|
||||
<template #actions="{ item }">
|
||||
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
|
||||
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
|
||||
{{ tm('availability.test') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:details="{ item }">
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template #item-details="{ item }">
|
||||
<!-- 测试状态 chip -->
|
||||
<v-tooltip v-if="getProviderStatus(item.id)" location="top" max-width="300">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip v-bind="props" :color="getStatusColor(getProviderStatus(item.id).status)" size="small">
|
||||
<v-icon start size="small">
|
||||
{{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
|
||||
getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
|
||||
'mdi-clock-outline' }}
|
||||
</v-icon>
|
||||
{{ getStatusText(getProviderStatus(item.id).status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-if="getProviderStatus(item.id).status === 'unavailable'">
|
||||
{{ getProviderStatus(item.id).error }}
|
||||
</span>
|
||||
<span v-else>{{ getStatusText(getProviderStatus(item.id).status) }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
|
||||
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
|
||||
{{ tm('availability.test') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 供应商状态部分 -->
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon class="me-2">mdi-heart-pulse</v-icon>
|
||||
<span class="text-h4">{{ tm('availability.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" :loading="testingProviders.length > 0" @click="fetchProviderStatus">
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
{{ tm('availability.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" color="primary" @click="showStatus = !showStatus" style="margin-left: 8px;">
|
||||
{{ showStatus ? tm('logs.collapse') : tm('logs.expand') }}
|
||||
<v-icon>{{ showStatus ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showStatus">
|
||||
<v-card-text class="px-4 py-3">
|
||||
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
|
||||
{{ tm('availability.noData') }}
|
||||
</v-alert>
|
||||
|
||||
<v-container v-else class="pa-0">
|
||||
<v-row>
|
||||
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" class="status-card" :class="`status-${status.status}`">
|
||||
<v-card-item>
|
||||
<v-icon v-if="status.status === 'available'" color="success"
|
||||
class="me-2">mdi-check-circle</v-icon>
|
||||
<v-icon v-else-if="status.status === 'unavailable'" color="error"
|
||||
class="me-2">mdi-alert-circle</v-icon>
|
||||
<v-progress-circular v-else-if="status.status === 'pending'" indeterminate color="primary"
|
||||
size="20" width="2" class="me-2"></v-progress-circular>
|
||||
|
||||
<span class="font-weight-bold">{{ status.id }}</span>
|
||||
|
||||
<v-chip :color="getStatusColor(status.status)" size="small" class="ml-2">
|
||||
{{ getStatusText(status.status) }}
|
||||
</v-chip>
|
||||
</v-card-item>
|
||||
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
|
||||
<span class="font-weight-bold">{{ tm('availability.errorMessage') }}:</span> {{ status.error }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
|
||||
<!-- 日志部分 -->
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card elevation="0" class="mt-4 mb-10">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon class="me-2">mdi-console-line</v-icon>
|
||||
<span class="text-h4">{{ tm('logs.title') }}</span>
|
||||
@@ -289,8 +309,8 @@ export default {
|
||||
"anthropic_chat_completion": "chat_completion",
|
||||
"googlegenai_chat_completion": "chat_completion",
|
||||
"zhipu_chat_completion": "chat_completion",
|
||||
"dify": "chat_completion",
|
||||
"coze": "chat_completion",
|
||||
"dify": "agent_runner",
|
||||
"coze": "agent_runner",
|
||||
"dashscope": "chat_completion",
|
||||
"openai_whisper_api": "speech_to_text",
|
||||
"openai_whisper_selfhost": "speech_to_text",
|
||||
@@ -334,6 +354,7 @@ export default {
|
||||
},
|
||||
tabTypes: {
|
||||
'chat_completion': this.tm('providers.tabs.chatCompletion'),
|
||||
'agent_runner': this.tm('providers.tabs.agentRunner'),
|
||||
'speech_to_text': this.tm('providers.tabs.speechToText'),
|
||||
'text_to_speech': this.tm('providers.tabs.textToSpeech'),
|
||||
'embedding': this.tm('providers.tabs.embedding'),
|
||||
@@ -363,6 +384,52 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
groupedProviders() {
|
||||
if (!this.config_data.provider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const typeOrder = [
|
||||
'chat_completion',
|
||||
'agent_runner',
|
||||
'speech_to_text',
|
||||
'text_to_speech',
|
||||
'embedding',
|
||||
'rerank',
|
||||
];
|
||||
|
||||
const assigned = new Set();
|
||||
const groups = typeOrder
|
||||
.map((typeKey) => {
|
||||
const items = this.config_data.provider.filter((provider) => {
|
||||
const resolved = this.getProviderType(provider);
|
||||
if (resolved === typeKey) {
|
||||
assigned.add(provider.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return {
|
||||
typeKey,
|
||||
label: this.messages.tabTypes[typeKey] || typeKey,
|
||||
items,
|
||||
};
|
||||
})
|
||||
.filter((group) => group.items.length > 0);
|
||||
|
||||
const remaining = this.config_data.provider.filter(
|
||||
(provider) => !assigned.has(provider.id),
|
||||
);
|
||||
if (remaining.length > 0) {
|
||||
groups.push({
|
||||
typeKey: 'others',
|
||||
label: this.tm('providers.tabs.all'),
|
||||
items: remaining,
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
|
||||
// 根据选择的标签过滤提供商列表
|
||||
filteredProviders() {
|
||||
if (!this.config_data.provider || this.activeProviderTypeTab === 'all') {
|
||||
@@ -371,13 +438,7 @@ export default {
|
||||
|
||||
return this.config_data.provider.filter(provider => {
|
||||
// 如果provider.provider_type已经存在,直接使用它
|
||||
if (provider.provider_type) {
|
||||
return provider.provider_type === this.activeProviderTypeTab;
|
||||
}
|
||||
|
||||
// 否则使用映射关系
|
||||
const mappedType = this.oldVersionProviderTypeMapping[provider.type];
|
||||
return mappedType === this.activeProviderTypeTab;
|
||||
return this.getProviderType(provider) === this.activeProviderTypeTab;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -387,6 +448,14 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
getProviderType(provider) {
|
||||
if (!provider) return undefined;
|
||||
if (provider.provider_type) {
|
||||
return provider.provider_type;
|
||||
}
|
||||
return this.oldVersionProviderTypeMapping[provider.type];
|
||||
},
|
||||
|
||||
getConfig() {
|
||||
axios.get('/api/config/get').then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
@@ -666,11 +735,14 @@ export default {
|
||||
return this.testingProviders.includes(providerId);
|
||||
},
|
||||
|
||||
getProviderStatus(providerId) {
|
||||
return this.providerStatuses.find(s => s.id === providerId);
|
||||
},
|
||||
|
||||
async testSingleProvider(provider) {
|
||||
if (this.isProviderTesting(provider.id)) return;
|
||||
|
||||
this.testingProviders.push(provider.id);
|
||||
this.showStatus = true; // 自动展开状态部分
|
||||
|
||||
// 更新UI为pending状态
|
||||
const statusIndex = this.providerStatuses.findIndex(s => s.id === provider.id);
|
||||
@@ -690,6 +762,9 @@ export default {
|
||||
if (!provider.enable) {
|
||||
throw new Error('该提供商未被用户启用');
|
||||
}
|
||||
if (provider.provider_type === 'agent_runner') {
|
||||
throw new Error('暂时无法测试 Agent Runner 类型的提供商');
|
||||
}
|
||||
|
||||
const res = await axios.get(`/api/config/provider/check_one?id=${provider.id}`);
|
||||
if (res.data && res.data.status === 'ok') {
|
||||
@@ -774,6 +849,7 @@ export default {
|
||||
.provider-page {
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,18 @@ import datetime
|
||||
|
||||
from astrbot.api import logger, sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.platform.astr_message_event import MessageSession
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.provider.sources.coze_source import ProviderCoze
|
||||
from astrbot.core.provider.sources.dify_source import ProviderDify
|
||||
|
||||
from ..long_term_memory import LongTermMemory
|
||||
from .utils.rst_scene import RstScene
|
||||
|
||||
THIRD_PARTY_AGENT_RUNNER_KEY = {
|
||||
"dify": "dify_conversation_id",
|
||||
"coze": "coze_conversation_id",
|
||||
}
|
||||
THIRD_PARTY_AGENT_RUNNER_STR = ", ".join(THIRD_PARTY_AGENT_RUNNER_KEY.keys())
|
||||
|
||||
|
||||
class ConversationCommands:
|
||||
def __init__(self, context: star.Context, ltm: LongTermMemory | None = None):
|
||||
@@ -38,9 +42,9 @@ class ConversationCommands:
|
||||
|
||||
async def reset(self, message: AstrMessageEvent):
|
||||
"""重置 LLM 会话"""
|
||||
is_unique_session = self.context.get_config()["platform_settings"][
|
||||
"unique_session"
|
||||
]
|
||||
umo = message.unified_msg_origin
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
is_unique_session = cfg["platform_settings"]["unique_session"]
|
||||
is_group = bool(message.get_group_id())
|
||||
|
||||
scene = RstScene.get_scene(is_group, is_unique_session)
|
||||
@@ -63,28 +67,23 @@ class ConversationCommands:
|
||||
)
|
||||
return
|
||||
|
||||
if not self.context.get_using_provider(message.unified_msg_origin):
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
await sp.remove_async(
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||
)
|
||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||
return
|
||||
|
||||
if not self.context.get_using_provider(umo):
|
||||
message.set_result(
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
)
|
||||
return
|
||||
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if provider and provider.meta().type in ["dify", "coze"]:
|
||||
assert isinstance(provider, (ProviderDify, ProviderCoze)), (
|
||||
"provider type is not dify or coze"
|
||||
)
|
||||
await provider.forget(message.unified_msg_origin)
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"已重置当前 Dify / Coze 会话,新聊天将更换到新的会话。",
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
message.unified_msg_origin,
|
||||
)
|
||||
cid = await self.context.conversation_manager.get_curr_conversation_id(umo)
|
||||
|
||||
if not cid:
|
||||
message.set_result(
|
||||
@@ -95,7 +94,7 @@ class ConversationCommands:
|
||||
return
|
||||
|
||||
await self.context.conversation_manager.update_conversation(
|
||||
message.unified_msg_origin,
|
||||
umo,
|
||||
cid,
|
||||
[],
|
||||
)
|
||||
@@ -152,29 +151,14 @@ class ConversationCommands:
|
||||
|
||||
async def convs(self, message: AstrMessageEvent, page: int = 1):
|
||||
"""查看对话列表"""
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if provider and provider.meta().type == "dify":
|
||||
"""原有的Dify处理逻辑保持不变"""
|
||||
parts = ["Dify 对话列表:\n"]
|
||||
assert isinstance(provider, ProviderDify)
|
||||
data = await provider.api_client.get_chat_convs(message.unified_msg_origin)
|
||||
idx = 1
|
||||
for conv in data["data"]:
|
||||
ts_h = datetime.datetime.fromtimestamp(conv["updated_at"]).strftime(
|
||||
"%m-%d %H:%M",
|
||||
)
|
||||
parts.append(
|
||||
f"{idx}. {conv['name']}({conv['id'][:4]})\n 上次更新:{ts_h}\n"
|
||||
)
|
||||
idx += 1
|
||||
if idx == 1:
|
||||
parts.append("没有找到任何对话。")
|
||||
dify_cid = provider.conversation_ids.get(message.unified_msg_origin, None)
|
||||
parts.append(
|
||||
f"\n\n用户: {message.unified_msg_origin}\n当前对话: {dify_cid}\n使用 /switch <序号> 切换对话。"
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。",
|
||||
),
|
||||
)
|
||||
ret = "".join(parts)
|
||||
message.set_result(MessageEventResult().message(ret))
|
||||
return
|
||||
|
||||
size_per_page = 6
|
||||
@@ -227,9 +211,8 @@ class ConversationCommands:
|
||||
else:
|
||||
ret += "\n当前对话: 无"
|
||||
|
||||
unique_session = self.context.get_config()["platform_settings"][
|
||||
"unique_session"
|
||||
]
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
unique_session = cfg["platform_settings"]["unique_session"]
|
||||
if unique_session:
|
||||
ret += "\n会话隔离粒度: 个人"
|
||||
else:
|
||||
@@ -243,15 +226,15 @@ class ConversationCommands:
|
||||
|
||||
async def new_conv(self, message: AstrMessageEvent):
|
||||
"""创建新对话"""
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if provider and provider.meta().type in ["dify", "coze"]:
|
||||
assert isinstance(provider, (ProviderDify, ProviderCoze)), (
|
||||
"provider type is not dify or coze"
|
||||
)
|
||||
await provider.forget(message.unified_msg_origin)
|
||||
message.set_result(
|
||||
MessageEventResult().message("成功,下次聊天将是新对话。"),
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
await sp.remove_async(
|
||||
scope="umo",
|
||||
scope_id=message.unified_msg_origin,
|
||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||
)
|
||||
message.set_result(MessageEventResult().message("已创建新对话。"))
|
||||
return
|
||||
|
||||
cpersona = await self._get_current_persona_id(message.unified_msg_origin)
|
||||
@@ -274,19 +257,9 @@ class ConversationCommands:
|
||||
|
||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str = ""):
|
||||
"""创建新群聊对话"""
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if provider and provider.meta().type in ["dify", "coze"]:
|
||||
assert isinstance(provider, (ProviderDify, ProviderCoze)), (
|
||||
"provider type is not dify or coze"
|
||||
)
|
||||
await provider.forget(message.unified_msg_origin)
|
||||
message.set_result(
|
||||
MessageEventResult().message("成功,下次聊天将是新对话。"),
|
||||
)
|
||||
return
|
||||
if sid:
|
||||
session = str(
|
||||
MessageSesion(
|
||||
MessageSession(
|
||||
platform_name=message.platform_meta.id,
|
||||
message_type=MessageType("GroupMessage"),
|
||||
session_id=sid,
|
||||
@@ -321,31 +294,6 @@ class ConversationCommands:
|
||||
)
|
||||
return
|
||||
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if provider and provider.meta().type == "dify":
|
||||
assert isinstance(provider, ProviderDify), "provider type is not dify"
|
||||
data = await provider.api_client.get_chat_convs(message.unified_msg_origin)
|
||||
if not data["data"]:
|
||||
message.set_result(MessageEventResult().message("未找到任何对话。"))
|
||||
return
|
||||
selected_conv = None
|
||||
if index is not None:
|
||||
try:
|
||||
selected_conv = data["data"][index - 1]
|
||||
except IndexError:
|
||||
message.set_result(
|
||||
MessageEventResult().message("对话序号错误,请使用 /ls 查看"),
|
||||
)
|
||||
return
|
||||
else:
|
||||
selected_conv = data["data"][0]
|
||||
ret = (
|
||||
f"Dify 切换到对话: {selected_conv['name']}({selected_conv['id'][:4]})。"
|
||||
)
|
||||
provider.conversation_ids[message.unified_msg_origin] = selected_conv["id"]
|
||||
message.set_result(MessageEventResult().message(ret))
|
||||
return
|
||||
|
||||
if index is None:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
@@ -378,19 +326,6 @@ class ConversationCommands:
|
||||
if not new_name:
|
||||
message.set_result(MessageEventResult().message("请输入新的对话名称。"))
|
||||
return
|
||||
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
|
||||
if provider and provider.meta().type == "dify":
|
||||
assert isinstance(provider, ProviderDify)
|
||||
cid = provider.conversation_ids.get(message.unified_msg_origin, None)
|
||||
if not cid:
|
||||
message.set_result(MessageEventResult().message("未找到当前对话。"))
|
||||
return
|
||||
await provider.api_client.rename(cid, new_name, message.unified_msg_origin)
|
||||
message.set_result(MessageEventResult().message("重命名对话成功。"))
|
||||
return
|
||||
|
||||
await self.context.conversation_manager.update_conversation_title(
|
||||
message.unified_msg_origin,
|
||||
new_name,
|
||||
@@ -399,9 +334,8 @@ class ConversationCommands:
|
||||
|
||||
async def del_conv(self, message: AstrMessageEvent):
|
||||
"""删除当前对话"""
|
||||
is_unique_session = self.context.get_config()["platform_settings"][
|
||||
"unique_session"
|
||||
]
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
is_unique_session = cfg["platform_settings"]["unique_session"]
|
||||
if message.get_group_id() and not is_unique_session and message.role != "admin":
|
||||
# 群聊,没开独立会话,发送人不是管理员
|
||||
message.set_result(
|
||||
@@ -411,20 +345,14 @@ class ConversationCommands:
|
||||
)
|
||||
return
|
||||
|
||||
provider = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if provider and provider.meta().type == "dify":
|
||||
assert isinstance(provider, ProviderDify)
|
||||
dify_cid = provider.conversation_ids.pop(message.unified_msg_origin, None)
|
||||
if dify_cid:
|
||||
await provider.api_client.delete_chat_conv(
|
||||
message.unified_msg_origin,
|
||||
dify_cid,
|
||||
)
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。",
|
||||
),
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
await sp.remove_async(
|
||||
scope="umo",
|
||||
scope_id=message.unified_msg_origin,
|
||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||
)
|
||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||
return
|
||||
|
||||
session_curr_cid = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import builtins
|
||||
|
||||
from astrbot.api import star
|
||||
from astrbot.api import sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
|
||||
|
||||
@@ -17,6 +17,13 @@ class PersonaCommands:
|
||||
default_persona = await self.context.persona_manager.get_default_persona_v3(
|
||||
umo=umo,
|
||||
)
|
||||
|
||||
force_applied_persona_id = (
|
||||
await sp.get_async(
|
||||
scope="umo", scope_id=umo, key="session_service_config", default={}
|
||||
)
|
||||
).get("persona_id")
|
||||
|
||||
curr_cid_title = "无"
|
||||
if cid:
|
||||
conv = await self.context.conversation_manager.get_conversation(
|
||||
@@ -36,6 +43,9 @@ class PersonaCommands:
|
||||
else:
|
||||
curr_persona_name = conv.persona_id
|
||||
|
||||
if force_applied_persona_id:
|
||||
curr_persona_name = f"{curr_persona_name} (自定义规则)"
|
||||
|
||||
curr_cid_title = conv.title if conv.title else "新对话"
|
||||
curr_cid_title += f"({cid[:4]})"
|
||||
|
||||
@@ -113,9 +123,15 @@ class PersonaCommands:
|
||||
message.unified_msg_origin,
|
||||
ps,
|
||||
)
|
||||
force_warn_msg = ""
|
||||
if force_applied_persona_id:
|
||||
force_warn_msg = (
|
||||
"提醒:由于自定义规则,您现在切换的人格将不会生效。"
|
||||
)
|
||||
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。",
|
||||
f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import defaultdict
|
||||
from astrbot import logger
|
||||
from astrbot.api import star
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
from astrbot.api.message_components import Image, Plain
|
||||
from astrbot.api.message_components import At, Image, Plain
|
||||
from astrbot.api.platform import MessageType
|
||||
from astrbot.api.provider import Provider, ProviderRequest
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
@@ -30,16 +30,13 @@ class LongTermMemory:
|
||||
except BaseException as e:
|
||||
logger.error(e)
|
||||
max_cnt = 300
|
||||
image_caption = (
|
||||
True
|
||||
if cfg["provider_settings"]["default_image_caption_provider_id"]
|
||||
and cfg["provider_ltm_settings"]["image_caption"]
|
||||
else False
|
||||
)
|
||||
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
|
||||
image_caption_provider_id = cfg["provider_settings"][
|
||||
"default_image_caption_provider_id"
|
||||
]
|
||||
image_caption_provider_id = cfg["provider_ltm_settings"].get(
|
||||
"image_caption_provider_id"
|
||||
)
|
||||
image_caption = cfg["provider_ltm_settings"]["image_caption"] and bool(
|
||||
image_caption_provider_id
|
||||
)
|
||||
active_reply = cfg["provider_ltm_settings"]["active_reply"]
|
||||
enable_active_reply = active_reply.get("enable", False)
|
||||
ar_method = active_reply["method"]
|
||||
@@ -142,6 +139,8 @@ class LongTermMemory:
|
||||
logger.error(f"获取图片描述失败: {e}")
|
||||
else:
|
||||
parts.append(" [Image]")
|
||||
elif isinstance(comp, At):
|
||||
parts.append(f" [At: {comp.name}]")
|
||||
|
||||
final_message = "".join(parts)
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
|
||||
@@ -5,7 +5,6 @@ from astrbot.api.event import AstrMessageEvent, filter
|
||||
from astrbot.api.message_components import Image, Plain
|
||||
from astrbot.api.provider import LLMResponse, ProviderRequest
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.provider.sources.dify_source import ProviderDify
|
||||
|
||||
from .commands import (
|
||||
AdminCommands,
|
||||
@@ -279,33 +278,20 @@ class Main(star.Star):
|
||||
return
|
||||
try:
|
||||
conv = None
|
||||
if provider.meta().type != "dify":
|
||||
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
event.unified_msg_origin,
|
||||
)
|
||||
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
event.unified_msg_origin,
|
||||
)
|
||||
|
||||
if not session_curr_cid:
|
||||
logger.error(
|
||||
"当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。",
|
||||
)
|
||||
return
|
||||
if not session_curr_cid:
|
||||
logger.error(
|
||||
"当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。",
|
||||
)
|
||||
return
|
||||
|
||||
conv = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin,
|
||||
session_curr_cid,
|
||||
)
|
||||
else:
|
||||
# Dify 自己有维护对话,不需要 bot 端维护。
|
||||
assert isinstance(provider, ProviderDify)
|
||||
cid = provider.conversation_ids.get(
|
||||
event.unified_msg_origin,
|
||||
None,
|
||||
)
|
||||
if cid is None:
|
||||
logger.error(
|
||||
"[Dify] 当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。",
|
||||
)
|
||||
return
|
||||
conv = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin,
|
||||
session_curr_cid,
|
||||
)
|
||||
|
||||
prompt = event.message_str
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import copy
|
||||
import datetime
|
||||
import zoneinfo
|
||||
|
||||
from astrbot.api import logger, star
|
||||
from astrbot.api import logger, sp, star
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
from astrbot.api.message_components import Image, Reply
|
||||
from astrbot.api.provider import Provider, ProviderRequest
|
||||
@@ -21,16 +21,26 @@ class ProcessLLMRequest:
|
||||
else:
|
||||
logger.info(f"Timezone set to: {self.timezone}")
|
||||
|
||||
def _ensure_persona(self, req: ProviderRequest, cfg: dict):
|
||||
async def _ensure_persona(self, req: ProviderRequest, cfg: dict, umo: str):
|
||||
"""确保用户人格已加载"""
|
||||
if not req.conversation:
|
||||
return
|
||||
# persona inject
|
||||
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
||||
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
|
||||
default_persona = self.ctx.persona_manager.selected_default_persona_v3
|
||||
if default_persona:
|
||||
persona_id = default_persona["name"]
|
||||
|
||||
# custom rule is preferred
|
||||
persona_id = (
|
||||
await sp.get_async(
|
||||
scope="umo", scope_id=umo, key="session_service_config", default={}
|
||||
)
|
||||
).get("persona_id")
|
||||
|
||||
if not persona_id:
|
||||
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
||||
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
|
||||
default_persona = self.ctx.persona_manager.selected_default_persona_v3
|
||||
if default_persona:
|
||||
persona_id = default_persona["name"]
|
||||
|
||||
persona = next(
|
||||
builtins.filter(
|
||||
lambda persona: persona["name"] == persona_id,
|
||||
@@ -152,7 +162,7 @@ class ProcessLLMRequest:
|
||||
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
||||
if req.conversation:
|
||||
# inject persona for this request
|
||||
self._ensure_persona(req, cfg)
|
||||
await self._ensure_persona(req, cfg, event.unified_msg_origin)
|
||||
|
||||
# image caption
|
||||
if img_cap_prov_id and req.image_urls:
|
||||
|
||||
@@ -14,7 +14,7 @@ from astrbot.core.utils.session_waiter import (
|
||||
)
|
||||
|
||||
|
||||
class Waiter(Star):
|
||||
class Main(Star):
|
||||
"""会话控制"""
|
||||
|
||||
def __init__(self, context: Context):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.6.0"
|
||||
version = "4.7.2"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -21,7 +21,17 @@ async def core_lifecycle_td(tmp_path_factory):
|
||||
log_broker = LogBroker()
|
||||
core_lifecycle = AstrBotCoreLifecycle(log_broker, db)
|
||||
await core_lifecycle.initialize()
|
||||
return core_lifecycle
|
||||
try:
|
||||
yield core_lifecycle
|
||||
finally:
|
||||
# 优先停止核心生命周期以释放资源(包括关闭 MCP 等后台任务)
|
||||
try:
|
||||
_stop_res = core_lifecycle.stop()
|
||||
if asyncio.iscoroutine(_stop_res):
|
||||
await _stop_res
|
||||
except Exception:
|
||||
# 停止过程中如有异常,不影响后续清理
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -39,6 +39,7 @@ def plugin_manager_pm(tmp_path):
|
||||
message_history_manager = MagicMock()
|
||||
persona_manager = MagicMock()
|
||||
astrbot_config_mgr = MagicMock()
|
||||
knowledge_base_manager = MagicMock()
|
||||
|
||||
star_context = Context(
|
||||
event_queue,
|
||||
@@ -50,6 +51,7 @@ def plugin_manager_pm(tmp_path):
|
||||
message_history_manager,
|
||||
persona_manager,
|
||||
astrbot_config_mgr,
|
||||
knowledge_base_manager=knowledge_base_manager,
|
||||
)
|
||||
|
||||
# Create the PluginManager instance
|
||||
|
||||
Reference in New Issue
Block a user