Compare commits
11 Commits
v4.10.1
...
multimessage
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cedf0d587 | |||
| aeb21f719e | |||
| 7c1dbecea5 | |||
| 05012af627 | |||
| 17b52ab5dd | |||
| 9449ff668b | |||
| c5a2827def | |||
| 701399c00c | |||
| eaee98d4b8 | |||
| 76c66000a7 | |||
| 4b365143c0 |
@@ -1,4 +1,4 @@
|
|||||||

|

|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.10.1"
|
__version__ = "4.10.2"
|
||||||
|
|||||||
@@ -77,10 +77,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
||||||
"""Yields chunks *and* a final LLMResponse."""
|
"""Yields chunks *and* a final LLMResponse."""
|
||||||
payload = {
|
payload = {
|
||||||
"contexts": self.run_context.messages,
|
"contexts": self.run_context.messages, # list[Message]
|
||||||
"func_tool": self.req.func_tool,
|
"func_tool": self.req.func_tool,
|
||||||
"model": self.req.model, # NOTE: in fact, this arg is None in most cases
|
"model": self.req.model, # NOTE: in fact, this arg is None in most cases
|
||||||
"session_id": self.req.session_id,
|
"session_id": self.req.session_id,
|
||||||
|
"extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.streaming:
|
if self.streaming:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
|||||||
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
VERSION = "4.10.1"
|
VERSION = "4.10.2"
|
||||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||||
|
|
||||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import astrbot.core.message.components as Comp
|
|||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.core.agent.message import (
|
from astrbot.core.agent.message import (
|
||||||
AssistantMessageSegment,
|
AssistantMessageSegment,
|
||||||
|
ContentPart,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
ToolCallMessageSegment,
|
ToolCallMessageSegment,
|
||||||
)
|
)
|
||||||
@@ -92,6 +93,8 @@ class ProviderRequest:
|
|||||||
"""会话 ID"""
|
"""会话 ID"""
|
||||||
image_urls: list[str] = field(default_factory=list)
|
image_urls: list[str] = field(default_factory=list)
|
||||||
"""图片 URL 列表"""
|
"""图片 URL 列表"""
|
||||||
|
extra_user_content_parts: list[ContentPart] = field(default_factory=list)
|
||||||
|
"""额外的用户消息内容部分列表,用于在用户消息后添加额外的内容块(如系统提醒、指令等)。"""
|
||||||
func_tool: ToolSet | None = None
|
func_tool: ToolSet | None = None
|
||||||
"""可用的函数工具"""
|
"""可用的函数工具"""
|
||||||
contexts: list[dict] = field(default_factory=list)
|
contexts: list[dict] = field(default_factory=list)
|
||||||
@@ -166,13 +169,23 @@ class ProviderRequest:
|
|||||||
|
|
||||||
async def assemble_context(self) -> dict:
|
async def assemble_context(self) -> dict:
|
||||||
"""将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
|
"""将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
|
||||||
|
# 构建内容块列表
|
||||||
|
content_blocks = []
|
||||||
|
|
||||||
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
|
if self.prompt and self.prompt.strip():
|
||||||
|
content_blocks.append({"type": "text", "text": self.prompt})
|
||||||
|
elif self.image_urls:
|
||||||
|
# 如果没有文本但有图片,添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": "[图片]"})
|
||||||
|
|
||||||
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
|
if self.extra_user_content_parts:
|
||||||
|
for part in self.extra_user_content_parts:
|
||||||
|
content_blocks.append(part.model_dump())
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
if self.image_urls:
|
if self.image_urls:
|
||||||
user_content = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "text", "text": self.prompt if self.prompt else "[图片]"},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
for image_url in self.image_urls:
|
for image_url in self.image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
image_path = await download_image_by_url(image_url)
|
image_path = await download_image_by_url(image_url)
|
||||||
@@ -185,11 +198,21 @@ class ProviderRequest:
|
|||||||
if not image_data:
|
if not image_data:
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
continue
|
continue
|
||||||
user_content["content"].append(
|
content_blocks.append(
|
||||||
{"type": "image_url", "image_url": {"url": image_data}},
|
{"type": "image_url", "image_url": {"url": image_data}},
|
||||||
)
|
)
|
||||||
return user_content
|
|
||||||
return {"role": "user", "content": self.prompt}
|
# 只有当只有一个来自 prompt 的文本块且没有额外内容块时,才降级为简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
len(content_blocks) == 1
|
||||||
|
and content_blocks[0]["type"] == "text"
|
||||||
|
and not self.extra_user_content_parts
|
||||||
|
and not self.image_urls
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content_blocks[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
|
return {"role": "user", "content": content_blocks}
|
||||||
|
|
||||||
async def _encode_image_bs64(self, image_url: str) -> str:
|
async def _encode_image_bs64(self, image_url: str) -> str:
|
||||||
"""将图片转换为 base64"""
|
"""将图片转换为 base64"""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import TypeAlias, Union
|
from typing import TypeAlias, Union
|
||||||
|
|
||||||
from astrbot.core.agent.message import Message
|
from astrbot.core.agent.message import ContentPart, Message
|
||||||
from astrbot.core.agent.tool import ToolSet
|
from astrbot.core.agent.tool import ToolSet
|
||||||
from astrbot.core.provider.entities import (
|
from astrbot.core.provider.entities import (
|
||||||
LLMResponse,
|
LLMResponse,
|
||||||
@@ -103,6 +103,7 @@ class Provider(AbstractProvider):
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
||||||
@@ -114,6 +115,7 @@ class Provider(AbstractProvider):
|
|||||||
tools: tool set
|
tools: tool set
|
||||||
contexts: 上下文,和 prompt 二选一使用
|
contexts: 上下文,和 prompt 二选一使用
|
||||||
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
||||||
|
extra_user_content_parts: 额外的用户内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等)
|
||||||
kwargs: 其他参数
|
kwargs: 其他参数
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
@@ -133,6 +135,7 @@ class Provider(AbstractProvider):
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
"""获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。
|
"""获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。
|
||||||
@@ -144,6 +147,7 @@ class Provider(AbstractProvider):
|
|||||||
tools: tool set
|
tools: tool set
|
||||||
contexts: 上下文,和 prompt 二选一使用
|
contexts: 上下文,和 prompt 二选一使用
|
||||||
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
||||||
|
extra_user_content_parts: 额外的用户内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等)
|
||||||
kwargs: 其他参数
|
kwargs: 其他参数
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from anthropic.types.usage import Usage
|
|||||||
|
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
|
from astrbot.core.agent.message import ContentPart
|
||||||
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
||||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
from astrbot.core.utils.io import download_image_by_url
|
from astrbot.core.utils.io import download_image_by_url
|
||||||
@@ -296,13 +297,16 @@ class ProviderAnthropic(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -350,13 +354,16 @@ class ProviderAnthropic(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -388,48 +395,116 @@ class ProviderAnthropic(Provider):
|
|||||||
async for llm_response in self._query_stream(payloads, func_tool):
|
async for llm_response in self._query_stream(payloads, func_tool):
|
||||||
yield llm_response
|
yield llm_response
|
||||||
|
|
||||||
async def assemble_context(self, text: str, image_urls: list[str] | None = None):
|
async def assemble_context(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
image_urls: list[str] | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
|
):
|
||||||
"""组装上下文,支持文本和图片"""
|
"""组装上下文,支持文本和图片"""
|
||||||
if not image_urls:
|
|
||||||
return {"role": "user", "content": text}
|
|
||||||
|
|
||||||
content = []
|
content = []
|
||||||
content.append({"type": "text", "text": text})
|
|
||||||
|
|
||||||
for image_url in image_urls:
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
if image_url.startswith("http"):
|
if text:
|
||||||
image_path = await download_image_by_url(image_url)
|
content.append({"type": "text", "text": text})
|
||||||
image_data = await self.encode_image_bs64(image_path)
|
elif image_urls:
|
||||||
elif image_url.startswith("file:///"):
|
# 如果没有文本但有图片,添加占位文本
|
||||||
image_path = image_url.replace("file:///", "")
|
content.append({"type": "text", "text": "[图片]"})
|
||||||
image_data = await self.encode_image_bs64(image_path)
|
elif extra_user_content_parts:
|
||||||
else:
|
# 如果只有额外内容块,也需要添加占位文本
|
||||||
image_data = await self.encode_image_bs64(image_url)
|
content.append({"type": "text", "text": " "})
|
||||||
|
|
||||||
if not image_data:
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
if extra_user_content_parts:
|
||||||
continue
|
for block in extra_user_content_parts:
|
||||||
|
block_type = block.get("type")
|
||||||
|
|
||||||
# Get mime type for the image
|
if block_type == "text":
|
||||||
mime_type, _ = guess_type(image_url)
|
# 文本直接添加
|
||||||
if not mime_type:
|
content.append(block)
|
||||||
mime_type = "image/jpeg" # Default to JPEG if can't determine
|
|
||||||
|
|
||||||
content.append(
|
elif block_type == "image_url":
|
||||||
{
|
# 转换 OpenAI 格式的图片为 Anthropic 格式
|
||||||
"type": "image",
|
image_url_data = block.get("image_url", {})
|
||||||
"source": {
|
if isinstance(image_url_data, dict):
|
||||||
"type": "base64",
|
url = image_url_data.get("url", "")
|
||||||
"media_type": mime_type,
|
else:
|
||||||
"data": (
|
# 兼容直接传 URL 字符串的情况
|
||||||
image_data.split("base64,")[1]
|
url = str(image_url_data)
|
||||||
if "base64," in image_data
|
|
||||||
else image_data
|
if url and url.startswith("data:"):
|
||||||
),
|
try:
|
||||||
|
# 提取 MIME 类型和 base64 数据
|
||||||
|
mime_type = url.split(":")[1].split(";")[0]
|
||||||
|
base64_data = (
|
||||||
|
url.split("base64,")[1] if "base64," in url else url
|
||||||
|
)
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": mime_type,
|
||||||
|
"data": base64_data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"转换 image_url 到 Anthropic 格式失败: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"image_url 不是有效的 data URI: {url[:50]}...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 其他类型(如 audio_url)Anthropic 不支持,记录警告
|
||||||
|
logger.debug(f"Anthropic 不支持的内容类型 '{block_type}',已忽略")
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
|
if image_urls:
|
||||||
|
for image_url in image_urls:
|
||||||
|
if image_url.startswith("http"):
|
||||||
|
image_path = await download_image_by_url(image_url)
|
||||||
|
image_data = await self.encode_image_bs64(image_path)
|
||||||
|
elif image_url.startswith("file:///"):
|
||||||
|
image_path = image_url.replace("file:///", "")
|
||||||
|
image_data = await self.encode_image_bs64(image_path)
|
||||||
|
else:
|
||||||
|
image_data = await self.encode_image_bs64(image_url)
|
||||||
|
|
||||||
|
if not image_data:
|
||||||
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get mime type for the image
|
||||||
|
mime_type, _ = guess_type(image_url)
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = "image/jpeg" # Default to JPEG if can't determine
|
||||||
|
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": mime_type,
|
||||||
|
"data": (
|
||||||
|
image_data.split("base64,")[1]
|
||||||
|
if "base64," in image_data
|
||||||
|
else image_data
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
text
|
||||||
|
and not extra_user_content_parts
|
||||||
|
and not image_urls
|
||||||
|
and len(content) == 1
|
||||||
|
and content[0]["type"] == "text"
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
return {"role": "user", "content": content}
|
return {"role": "user", "content": content}
|
||||||
|
|
||||||
async def encode_image_bs64(self, image_url: str) -> str:
|
async def encode_image_bs64(self, image_url: str) -> str:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from google.genai.errors import APIError
|
|||||||
import astrbot.core.message.components as Comp
|
import astrbot.core.message.components as Comp
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
|
from astrbot.core.agent.message import ContentPart
|
||||||
from astrbot.core.message.message_event_result import MessageChain
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
||||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
@@ -680,13 +681,16 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -732,13 +736,16 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -797,13 +804,33 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
self.chosen_api_key = key
|
self.chosen_api_key = key
|
||||||
self._init_client()
|
self._init_client()
|
||||||
|
|
||||||
async def assemble_context(self, text: str, image_urls: list[str] | None = None):
|
async def assemble_context(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
image_urls: list[str] | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
|
):
|
||||||
"""组装上下文。"""
|
"""组装上下文。"""
|
||||||
|
# 构建内容块列表
|
||||||
|
content_blocks = []
|
||||||
|
|
||||||
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
|
if text:
|
||||||
|
content_blocks.append({"type": "text", "text": text})
|
||||||
|
elif image_urls:
|
||||||
|
# 如果没有文本但有图片,添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": "[图片]"})
|
||||||
|
elif extra_user_content_parts:
|
||||||
|
# 如果只有额外内容块,也需要添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": " "})
|
||||||
|
|
||||||
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
|
if extra_user_content_parts:
|
||||||
|
for part in extra_user_content_parts:
|
||||||
|
content_blocks.append(part.model_dump())
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
if image_urls:
|
if image_urls:
|
||||||
user_content = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "text", "text": text if text else "[图片]"}],
|
|
||||||
}
|
|
||||||
for image_url in image_urls:
|
for image_url in image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
image_path = await download_image_by_url(image_url)
|
image_path = await download_image_by_url(image_url)
|
||||||
@@ -816,14 +843,25 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
if not image_data:
|
if not image_data:
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
continue
|
continue
|
||||||
user_content["content"].append(
|
content_blocks.append(
|
||||||
{
|
{
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": image_data},
|
"image_url": {"url": image_data},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return user_content
|
|
||||||
return {"role": "user", "content": text}
|
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
text
|
||||||
|
and not extra_user_content_parts
|
||||||
|
and not image_urls
|
||||||
|
and len(content_blocks) == 1
|
||||||
|
and content_blocks[0]["type"] == "text"
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content_blocks[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
|
return {"role": "user", "content": content_blocks}
|
||||||
|
|
||||||
async def encode_image_bs64(self, image_url: str) -> str:
|
async def encode_image_bs64(self, image_url: str) -> str:
|
||||||
"""将图片转换为 base64"""
|
"""将图片转换为 base64"""
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from openai.types.completion_usage import CompletionUsage
|
|||||||
import astrbot.core.message.components as Comp
|
import astrbot.core.message.components as Comp
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
from astrbot.core.agent.message import Message
|
from astrbot.core.agent.message import ContentPart, Message
|
||||||
from astrbot.core.agent.tool import ToolSet
|
from astrbot.core.agent.tool import ToolSet
|
||||||
from astrbot.core.message.message_event_result import MessageChain
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
|
||||||
@@ -348,6 +348,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""准备聊天所需的有效载荷和上下文"""
|
"""准备聊天所需的有效载荷和上下文"""
|
||||||
@@ -355,7 +356,9 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -476,6 +479,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
payloads, context_query = await self._prepare_chat_payload(
|
payloads, context_query = await self._prepare_chat_payload(
|
||||||
@@ -485,6 +489,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt,
|
system_prompt,
|
||||||
tool_calls_result,
|
tool_calls_result,
|
||||||
model=model,
|
model=model,
|
||||||
|
extra_user_content_parts=extra_user_content_parts,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -539,6 +544,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
"""流式对话,与服务商交互并逐步返回结果"""
|
"""流式对话,与服务商交互并逐步返回结果"""
|
||||||
@@ -549,6 +555,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt,
|
system_prompt,
|
||||||
tool_calls_result,
|
tool_calls_result,
|
||||||
model=model,
|
model=model,
|
||||||
|
extra_user_content_parts=extra_user_content_parts,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -624,13 +631,29 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
self,
|
self,
|
||||||
text: str,
|
text: str,
|
||||||
image_urls: list[str] | None = None,
|
image_urls: list[str] | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""组装成符合 OpenAI 格式的 role 为 user 的消息段"""
|
"""组装成符合 OpenAI 格式的 role 为 user 的消息段"""
|
||||||
|
# 构建内容块列表
|
||||||
|
content_blocks = []
|
||||||
|
|
||||||
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
|
if text:
|
||||||
|
content_blocks.append({"type": "text", "text": text})
|
||||||
|
elif image_urls:
|
||||||
|
# 如果没有文本但有图片,添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": "[图片]"})
|
||||||
|
elif extra_user_content_parts:
|
||||||
|
# 如果只有额外内容块,也需要添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": " "})
|
||||||
|
|
||||||
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
|
if extra_user_content_parts:
|
||||||
|
for part in extra_user_content_parts:
|
||||||
|
content_blocks.append(part.model_dump())
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
if image_urls:
|
if image_urls:
|
||||||
user_content = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "text", "text": text if text else "[图片]"}],
|
|
||||||
}
|
|
||||||
for image_url in image_urls:
|
for image_url in image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
image_path = await download_image_by_url(image_url)
|
image_path = await download_image_by_url(image_url)
|
||||||
@@ -643,14 +666,25 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
if not image_data:
|
if not image_data:
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
continue
|
continue
|
||||||
user_content["content"].append(
|
content_blocks.append(
|
||||||
{
|
{
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": image_data},
|
"image_url": {"url": image_data},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return user_content
|
|
||||||
return {"role": "user", "content": text}
|
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
text
|
||||||
|
and not extra_user_content_parts
|
||||||
|
and not image_urls
|
||||||
|
and len(content_blocks) == 1
|
||||||
|
and content_blocks[0]["type"] == "text"
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content_blocks[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
|
return {"role": "user", "content": content_blocks}
|
||||||
|
|
||||||
async def encode_image_bs64(self, image_url: str) -> str:
|
async def encode_image_bs64(self, image_url: str) -> str:
|
||||||
"""将图片转换为 base64"""
|
"""将图片转换为 base64"""
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescri
|
|||||||
async def rename_command(
|
async def rename_command(
|
||||||
handler_full_name: str,
|
handler_full_name: str,
|
||||||
new_fragment: str,
|
new_fragment: str,
|
||||||
|
aliases: list[str] | None = None,
|
||||||
) -> CommandDescriptor:
|
) -> CommandDescriptor:
|
||||||
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
||||||
if not descriptor:
|
if not descriptor:
|
||||||
@@ -99,9 +100,24 @@ async def rename_command(
|
|||||||
if not new_fragment:
|
if not new_fragment:
|
||||||
raise ValueError("指令名不能为空。")
|
raise ValueError("指令名不能为空。")
|
||||||
|
|
||||||
|
# 校验主指令名
|
||||||
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
||||||
if _is_command_in_use(handler_full_name, candidate_full):
|
if _is_command_in_use(handler_full_name, candidate_full):
|
||||||
raise ValueError("新的指令名已被其他指令占用,请换一个名称。")
|
raise ValueError(f"指令名 '{candidate_full}' 已被其他指令占用。")
|
||||||
|
|
||||||
|
# 校验别名
|
||||||
|
if aliases:
|
||||||
|
for alias in aliases:
|
||||||
|
alias = alias.strip()
|
||||||
|
if not alias:
|
||||||
|
continue
|
||||||
|
alias_full = _compose_command(descriptor.parent_signature, alias)
|
||||||
|
if _is_command_in_use(handler_full_name, alias_full):
|
||||||
|
raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。")
|
||||||
|
|
||||||
|
existing_cfg = await db_helper.get_command_config(handler_full_name)
|
||||||
|
merged_extra = dict(existing_cfg.extra_data or {}) if existing_cfg else {}
|
||||||
|
merged_extra["resolved_aliases"] = aliases or []
|
||||||
|
|
||||||
config = await db_helper.upsert_command_config(
|
config = await db_helper.upsert_command_config(
|
||||||
handler_full_name=handler_full_name,
|
handler_full_name=handler_full_name,
|
||||||
@@ -114,7 +130,7 @@ async def rename_command(
|
|||||||
conflict_key=descriptor.original_command,
|
conflict_key=descriptor.original_command,
|
||||||
resolution_strategy="manual_rename",
|
resolution_strategy="manual_rename",
|
||||||
note=None,
|
note=None,
|
||||||
extra_data=None,
|
extra_data=merged_extra,
|
||||||
auto_managed=False,
|
auto_managed=False,
|
||||||
)
|
)
|
||||||
_bind_descriptor_with_config(descriptor, config)
|
_bind_descriptor_with_config(descriptor, config)
|
||||||
@@ -363,14 +379,27 @@ def _apply_config_to_descriptor(
|
|||||||
new_fragment,
|
new_fragment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extra = config.extra_data or {}
|
||||||
|
resolved_aliases = extra.get("resolved_aliases")
|
||||||
|
if isinstance(resolved_aliases, list):
|
||||||
|
descriptor.aliases = [str(x) for x in resolved_aliases if str(x).strip()]
|
||||||
|
|
||||||
|
|
||||||
def _apply_config_to_runtime(
|
def _apply_config_to_runtime(
|
||||||
descriptor: CommandDescriptor,
|
descriptor: CommandDescriptor,
|
||||||
config: CommandConfig,
|
config: CommandConfig,
|
||||||
) -> None:
|
) -> None:
|
||||||
descriptor.handler.enabled = config.enabled
|
descriptor.handler.enabled = config.enabled
|
||||||
if descriptor.filter_ref and descriptor.current_fragment:
|
if descriptor.filter_ref:
|
||||||
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
if descriptor.current_fragment:
|
||||||
|
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
||||||
|
extra = config.extra_data or {}
|
||||||
|
resolved_aliases = extra.get("resolved_aliases")
|
||||||
|
if isinstance(resolved_aliases, list):
|
||||||
|
_set_filter_aliases(
|
||||||
|
descriptor.filter_ref,
|
||||||
|
[str(x) for x in resolved_aliases if str(x).strip()],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _bind_configs_to_descriptors(
|
def _bind_configs_to_descriptors(
|
||||||
@@ -409,6 +438,18 @@ def _set_filter_fragment(
|
|||||||
filter_ref._cmpl_cmd_names = None
|
filter_ref._cmpl_cmd_names = None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_filter_aliases(
|
||||||
|
filter_ref: CommandFilter | CommandGroupFilter,
|
||||||
|
aliases: list[str],
|
||||||
|
) -> None:
|
||||||
|
current_aliases = getattr(filter_ref, "alias", set())
|
||||||
|
if set(aliases) == current_aliases:
|
||||||
|
return
|
||||||
|
setattr(filter_ref, "alias", set(aliases))
|
||||||
|
if hasattr(filter_ref, "_cmpl_cmd_names"):
|
||||||
|
filter_ref._cmpl_cmd_names = None
|
||||||
|
|
||||||
|
|
||||||
def _is_command_in_use(
|
def _is_command_in_use(
|
||||||
target_handler_full_name: str,
|
target_handler_full_name: str,
|
||||||
candidate_full_command: str,
|
candidate_full_command: str,
|
||||||
|
|||||||
@@ -61,12 +61,13 @@ class CommandRoute(Route):
|
|||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
handler_full_name = data.get("handler_full_name")
|
handler_full_name = data.get("handler_full_name")
|
||||||
new_name = data.get("new_name")
|
new_name = data.get("new_name")
|
||||||
|
aliases = data.get("aliases")
|
||||||
|
|
||||||
if not handler_full_name or not new_name:
|
if not handler_full_name or not new_name:
|
||||||
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
|
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await rename_command_service(handler_full_name, new_name)
|
await rename_command_service(handler_full_name, new_name, aliases=aliases)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return Response().error(str(exc)).__dict__
|
return Response().error(str(exc)).__dict__
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
1. ‼️‼️ 修复了由 `psutil` 新版本导致的启动时报错的问题。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
1. 插件指令管理支持管理别名。
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useModuleI18n } from '@/i18n/composables';
|
import { useModuleI18n } from '@/i18n/composables';
|
||||||
import type { CommandItem } from '../types';
|
import type { CommandItem } from '../types';
|
||||||
|
|
||||||
const { tm } = useModuleI18n('features/command');
|
const { tm } = useModuleI18n('features/command');
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
command: CommandItem | null;
|
command: CommandItem | null;
|
||||||
newName: string;
|
newName: string;
|
||||||
|
aliases: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -16,8 +18,42 @@ defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:show', value: boolean): void;
|
(e: 'update:show', value: boolean): void;
|
||||||
(e: 'update:newName', value: string): void;
|
(e: 'update:newName', value: string): void;
|
||||||
|
(e: 'update:aliases', value: string[]): void;
|
||||||
(e: 'confirm'): void;
|
(e: 'confirm'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const addAlias = () => {
|
||||||
|
emit('update:aliases', [...props.aliases, '']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAlias = (index: number) => {
|
||||||
|
const newAliases = [...props.aliases];
|
||||||
|
newAliases.splice(index, 1);
|
||||||
|
emit('update:aliases', newAliases);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAlias = (index: number, value: string) => {
|
||||||
|
const newAliases = [...props.aliases];
|
||||||
|
newAliases[index] = value;
|
||||||
|
emit('update:aliases', newAliases);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAliases = computed(() => (props.aliases || []).some(a => (a ?? '').toString().trim()));
|
||||||
|
const showAliasEditor = ref(false);
|
||||||
|
const aliasEditorEverOpened = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(open) => {
|
||||||
|
if (!open) return;
|
||||||
|
// 如果已有别名则默认展开,否则默认收起
|
||||||
|
showAliasEditor.value = hasAliases.value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(showAliasEditor, (open) => {
|
||||||
|
if (open) aliasEditorEverOpened.value = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,7 +68,49 @@ const emit = defineEmits<{
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
autofocus
|
autofocus
|
||||||
|
class="mb-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-card variant="outlined" class="mt-2" elevation="0">
|
||||||
|
<div
|
||||||
|
class="d-flex align-center justify-space-between px-4 py-3"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="showAliasEditor = !showAliasEditor"
|
||||||
|
@keydown.enter.prevent="showAliasEditor = !showAliasEditor"
|
||||||
|
@keydown.space.prevent="showAliasEditor = !showAliasEditor"
|
||||||
|
>
|
||||||
|
<div class="text-subtitle-1">{{ tm('dialogs.rename.aliases') }}</div>
|
||||||
|
<v-icon size="20">{{ showAliasEditor ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||||
|
</div>
|
||||||
|
<v-divider v-if="showAliasEditor" />
|
||||||
|
<v-slide-y-transition>
|
||||||
|
<div v-if="aliasEditorEverOpened" v-show="showAliasEditor" class="px-4 py-3">
|
||||||
|
<div v-for="(alias, index) in aliases" :key="index" class="d-flex align-center mb-2">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="alias"
|
||||||
|
@update:model-value="updateAlias(index, $event)"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
class="flex-grow-1 mr-2"
|
||||||
|
/>
|
||||||
|
<v-btn icon="mdi-delete" variant="text" color="error" density="compact" @click="removeAlias(index)" />
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
size="small"
|
||||||
|
class="mt-2"
|
||||||
|
@click="addAlias"
|
||||||
|
>
|
||||||
|
{{ tm('dialogs.rename.addAlias') }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-slide-y-transition>
|
||||||
|
</v-card>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function useCommandActions(
|
|||||||
show: false,
|
show: false,
|
||||||
command: null,
|
command: null,
|
||||||
newName: '',
|
newName: '',
|
||||||
|
aliases: [],
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export function useCommandActions(
|
|||||||
const openRenameDialog = (cmd: CommandItem) => {
|
const openRenameDialog = (cmd: CommandItem) => {
|
||||||
renameDialog.command = cmd;
|
renameDialog.command = cmd;
|
||||||
renameDialog.newName = cmd.current_fragment || '';
|
renameDialog.newName = cmd.current_fragment || '';
|
||||||
|
renameDialog.aliases = [...(cmd.aliases || [])];
|
||||||
renameDialog.show = true;
|
renameDialog.show = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,7 +68,8 @@ export function useCommandActions(
|
|||||||
try {
|
try {
|
||||||
const res = await axios.post('/api/commands/rename', {
|
const res = await axios.post('/api/commands/rename', {
|
||||||
handler_full_name: renameDialog.command.handler_full_name,
|
handler_full_name: renameDialog.command.handler_full_name,
|
||||||
new_name: renameDialog.newName.trim()
|
new_name: renameDialog.newName.trim(),
|
||||||
|
aliases: renameDialog.aliases.filter(a => a.trim())
|
||||||
});
|
});
|
||||||
if (res.data.status === 'ok') {
|
if (res.data.status === 'ok') {
|
||||||
toast(successMessage, 'success');
|
toast(successMessage, 'success');
|
||||||
|
|||||||
@@ -288,6 +288,8 @@ watch(viewMode, async (mode) => {
|
|||||||
@update:show="renameDialog.show = $event"
|
@update:show="renameDialog.show = $event"
|
||||||
:new-name="renameDialog.newName"
|
:new-name="renameDialog.newName"
|
||||||
@update:new-name="renameDialog.newName = $event"
|
@update:new-name="renameDialog.newName = $event"
|
||||||
|
:aliases="renameDialog.aliases"
|
||||||
|
@update:aliases="renameDialog.aliases = $event"
|
||||||
:command="renameDialog.command"
|
:command="renameDialog.command"
|
||||||
:loading="renameDialog.loading"
|
:loading="renameDialog.loading"
|
||||||
@confirm="handleConfirmRename"
|
@confirm="handleConfirmRename"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface RenameDialogState {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
command: CommandItem | null;
|
command: CommandItem | null;
|
||||||
newName: string;
|
newName: string;
|
||||||
|
aliases: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@
|
|||||||
"rename": {
|
"rename": {
|
||||||
"title": "Rename Command",
|
"title": "Rename Command",
|
||||||
"newName": "New command name",
|
"newName": "New command name",
|
||||||
|
"aliases": "Manage aliases",
|
||||||
|
"addAlias": "Add alias",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Confirm"
|
"confirm": "Confirm"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,8 @@
|
|||||||
"rename": {
|
"rename": {
|
||||||
"title": "重命名指令",
|
"title": "重命名指令",
|
||||||
"newName": "新指令名",
|
"newName": "新指令名",
|
||||||
|
"aliases": "管理别名",
|
||||||
|
"addAlias": "添加别名",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"confirm": "确认"
|
"confirm": "确认"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from astrbot.api import logger, sp, star
|
|||||||
from astrbot.api.event import AstrMessageEvent
|
from astrbot.api.event import AstrMessageEvent
|
||||||
from astrbot.api.message_components import Image, Reply
|
from astrbot.api.message_components import Image, Reply
|
||||||
from astrbot.api.provider import Provider, ProviderRequest
|
from astrbot.api.provider import Provider, ProviderRequest
|
||||||
|
from astrbot.core.agent.message import TextPart
|
||||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +86,9 @@ class ProcessLLMRequest:
|
|||||||
req.image_urls,
|
req.image_urls,
|
||||||
)
|
)
|
||||||
if caption:
|
if caption:
|
||||||
req.prompt = f"(Image Caption: {caption})\n\n{req.prompt}"
|
req.extra_user_content_parts.append(
|
||||||
|
TextPart(text=f"<image_caption>{caption}</image_caption>")
|
||||||
|
)
|
||||||
req.image_urls = []
|
req.image_urls = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理图片描述失败: {e}")
|
logger.error(f"处理图片描述失败: {e}")
|
||||||
@@ -129,13 +132,14 @@ class ProcessLLMRequest:
|
|||||||
else:
|
else:
|
||||||
req.prompt = prefix + req.prompt
|
req.prompt = prefix + req.prompt
|
||||||
|
|
||||||
|
# 收集系统提醒信息
|
||||||
|
system_parts = []
|
||||||
|
|
||||||
# user identifier
|
# user identifier
|
||||||
if cfg.get("identifier"):
|
if cfg.get("identifier"):
|
||||||
user_id = event.message_obj.sender.user_id
|
user_id = event.message_obj.sender.user_id
|
||||||
user_nickname = event.message_obj.sender.nickname
|
user_nickname = event.message_obj.sender.nickname
|
||||||
req.prompt = (
|
system_parts.append(f"User ID: {user_id}, Nickname: {user_nickname}")
|
||||||
f"\n[User ID: {user_id}, Nickname: {user_nickname}]\n{req.prompt}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# group name identifier
|
# group name identifier
|
||||||
if cfg.get("group_name_display") and event.message_obj.group_id:
|
if cfg.get("group_name_display") and event.message_obj.group_id:
|
||||||
@@ -146,7 +150,7 @@ class ProcessLLMRequest:
|
|||||||
return
|
return
|
||||||
group_name = event.message_obj.group.group_name
|
group_name = event.message_obj.group.group_name
|
||||||
if group_name:
|
if group_name:
|
||||||
req.system_prompt += f"\nGroup name: {group_name}\n"
|
system_parts.append(f"Group name: {group_name}")
|
||||||
|
|
||||||
# time info
|
# time info
|
||||||
if cfg.get("datetime_system_prompt"):
|
if cfg.get("datetime_system_prompt"):
|
||||||
@@ -162,7 +166,7 @@ class ProcessLLMRequest:
|
|||||||
current_time = (
|
current_time = (
|
||||||
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
||||||
)
|
)
|
||||||
req.system_prompt += f"\nCurrent datetime: {current_time}\n"
|
system_parts.append(f"Current datetime: {current_time}")
|
||||||
|
|
||||||
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
||||||
if req.conversation:
|
if req.conversation:
|
||||||
@@ -225,10 +229,17 @@ class ProcessLLMRequest:
|
|||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(f"处理引用图片失败: {e}")
|
logger.error(f"处理引用图片失败: {e}")
|
||||||
|
|
||||||
# 3. 将所有部分组合成文本并直接注入到当前消息中
|
# 3. 将所有部分组合成文本并添加到 extra_user_content_parts 中
|
||||||
# 确保引用内容被正确的标签包裹
|
# 确保引用内容被正确的标签包裹
|
||||||
quoted_content = "\n".join(content_parts)
|
quoted_content = "\n".join(content_parts)
|
||||||
# 确保所有内容都在<Quoted Message>标签内
|
# 确保所有内容都在<Quoted Message>标签内
|
||||||
quoted_text = f"<Quoted Message>\n{quoted_content}\n</Quoted Message>"
|
quoted_text = f"<Quoted Message>\n{quoted_content}\n</Quoted Message>"
|
||||||
|
|
||||||
req.prompt = f"{quoted_text}\n\n{req.prompt}"
|
req.extra_user_content_parts.append(TextPart(text=quoted_text))
|
||||||
|
|
||||||
|
# 统一包裹所有系统提醒
|
||||||
|
if system_parts:
|
||||||
|
system_content = (
|
||||||
|
"<system_reminder>" + "\n".join(system_parts) + "</system_reminder>"
|
||||||
|
)
|
||||||
|
req.extra_user_content_parts.append(TextPart(text=system_content))
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "AstrBot"
|
name = "AstrBot"
|
||||||
version = "4.10.1"
|
version = "4.10.2"
|
||||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -34,7 +34,7 @@ dependencies = [
|
|||||||
"ormsgpack>=1.9.1",
|
"ormsgpack>=1.9.1",
|
||||||
"pillow>=11.2.1",
|
"pillow>=11.2.1",
|
||||||
"pip>=25.1.1",
|
"pip>=25.1.1",
|
||||||
"psutil>=5.8.0",
|
"psutil>=5.8.0,<7.2.0",
|
||||||
"py-cord>=2.6.1",
|
"py-cord>=2.6.1",
|
||||||
"pydantic~=2.10.3",
|
"pydantic~=2.10.3",
|
||||||
"pydub>=0.25.1",
|
"pydub>=0.25.1",
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ openai>=1.78.0
|
|||||||
ormsgpack>=1.9.1
|
ormsgpack>=1.9.1
|
||||||
pillow>=11.2.1
|
pillow>=11.2.1
|
||||||
pip>=25.1.1
|
pip>=25.1.1
|
||||||
psutil>=5.8.0
|
psutil>=5.8.0,<7.2.0
|
||||||
py-cord>=2.6.1
|
py-cord>=2.6.1
|
||||||
pydantic~=2.10.3
|
pydantic~=2.10.3
|
||||||
pydub>=0.25.1
|
pydub>=0.25.1
|
||||||
|
|||||||
Reference in New Issue
Block a user