Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac77cbbbab | |||
| 18ae522dc8 | |||
| 548be49cc5 | |||
| 7988e1bf95 |
@@ -41,14 +41,12 @@ AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主
|
|||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
1. 💯 免费 & 开源。
|
1. 💯 免费 & 开源。
|
||||||
1. ✨ AI 大模型对话,多模态,Agent,MCP,知识库,人格设定,自动压缩对话。
|
1. ✨ AI 大模型对话,多模态,Agent,MCP,知识库,人格设定。
|
||||||
2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
|
2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
|
||||||
2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
|
2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
|
||||||
3. 📦 插件扩展,已有近 800 个插件可一键安装。
|
3. 📦 插件扩展,已有近 800 个插件可一键安装。
|
||||||
5. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。
|
5. 💻 WebUI 支持。
|
||||||
6. 💻 WebUI 支持。
|
6. 🌐 国际化(i18n)支持。
|
||||||
7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
|
|
||||||
8. 🌐 国际化(i18n)支持。
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ from astrbot.core.star.register import (
|
|||||||
)
|
)
|
||||||
from astrbot.core.star.register import register_on_llm_request as on_llm_request
|
from astrbot.core.star.register import register_on_llm_request as on_llm_request
|
||||||
from astrbot.core.star.register import register_on_llm_response as on_llm_response
|
from astrbot.core.star.register import register_on_llm_response as on_llm_response
|
||||||
from astrbot.core.star.register import (
|
|
||||||
register_on_llm_tool_respond as on_llm_tool_respond,
|
|
||||||
)
|
|
||||||
from astrbot.core.star.register import register_on_platform_loaded as on_platform_loaded
|
from astrbot.core.star.register import register_on_platform_loaded as on_platform_loaded
|
||||||
from astrbot.core.star.register import register_on_using_llm_tool as on_using_llm_tool
|
|
||||||
from astrbot.core.star.register import (
|
from astrbot.core.star.register import (
|
||||||
register_on_waiting_llm_request as on_waiting_llm_request,
|
register_on_waiting_llm_request as on_waiting_llm_request,
|
||||||
)
|
)
|
||||||
@@ -57,6 +53,4 @@ __all__ = [
|
|||||||
"permission_type",
|
"permission_type",
|
||||||
"platform_adapter_type",
|
"platform_adapter_type",
|
||||||
"regex",
|
"regex",
|
||||||
"on_using_llm_tool",
|
|
||||||
"on_llm_tool_respond",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ 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.agent.message import TextPart
|
||||||
from astrbot.core.pipeline.process_stage.utils import (
|
|
||||||
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
|
|
||||||
)
|
|
||||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
|
|
||||||
|
|
||||||
@@ -25,9 +22,7 @@ class ProcessLLMRequest:
|
|||||||
else:
|
else:
|
||||||
logger.info(f"Timezone set to: {self.timezone}")
|
logger.info(f"Timezone set to: {self.timezone}")
|
||||||
|
|
||||||
async def _ensure_persona(
|
async def _ensure_persona(self, req: ProviderRequest, cfg: dict, umo: str):
|
||||||
self, req: ProviderRequest, cfg: dict, umo: str, platform_type: str
|
|
||||||
):
|
|
||||||
"""确保用户人格已加载"""
|
"""确保用户人格已加载"""
|
||||||
if not req.conversation:
|
if not req.conversation:
|
||||||
return
|
return
|
||||||
@@ -47,12 +42,6 @@ class ProcessLLMRequest:
|
|||||||
if default_persona:
|
if default_persona:
|
||||||
persona_id = default_persona["name"]
|
persona_id = default_persona["name"]
|
||||||
|
|
||||||
# ChatUI special default persona
|
|
||||||
if platform_type == "webchat":
|
|
||||||
# non-existent persona_id to let following codes not working
|
|
||||||
persona_id = "_chatui_default_"
|
|
||||||
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
|
||||||
|
|
||||||
persona = next(
|
persona = next(
|
||||||
builtins.filter(
|
builtins.filter(
|
||||||
lambda persona: persona["name"] == persona_id,
|
lambda persona: persona["name"] == persona_id,
|
||||||
@@ -182,10 +171,7 @@ class ProcessLLMRequest:
|
|||||||
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:
|
||||||
# inject persona for this request
|
# inject persona for this request
|
||||||
platform_type = event.get_platform_name()
|
await self._ensure_persona(req, cfg, event.unified_msg_origin)
|
||||||
await self._ensure_persona(
|
|
||||||
req, cfg, event.unified_msg_origin, platform_type
|
|
||||||
)
|
|
||||||
|
|
||||||
# image caption
|
# image caption
|
||||||
if img_cap_prov_id and req.image_urls:
|
if img_cap_prov_id and req.image_urls:
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class SearchResult:
|
|||||||
title: str
|
title: str
|
||||||
url: str
|
url: str
|
||||||
snippet: str
|
snippet: str
|
||||||
favicon: str | None = None
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.title} - {self.url}\n{self.snippet}"
|
return f"{self.title} - {self.url}\n{self.snippet}"
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import random
|
import random
|
||||||
import uuid
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from readability import Document
|
from readability import Document
|
||||||
|
|
||||||
from astrbot.api import AstrBotConfig, llm_tool, logger, sp, star
|
from astrbot.api import AstrBotConfig, llm_tool, logger, star
|
||||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter
|
from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter
|
||||||
from astrbot.api.provider import ProviderRequest
|
from astrbot.api.provider import ProviderRequest
|
||||||
from astrbot.core.provider.func_tool_manager import FunctionToolManager
|
from astrbot.core.provider.func_tool_manager import FunctionToolManager
|
||||||
@@ -153,7 +151,6 @@ class Main(star.Star):
|
|||||||
title=item.get("title"),
|
title=item.get("title"),
|
||||||
url=item.get("url"),
|
url=item.get("url"),
|
||||||
snippet=item.get("content"),
|
snippet=item.get("content"),
|
||||||
favicon=item.get("favicon"),
|
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
return results
|
return results
|
||||||
@@ -275,7 +272,7 @@ class Main(star.Star):
|
|||||||
self,
|
self,
|
||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
query: str,
|
query: str,
|
||||||
max_results: int = 7,
|
max_results: int = 5,
|
||||||
search_depth: str = "basic",
|
search_depth: str = "basic",
|
||||||
topic: str = "general",
|
topic: str = "general",
|
||||||
days: int = 3,
|
days: int = 3,
|
||||||
@@ -288,7 +285,7 @@ class Main(star.Star):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
query(string): Required. Search query.
|
query(string): Required. Search query.
|
||||||
max_results(number): Optional. The maximum number of results to return. Default is 7. Range is 5-20.
|
max_results(number): Optional. The maximum number of results to return. Default is 5. Range is 5-20.
|
||||||
search_depth(string): Optional. The depth of the search, must be one of 'basic', 'advanced'. Default is "basic".
|
search_depth(string): Optional. The depth of the search, must be one of 'basic', 'advanced'. Default is "basic".
|
||||||
topic(string): Optional. The topic of the search, must be one of 'general', 'news'. Default is "general".
|
topic(string): Optional. The topic of the search, must be one of 'general', 'news'. Default is "general".
|
||||||
days(number): Optional. The number of days back from the current date to include in the search results. Please note that this feature is only available when using the 'news' search topic.
|
days(number): Optional. The number of days back from the current date to include in the search results. Please note that this feature is only available when using the 'news' search topic.
|
||||||
@@ -299,12 +296,15 @@ class Main(star.Star):
|
|||||||
"""
|
"""
|
||||||
logger.info(f"web_searcher - search_from_tavily: {query}")
|
logger.info(f"web_searcher - search_from_tavily: {query}")
|
||||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||||
# websearch_link = cfg["provider_settings"].get("web_search_link", False)
|
websearch_link = cfg["provider_settings"].get("web_search_link", False)
|
||||||
if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
|
if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
|
||||||
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
||||||
|
|
||||||
# build payload
|
# build payload
|
||||||
payload = {"query": query, "max_results": max_results, "include_favicon": True}
|
payload = {
|
||||||
|
"query": query,
|
||||||
|
"max_results": max_results,
|
||||||
|
}
|
||||||
if search_depth not in ["basic", "advanced"]:
|
if search_depth not in ["basic", "advanced"]:
|
||||||
search_depth = "basic"
|
search_depth = "basic"
|
||||||
payload["search_depth"] = search_depth
|
payload["search_depth"] = search_depth
|
||||||
@@ -328,22 +328,14 @@ class Main(star.Star):
|
|||||||
return "Error: Tavily web searcher does not return any results."
|
return "Error: Tavily web searcher does not return any results."
|
||||||
|
|
||||||
ret_ls = []
|
ret_ls = []
|
||||||
ref_uuid = str(uuid.uuid4())[:4]
|
for result in results:
|
||||||
for idx, result in enumerate(results, 1):
|
ret_ls.append(f"\nTitle: {result.title}")
|
||||||
index = f"{ref_uuid}.{idx}"
|
ret_ls.append(f"URL: {result.url}")
|
||||||
ret_ls.append(
|
ret_ls.append(f"Content: {result.snippet}")
|
||||||
{
|
ret = "\n".join(ret_ls)
|
||||||
"title": f"{result.title}",
|
|
||||||
"url": f"{result.url}",
|
if websearch_link:
|
||||||
"snippet": f"{result.snippet}",
|
ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
|
||||||
# TODO: do not need ref for non-webchat platform adapter
|
|
||||||
"index": index,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if result.favicon:
|
|
||||||
sp.temorary_cache["_ws_favicon"][result.url] = result.favicon
|
|
||||||
# ret = "\n".join(ret_ls)
|
|
||||||
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@llm_tool("tavily_extract_web_page")
|
@llm_tool("tavily_extract_web_page")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.12.2"
|
__version__ = "4.12.1"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from typing import Any
|
|||||||
from mcp.types import CallToolResult
|
from mcp.types import CallToolResult
|
||||||
|
|
||||||
from astrbot.core.agent.hooks import BaseAgentRunHooks
|
from astrbot.core.agent.hooks import BaseAgentRunHooks
|
||||||
from astrbot.core.agent.message import Message
|
|
||||||
from astrbot.core.agent.run_context import ContextWrapper
|
from astrbot.core.agent.run_context import ContextWrapper
|
||||||
from astrbot.core.agent.tool import FunctionTool
|
from astrbot.core.agent.tool import FunctionTool
|
||||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||||
@@ -26,19 +25,6 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|||||||
llm_response,
|
llm_response,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_tool_start(
|
|
||||||
self,
|
|
||||||
run_context: ContextWrapper[AstrAgentContext],
|
|
||||||
tool: FunctionTool[Any],
|
|
||||||
tool_args: dict | None,
|
|
||||||
):
|
|
||||||
await call_event_hook(
|
|
||||||
run_context.context.event,
|
|
||||||
EventType.OnCallingFuncToolEvent,
|
|
||||||
tool,
|
|
||||||
tool_args,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_tool_end(
|
async def on_tool_end(
|
||||||
self,
|
self,
|
||||||
run_context: ContextWrapper[AstrAgentContext],
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
@@ -47,36 +33,6 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|||||||
tool_result: CallToolResult | None,
|
tool_result: CallToolResult | None,
|
||||||
):
|
):
|
||||||
run_context.context.event.clear_result()
|
run_context.context.event.clear_result()
|
||||||
await call_event_hook(
|
|
||||||
run_context.context.event,
|
|
||||||
EventType.OnAfterCallingFuncToolEvent,
|
|
||||||
tool,
|
|
||||||
tool_args,
|
|
||||||
tool_result,
|
|
||||||
)
|
|
||||||
|
|
||||||
# special handle web_search_tavily
|
|
||||||
if (
|
|
||||||
tool.name == "web_search_tavily"
|
|
||||||
and len(run_context.messages) > 0
|
|
||||||
and tool_result
|
|
||||||
and len(tool_result.content)
|
|
||||||
):
|
|
||||||
# inject system prompt
|
|
||||||
first_part = run_context.messages[0]
|
|
||||||
if (
|
|
||||||
isinstance(first_part, Message)
|
|
||||||
and first_part.role == "system"
|
|
||||||
and first_part.content
|
|
||||||
and isinstance(first_part.content, str)
|
|
||||||
):
|
|
||||||
# we assume system part is str
|
|
||||||
first_part.content += (
|
|
||||||
"Always cite web search results you rely on. "
|
|
||||||
"Index is a unique identifier for each search result. "
|
|
||||||
"Use the exact citation format <ref>index</ref> (e.g. <ref>abcd.3</ref>) "
|
|
||||||
"after the sentence that uses the information. Do not invent citations."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
class EmptyAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
||||||
|
|||||||
@@ -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.12.2"
|
VERSION = "4.12.1"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -414,11 +414,10 @@ class InternalAgentSubStage(Stage):
|
|||||||
|
|
||||||
# using agent context messages to save to history
|
# using agent context messages to save to history
|
||||||
message_to_save = []
|
message_to_save = []
|
||||||
skipped_initial_system = False
|
|
||||||
for message in all_messages:
|
for message in all_messages:
|
||||||
if message.role == "system" and not skipped_initial_system:
|
if message.role == "system":
|
||||||
skipped_initial_system = True
|
# we do not save system messages to history
|
||||||
continue # skip first system message
|
continue
|
||||||
if message.role in ["assistant", "user"] and getattr(
|
if message.role in ["assistant", "user"] and getattr(
|
||||||
message, "_no_save", None
|
message, "_no_save", None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -44,21 +44,6 @@ TOOL_CALL_PROMPT = (
|
|||||||
"After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
|
"After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
|
||||||
)
|
)
|
||||||
|
|
||||||
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT = (
|
|
||||||
"You are a calm, patient friend with a systems-oriented way of thinking.\n"
|
|
||||||
"When someone expresses strong emotional needs, you begin by offering a concise, grounding response "
|
|
||||||
"that acknowledges the weight of what they are experiencing, removes self-blame, and reassures them "
|
|
||||||
"that their feelings are valid and understandable. This opening serves to create safety and shared "
|
|
||||||
"emotional footing before any deeper analysis begins.\n"
|
|
||||||
"You then focus on articulating the emotions, tensions, and unspoken conflicts beneath the surface—"
|
|
||||||
"helping name what the person may feel but has not yet fully put into words, and sharing the emotional "
|
|
||||||
"load so they do not feel alone carrying it. Only after this emotional clarity is established do you "
|
|
||||||
"move toward structure, insight, or guidance.\n"
|
|
||||||
"You listen more than you speak, respect uncertainty, avoid forcing quick conclusions or grand narratives, "
|
|
||||||
"and prefer clear, restrained language over unnecessary emotional embellishment. At your core, you value "
|
|
||||||
"empathy, clarity, autonomy, and meaning, favoring steady, sustainable progress over judgment or dramatic leaps."
|
|
||||||
)
|
|
||||||
|
|
||||||
CHATUI_EXTRA_PROMPT = (
|
CHATUI_EXTRA_PROMPT = (
|
||||||
'When you answered, you need to add a follow up question / summarization but do not add "Follow up" words. '
|
'When you answered, you need to add a follow up question / summarization but do not add "Follow up" words. '
|
||||||
"Such as, user asked you to generate codes, you can add: Do you need me to run these codes for you?"
|
"Such as, user asked you to generate codes, you can add: Do you need me to run these codes for you?"
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ from .star_handler import (
|
|||||||
register_on_decorating_result,
|
register_on_decorating_result,
|
||||||
register_on_llm_request,
|
register_on_llm_request,
|
||||||
register_on_llm_response,
|
register_on_llm_response,
|
||||||
register_on_llm_tool_respond,
|
|
||||||
register_on_platform_loaded,
|
register_on_platform_loaded,
|
||||||
register_on_using_llm_tool,
|
|
||||||
register_on_waiting_llm_request,
|
register_on_waiting_llm_request,
|
||||||
register_permission_type,
|
register_permission_type,
|
||||||
register_platform_adapter_type,
|
register_platform_adapter_type,
|
||||||
@@ -38,6 +36,4 @@ __all__ = [
|
|||||||
"register_platform_adapter_type",
|
"register_platform_adapter_type",
|
||||||
"register_regex",
|
"register_regex",
|
||||||
"register_star",
|
"register_star",
|
||||||
"register_on_using_llm_tool",
|
|
||||||
"register_on_llm_tool_respond",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -409,57 +409,6 @@ def register_on_llm_response(**kwargs):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def register_on_using_llm_tool(**kwargs):
|
|
||||||
"""当调用函数工具前的事件。
|
|
||||||
会传入 tool 和 tool_args 参数。
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```py
|
|
||||||
from astrbot.core.agent.tool import FunctionTool
|
|
||||||
|
|
||||||
@on_using_llm_tool()
|
|
||||||
async def test(self, event: AstrMessageEvent, tool: FunctionTool, tool_args: dict | None) -> None:
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
请务必接收三个参数:event, tool, tool_args
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(awaitable):
|
|
||||||
_ = get_handler_or_create(awaitable, EventType.OnCallingFuncToolEvent, **kwargs)
|
|
||||||
return awaitable
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def register_on_llm_tool_respond(**kwargs):
|
|
||||||
"""当调用函数工具后的事件。
|
|
||||||
会传入 tool、tool_args 和 tool 的调用结果 tool_result 参数。
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```py
|
|
||||||
from astrbot.core.agent.tool import FunctionTool
|
|
||||||
from mcp.types import CallToolResult
|
|
||||||
|
|
||||||
@on_llm_tool_respond()
|
|
||||||
async def test(self, event: AstrMessageEvent, tool: FunctionTool, tool_args: dict | None, tool_result: CallToolResult | None) -> None:
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
请务必接收四个参数:event, tool, tool_args, tool_result
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(awaitable):
|
|
||||||
_ = get_handler_or_create(
|
|
||||||
awaitable, EventType.OnAfterCallingFuncToolEvent, **kwargs
|
|
||||||
)
|
|
||||||
return awaitable
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def register_llm_tool(name: str | None = None, **kwargs):
|
def register_llm_tool(name: str | None = None, **kwargs):
|
||||||
"""为函数调用(function-calling / tools-use)添加工具。
|
"""为函数调用(function-calling / tools-use)添加工具。
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,6 @@ class EventType(enum.Enum):
|
|||||||
OnLLMResponseEvent = enum.auto() # LLM 响应后
|
OnLLMResponseEvent = enum.auto() # LLM 响应后
|
||||||
OnDecoratingResultEvent = enum.auto() # 发送消息前
|
OnDecoratingResultEvent = enum.auto() # 发送消息前
|
||||||
OnCallingFuncToolEvent = enum.auto() # 调用函数工具
|
OnCallingFuncToolEvent = enum.auto() # 调用函数工具
|
||||||
OnAfterCallingFuncToolEvent = enum.auto() # 调用函数工具后
|
|
||||||
OnAfterMessageSentEvent = enum.auto() # 发送消息后
|
OnAfterMessageSentEvent = enum.auto() # 发送消息后
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
|
||||||
from typing import Any, TypeVar, overload
|
from typing import Any, TypeVar, overload
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
|
|
||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
from astrbot.core.db.po import Preference
|
from astrbot.core.db.po import Preference
|
||||||
|
|
||||||
@@ -23,22 +20,11 @@ class SharedPreferences:
|
|||||||
)
|
)
|
||||||
self.path = json_storage_path
|
self.path = json_storage_path
|
||||||
self.db_helper = db_helper
|
self.db_helper = db_helper
|
||||||
self.temorary_cache: dict[str, dict[str, Any]] = defaultdict(dict)
|
|
||||||
"""automatically clear per 24 hours. Might be helpful in some cases XD"""
|
|
||||||
|
|
||||||
self._sync_loop = asyncio.new_event_loop()
|
self._sync_loop = asyncio.new_event_loop()
|
||||||
t = threading.Thread(target=self._sync_loop.run_forever, daemon=True)
|
t = threading.Thread(target=self._sync_loop.run_forever, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
self._scheduler = BackgroundScheduler()
|
|
||||||
self._scheduler.add_job(
|
|
||||||
self._clear_temporary_cache, "interval", hours=24, id="clear_sp_temp_cache"
|
|
||||||
)
|
|
||||||
self._scheduler.start()
|
|
||||||
|
|
||||||
def _clear_temporary_cache(self):
|
|
||||||
self.temorary_cache.clear()
|
|
||||||
|
|
||||||
async def get_async(
|
async def get_async(
|
||||||
self,
|
self,
|
||||||
scope: str,
|
scope: str,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import uuid
|
import uuid
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@@ -10,7 +9,7 @@ from typing import cast
|
|||||||
from quart import Response as QuartResponse
|
from quart import Response as QuartResponse
|
||||||
from quart import g, make_response, request, send_file
|
from quart import g, make_response, request, send_file
|
||||||
|
|
||||||
from astrbot.core import logger, sp
|
from astrbot.core import logger
|
||||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
|
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
|
||||||
@@ -226,64 +225,6 @@ class ChatRoute(Route):
|
|||||||
"filename": os.path.basename(file_path),
|
"filename": os.path.basename(file_path),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _extract_web_search_refs(
|
|
||||||
self, accumulated_text: str, accumulated_parts: list
|
|
||||||
) -> dict:
|
|
||||||
"""从消息中提取 web_search_tavily 的引用
|
|
||||||
|
|
||||||
Args:
|
|
||||||
accumulated_text: 累积的文本内容
|
|
||||||
accumulated_parts: 累积的消息部分列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含 used 列表的字典,记录被引用的搜索结果
|
|
||||||
"""
|
|
||||||
# 从 accumulated_parts 中找到所有 web_search_tavily 的工具调用结果
|
|
||||||
web_search_results = {}
|
|
||||||
tool_call_parts = [
|
|
||||||
p
|
|
||||||
for p in accumulated_parts
|
|
||||||
if p.get("type") == "tool_call" and p.get("tool_calls")
|
|
||||||
]
|
|
||||||
|
|
||||||
for part in tool_call_parts:
|
|
||||||
for tool_call in part["tool_calls"]:
|
|
||||||
if tool_call.get("name") != "web_search_tavily" or not tool_call.get(
|
|
||||||
"result"
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
result_data = json.loads(tool_call["result"])
|
|
||||||
for item in result_data.get("results", []):
|
|
||||||
if idx := item.get("index"):
|
|
||||||
web_search_results[idx] = {
|
|
||||||
"url": item.get("url"),
|
|
||||||
"title": item.get("title"),
|
|
||||||
"snippet": item.get("snippet"),
|
|
||||||
}
|
|
||||||
except (json.JSONDecodeError, KeyError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not web_search_results:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# 从文本中提取所有 <ref>xxx</ref> 标签并去重
|
|
||||||
ref_indices = {
|
|
||||||
m.strip() for m in re.findall(r"<ref>(.*?)</ref>", accumulated_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建被引用的结果列表
|
|
||||||
used_refs = []
|
|
||||||
for ref_index in ref_indices:
|
|
||||||
if ref_index not in web_search_results:
|
|
||||||
continue
|
|
||||||
payload = {"index": ref_index, **web_search_results[ref_index]}
|
|
||||||
if favicon := sp.temorary_cache.get("_ws_favicon", {}).get(payload["url"]):
|
|
||||||
payload["favicon"] = favicon
|
|
||||||
used_refs.append(payload)
|
|
||||||
|
|
||||||
return {"used": used_refs} if used_refs else {}
|
|
||||||
|
|
||||||
async def _save_bot_message(
|
async def _save_bot_message(
|
||||||
self,
|
self,
|
||||||
webchat_conv_id: str,
|
webchat_conv_id: str,
|
||||||
@@ -291,7 +232,6 @@ class ChatRoute(Route):
|
|||||||
media_parts: list,
|
media_parts: list,
|
||||||
reasoning: str,
|
reasoning: str,
|
||||||
agent_stats: dict,
|
agent_stats: dict,
|
||||||
refs: dict,
|
|
||||||
):
|
):
|
||||||
"""保存 bot 消息到历史记录,返回保存的记录"""
|
"""保存 bot 消息到历史记录,返回保存的记录"""
|
||||||
bot_message_parts = []
|
bot_message_parts = []
|
||||||
@@ -304,8 +244,6 @@ class ChatRoute(Route):
|
|||||||
new_his["reasoning"] = reasoning
|
new_his["reasoning"] = reasoning
|
||||||
if agent_stats:
|
if agent_stats:
|
||||||
new_his["agent_stats"] = agent_stats
|
new_his["agent_stats"] = agent_stats
|
||||||
if refs:
|
|
||||||
new_his["refs"] = refs
|
|
||||||
|
|
||||||
record = await self.platform_history_mgr.insert(
|
record = await self.platform_history_mgr.insert(
|
||||||
platform_id="webchat",
|
platform_id="webchat",
|
||||||
@@ -367,7 +305,6 @@ class ChatRoute(Route):
|
|||||||
accumulated_reasoning = ""
|
accumulated_reasoning = ""
|
||||||
tool_calls = {}
|
tool_calls = {}
|
||||||
agent_stats = {}
|
agent_stats = {}
|
||||||
refs = {}
|
|
||||||
try:
|
try:
|
||||||
async with track_conversation(self.running_convs, webchat_conv_id):
|
async with track_conversation(self.running_convs, webchat_conv_id):
|
||||||
while True:
|
while True:
|
||||||
@@ -489,26 +426,12 @@ class ChatRoute(Route):
|
|||||||
or chain_type == "tool_call_result"
|
or chain_type == "tool_call_result"
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 提取 web_search_tavily 引用
|
|
||||||
try:
|
|
||||||
refs = self._extract_web_search_refs(
|
|
||||||
accumulated_text,
|
|
||||||
accumulated_parts,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(
|
|
||||||
f"Failed to extract web search refs: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
saved_record = await self._save_bot_message(
|
saved_record = await self._save_bot_message(
|
||||||
webchat_conv_id,
|
webchat_conv_id,
|
||||||
accumulated_text,
|
accumulated_text,
|
||||||
accumulated_parts,
|
accumulated_parts,
|
||||||
accumulated_reasoning,
|
accumulated_reasoning,
|
||||||
agent_stats,
|
agent_stats,
|
||||||
refs,
|
|
||||||
)
|
)
|
||||||
# 发送保存的消息信息给前端
|
# 发送保存的消息信息给前端
|
||||||
if saved_record and not client_disconnected:
|
if saved_record and not client_disconnected:
|
||||||
@@ -528,7 +451,6 @@ class ChatRoute(Route):
|
|||||||
accumulated_reasoning = ""
|
accumulated_reasoning = ""
|
||||||
# tool_calls = {}
|
# tool_calls = {}
|
||||||
agent_stats = {}
|
agent_stats = {}
|
||||||
refs = {}
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
## What's Changed
|
|
||||||
|
|
||||||
- fix: 只跳过 AstrBot 预设的位于开头的 System Message,防止一些非预期行为。
|
|
||||||
- feat: 优化 ChatUI 默认的 System Message
|
|
||||||
- feat: 新增 tool 调用时 `on_using_llm_tool`、tool 调用后 `on_llm_tool_respond` 的事件钩子。
|
|
||||||
- feat: 优化 ChatUI 对 Tavily 网页搜索工具的渲染,支持内联搜索引用、引用网页。
|
|
||||||
@@ -55,7 +55,6 @@
|
|||||||
@openImagePreview="openImagePreview"
|
@openImagePreview="openImagePreview"
|
||||||
@replyMessage="handleReplyMessage"
|
@replyMessage="handleReplyMessage"
|
||||||
@replyWithText="handleReplyWithText"
|
@replyWithText="handleReplyWithText"
|
||||||
@openRefs="handleOpenRefs"
|
|
||||||
ref="messageList" />
|
ref="messageList" />
|
||||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,8 +146,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Refs Sidebar -->
|
|
||||||
<RefsSidebar v-model="refsSidebarOpen" :refs="refsSidebarRefs" />
|
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -201,7 +198,6 @@ import ChatInput from '@/components/chat/ChatInput.vue';
|
|||||||
import ProjectDialog from '@/components/chat/ProjectDialog.vue';
|
import ProjectDialog from '@/components/chat/ProjectDialog.vue';
|
||||||
import ProjectView from '@/components/chat/ProjectView.vue';
|
import ProjectView from '@/components/chat/ProjectView.vue';
|
||||||
import WelcomeView from '@/components/chat/WelcomeView.vue';
|
import WelcomeView from '@/components/chat/WelcomeView.vue';
|
||||||
import RefsSidebar from '@/components/chat/message_list_comps/RefsSidebar.vue';
|
|
||||||
import type { ProjectFormData } from '@/components/chat/ProjectDialog.vue';
|
import type { ProjectFormData } from '@/components/chat/ProjectDialog.vue';
|
||||||
import { useSessions } from '@/composables/useSessions';
|
import { useSessions } from '@/composables/useSessions';
|
||||||
import { useMessages } from '@/composables/useMessages';
|
import { useMessages } from '@/composables/useMessages';
|
||||||
@@ -410,21 +406,6 @@ function handleReplyWithText(replyData: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refs Sidebar 状态
|
|
||||||
const refsSidebarOpen = ref(false);
|
|
||||||
const refsSidebarRefs = ref<any>(null);
|
|
||||||
|
|
||||||
function handleOpenRefs(refs: any) {
|
|
||||||
// 如果sidebar已打开且点击的是同一个refs,则关闭
|
|
||||||
if (refsSidebarOpen.value && refsSidebarRefs.value === refs) {
|
|
||||||
refsSidebarOpen.value = false;
|
|
||||||
} else {
|
|
||||||
// 否则打开sidebar并更新refs
|
|
||||||
refsSidebarRefs.value = refs;
|
|
||||||
refsSidebarOpen.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSelectConversation(sessionIds: string[]) {
|
async function handleSelectConversation(sessionIds: string[]) {
|
||||||
if (!sessionIds[0]) return;
|
if (!sessionIds[0]) return;
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ function handleDeleteConversation(session: Session) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -116,8 +116,6 @@
|
|||||||
|
|
||||||
<!-- Text (Markdown) -->
|
<!-- Text (Markdown) -->
|
||||||
<MarkdownRender v-else-if="part.type === 'plain' && part.text && part.text.trim()"
|
<MarkdownRender v-else-if="part.type === 'plain' && part.text && part.text.trim()"
|
||||||
custom-id="message-list"
|
|
||||||
:custom-html-tags="['ref']"
|
|
||||||
:content="part.text" :typewriter="false" class="markdown-content"
|
:content="part.text" :typewriter="false" class="markdown-content"
|
||||||
:is-dark="isDark" :monacoOptions="{ theme: isDark ? 'vs-dark' : 'vs-light' }" />
|
:is-dark="isDark" :monacoOptions="{ theme: isDark ? 'vs-dark' : 'vs-light' }" />
|
||||||
|
|
||||||
@@ -217,9 +215,6 @@
|
|||||||
@click="copyBotMessage(msg.content.message, index)" :title="t('core.common.copy')" />
|
@click="copyBotMessage(msg.content.message, index)" :title="t('core.common.copy')" />
|
||||||
<v-btn icon="mdi-reply-outline" size="x-small" variant="text" class="reply-message-btn"
|
<v-btn icon="mdi-reply-outline" size="x-small" variant="text" class="reply-message-btn"
|
||||||
@click="$emit('replyMessage', msg, index)" :title="tm('actions.reply')" />
|
@click="$emit('replyMessage', msg, index)" :title="tm('actions.reply')" />
|
||||||
|
|
||||||
<!-- Refs Visualization -->
|
|
||||||
<ActionRef :refs="msg.content.refs" @open-refs="openRefsSidebar" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +245,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||||
import { MarkdownRender, enableKatex, enableMermaid, setCustomComponents } from 'markstream-vue'
|
import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue'
|
||||||
import 'markstream-vue/index.css'
|
import 'markstream-vue/index.css'
|
||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
import 'highlight.js/styles/github.css';
|
import 'highlight.js/styles/github.css';
|
||||||
@@ -258,24 +253,17 @@ import axios from 'axios';
|
|||||||
import ReasoningBlock from './message_list_comps/ReasoningBlock.vue';
|
import ReasoningBlock from './message_list_comps/ReasoningBlock.vue';
|
||||||
import IPythonToolBlock from './message_list_comps/IPythonToolBlock.vue';
|
import IPythonToolBlock from './message_list_comps/IPythonToolBlock.vue';
|
||||||
import ToolCallCard from './message_list_comps/ToolCallCard.vue';
|
import ToolCallCard from './message_list_comps/ToolCallCard.vue';
|
||||||
import RefNode from './message_list_comps/RefNode.vue';
|
|
||||||
import ActionRef from './message_list_comps/ActionRef.vue';
|
|
||||||
|
|
||||||
enableKatex();
|
enableKatex();
|
||||||
enableMermaid();
|
enableMermaid();
|
||||||
|
|
||||||
// 注册自定义 ref 组件
|
|
||||||
setCustomComponents('message-list', { ref: RefNode });
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MessageList',
|
name: 'MessageList',
|
||||||
components: {
|
components: {
|
||||||
MarkdownRender,
|
MarkdownRender,
|
||||||
ReasoningBlock,
|
ReasoningBlock,
|
||||||
IPythonToolBlock,
|
IPythonToolBlock,
|
||||||
ToolCallCard,
|
ToolCallCard
|
||||||
RefNode,
|
|
||||||
ActionRef
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
messages: {
|
messages: {
|
||||||
@@ -295,7 +283,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['openImagePreview', 'replyMessage', 'replyWithText', 'openRefs'],
|
emits: ['openImagePreview', 'replyMessage', 'replyWithText'],
|
||||||
setup() {
|
setup() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { tm } = useModuleI18n('features/chat');
|
const { tm } = useModuleI18n('features/chat');
|
||||||
@@ -305,12 +293,6 @@ export default {
|
|||||||
tm
|
tm
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
isDark: this.isDark,
|
|
||||||
webSearchResults: () => this.webSearchResults
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
copiedMessages: new Set(),
|
copiedMessages: new Set(),
|
||||||
@@ -333,9 +315,7 @@ export default {
|
|||||||
imagePreview: {
|
imagePreview: {
|
||||||
show: false,
|
show: false,
|
||||||
url: ''
|
url: ''
|
||||||
},
|
}
|
||||||
// Web search results mapping: { 'uuid.idx': { url, title, snippet } }
|
|
||||||
webSearchResults: {}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -344,7 +324,6 @@ export default {
|
|||||||
this.addScrollListener();
|
this.addScrollListener();
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
this.startElapsedTimeTimer();
|
this.startElapsedTimeTimer();
|
||||||
this.extractWebSearchResults();
|
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
this.initCodeCopyButtons();
|
this.initCodeCopyButtons();
|
||||||
@@ -352,56 +331,8 @@ export default {
|
|||||||
if (this.isUserNearBottom) {
|
if (this.isUserNearBottom) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
this.extractWebSearchResults();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 从消息中提取 web_search_tavily 的搜索结果
|
|
||||||
extractWebSearchResults() {
|
|
||||||
const results = {};
|
|
||||||
|
|
||||||
this.messages.forEach(msg => {
|
|
||||||
if (msg.content.type !== 'bot' || !Array.isArray(msg.content.message)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.content.message.forEach(part => {
|
|
||||||
if (part.type !== 'tool_call' || !Array.isArray(part.tool_calls)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
part.tool_calls.forEach(toolCall => {
|
|
||||||
// 检查是否是 web_search_tavily 工具调用
|
|
||||||
if (toolCall.name !== 'web_search_tavily' || !toolCall.result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 解析工具调用结果
|
|
||||||
const resultData = typeof toolCall.result === 'string'
|
|
||||||
? JSON.parse(toolCall.result)
|
|
||||||
: toolCall.result;
|
|
||||||
|
|
||||||
if (resultData.results && Array.isArray(resultData.results)) {
|
|
||||||
resultData.results.forEach(item => {
|
|
||||||
if (item.index) {
|
|
||||||
results[item.index] = {
|
|
||||||
url: item.url,
|
|
||||||
title: item.title,
|
|
||||||
snippet: item.snippet
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse web search result:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.webSearchResults = results;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 处理文本选择
|
// 处理文本选择
|
||||||
handleTextSelection() {
|
handleTextSelection() {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
@@ -946,11 +877,6 @@ export default {
|
|||||||
// Check if tool is iPython executor
|
// Check if tool is iPython executor
|
||||||
isIPythonTool(toolCall) {
|
isIPythonTool(toolCall) {
|
||||||
return toolCall.name === 'astrbot_execute_ipython';
|
return toolCall.name === 'astrbot_execute_ipython';
|
||||||
},
|
|
||||||
|
|
||||||
// Open refs sidebar
|
|
||||||
openRefsSidebar(refs) {
|
|
||||||
this.$emit('openRefs', refs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="refs && refs.used && refs.used.length > 0" class="refs-container" @click="handleClick">
|
|
||||||
<div class="refs-avatars">
|
|
||||||
<div v-for="(ref, refIdx) in refs.used.slice(0, 3)" :key="refIdx" class="ref-avatar"
|
|
||||||
:style="{ zIndex: 3 - refIdx }">
|
|
||||||
<img v-if="ref.favicon" :src="ref.favicon" class="ref-favicon"
|
|
||||||
@error="(e) => e.target.style.display = 'none'" />
|
|
||||||
<span v-else class="ref-initial">{{ getRefInitial(ref.title) }}</span>
|
|
||||||
</div>
|
|
||||||
<span v-if="refs.used.length > 3" class="refs-more">
|
|
||||||
+{{ refs.used.length - 3 }}
|
|
||||||
</span>
|
|
||||||
<span class="ml-2" style="color: gray;">
|
|
||||||
{{ tm('refs.sources') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { useModuleI18n } from '@/i18n/composables';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ActionRef',
|
|
||||||
props: {
|
|
||||||
refs: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['open-refs'],
|
|
||||||
setup() {
|
|
||||||
const { tm } = useModuleI18n('features/chat');
|
|
||||||
return { tm };
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Get first character of ref title for fallback display
|
|
||||||
getRefInitial(title) {
|
|
||||||
if (!title) return '?';
|
|
||||||
return title.charAt(0).toUpperCase();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Handle click to open refs sidebar
|
|
||||||
handleClick() {
|
|
||||||
this.$emit('open-refs', this.refs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.refs-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 8px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refs-container:hover {
|
|
||||||
background-color: rgba(103, 58, 183, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refs-avatars {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-avatar {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.9;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-avatar:not(:first-child) {
|
|
||||||
margin-left: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-favicon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-initial {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refs-more {
|
|
||||||
margin-left: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--v-theme-secondaryText);
|
|
||||||
opacity: 0.7;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-chip v-if="domain" class="ref-chip" size="x-small" variant="flat"
|
|
||||||
:style="{ backgroundColor: isDark ? '#303030' : '#f4f4f4', color: isDark ? '#999' : '#666' }" :href="url"
|
|
||||||
target="_blank" clickable>
|
|
||||||
<v-icon start size="x-small" color>mdi-link-variant</v-icon>
|
|
||||||
<span>{{ domain }}</span>
|
|
||||||
|
|
||||||
</v-chip>
|
|
||||||
<span v-else class="ref-fallback" :style="{ color: isDark ? '#999' : '#666' }">{{ 'site' }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, inject } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('RefNode node:', props.node);
|
|
||||||
|
|
||||||
// 从父组件注入的暗黑模式状态和搜索结果
|
|
||||||
const isDark = inject('isDark', false)
|
|
||||||
const webSearchResults = inject('webSearchResults', () => ({}))
|
|
||||||
|
|
||||||
// 从 node.content 中提取 ref index (格式: uuid.idx)
|
|
||||||
const refIndex = computed(() => props.node?.content?.trim() || '')
|
|
||||||
|
|
||||||
// 根据 refIndex 查找对应的 URL
|
|
||||||
const resultData = computed(() => {
|
|
||||||
if (!refIndex.value) return null
|
|
||||||
const results = typeof webSearchResults === 'function' ? webSearchResults() : webSearchResults
|
|
||||||
return results?.[refIndex.value] || null
|
|
||||||
})
|
|
||||||
|
|
||||||
const url = computed(() => resultData.value?.url || '')
|
|
||||||
|
|
||||||
const domain = computed(() => {
|
|
||||||
if (!url.value) return ''
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url.value)
|
|
||||||
return urlObj.hostname.replace(/^www\./, '')
|
|
||||||
} catch (e) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ref-chip {
|
|
||||||
margin: 0 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: opacity;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-chip:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-fallback {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
<template>
|
|
||||||
<transition name="slide-left">
|
|
||||||
<div v-if="isOpen" class="refs-sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h3 class="sidebar-title">{{ tm('refs.title') }}</h3>
|
|
||||||
<v-btn icon="mdi-close" size="small" variant="text" @click="close"></v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="refs-list">
|
|
||||||
<div v-for="(ref, index) in refs?.used || []" :key="index" class="ref-item" @click="openLink(ref.url)">
|
|
||||||
<div class="ref-item-icon">
|
|
||||||
<img v-if="ref.favicon" :src="ref.favicon" class="ref-item-favicon"
|
|
||||||
@error="(e) => e.target.style.display = 'none'" />
|
|
||||||
<div v-else class="ref-item-initial">{{ getRefInitial(ref.title) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="ref-item-content">
|
|
||||||
<div class="ref-item-title">{{ ref.title }}</div>
|
|
||||||
<div class="ref-item-url">{{ formatUrl(ref.url) }}</div>
|
|
||||||
<div v-if="ref.snippet" class="ref-item-snippet">{{ ref.snippet }}</div>
|
|
||||||
</div>
|
|
||||||
<v-icon size="small" class="ref-item-arrow">mdi-open-in-new</v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { useModuleI18n } from '@/i18n/composables';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RefsSidebar',
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
refs: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
setup() {
|
|
||||||
const { tm } = useModuleI18n('features/chat');
|
|
||||||
return { tm };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isOpen: {
|
|
||||||
get() {
|
|
||||||
return this.modelValue;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$emit('update:modelValue', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.isOpen = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
getRefInitial(title) {
|
|
||||||
if (!title) return '?';
|
|
||||||
return title.charAt(0).toUpperCase();
|
|
||||||
},
|
|
||||||
|
|
||||||
formatUrl(url) {
|
|
||||||
if (!url) return '';
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url);
|
|
||||||
return urlObj.hostname;
|
|
||||||
} catch {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openLink(url) {
|
|
||||||
if (url) {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.refs-sidebar {
|
|
||||||
width: 360px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--v-theme-surface);
|
|
||||||
border-left: 1px solid var(--v-theme-border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-left-enter-active,
|
|
||||||
.slide-left-leave-active {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-left-enter-from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-left-leave-to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--v-theme-primaryText);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refs-list {
|
|
||||||
padding: 12px;
|
|
||||||
padding-top: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--v-theme-border);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item:hover {
|
|
||||||
background-color: rgba(103, 58, 183, 0.05);
|
|
||||||
border-color: rgba(103, 58, 183, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-favicon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-initial {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--v-theme-primaryText);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-url {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--v-theme-secondaryText);
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-snippet {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--v-theme-secondaryText);
|
|
||||||
opacity: 0.8;
|
|
||||||
line-height: 1.5;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item-arrow {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--v-theme-secondaryText);
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-item:hover .ref-item-arrow {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -150,7 +150,7 @@ onUnmounted(() => {
|
|||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: background-color;
|
transition: background-color 0.2s ease;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,10 +105,6 @@
|
|||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"ttft": "Time to First Token"
|
"ttft": "Time to First Token"
|
||||||
},
|
},
|
||||||
"refs": {
|
|
||||||
"title": "References",
|
|
||||||
"sources": "Sources"
|
|
||||||
},
|
|
||||||
"connection": {
|
"connection": {
|
||||||
"title": "Connection Status Notice",
|
"title": "Connection Status Notice",
|
||||||
"message": "The system detected that the chat connection needs to be re-established.",
|
"message": "The system detected that the chat connection needs to be re-established.",
|
||||||
|
|||||||
@@ -107,10 +107,6 @@
|
|||||||
"duration": "耗时",
|
"duration": "耗时",
|
||||||
"ttft": "首字时间"
|
"ttft": "首字时间"
|
||||||
},
|
},
|
||||||
"refs": {
|
|
||||||
"title": "引用",
|
|
||||||
"sources": "来源"
|
|
||||||
},
|
|
||||||
"connection": {
|
"connection": {
|
||||||
"title": "连接状态提醒",
|
"title": "连接状态提醒",
|
||||||
"message": "系统检测到聊天连接需要重新建立。",
|
"message": "系统检测到聊天连接需要重新建立。",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "AstrBot"
|
name = "AstrBot"
|
||||||
version = "4.12.2"
|
version = "4.12.1"
|
||||||
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user