Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ab11f3f7f |
@@ -41,14 +41,12 @@ AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主
|
||||
## 主要功能
|
||||
|
||||
1. 💯 免费 & 开源。
|
||||
1. ✨ AI 大模型对话,多模态,Agent,MCP,知识库,人格设定,自动压缩对话。
|
||||
1. ✨ AI 大模型对话,多模态,Agent,MCP,知识库,人格设定。
|
||||
2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
|
||||
2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
|
||||
3. 📦 插件扩展,已有近 800 个插件可一键安装。
|
||||
5. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。
|
||||
6. 💻 WebUI 支持。
|
||||
7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
|
||||
8. 🌐 国际化(i18n)支持。
|
||||
5. 💻 WebUI 支持。
|
||||
6. 🌐 国际化(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_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_using_llm_tool as on_using_llm_tool
|
||||
from astrbot.core.star.register import (
|
||||
register_on_waiting_llm_request as on_waiting_llm_request,
|
||||
)
|
||||
@@ -57,6 +53,4 @@ __all__ = [
|
||||
"permission_type",
|
||||
"platform_adapter_type",
|
||||
"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.provider import Provider, ProviderRequest
|
||||
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
|
||||
|
||||
|
||||
@@ -25,9 +22,7 @@ class ProcessLLMRequest:
|
||||
else:
|
||||
logger.info(f"Timezone set to: {self.timezone}")
|
||||
|
||||
async def _ensure_persona(
|
||||
self, req: ProviderRequest, cfg: dict, umo: str, platform_type: str
|
||||
):
|
||||
async def _ensure_persona(self, req: ProviderRequest, cfg: dict, umo: str):
|
||||
"""确保用户人格已加载"""
|
||||
if not req.conversation:
|
||||
return
|
||||
@@ -47,12 +42,6 @@ class ProcessLLMRequest:
|
||||
if default_persona:
|
||||
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(
|
||||
builtins.filter(
|
||||
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 ""
|
||||
if req.conversation:
|
||||
# inject persona for this request
|
||||
platform_type = event.get_platform_name()
|
||||
await self._ensure_persona(
|
||||
req, cfg, event.unified_msg_origin, platform_type
|
||||
)
|
||||
await self._ensure_persona(req, cfg, event.unified_msg_origin)
|
||||
|
||||
# image caption
|
||||
if img_cap_prov_id and req.image_urls:
|
||||
|
||||
@@ -32,7 +32,6 @@ class SearchResult:
|
||||
title: str
|
||||
url: str
|
||||
snippet: str
|
||||
favicon: str | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.title} - {self.url}\n{self.snippet}"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
|
||||
import aiohttp
|
||||
from bs4 import BeautifulSoup
|
||||
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.provider import ProviderRequest
|
||||
from astrbot.core.provider.func_tool_manager import FunctionToolManager
|
||||
@@ -153,7 +151,6 @@ class Main(star.Star):
|
||||
title=item.get("title"),
|
||||
url=item.get("url"),
|
||||
snippet=item.get("content"),
|
||||
favicon=item.get("favicon"),
|
||||
)
|
||||
results.append(result)
|
||||
return results
|
||||
@@ -275,7 +272,7 @@ class Main(star.Star):
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
query: str,
|
||||
max_results: int = 7,
|
||||
max_results: int = 5,
|
||||
search_depth: str = "basic",
|
||||
topic: str = "general",
|
||||
days: int = 3,
|
||||
@@ -288,7 +285,7 @@ class Main(star.Star):
|
||||
|
||||
Args:
|
||||
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".
|
||||
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.
|
||||
@@ -299,12 +296,15 @@ class Main(star.Star):
|
||||
"""
|
||||
logger.info(f"web_searcher - search_from_tavily: {query}")
|
||||
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", []):
|
||||
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
||||
|
||||
# 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"]:
|
||||
search_depth = "basic"
|
||||
payload["search_depth"] = search_depth
|
||||
@@ -328,22 +328,14 @@ class Main(star.Star):
|
||||
return "Error: Tavily web searcher does not return any results."
|
||||
|
||||
ret_ls = []
|
||||
ref_uuid = str(uuid.uuid4())[:4]
|
||||
for idx, result in enumerate(results, 1):
|
||||
index = f"{ref_uuid}.{idx}"
|
||||
ret_ls.append(
|
||||
{
|
||||
"title": f"{result.title}",
|
||||
"url": f"{result.url}",
|
||||
"snippet": f"{result.snippet}",
|
||||
# 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)
|
||||
for result in results:
|
||||
ret_ls.append(f"\nTitle: {result.title}")
|
||||
ret_ls.append(f"URL: {result.url}")
|
||||
ret_ls.append(f"Content: {result.snippet}")
|
||||
ret = "\n".join(ret_ls)
|
||||
|
||||
if websearch_link:
|
||||
ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
|
||||
return ret
|
||||
|
||||
@llm_tool("tavily_extract_web_page")
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.12.3"
|
||||
__version__ = "4.11.4"
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any
|
||||
from mcp.types import CallToolResult
|
||||
|
||||
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.tool import FunctionTool
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
@@ -26,19 +25,6 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
||||
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.OnUsingLLMToolEvent,
|
||||
tool,
|
||||
tool_args,
|
||||
)
|
||||
|
||||
async def on_tool_end(
|
||||
self,
|
||||
run_context: ContextWrapper[AstrAgentContext],
|
||||
@@ -47,36 +33,6 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
||||
tool_result: CallToolResult | None,
|
||||
):
|
||||
run_context.context.event.clear_result()
|
||||
await call_event_hook(
|
||||
run_context.context.event,
|
||||
EventType.OnLLMToolRespondEvent,
|
||||
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]):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.12.3"
|
||||
VERSION = "4.11.4"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
|
||||
@@ -414,11 +414,10 @@ class InternalAgentSubStage(Stage):
|
||||
|
||||
# using agent context messages to save to history
|
||||
message_to_save = []
|
||||
skipped_initial_system = False
|
||||
for message in all_messages:
|
||||
if message.role == "system" and not skipped_initial_system:
|
||||
skipped_initial_system = True
|
||||
continue # skip first system message
|
||||
if message.role == "system":
|
||||
# we do not save system messages to history
|
||||
continue
|
||||
if message.role in ["assistant", "user"] and getattr(
|
||||
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."
|
||||
)
|
||||
|
||||
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 = (
|
||||
'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?"
|
||||
|
||||
@@ -42,6 +42,8 @@ class AstrMessageEvent(abc.ABC):
|
||||
"""消息对象, AstrBotMessage。带有完整的消息结构。"""
|
||||
self.platform_meta = platform_meta
|
||||
"""消息平台的信息, 其中 name 是平台的类型,如 aiocqhttp"""
|
||||
self.session_id = session_id
|
||||
"""用户的会话 ID。可以直接使用下面的 unified_msg_origin"""
|
||||
self.role = "member"
|
||||
"""用户是否是管理员。如果是管理员,这里是 admin"""
|
||||
self.is_wake = False
|
||||
@@ -49,12 +51,12 @@ class AstrMessageEvent(abc.ABC):
|
||||
self.is_at_or_wake_command = False
|
||||
"""是否是 At 机器人或者带有唤醒词或者是私聊(插件注册的事件监听器会让 is_wake 设为 True, 但是不会让这个属性置为 True)"""
|
||||
self._extras: dict[str, Any] = {}
|
||||
self.session = MessageSession(
|
||||
self.session = MessageSesion(
|
||||
platform_name=platform_meta.id,
|
||||
message_type=message_obj.type,
|
||||
session_id=session_id,
|
||||
)
|
||||
# self.unified_msg_origin = str(self.session)
|
||||
self.unified_msg_origin = str(self.session)
|
||||
"""统一的消息来源字符串。格式为 platform_name:message_type:session_id"""
|
||||
self._result: MessageEventResult | None = None
|
||||
"""消息事件的结果"""
|
||||
@@ -70,27 +72,6 @@ class AstrMessageEvent(abc.ABC):
|
||||
# back_compability
|
||||
self.platform = platform_meta
|
||||
|
||||
@property
|
||||
def unified_msg_origin(self) -> str:
|
||||
"""统一的消息来源字符串。格式为 platform_name:message_type:session_id"""
|
||||
return str(self.session)
|
||||
|
||||
@unified_msg_origin.setter
|
||||
def unified_msg_origin(self, value: str):
|
||||
"""设置统一的消息来源字符串。格式为 platform_name:message_type:session_id"""
|
||||
self.new_session = MessageSession.from_str(value)
|
||||
self.session = self.new_session
|
||||
|
||||
@property
|
||||
def session_id(self) -> str:
|
||||
"""用户的会话 ID。可以直接使用下面的 unified_msg_origin"""
|
||||
return self.session.session_id
|
||||
|
||||
@session_id.setter
|
||||
def session_id(self, value: str):
|
||||
"""设置用户的会话 ID。可以直接使用下面的 unified_msg_origin"""
|
||||
self.session.session_id = value
|
||||
|
||||
def get_platform_name(self):
|
||||
"""获取这个事件所属的平台的类型(如 aiocqhttp, slack, discord 等)。
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ from .star_handler import (
|
||||
register_on_decorating_result,
|
||||
register_on_llm_request,
|
||||
register_on_llm_response,
|
||||
register_on_llm_tool_respond,
|
||||
register_on_platform_loaded,
|
||||
register_on_using_llm_tool,
|
||||
register_on_waiting_llm_request,
|
||||
register_permission_type,
|
||||
register_platform_adapter_type,
|
||||
@@ -38,6 +36,4 @@ __all__ = [
|
||||
"register_platform_adapter_type",
|
||||
"register_regex",
|
||||
"register_star",
|
||||
"register_on_using_llm_tool",
|
||||
"register_on_llm_tool_respond",
|
||||
]
|
||||
|
||||
@@ -409,57 +409,6 @@ def register_on_llm_response(**kwargs):
|
||||
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.OnUsingLLMToolEvent, **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.OnLLMToolRespondEvent, **kwargs
|
||||
)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_llm_tool(name: str | None = None, **kwargs):
|
||||
"""为函数调用(function-calling / tools-use)添加工具。
|
||||
|
||||
|
||||
@@ -189,8 +189,6 @@ class EventType(enum.Enum):
|
||||
OnLLMResponseEvent = enum.auto() # LLM 响应后
|
||||
OnDecoratingResultEvent = enum.auto() # 发送消息前
|
||||
OnCallingFuncToolEvent = enum.auto() # 调用函数工具
|
||||
OnUsingLLMToolEvent = enum.auto() # 使用 LLM 工具
|
||||
OnLLMToolRespondEvent = enum.auto() # 调用函数工具后
|
||||
OnAfterMessageSentEvent = enum.auto() # 发送消息后
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from typing import Any, TypeVar, overload
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.po import Preference
|
||||
|
||||
@@ -23,22 +20,11 @@ class SharedPreferences:
|
||||
)
|
||||
self.path = json_storage_path
|
||||
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()
|
||||
t = threading.Thread(target=self._sync_loop.run_forever, daemon=True)
|
||||
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(
|
||||
self,
|
||||
scope: str,
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import cast
|
||||
@@ -10,7 +9,7 @@ from typing import cast
|
||||
from quart import Response as QuartResponse
|
||||
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.db import BaseDatabase
|
||||
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),
|
||||
}
|
||||
|
||||
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(
|
||||
self,
|
||||
webchat_conv_id: str,
|
||||
@@ -291,7 +232,6 @@ class ChatRoute(Route):
|
||||
media_parts: list,
|
||||
reasoning: str,
|
||||
agent_stats: dict,
|
||||
refs: dict,
|
||||
):
|
||||
"""保存 bot 消息到历史记录,返回保存的记录"""
|
||||
bot_message_parts = []
|
||||
@@ -304,8 +244,6 @@ class ChatRoute(Route):
|
||||
new_his["reasoning"] = reasoning
|
||||
if agent_stats:
|
||||
new_his["agent_stats"] = agent_stats
|
||||
if refs:
|
||||
new_his["refs"] = refs
|
||||
|
||||
record = await self.platform_history_mgr.insert(
|
||||
platform_id="webchat",
|
||||
@@ -367,7 +305,6 @@ class ChatRoute(Route):
|
||||
accumulated_reasoning = ""
|
||||
tool_calls = {}
|
||||
agent_stats = {}
|
||||
refs = {}
|
||||
try:
|
||||
async with track_conversation(self.running_convs, webchat_conv_id):
|
||||
while True:
|
||||
@@ -489,26 +426,12 @@ class ChatRoute(Route):
|
||||
or chain_type == "tool_call_result"
|
||||
):
|
||||
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(
|
||||
webchat_conv_id,
|
||||
accumulated_text,
|
||||
accumulated_parts,
|
||||
accumulated_reasoning,
|
||||
agent_stats,
|
||||
refs,
|
||||
)
|
||||
# 发送保存的消息信息给前端
|
||||
if saved_record and not client_disconnected:
|
||||
@@ -528,7 +451,6 @@ class ChatRoute(Route):
|
||||
accumulated_reasoning = ""
|
||||
# tool_calls = {}
|
||||
agent_stats = {}
|
||||
refs = {}
|
||||
except BaseException as e:
|
||||
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
||||
|
||||
|
||||
@@ -35,14 +35,6 @@ class SessionManagementRoute(Route):
|
||||
"/session/delete-rule": ("POST", self.delete_session_rule),
|
||||
"/session/batch-delete-rule": ("POST", self.batch_delete_session_rule),
|
||||
"/session/active-umos": ("GET", self.list_umos),
|
||||
"/session/list-all-with-status": ("GET", self.list_all_umos_with_status),
|
||||
"/session/batch-update-service": ("POST", self.batch_update_service),
|
||||
"/session/batch-update-provider": ("POST", self.batch_update_provider),
|
||||
# 分组管理 API
|
||||
"/session/groups": ("GET", self.list_groups),
|
||||
"/session/group/create": ("POST", self.create_group),
|
||||
"/session/group/update": ("POST", self.update_group),
|
||||
"/session/group/delete": ("POST", self.delete_group),
|
||||
}
|
||||
self.conv_mgr = core_lifecycle.conversation_manager
|
||||
self.core_lifecycle = core_lifecycle
|
||||
@@ -399,540 +391,3 @@ class SessionManagementRoute(Route):
|
||||
except Exception as e:
|
||||
logger.error(f"获取 UMO 列表失败: {e!s}")
|
||||
return Response().error(f"获取 UMO 列表失败: {e!s}").__dict__
|
||||
|
||||
async def list_all_umos_with_status(self):
|
||||
"""获取所有有对话记录的 UMO 及其服务状态(支持分页、搜索、筛选)
|
||||
|
||||
Query 参数:
|
||||
page: 页码,默认为 1
|
||||
page_size: 每页数量,默认为 20
|
||||
search: 搜索关键词
|
||||
message_type: 筛选消息类型 (group/private/all)
|
||||
platform: 筛选平台
|
||||
"""
|
||||
try:
|
||||
page = request.args.get("page", 1, type=int)
|
||||
page_size = request.args.get("page_size", 20, type=int)
|
||||
search = request.args.get("search", "", type=str).strip()
|
||||
message_type = request.args.get("message_type", "all", type=str)
|
||||
platform = request.args.get("platform", "", type=str)
|
||||
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 1:
|
||||
page_size = 20
|
||||
if page_size > 100:
|
||||
page_size = 100
|
||||
|
||||
# 从 Conversation 表获取所有 distinct user_id (即 umo)
|
||||
async with self.db_helper.get_db() as session:
|
||||
session: AsyncSession
|
||||
result = await session.execute(
|
||||
select(ConversationV2.user_id)
|
||||
.distinct()
|
||||
.order_by(ConversationV2.user_id)
|
||||
)
|
||||
all_umos = [row[0] for row in result.fetchall()]
|
||||
|
||||
# 获取所有 umo 的规则配置
|
||||
umo_rules, _ = await self._get_umo_rules(page=1, page_size=99999, search="")
|
||||
|
||||
# 构建带状态的 umo 列表
|
||||
umos_with_status = []
|
||||
for umo in all_umos:
|
||||
parts = umo.split(":")
|
||||
umo_platform = parts[0] if len(parts) >= 1 else "unknown"
|
||||
umo_message_type = parts[1] if len(parts) >= 2 else "unknown"
|
||||
umo_session_id = parts[2] if len(parts) >= 3 else umo
|
||||
|
||||
# 筛选消息类型
|
||||
if message_type != "all":
|
||||
if message_type == "group" and umo_message_type not in [
|
||||
"group",
|
||||
"GroupMessage",
|
||||
]:
|
||||
continue
|
||||
if message_type == "private" and umo_message_type not in [
|
||||
"private",
|
||||
"FriendMessage",
|
||||
"friend",
|
||||
]:
|
||||
continue
|
||||
|
||||
# 筛选平台
|
||||
if platform and umo_platform != platform:
|
||||
continue
|
||||
|
||||
# 获取服务配置
|
||||
rules = umo_rules.get(umo, {})
|
||||
svc_config = rules.get("session_service_config", {})
|
||||
|
||||
custom_name = svc_config.get("custom_name", "") if svc_config else ""
|
||||
session_enabled = (
|
||||
svc_config.get("session_enabled", True) if svc_config else True
|
||||
)
|
||||
llm_enabled = (
|
||||
svc_config.get("llm_enabled", True) if svc_config else True
|
||||
)
|
||||
tts_enabled = (
|
||||
svc_config.get("tts_enabled", True) if svc_config else True
|
||||
)
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
search_lower = search.lower()
|
||||
if (
|
||||
search_lower not in umo.lower()
|
||||
and search_lower not in custom_name.lower()
|
||||
):
|
||||
continue
|
||||
|
||||
# 获取 provider 配置
|
||||
chat_provider_key = (
|
||||
f"provider_perf_{ProviderType.CHAT_COMPLETION.value}"
|
||||
)
|
||||
tts_provider_key = f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}"
|
||||
stt_provider_key = f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}"
|
||||
|
||||
umos_with_status.append(
|
||||
{
|
||||
"umo": umo,
|
||||
"platform": umo_platform,
|
||||
"message_type": umo_message_type,
|
||||
"session_id": umo_session_id,
|
||||
"custom_name": custom_name,
|
||||
"session_enabled": session_enabled,
|
||||
"llm_enabled": llm_enabled,
|
||||
"tts_enabled": tts_enabled,
|
||||
"has_rules": umo in umo_rules,
|
||||
"chat_provider": rules.get(chat_provider_key),
|
||||
"tts_provider": rules.get(tts_provider_key),
|
||||
"stt_provider": rules.get(stt_provider_key),
|
||||
}
|
||||
)
|
||||
|
||||
# 分页
|
||||
total = len(umos_with_status)
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
paginated = umos_with_status[start_idx:end_idx]
|
||||
|
||||
# 获取可用的平台列表
|
||||
platforms = list({u["platform"] for u in umos_with_status})
|
||||
|
||||
# 获取可用的 providers
|
||||
provider_manager = self.core_lifecycle.provider_manager
|
||||
available_chat_providers = [
|
||||
{"id": p.meta().id, "name": p.meta().id, "model": p.meta().model}
|
||||
for p in provider_manager.provider_insts
|
||||
]
|
||||
available_tts_providers = [
|
||||
{"id": p.meta().id, "name": p.meta().id, "model": p.meta().model}
|
||||
for p in provider_manager.tts_provider_insts
|
||||
]
|
||||
available_stt_providers = [
|
||||
{"id": p.meta().id, "name": p.meta().id, "model": p.meta().model}
|
||||
for p in provider_manager.stt_provider_insts
|
||||
]
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"sessions": paginated,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"platforms": platforms,
|
||||
"available_chat_providers": available_chat_providers,
|
||||
"available_tts_providers": available_tts_providers,
|
||||
"available_stt_providers": available_stt_providers,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"获取会话状态列表失败: {e!s}")
|
||||
return Response().error(f"获取会话状态列表失败: {e!s}").__dict__
|
||||
|
||||
async def batch_update_service(self):
|
||||
"""批量更新多个 UMO 的服务状态 (LLM/TTS/Session)
|
||||
|
||||
请求体:
|
||||
{
|
||||
"umos": ["平台:消息类型:会话ID", ...], // 可选,如果不传则根据 scope 筛选
|
||||
"scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围
|
||||
"group_id": "分组ID", // 当 scope 为 custom_group 时必填
|
||||
"llm_enabled": true/false/null, // 可选,null表示不修改
|
||||
"tts_enabled": true/false/null, // 可选
|
||||
"session_enabled": true/false/null // 可选
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = await request.get_json()
|
||||
umos = data.get("umos", [])
|
||||
scope = data.get("scope", "")
|
||||
group_id = data.get("group_id", "")
|
||||
llm_enabled = data.get("llm_enabled")
|
||||
tts_enabled = data.get("tts_enabled")
|
||||
session_enabled = data.get("session_enabled")
|
||||
|
||||
# 如果没有任何修改
|
||||
if llm_enabled is None and tts_enabled is None and session_enabled is None:
|
||||
return Response().error("至少需要指定一个要修改的状态").__dict__
|
||||
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
if scope and not umos:
|
||||
# 如果是自定义分组
|
||||
if scope == "custom_group":
|
||||
if not group_id:
|
||||
return Response().error("请指定分组 ID").__dict__
|
||||
groups = self._get_groups()
|
||||
if group_id not in groups:
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
umos = groups[group_id].get("umos", [])
|
||||
else:
|
||||
async with self.db_helper.get_db() as session:
|
||||
session: AsyncSession
|
||||
result = await session.execute(
|
||||
select(ConversationV2.user_id).distinct()
|
||||
)
|
||||
all_umos = [row[0] for row in result.fetchall()]
|
||||
|
||||
if scope == "group":
|
||||
umos = [
|
||||
u
|
||||
for u in all_umos
|
||||
if ":group:" in u.lower() or ":groupmessage:" in u.lower()
|
||||
]
|
||||
elif scope == "private":
|
||||
umos = [
|
||||
u
|
||||
for u in all_umos
|
||||
if ":private:" in u.lower() or ":friend" in u.lower()
|
||||
]
|
||||
elif scope == "all":
|
||||
umos = all_umos
|
||||
|
||||
if not umos:
|
||||
return Response().error("没有找到符合条件的会话").__dict__
|
||||
|
||||
# 批量更新
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
|
||||
for umo in umos:
|
||||
try:
|
||||
# 获取现有配置
|
||||
session_config = (
|
||||
sp.get("session_service_config", {}, scope="umo", scope_id=umo)
|
||||
or {}
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
if llm_enabled is not None:
|
||||
session_config["llm_enabled"] = llm_enabled
|
||||
if tts_enabled is not None:
|
||||
session_config["tts_enabled"] = tts_enabled
|
||||
if session_enabled is not None:
|
||||
session_config["session_enabled"] = session_enabled
|
||||
|
||||
# 保存
|
||||
sp.put(
|
||||
"session_service_config",
|
||||
session_config,
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"更新 {umo} 服务状态失败: {e!s}")
|
||||
failed_umos.append(umo)
|
||||
|
||||
status_changes = []
|
||||
if llm_enabled is not None:
|
||||
status_changes.append(f"LLM={'启用' if llm_enabled else '禁用'}")
|
||||
if tts_enabled is not None:
|
||||
status_changes.append(f"TTS={'启用' if tts_enabled else '禁用'}")
|
||||
if session_enabled is not None:
|
||||
status_changes.append(f"会话={'启用' if session_enabled else '禁用'}")
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": f"已更新 {success_count} 个会话 ({', '.join(status_changes)})",
|
||||
"success_count": success_count,
|
||||
"failed_count": len(failed_umos),
|
||||
"failed_umos": failed_umos,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"批量更新服务状态失败: {e!s}")
|
||||
return Response().error(f"批量更新服务状态失败: {e!s}").__dict__
|
||||
|
||||
async def batch_update_provider(self):
|
||||
"""批量更新多个 UMO 的 Provider 配置
|
||||
|
||||
请求体:
|
||||
{
|
||||
"umos": ["平台:消息类型:会话ID", ...], // 可选
|
||||
"scope": "all" | "group" | "private", // 可选
|
||||
"provider_type": "chat_completion" | "text_to_speech" | "speech_to_text",
|
||||
"provider_id": "provider_id"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = await request.get_json()
|
||||
umos = data.get("umos", [])
|
||||
scope = data.get("scope", "")
|
||||
provider_type = data.get("provider_type")
|
||||
provider_id = data.get("provider_id")
|
||||
|
||||
if not provider_type or not provider_id:
|
||||
return (
|
||||
Response()
|
||||
.error("缺少必要参数: provider_type, provider_id")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
# 转换 provider_type
|
||||
provider_type_map = {
|
||||
"chat_completion": ProviderType.CHAT_COMPLETION,
|
||||
"text_to_speech": ProviderType.TEXT_TO_SPEECH,
|
||||
"speech_to_text": ProviderType.SPEECH_TO_TEXT,
|
||||
}
|
||||
if provider_type not in provider_type_map:
|
||||
return (
|
||||
Response()
|
||||
.error(f"不支持的 provider_type: {provider_type}")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
provider_type_enum = provider_type_map[provider_type]
|
||||
|
||||
# 如果指定了 scope,获取符合条件的所有 umo
|
||||
group_id = data.get("group_id", "")
|
||||
if scope and not umos:
|
||||
# 如果是自定义分组
|
||||
if scope == "custom_group":
|
||||
if not group_id:
|
||||
return Response().error("请指定分组 ID").__dict__
|
||||
groups = self._get_groups()
|
||||
if group_id not in groups:
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
umos = groups[group_id].get("umos", [])
|
||||
else:
|
||||
async with self.db_helper.get_db() as session:
|
||||
session: AsyncSession
|
||||
result = await session.execute(
|
||||
select(ConversationV2.user_id).distinct()
|
||||
)
|
||||
all_umos = [row[0] for row in result.fetchall()]
|
||||
|
||||
if scope == "group":
|
||||
umos = [
|
||||
u
|
||||
for u in all_umos
|
||||
if ":group:" in u.lower() or ":groupmessage:" in u.lower()
|
||||
]
|
||||
elif scope == "private":
|
||||
umos = [
|
||||
u
|
||||
for u in all_umos
|
||||
if ":private:" in u.lower() or ":friend" in u.lower()
|
||||
]
|
||||
elif scope == "all":
|
||||
umos = all_umos
|
||||
|
||||
if not umos:
|
||||
return Response().error("没有找到符合条件的会话").__dict__
|
||||
|
||||
# 批量更新
|
||||
success_count = 0
|
||||
failed_umos = []
|
||||
provider_manager = self.core_lifecycle.provider_manager
|
||||
|
||||
for umo in umos:
|
||||
try:
|
||||
await provider_manager.set_provider(
|
||||
provider_id=provider_id,
|
||||
provider_type=provider_type_enum,
|
||||
umo=umo,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"更新 {umo} Provider 失败: {e!s}")
|
||||
failed_umos.append(umo)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": f"已更新 {success_count} 个会话的 {provider_type} 为 {provider_id}",
|
||||
"success_count": success_count,
|
||||
"failed_count": len(failed_umos),
|
||||
"failed_umos": failed_umos,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"批量更新 Provider 失败: {e!s}")
|
||||
return Response().error(f"批量更新 Provider 失败: {e!s}").__dict__
|
||||
|
||||
# ==================== 分组管理 API ====================
|
||||
|
||||
def _get_groups(self) -> dict:
|
||||
"""获取所有分组"""
|
||||
return sp.get("session_groups", {})
|
||||
|
||||
def _save_groups(self, groups: dict) -> None:
|
||||
"""保存分组"""
|
||||
sp.put("session_groups", groups)
|
||||
|
||||
async def list_groups(self):
|
||||
"""获取所有分组列表"""
|
||||
try:
|
||||
groups = self._get_groups()
|
||||
# 转换为列表格式,方便前端使用
|
||||
groups_list = []
|
||||
for group_id, group_data in groups.items():
|
||||
groups_list.append(
|
||||
{
|
||||
"id": group_id,
|
||||
"name": group_data.get("name", ""),
|
||||
"umos": group_data.get("umos", []),
|
||||
"umo_count": len(group_data.get("umos", [])),
|
||||
}
|
||||
)
|
||||
return Response().ok({"groups": groups_list}).__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"获取分组列表失败: {e!s}")
|
||||
return Response().error(f"获取分组列表失败: {e!s}").__dict__
|
||||
|
||||
async def create_group(self):
|
||||
"""创建新分组"""
|
||||
try:
|
||||
data = await request.json
|
||||
name = data.get("name", "").strip()
|
||||
umos = data.get("umos", [])
|
||||
|
||||
if not name:
|
||||
return Response().error("分组名称不能为空").__dict__
|
||||
|
||||
groups = self._get_groups()
|
||||
|
||||
# 生成唯一 ID
|
||||
import uuid
|
||||
|
||||
group_id = str(uuid.uuid4())[:8]
|
||||
|
||||
groups[group_id] = {
|
||||
"name": name,
|
||||
"umos": umos,
|
||||
}
|
||||
|
||||
self._save_groups(groups)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": f"分组 '{name}' 创建成功",
|
||||
"group": {
|
||||
"id": group_id,
|
||||
"name": name,
|
||||
"umos": umos,
|
||||
"umo_count": len(umos),
|
||||
},
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"创建分组失败: {e!s}")
|
||||
return Response().error(f"创建分组失败: {e!s}").__dict__
|
||||
|
||||
async def update_group(self):
|
||||
"""更新分组(改名、增删成员)"""
|
||||
try:
|
||||
data = await request.json
|
||||
group_id = data.get("id")
|
||||
name = data.get("name")
|
||||
umos = data.get("umos")
|
||||
add_umos = data.get("add_umos", [])
|
||||
remove_umos = data.get("remove_umos", [])
|
||||
|
||||
if not group_id:
|
||||
return Response().error("分组 ID 不能为空").__dict__
|
||||
|
||||
groups = self._get_groups()
|
||||
|
||||
if group_id not in groups:
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
|
||||
group = groups[group_id]
|
||||
|
||||
# 更新名称
|
||||
if name is not None:
|
||||
group["name"] = name.strip()
|
||||
|
||||
# 直接设置 umos 列表
|
||||
if umos is not None:
|
||||
group["umos"] = umos
|
||||
else:
|
||||
# 增量更新
|
||||
current_umos = set(group.get("umos", []))
|
||||
if add_umos:
|
||||
current_umos.update(add_umos)
|
||||
if remove_umos:
|
||||
current_umos.difference_update(remove_umos)
|
||||
group["umos"] = list(current_umos)
|
||||
|
||||
self._save_groups(groups)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": f"分组 '{group['name']}' 更新成功",
|
||||
"group": {
|
||||
"id": group_id,
|
||||
"name": group["name"],
|
||||
"umos": group["umos"],
|
||||
"umo_count": len(group["umos"]),
|
||||
},
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"更新分组失败: {e!s}")
|
||||
return Response().error(f"更新分组失败: {e!s}").__dict__
|
||||
|
||||
async def delete_group(self):
|
||||
"""删除分组"""
|
||||
try:
|
||||
data = await request.json
|
||||
group_id = data.get("id")
|
||||
|
||||
if not group_id:
|
||||
return Response().error("分组 ID 不能为空").__dict__
|
||||
|
||||
groups = self._get_groups()
|
||||
|
||||
if group_id not in groups:
|
||||
return Response().error(f"分组 '{group_id}' 不存在").__dict__
|
||||
|
||||
group_name = groups[group_id].get("name", group_id)
|
||||
del groups[group_id]
|
||||
|
||||
self._save_groups(groups)
|
||||
|
||||
return Response().ok({"message": f"分组 '{group_name}' 已删除"}).__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"删除分组失败: {e!s}")
|
||||
return Response().error(f"删除分组失败: {e!s}").__dict__
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
|
||||
- AstrBot 代理沙箱环境(改进的代码解释器) ([#4449](https://github.com/AstrBotDevs/AstrBot/issues/4449)),详见[文档](https://docs.astrbot.app/use/astrbot-agent-sandbox.html)
|
||||
- ChatUI 支持项目管理 ([#4477](https://github.com/AstrBotDevs/AstrBot/issues/4477))
|
||||
- 自定义规则支持批量处理。
|
||||
|
||||
### 修复
|
||||
|
||||
- 发送 OpenAI 风格的 image_url 导致 Anthropic 返回 400 无效标签错误 ([#4444](https://github.com/AstrBotDevs/AstrBot/issues/4444))
|
||||
- ChatUI 标题显示问题 ([#4486](https://github.com/AstrBotDevs/AstrBot/issues/4486))
|
||||
- 确保 ChatUI 消息流顺序正确 ([#4487](https://github.com/AstrBotDevs/AstrBot/issues/4487))
|
||||
- 从 Telegram 和 Discord 平台命令注册中排除已禁用的命令 ([#4485](https://github.com/AstrBotDevs/AstrBot/issues/4485))
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化工具调用相关的提示词
|
||||
- 标准化 Context 类文档格式 ([#4436](https://github.com/AstrBotDevs/AstrBot/issues/4436))
|
||||
@@ -1,23 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
hotfix of v4.12.0
|
||||
|
||||
fix: 修复会话隔离功能失效的问题。
|
||||
|
||||
### 新增
|
||||
|
||||
- AstrBot 代理沙箱环境(改进的代码解释器) ([#4449](https://github.com/AstrBotDevs/AstrBot/issues/4449)),详见[文档](https://docs.astrbot.app/use/astrbot-agent-sandbox.html)
|
||||
- ChatUI 支持项目管理 ([#4477](https://github.com/AstrBotDevs/AstrBot/issues/4477))
|
||||
- 自定义规则支持批量处理。
|
||||
|
||||
### 修复
|
||||
|
||||
- 发送 OpenAI 风格的 image_url 导致 Anthropic 返回 400 无效标签错误 ([#4444](https://github.com/AstrBotDevs/AstrBot/issues/4444))
|
||||
- ChatUI 标题显示问题 ([#4486](https://github.com/AstrBotDevs/AstrBot/issues/4486))
|
||||
- 确保 ChatUI 消息流顺序正确 ([#4487](https://github.com/AstrBotDevs/AstrBot/issues/4487))
|
||||
- 从 Telegram 和 Discord 平台命令注册中排除已禁用的命令 ([#4485](https://github.com/AstrBotDevs/AstrBot/issues/4485))
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化工具调用相关的提示词
|
||||
- 标准化 Context 类文档格式 ([#4436](https://github.com/AstrBotDevs/AstrBot/issues/4436))
|
||||
@@ -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 网页搜索工具的渲染,支持内联搜索引用、引用网页。
|
||||
@@ -1,12 +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 网页搜索工具的渲染,支持内联搜索引用、引用网页。
|
||||
|
||||
|
||||
hotfix of 4.12.2
|
||||
|
||||
- fix: tool call error in some cases
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"remixicon": "3.5.0",
|
||||
"shiki": "^3.20.0",
|
||||
"stream-markdown": "^0.0.13",
|
||||
"stream-monaco": "^0.0.15",
|
||||
"stream-markdown": "^0.0.11",
|
||||
"stream-monaco": "^0.0.8",
|
||||
"vee-validate": "4.11.3",
|
||||
"vite-plugin-vuetify": "1.0.2",
|
||||
"vue": "3.3.4",
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
@openImagePreview="openImagePreview"
|
||||
@replyMessage="handleReplyMessage"
|
||||
@replyWithText="handleReplyWithText"
|
||||
@openRefs="handleOpenRefs"
|
||||
ref="messageList" />
|
||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||
</div>
|
||||
@@ -147,8 +146,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Refs Sidebar -->
|
||||
<RefsSidebar v-model="refsSidebarOpen" :refs="refsSidebarRefs" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -201,7 +198,6 @@ import ChatInput from '@/components/chat/ChatInput.vue';
|
||||
import ProjectDialog from '@/components/chat/ProjectDialog.vue';
|
||||
import ProjectView from '@/components/chat/ProjectView.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 { useSessions } from '@/composables/useSessions';
|
||||
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[]) {
|
||||
if (!sessionIds[0]) return;
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ function handleDeleteConversation(session: Session) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.04);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
|
||||
@@ -116,8 +116,6 @@
|
||||
|
||||
<!-- Text (Markdown) -->
|
||||
<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"
|
||||
: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')" />
|
||||
<v-btn icon="mdi-reply-outline" size="x-small" variant="text" class="reply-message-btn"
|
||||
@click="$emit('replyMessage', msg, index)" :title="tm('actions.reply')" />
|
||||
|
||||
<!-- Refs Visualization -->
|
||||
<ActionRef :refs="msg.content.refs" @open-refs="openRefsSidebar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +245,7 @@
|
||||
|
||||
<script>
|
||||
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 'katex/dist/katex.min.css'
|
||||
import 'highlight.js/styles/github.css';
|
||||
@@ -258,24 +253,17 @@ import axios from 'axios';
|
||||
import ReasoningBlock from './message_list_comps/ReasoningBlock.vue';
|
||||
import IPythonToolBlock from './message_list_comps/IPythonToolBlock.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();
|
||||
enableMermaid();
|
||||
|
||||
// 注册自定义 ref 组件
|
||||
setCustomComponents('message-list', { ref: RefNode });
|
||||
|
||||
export default {
|
||||
name: 'MessageList',
|
||||
components: {
|
||||
MarkdownRender,
|
||||
ReasoningBlock,
|
||||
IPythonToolBlock,
|
||||
ToolCallCard,
|
||||
RefNode,
|
||||
ActionRef
|
||||
ToolCallCard
|
||||
},
|
||||
props: {
|
||||
messages: {
|
||||
@@ -295,7 +283,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['openImagePreview', 'replyMessage', 'replyWithText', 'openRefs'],
|
||||
emits: ['openImagePreview', 'replyMessage', 'replyWithText'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/chat');
|
||||
@@ -305,12 +293,6 @@ export default {
|
||||
tm
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
isDark: this.isDark,
|
||||
webSearchResults: () => this.webSearchResults
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copiedMessages: new Set(),
|
||||
@@ -333,9 +315,7 @@ export default {
|
||||
imagePreview: {
|
||||
show: false,
|
||||
url: ''
|
||||
},
|
||||
// Web search results mapping: { 'uuid.idx': { url, title, snippet } }
|
||||
webSearchResults: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
@@ -344,7 +324,6 @@ export default {
|
||||
this.addScrollListener();
|
||||
this.scrollToBottom();
|
||||
this.startElapsedTimeTimer();
|
||||
this.extractWebSearchResults();
|
||||
},
|
||||
updated() {
|
||||
this.initCodeCopyButtons();
|
||||
@@ -352,56 +331,8 @@ export default {
|
||||
if (this.isUserNearBottom) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
this.extractWebSearchResults();
|
||||
},
|
||||
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() {
|
||||
const selection = window.getSelection();
|
||||
@@ -946,11 +877,6 @@ export default {
|
||||
// Check if tool is iPython executor
|
||||
isIPythonTool(toolCall) {
|
||||
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;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color;
|
||||
transition: background-color 0.2s ease;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,10 +105,6 @@
|
||||
"duration": "Duration",
|
||||
"ttft": "Time to First Token"
|
||||
},
|
||||
"refs": {
|
||||
"title": "References",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"connection": {
|
||||
"title": "Connection Status Notice",
|
||||
"message": "The system detected that the chat connection needs to be re-established.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"title": "Custom Rules",
|
||||
"subtitle": "Set custom rules for specific sessions, which take priority over global settings",
|
||||
"buttons": {
|
||||
@@ -93,42 +93,6 @@
|
||||
"batchDeleteConfirm": {
|
||||
"title": "Confirm Batch Delete",
|
||||
"message": "Are you sure you want to delete {count} selected rules? Global settings will be used after deletion."
|
||||
},
|
||||
"batchOperations": {
|
||||
"title": "Batch Operations",
|
||||
"hint": "Quick batch modify session settings",
|
||||
"scope": "Apply to",
|
||||
"scopeSelected": "Selected sessions",
|
||||
"scopeAll": "All sessions",
|
||||
"scopeGroup": "All groups",
|
||||
"scopePrivate": "All private chats",
|
||||
"llmStatus": "LLM Status",
|
||||
"ttsStatus": "TTS Status",
|
||||
"chatProvider": "Chat Model",
|
||||
"ttsProvider": "TTS Model",
|
||||
"apply": "Apply Changes"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"batchOperations": {
|
||||
"title": "Batch Operations",
|
||||
"hint": "Quick batch modify session settings",
|
||||
"scope": "Apply to",
|
||||
"scopeSelected": "Selected sessions",
|
||||
"scopeAll": "All sessions",
|
||||
"scopeGroup": "All groups",
|
||||
"scopePrivate": "All private chats",
|
||||
"llmStatus": "LLM Status",
|
||||
"ttsStatus": "TTS Status",
|
||||
"chatProvider": "Chat Model",
|
||||
"ttsProvider": "TTS Model",
|
||||
"apply": "Apply Changes"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"messages": {
|
||||
"refreshSuccess": "Data refreshed",
|
||||
@@ -141,8 +105,6 @@
|
||||
"deleteError": "Failed to delete",
|
||||
"noChanges": "No changes to save",
|
||||
"batchDeleteSuccess": "Batch delete successful",
|
||||
"batchDeleteError": "Batch delete failed",
|
||||
"batchUpdateError": "Batch update failed",
|
||||
"batchUpdateSuccess": "Batch update success"
|
||||
"batchDeleteError": "Batch delete failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +107,6 @@
|
||||
"duration": "耗时",
|
||||
"ttft": "首字时间"
|
||||
},
|
||||
"refs": {
|
||||
"title": "引用",
|
||||
"sources": "来源"
|
||||
},
|
||||
"connection": {
|
||||
"title": "连接状态提醒",
|
||||
"message": "系统检测到聊天连接需要重新建立。",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"title": "自定义规则",
|
||||
"subtitle": "为特定会话设置自定义规则,优先级高于全局配置",
|
||||
"buttons": {
|
||||
@@ -94,24 +94,6 @@
|
||||
"title": "确认批量删除",
|
||||
"message": "确定要删除选中的 {count} 条规则吗?删除后将恢复使用全局配置。"
|
||||
},
|
||||
"batchOperations": {
|
||||
"title": "批量操作",
|
||||
"hint": "快速批量修改会话配置",
|
||||
"scope": "应用范围",
|
||||
"scopeSelected": "选中的会话",
|
||||
"scopeAll": "所有会话",
|
||||
"scopeGroup": "所有群聊",
|
||||
"scopePrivate": "所有私聊",
|
||||
"llmStatus": "LLM 状态",
|
||||
"ttsStatus": "TTS 状态",
|
||||
"chatProvider": "聊天模型",
|
||||
"ttsProvider": "TTS 模型",
|
||||
"apply": "应用更改"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用"
|
||||
},
|
||||
"messages": {
|
||||
"refreshSuccess": "数据已刷新",
|
||||
"loadError": "加载数据失败",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="session-management-page">
|
||||
<v-container fluid class="pa-0">
|
||||
<v-card flat>
|
||||
@@ -111,160 +111,6 @@
|
||||
</v-data-table-server>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- 批量操作面板 -->
|
||||
<v-card flat class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<span class="text-h6">{{ tm('batchOperations.title') }}</span>
|
||||
<v-chip size="small" class="ml-2" color="info" variant="outlined">
|
||||
{{ tm('batchOperations.hint') }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="6" lg="3">
|
||||
<v-select v-model="batchScope" :items="batchScopeOptions" item-title="label" item-value="value"
|
||||
:label="tm('batchOperations.scope')" hide-details variant="solo-filled" flat density="comfortable">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="3">
|
||||
<v-select v-model="batchLlmStatus" :items="statusOptions" item-title="label" item-value="value"
|
||||
:label="tm('batchOperations.llmStatus')" hide-details clearable variant="solo-filled" flat density="comfortable">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="3">
|
||||
<v-select v-model="batchTtsStatus" :items="statusOptions" item-title="label" item-value="value"
|
||||
:label="tm('batchOperations.ttsStatus')" hide-details clearable variant="solo-filled" flat density="comfortable">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="3">
|
||||
<v-select v-model="batchChatProvider" :items="chatProviderOptions" item-title="label" item-value="value"
|
||||
:label="tm('batchOperations.chatProvider')" hide-details clearable variant="solo-filled" flat density="comfortable">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense class="mt-3">
|
||||
<v-col cols="12" class="d-flex justify-end">
|
||||
<v-btn color="primary" variant="tonal" size="large" @click="applyBatchChanges"
|
||||
:disabled="!canApplyBatch" :loading="batchUpdating" prepend-icon="mdi-check-all">
|
||||
{{ tm('batchOperations.apply') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 分组管理面板 -->
|
||||
<v-card flat class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<span class="text-h6">分组管理</span>
|
||||
<v-chip size="small" class="ml-2" color="secondary" variant="outlined">
|
||||
{{ groups.length }} 个分组
|
||||
</v-chip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="selectedItems.length > 0 && groups.length > 0" color="info" variant="tonal" size="small" class="mr-2">
|
||||
<v-icon start>mdi-folder-plus</v-icon>
|
||||
添加到分组
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item v-for="g in groups" :key="g.id" @click="addSelectedToGroup(g.id)">
|
||||
<v-list-item-title>{{ g.name }} ({{ g.umo_count }})</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn color="success" variant="tonal" size="small" @click="openCreateGroupDialog" prepend-icon="mdi-folder-plus">
|
||||
新建分组
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text v-if="groups.length > 0">
|
||||
<v-row dense>
|
||||
<v-col v-for="group in groups" :key="group.id" cols="12" sm="6" md="4" lg="3">
|
||||
<v-card variant="outlined" class="pa-3">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="font-weight-bold">{{ group.name }}</div>
|
||||
<div class="text-caption text-grey">{{ group.umo_count }} 个会话</div>
|
||||
</div>
|
||||
<div>
|
||||
<v-btn icon size="small" variant="text" @click="openEditGroupDialog(group)">
|
||||
<v-icon size="small">mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon size="small" variant="text" color="error" @click="deleteGroup(group)">
|
||||
<v-icon size="small">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text v-else class="text-center text-grey py-6">
|
||||
暂无分组,点击「新建分组」创建
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 分组编辑对话框 -->
|
||||
<v-dialog v-model="groupDialog" max-width="800" @after-enter="loadAvailableUmos">
|
||||
<v-card>
|
||||
<v-card-title class="py-3 px-4">
|
||||
{{ groupDialogMode === 'create' ? '新建分组' : '编辑分组' }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="editingGroup.name" label="分组名称" variant="outlined" hide-details class="mb-4"></v-text-field>
|
||||
<v-row dense>
|
||||
<!-- 左侧:可选会话 -->
|
||||
<v-col cols="5">
|
||||
<div class="text-subtitle-2 mb-2">可选会话 ({{ unselectedUmos.length }})</div>
|
||||
<v-text-field v-model="groupMemberSearch" placeholder="搜索..." variant="outlined" density="compact" hide-details class="mb-2" clearable prepend-inner-icon="mdi-magnify"></v-text-field>
|
||||
<v-list density="compact" class="transfer-list" lines="one">
|
||||
<v-list-item v-for="umo in filteredUnselectedUmos" :key="umo" @click="addToGroup(umo)" class="transfer-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" color="grey">mdi-plus</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-caption">{{ formatUmoShort(umo) }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="filteredUnselectedUmos.length === 0 && !loadingUmos">
|
||||
<v-list-item-title class="text-caption text-grey text-center">无匹配项</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="loadingUmos">
|
||||
<v-list-item-title class="text-center"><v-progress-circular indeterminate size="20"></v-progress-circular></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<!-- 中间:操作按钮 -->
|
||||
<v-col cols="2" class="d-flex flex-column align-center justify-center">
|
||||
<v-btn icon size="small" variant="tonal" color="primary" class="mb-2" @click="addAllToGroup" :disabled="unselectedUmos.length === 0">
|
||||
<v-icon>mdi-chevron-double-right</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon size="small" variant="tonal" color="error" @click="removeAllFromGroup" :disabled="editingGroup.umos.length === 0">
|
||||
<v-icon>mdi-chevron-double-left</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<!-- 右侧:已选会话 -->
|
||||
<v-col cols="5">
|
||||
<div class="text-subtitle-2 mb-2">已选会话 ({{ editingGroup.umos.length }})</div>
|
||||
<v-text-field v-model="groupSelectedSearch" placeholder="搜索..." variant="outlined" density="compact" hide-details class="mb-2" clearable prepend-inner-icon="mdi-magnify"></v-text-field>
|
||||
<v-list density="compact" class="transfer-list" lines="one">
|
||||
<v-list-item v-for="umo in filteredSelectedUmos" :key="umo" @click="removeFromGroup(umo)" class="transfer-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" color="error">mdi-minus</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-caption">{{ formatUmoShort(umo) }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="editingGroup.umos.length === 0">
|
||||
<v-list-item-title class="text-caption text-grey text-center">暂无成员</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="groupDialog = false">取消</v-btn>
|
||||
<v-btn color="primary" variant="tonal" @click="saveGroup">保存</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 添加规则对话框 - 选择 UMO -->
|
||||
<v-dialog v-model="addRuleDialog" max-width="600">
|
||||
@@ -608,29 +454,6 @@ export default {
|
||||
quickEditNameDialog: false,
|
||||
quickEditNameTarget: null,
|
||||
quickEditNameValue: '',
|
||||
// 批量操作
|
||||
batchScope: 'selected',
|
||||
batchGroupId: null,
|
||||
batchLlmStatus: null,
|
||||
batchTtsStatus: null,
|
||||
batchChatProvider: null,
|
||||
batchTtsProvider: null,
|
||||
batchUpdating: false,
|
||||
|
||||
// 分组管理
|
||||
groups: [],
|
||||
groupsLoading: false,
|
||||
groupDialog: false,
|
||||
groupDialogMode: 'create',
|
||||
editingGroup: {
|
||||
id: null,
|
||||
name: '',
|
||||
umos: [],
|
||||
},
|
||||
groupMemberDialog: false,
|
||||
groupMemberTarget: null,
|
||||
groupMemberSearch: '',
|
||||
groupSelectedSearch: '',
|
||||
|
||||
// 提示信息
|
||||
snackbar: false,
|
||||
@@ -706,65 +529,6 @@ export default {
|
||||
value: kb.kb_id
|
||||
}))
|
||||
},
|
||||
batchScopeOptions() {
|
||||
const options = [
|
||||
{ label: this.tm('batchOperations.scopeSelected'), value: 'selected' },
|
||||
{ label: this.tm('batchOperations.scopeAll'), value: 'all' },
|
||||
{ label: this.tm('batchOperations.scopeGroup'), value: 'group' },
|
||||
{ label: this.tm('batchOperations.scopePrivate'), value: 'private' },
|
||||
]
|
||||
// 添加自定义分组选项
|
||||
if (this.groups.length > 0) {
|
||||
options.push({ label: '── 自定义分组 ──', value: '_divider', disabled: true })
|
||||
this.groups.forEach(g => {
|
||||
options.push({ label: `📁 ${g.name} (${g.umo_count})`, value: `custom_group:${g.id}` })
|
||||
})
|
||||
}
|
||||
return options
|
||||
},
|
||||
|
||||
groupOptions() {
|
||||
return this.groups.map(g => ({
|
||||
label: `${g.name} (${g.umo_count} 个会话)`,
|
||||
value: g.id
|
||||
}))
|
||||
},
|
||||
|
||||
statusOptions() {
|
||||
return [
|
||||
{ label: this.tm('status.enabled'), value: true },
|
||||
{ label: this.tm('status.disabled'), value: false },
|
||||
]
|
||||
},
|
||||
|
||||
canApplyBatch() {
|
||||
const hasChanges = this.batchLlmStatus !== null || this.batchTtsStatus !== null ||
|
||||
this.batchChatProvider !== null || this.batchTtsProvider !== null
|
||||
if (this.batchScope === 'selected') {
|
||||
return hasChanges && this.selectedItems.length > 0
|
||||
}
|
||||
return hasChanges
|
||||
},
|
||||
|
||||
// 穿梭框:未选中的UMO列表
|
||||
unselectedUmos() {
|
||||
const selected = new Set(this.editingGroup.umos || [])
|
||||
return this.availableUmos.filter(u => !selected.has(u))
|
||||
},
|
||||
|
||||
// 穿梭框:过滤后的未选中列表
|
||||
filteredUnselectedUmos() {
|
||||
if (!this.groupMemberSearch) return this.unselectedUmos
|
||||
const search = this.groupMemberSearch.toLowerCase()
|
||||
return this.unselectedUmos.filter(u => u.toLowerCase().includes(search))
|
||||
},
|
||||
|
||||
// 穿梭框:过滤后的已选中列表
|
||||
filteredSelectedUmos() {
|
||||
if (!this.groupSelectedSearch) return this.editingGroup.umos || []
|
||||
const search = this.groupSelectedSearch.toLowerCase()
|
||||
return (this.editingGroup.umos || []).filter(u => u.toLowerCase().includes(search))
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -783,7 +547,6 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.loadData()
|
||||
this.loadGroups()
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@@ -1308,242 +1071,6 @@ export default {
|
||||
}
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
async applyBatchChanges() {
|
||||
this.batchUpdating = true
|
||||
try {
|
||||
let scope = this.batchScope
|
||||
let groupId = null
|
||||
let umos = []
|
||||
|
||||
// 处理自定义分组
|
||||
if (scope.startsWith('custom_group:')) {
|
||||
groupId = scope.split(':')[1]
|
||||
scope = 'custom_group'
|
||||
}
|
||||
|
||||
if (scope === 'selected') {
|
||||
umos = this.selectedItems.map(item => item.umo)
|
||||
if (umos.length === 0) {
|
||||
this.showError('请先选择要操作的会话')
|
||||
this.batchUpdating = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const tasks = []
|
||||
|
||||
if (this.batchLlmStatus !== null || this.batchTtsStatus !== null) {
|
||||
const serviceData = { scope, umos, group_id: groupId }
|
||||
if (this.batchLlmStatus !== null) {
|
||||
serviceData.llm_enabled = this.batchLlmStatus
|
||||
}
|
||||
if (this.batchTtsStatus !== null) {
|
||||
serviceData.tts_enabled = this.batchTtsStatus
|
||||
}
|
||||
tasks.push(axios.post('/api/session/batch-update-service', serviceData))
|
||||
}
|
||||
|
||||
if (this.batchChatProvider !== null) {
|
||||
tasks.push(axios.post('/api/session/batch-update-provider', {
|
||||
scope,
|
||||
umos,
|
||||
group_id: groupId,
|
||||
provider_type: 'chat_completion',
|
||||
provider_id: this.batchChatProvider || null
|
||||
}))
|
||||
}
|
||||
|
||||
if (this.batchTtsProvider !== null) {
|
||||
tasks.push(axios.post('/api/session/batch-update-provider', {
|
||||
scope,
|
||||
umos,
|
||||
group_id: groupId,
|
||||
provider_type: 'text_to_speech',
|
||||
provider_id: this.batchTtsProvider || null
|
||||
}))
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
this.showError('请至少选择一项要修改的配置')
|
||||
this.batchUpdating = false
|
||||
return
|
||||
}
|
||||
|
||||
const results = await Promise.all(tasks)
|
||||
const allOk = results.every(r => r.data.status === 'ok')
|
||||
|
||||
if (allOk) {
|
||||
this.showSuccess('批量更新成功')
|
||||
this.batchLlmStatus = null
|
||||
this.batchTtsStatus = null
|
||||
this.batchChatProvider = null
|
||||
this.batchTtsProvider = null
|
||||
await this.loadData()
|
||||
} else {
|
||||
this.showError('部分更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || '批量更新失败')
|
||||
}
|
||||
this.batchUpdating = false
|
||||
},
|
||||
|
||||
// ==================== 分组管理方法 ====================
|
||||
|
||||
async loadGroups() {
|
||||
this.groupsLoading = true
|
||||
try {
|
||||
const response = await axios.get('/api/session/groups')
|
||||
if (response.data.status === 'ok') {
|
||||
this.groups = response.data.data.groups || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分组失败:', error)
|
||||
}
|
||||
this.groupsLoading = false
|
||||
},
|
||||
|
||||
async loadAvailableUmos() {
|
||||
if (this.availableUmos.length > 0) return
|
||||
this.loadingUmos = true
|
||||
try {
|
||||
const response = await axios.get('/api/session/active-umos')
|
||||
if (response.data.status === 'ok') {
|
||||
this.availableUmos = response.data.data.umos || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会话列表失败:', error)
|
||||
}
|
||||
this.loadingUmos = false
|
||||
},
|
||||
|
||||
openCreateGroupDialog() {
|
||||
this.groupDialogMode = 'create'
|
||||
this.editingGroup = { id: null, name: '', umos: [] }
|
||||
this.groupMemberSearch = ''
|
||||
this.groupSelectedSearch = ''
|
||||
this.groupDialog = true
|
||||
},
|
||||
|
||||
openEditGroupDialog(group) {
|
||||
this.groupDialogMode = 'edit'
|
||||
this.editingGroup = { ...group, umos: [...(group.umos || [])] }
|
||||
this.groupMemberSearch = ''
|
||||
this.groupSelectedSearch = ''
|
||||
this.groupDialog = true
|
||||
},
|
||||
|
||||
// 穿梭框操作方法
|
||||
addToGroup(umo) {
|
||||
if (!this.editingGroup.umos.includes(umo)) {
|
||||
this.editingGroup.umos.push(umo)
|
||||
}
|
||||
},
|
||||
|
||||
removeFromGroup(umo) {
|
||||
const idx = this.editingGroup.umos.indexOf(umo)
|
||||
if (idx > -1) {
|
||||
this.editingGroup.umos.splice(idx, 1)
|
||||
}
|
||||
},
|
||||
|
||||
addAllToGroup() {
|
||||
this.unselectedUmos.forEach(umo => {
|
||||
if (!this.editingGroup.umos.includes(umo)) {
|
||||
this.editingGroup.umos.push(umo)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
removeAllFromGroup() {
|
||||
this.editingGroup.umos = []
|
||||
},
|
||||
|
||||
formatUmoShort(umo) {
|
||||
// 简化显示:平台:类型:ID -> 只显示ID部分
|
||||
const parts = umo.split(':')
|
||||
if (parts.length >= 3) {
|
||||
return `${parts[0]}:${parts[2]}`
|
||||
}
|
||||
return umo
|
||||
},
|
||||
|
||||
async saveGroup() {
|
||||
if (!this.editingGroup.name.trim()) {
|
||||
this.showError('分组名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let response
|
||||
if (this.groupDialogMode === 'create') {
|
||||
response = await axios.post('/api/session/group/create', {
|
||||
name: this.editingGroup.name,
|
||||
umos: this.editingGroup.umos
|
||||
})
|
||||
} else {
|
||||
response = await axios.post('/api/session/group/update', {
|
||||
id: this.editingGroup.id,
|
||||
name: this.editingGroup.name,
|
||||
umos: this.editingGroup.umos
|
||||
})
|
||||
}
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.data.message)
|
||||
this.groupDialog = false
|
||||
await this.loadGroups()
|
||||
} else {
|
||||
this.showError(response.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || '保存分组失败')
|
||||
}
|
||||
},
|
||||
|
||||
async deleteGroup(group) {
|
||||
if (!confirm(`确定要删除分组 "${group.name}" 吗?`)) return
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/session/group/delete', { id: group.id })
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.data.message)
|
||||
await this.loadGroups()
|
||||
} else {
|
||||
this.showError(response.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || '删除分组失败')
|
||||
}
|
||||
},
|
||||
|
||||
openGroupMemberDialog(group) {
|
||||
this.groupMemberTarget = { ...group }
|
||||
this.groupMemberDialog = true
|
||||
},
|
||||
|
||||
async addSelectedToGroup(groupId) {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.showError('请先选择要添加的会话')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/session/group/update', {
|
||||
id: groupId,
|
||||
add_umos: this.selectedItems.map(item => item.umo)
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(`已添加 ${this.selectedItems.length} 个会话到分组`)
|
||||
await this.loadGroups()
|
||||
} else {
|
||||
this.showError(response.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || '添加失败')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1560,20 +1087,4 @@ code {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.transfer-list {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.transfer-item {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.transfer-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.12.3"
|
||||
version = "4.11.4"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user