Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0c967390c | |||
| aec5f4e9e6 | |||
| 991b85e0c0 | |||
| 473d258b69 | |||
| 93cc4cebe6 | |||
| 4d28de6b4a | |||
| e7540b80ad | |||
| 97ee36b422 | |||
| 242cf8745b | |||
| 625401a4d0 | |||
| c95bbd11ae | |||
| 831907b22a | |||
| ad2dae3a8c | |||
| 92de1061aa | |||
| ddff652003 | |||
| fa4df28c22 | |||
| 06fa7be63e | |||
| e92b103fd0 | |||
| dcd699d733 | |||
| 2e53d8116e | |||
| 856d3496fa | |||
| 19e6253d5d | |||
| 1d426a7458 | |||
| c0846bc789 |
@@ -1,33 +0,0 @@
|
||||
## Setup commands
|
||||
|
||||
### Core
|
||||
|
||||
```
|
||||
uv sync
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
Exposed an API server on `http://localhost:6185` by default.
|
||||
|
||||
### Dashboard(WebUI)
|
||||
|
||||
```
|
||||
cd dashboard
|
||||
pnpm install # First time only. Use npm install -g pnpm if pnpm is not installed.
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Runs on `http://localhost:3000` by default.
|
||||
|
||||
## Dev environment tips
|
||||
|
||||
1. When modifying the WebUI, be sure to maintain componentization and clean code. Avoid duplicate code.
|
||||
2. Do not add any report files such as xxx_SUMMARY.md.
|
||||
3. After finishing, use `ruff format .` and `ruff check .` to format and check the code.
|
||||
4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`.
|
||||
5. Use English for all new comments.
|
||||
|
||||
## PR instructions
|
||||
|
||||
1. Title format: use conventional commit messages
|
||||
2. Use English to write PR title and descriptions.
|
||||
@@ -11,6 +11,7 @@ from .provider import ProviderCommands
|
||||
from .setunset import SetUnsetCommands
|
||||
from .sid import SIDCommand
|
||||
from .t2i import T2ICommand
|
||||
from .tool import ToolCommands
|
||||
from .tts import TTSCommand
|
||||
|
||||
__all__ = [
|
||||
@@ -26,4 +27,5 @@ __all__ = [
|
||||
"SetUnsetCommands",
|
||||
"T2ICommand",
|
||||
"TTSCommand",
|
||||
"ToolCommands",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from astrbot.api import star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
|
||||
|
||||
class ToolCommands:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def tool_ls(self, event: AstrMessageEvent):
|
||||
"""查看函数工具列表"""
|
||||
event.set_result(
|
||||
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
|
||||
)
|
||||
|
||||
async def tool_on(self, event: AstrMessageEvent, tool_name: str = ""):
|
||||
"""启用一个函数工具"""
|
||||
event.set_result(
|
||||
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
|
||||
)
|
||||
|
||||
async def tool_off(self, event: AstrMessageEvent, tool_name: str = ""):
|
||||
"""停用一个函数工具"""
|
||||
event.set_result(
|
||||
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
|
||||
)
|
||||
|
||||
async def tool_all_off(self, event: AstrMessageEvent):
|
||||
"""停用所有函数工具"""
|
||||
event.set_result(
|
||||
MessageEventResult().message("tool 指令在 AstrBot v4.0.0 已经被移除。"),
|
||||
)
|
||||
@@ -13,6 +13,7 @@ from .commands import (
|
||||
SetUnsetCommands,
|
||||
SIDCommand,
|
||||
T2ICommand,
|
||||
ToolCommands,
|
||||
TTSCommand,
|
||||
)
|
||||
|
||||
@@ -23,6 +24,7 @@ class Main(star.Star):
|
||||
|
||||
self.help_c = HelpCommand(self.context)
|
||||
self.llm_c = LLMCommands(self.context)
|
||||
self.tool_c = ToolCommands(self.context)
|
||||
self.plugin_c = PluginCommands(self.context)
|
||||
self.admin_c = AdminCommands(self.context)
|
||||
self.conversation_c = ConversationCommands(self.context)
|
||||
@@ -45,6 +47,30 @@ class Main(star.Star):
|
||||
"""开启/关闭 LLM"""
|
||||
await self.llm_c.llm(event)
|
||||
|
||||
@filter.command_group("tool")
|
||||
def tool(self):
|
||||
"""函数工具管理"""
|
||||
|
||||
@tool.command("ls")
|
||||
async def tool_ls(self, event: AstrMessageEvent):
|
||||
"""查看函数工具列表"""
|
||||
await self.tool_c.tool_ls(event)
|
||||
|
||||
@tool.command("on")
|
||||
async def tool_on(self, event: AstrMessageEvent, tool_name: str):
|
||||
"""启用一个函数工具"""
|
||||
await self.tool_c.tool_on(event, tool_name)
|
||||
|
||||
@tool.command("off")
|
||||
async def tool_off(self, event: AstrMessageEvent, tool_name: str):
|
||||
"""停用一个函数工具"""
|
||||
await self.tool_c.tool_off(event, tool_name)
|
||||
|
||||
@tool.command("off_all")
|
||||
async def tool_all_off(self, event: AstrMessageEvent):
|
||||
"""停用所有函数工具"""
|
||||
await self.tool_c.tool_all_off(event)
|
||||
|
||||
@filter.command_group("plugin")
|
||||
def plugin(self):
|
||||
"""插件管理"""
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.12.4"
|
||||
__version__ = "4.12.3"
|
||||
|
||||
@@ -56,10 +56,8 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
||||
)
|
||||
|
||||
# special handle web_search_tavily
|
||||
platform_name = run_context.context.event.get_platform_name()
|
||||
if (
|
||||
platform_name == "webchat"
|
||||
and tool.name == "web_search_tavily"
|
||||
tool.name == "web_search_tavily"
|
||||
and len(run_context.messages) > 0
|
||||
and tool_result
|
||||
and len(tool_result.content)
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.12.4"
|
||||
VERSION = "4.12.3"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -166,7 +166,6 @@ DEFAULT_CONFIG = {
|
||||
"jwt_secret": "",
|
||||
"host": "0.0.0.0",
|
||||
"port": 6185,
|
||||
"disable_access_log": True,
|
||||
},
|
||||
"platform": [],
|
||||
"platform_specific": {
|
||||
@@ -774,21 +773,27 @@ CONFIG_METADATA_2 = {
|
||||
"interval_method": {
|
||||
"type": "string",
|
||||
"options": ["random", "log"],
|
||||
"hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
|
||||
},
|
||||
"interval": {
|
||||
"type": "string",
|
||||
"hint": "`random` 方法用。每一段回复的间隔时间,格式为 `最小时间,最大时间`。如 `0.75,2.5`",
|
||||
},
|
||||
"log_base": {
|
||||
"type": "float",
|
||||
"hint": "`log` 方法用。对数函数的底数。默认为 2.6",
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"type": "int",
|
||||
"hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
|
||||
},
|
||||
"regex": {
|
||||
"type": "string",
|
||||
"hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。re.findall(r'<regex>', text)",
|
||||
},
|
||||
"content_cleanup_rule": {
|
||||
"type": "string",
|
||||
"hint": "移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.sub(r'<regex>', '', text)",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1186,11 +1191,7 @@ CONFIG_METADATA_2 = {
|
||||
"type": "genie_tts",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
"genie_character_name": "mika",
|
||||
"genie_onnx_model_dir": "CharacterModels/v2ProPlus/mika/tts_models",
|
||||
"genie_language": "Japanese",
|
||||
"genie_refer_audio_path": "",
|
||||
"genie_refer_text": "",
|
||||
"character_name": "mika",
|
||||
"timeout": 20,
|
||||
},
|
||||
"Edge TTS": {
|
||||
@@ -1409,16 +1410,6 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"genie_onnx_model_dir": {
|
||||
"description": "ONNX Model Directory",
|
||||
"type": "string",
|
||||
"hint": "The directory path containing the ONNX model files",
|
||||
},
|
||||
"genie_language": {
|
||||
"description": "Language",
|
||||
"type": "string",
|
||||
"options": ["Japanese", "English", "Chinese"],
|
||||
},
|
||||
"provider_source_id": {
|
||||
"invisible": True,
|
||||
"type": "string",
|
||||
@@ -3040,8 +3031,7 @@ CONFIG_METADATA_3 = {
|
||||
"type": "bool",
|
||||
},
|
||||
"platform_settings.segmented_reply.interval_method": {
|
||||
"description": "间隔方法。",
|
||||
"hint": "random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
|
||||
"description": "间隔方法",
|
||||
"type": "string",
|
||||
"options": ["random", "log"],
|
||||
},
|
||||
@@ -3056,14 +3046,13 @@ CONFIG_METADATA_3 = {
|
||||
"platform_settings.segmented_reply.log_base": {
|
||||
"description": "对数底数",
|
||||
"type": "float",
|
||||
"hint": "对数间隔的底数,默认为 2.6。取值范围为 1.0-10.0。",
|
||||
"hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。",
|
||||
"condition": {
|
||||
"platform_settings.segmented_reply.interval_method": "log",
|
||||
},
|
||||
},
|
||||
"platform_settings.segmented_reply.words_count_threshold": {
|
||||
"description": "分段回复字数阈值",
|
||||
"hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
|
||||
"type": "int",
|
||||
},
|
||||
"platform_settings.segmented_reply.split_mode": {
|
||||
@@ -3074,7 +3063,6 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
"platform_settings.segmented_reply.regex": {
|
||||
"description": "分段正则表达式",
|
||||
"hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.findall(r'<regex>', text)",
|
||||
"type": "string",
|
||||
"condition": {
|
||||
"platform_settings.segmented_reply.split_mode": "regex",
|
||||
|
||||
@@ -203,23 +203,6 @@ class BaseDatabase(abc.ABC):
|
||||
"""Get platform message history for a specific user."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def search_platform_sessions(
|
||||
self,
|
||||
creator: str,
|
||||
query: str,
|
||||
context_len: int = 40,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
) -> tuple[list[dict], int]:
|
||||
"""Search platform sessions (title or message content) for a given creator.
|
||||
|
||||
Returns a tuple of (results, total) where results is a list of dicts with keys:
|
||||
session_id, title, match_field, match_index, match_length, snippet, snippet_start,
|
||||
created_at, updated_at
|
||||
"""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_platform_message_history_by_id(
|
||||
self,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
import typing as T
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -484,144 +483,6 @@ class SQLiteDatabase(BaseDatabase):
|
||||
result = await session.execute(query.offset(offset).limit(page_size))
|
||||
return result.scalars().all()
|
||||
|
||||
def _build_snippet(self, text: str, match_index: int, match_length: int, context_len: int):
|
||||
if match_index < 0:
|
||||
return "", 0
|
||||
start = max(match_index - context_len, 0)
|
||||
end = min(match_index + match_length + context_len, len(text))
|
||||
return text[start:end], start
|
||||
|
||||
async def search_platform_sessions(
|
||||
self,
|
||||
creator: str,
|
||||
query: str,
|
||||
context_len: int = 40,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
) -> tuple[list[dict], int]:
|
||||
"""Search platform sessions (by title or by message content) for a given creator.
|
||||
|
||||
This implementation performs searching at DB level using SQL LIKE on
|
||||
`platform_sessions.display_name` and the JSON `platform_message_history.content`.
|
||||
To keep work minimal and compatible with SQLite JSON storage, the content
|
||||
column is searched as text using LIKE.
|
||||
Returns (results, total) where results are dicts suitable for the caller.
|
||||
"""
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
pattern = f"%{query}%"
|
||||
|
||||
# 1) Title matches
|
||||
title_q = (
|
||||
select(PlatformSession)
|
||||
.where(col(PlatformSession.creator) == creator)
|
||||
.where(col(PlatformSession.display_name).ilike(pattern))
|
||||
.order_by(desc(PlatformSession.updated_at))
|
||||
)
|
||||
title_result = await session.execute(title_q)
|
||||
title_rows = title_result.scalars().all()
|
||||
|
||||
results: list[dict] = []
|
||||
for session_row in title_rows:
|
||||
title = session_row.display_name or ""
|
||||
title_lower = title.lower()
|
||||
qlower = query.lower()
|
||||
match_index = title_lower.find(qlower) if title else -1
|
||||
snippet, snippet_start = self._build_snippet(title, match_index, len(query), context_len)
|
||||
results.append(
|
||||
{
|
||||
"session_id": session_row.session_id,
|
||||
"title": session_row.display_name,
|
||||
"match_field": "title",
|
||||
"match_index": match_index,
|
||||
"match_length": len(query),
|
||||
"snippet": snippet,
|
||||
"snippet_start": snippet_start,
|
||||
"created_at": session_row.created_at.astimezone().isoformat(),
|
||||
"updated_at": session_row.updated_at.astimezone().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
# 2) Content matches: find latest matching message per session (user_id)
|
||||
# Use a subquery to select the latest message id per user that matches the pattern
|
||||
subq = (
|
||||
select(func.max(col(PlatformMessageHistory.id)).label("max_id"))
|
||||
.select_from(PlatformMessageHistory)
|
||||
.join(
|
||||
PlatformSession,
|
||||
col(PlatformMessageHistory.user_id) == col(PlatformSession.session_id),
|
||||
)
|
||||
.where(col(PlatformSession.creator) == creator)
|
||||
.where(col(PlatformMessageHistory.content).ilike(pattern))
|
||||
.group_by(col(PlatformMessageHistory.user_id))
|
||||
)
|
||||
|
||||
ids_result = await session.execute(subq)
|
||||
id_rows = [r[0] for r in ids_result.fetchall() if r[0] is not None]
|
||||
|
||||
if id_rows:
|
||||
q = select(PlatformMessageHistory).where(col(PlatformMessageHistory.id).in_(id_rows))
|
||||
q = q.order_by(desc(PlatformMessageHistory.created_at))
|
||||
hist_result = await session.execute(q)
|
||||
histories = hist_result.scalars().all()
|
||||
|
||||
for history in histories:
|
||||
# find associated session to get display_name/created_at/updated_at
|
||||
ps_q = select(PlatformSession).where(col(PlatformSession.session_id) == history.user_id)
|
||||
ps_res = await session.execute(ps_q)
|
||||
ps = ps_res.scalar_one_or_none()
|
||||
text = None
|
||||
try:
|
||||
# convert content json to plain text similar to ChatRoute._extract_plain_text
|
||||
msg = history.content
|
||||
if isinstance(msg, dict):
|
||||
message = msg.get("message")
|
||||
if isinstance(message, str):
|
||||
text = message
|
||||
elif isinstance(message, list):
|
||||
parts = []
|
||||
for part in message:
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
part_type = part.get("type")
|
||||
if part_type == "plain" and part.get("text"):
|
||||
parts.append(str(part.get("text")))
|
||||
elif part_type == "reply" and part.get("selected_text"):
|
||||
parts.append(str(part.get("selected_text")))
|
||||
text = "\n".join(parts)
|
||||
except Exception:
|
||||
text = None
|
||||
|
||||
if not text:
|
||||
# fallback to stringified JSON
|
||||
text = json.dumps(history.content, ensure_ascii=False)
|
||||
|
||||
lower_text = text.lower() if isinstance(text, str) else ""
|
||||
match_index = lower_text.find(query.lower())
|
||||
if match_index == -1:
|
||||
continue
|
||||
snippet, snippet_start = self._build_snippet(text, match_index, len(query), context_len)
|
||||
results.append(
|
||||
{
|
||||
"session_id": history.user_id,
|
||||
"title": ps.display_name if ps else None,
|
||||
"match_field": "content",
|
||||
"match_index": match_index,
|
||||
"match_length": len(query),
|
||||
"snippet": snippet,
|
||||
"snippet_start": snippet_start,
|
||||
"created_at": ps.created_at.astimezone().isoformat() if ps else history.created_at.astimezone().isoformat(),
|
||||
"updated_at": ps.updated_at.astimezone().isoformat() if ps else history.updated_at.astimezone().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
# sort and paginate
|
||||
results.sort(key=lambda item: item["updated_at"], reverse=True)
|
||||
total = len(results)
|
||||
offset = (page - 1) * page_size
|
||||
paged = results[offset : offset + page_size]
|
||||
return paged, total
|
||||
|
||||
async def get_platform_message_history_by_id(
|
||||
self, message_id: int
|
||||
) -> PlatformMessageHistory | None:
|
||||
|
||||
@@ -116,12 +116,8 @@ class InternalAgentSubStage(Stage):
|
||||
if not provider:
|
||||
logger.error(f"未找到指定的提供商: {sel_provider}。")
|
||||
return provider
|
||||
try:
|
||||
prov = _ctx.get_using_provider(umo=event.unified_msg_origin)
|
||||
except ValueError as e:
|
||||
logger.error(f"Error occurred while selecting provider: {e}")
|
||||
return None
|
||||
return prov
|
||||
|
||||
return _ctx.get_using_provider(umo=event.unified_msg_origin)
|
||||
|
||||
async def _get_session_conv(self, event: AstrMessageEvent) -> Conversation:
|
||||
umo = event.unified_msg_origin
|
||||
@@ -500,7 +496,6 @@ class InternalAgentSubStage(Stage):
|
||||
try:
|
||||
provider = self._select_provider(event)
|
||||
if provider is None:
|
||||
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
|
||||
return
|
||||
if not isinstance(provider, Provider):
|
||||
logger.error(
|
||||
|
||||
@@ -41,7 +41,6 @@ TOOL_CALL_PROMPT = (
|
||||
"You MUST NOT return an empty response, especially after invoking a tool."
|
||||
"Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
|
||||
"After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
|
||||
"Keep the role-play and style consistent throughout the conversation."
|
||||
)
|
||||
|
||||
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT = (
|
||||
|
||||
@@ -165,6 +165,7 @@ class WakingCheckStage(Stage):
|
||||
and handler.handler_module_path
|
||||
== "astrbot.builtin_stars.builtin_commands.main"
|
||||
):
|
||||
logger.debug("skipping builtin command")
|
||||
continue
|
||||
|
||||
# filter 需满足 AND 逻辑关系
|
||||
|
||||
@@ -62,44 +62,27 @@ class AiocqhttpAdapter(Platform):
|
||||
|
||||
@self.bot.on_request()
|
||||
async def request(event: Event):
|
||||
try:
|
||||
abm = await self.convert_message(event)
|
||||
if not abm:
|
||||
return
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
except Exception as e:
|
||||
logger.exception(f"Handle request message failed: {e}")
|
||||
return
|
||||
|
||||
@self.bot.on_notice()
|
||||
async def notice(event: Event):
|
||||
try:
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
except Exception as e:
|
||||
logger.exception(f"Handle notice message failed: {e}")
|
||||
return
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@self.bot.on_message("group")
|
||||
async def group(event: Event):
|
||||
try:
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
except Exception as e:
|
||||
logger.exception(f"Handle group message failed: {e}")
|
||||
return
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@self.bot.on_message("private")
|
||||
async def private(event: Event):
|
||||
try:
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
except Exception as e:
|
||||
logger.exception(f"Handle private message failed: {e}")
|
||||
return
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@self.bot.on_websocket_connection
|
||||
def on_websocket_connection(_):
|
||||
@@ -389,10 +372,9 @@ class AiocqhttpAdapter(Platform):
|
||||
|
||||
message_str += "".join(at_parts)
|
||||
elif t == "markdown":
|
||||
for m in m_group:
|
||||
text = m["data"].get("markdown") or m["data"].get("content", "")
|
||||
abm.message.append(Plain(text=text))
|
||||
message_str += text
|
||||
text = m["data"].get("markdown") or m["data"].get("content", "")
|
||||
abm.message.append(Plain(text=text))
|
||||
message_str += text
|
||||
else:
|
||||
for m in m_group:
|
||||
try:
|
||||
|
||||
@@ -382,18 +382,15 @@ class ProviderGoogleGenAI(Provider):
|
||||
append_or_extend(gemini_contents, parts, types.ModelContent)
|
||||
|
||||
elif role == "tool" and not native_tool_enabled:
|
||||
func_name = message.get("name", message["tool_call_id"])
|
||||
part = types.Part.from_function_response(
|
||||
name=func_name,
|
||||
response={
|
||||
"name": func_name,
|
||||
"content": message["content"],
|
||||
},
|
||||
)
|
||||
if part.function_response:
|
||||
part.function_response.id = message["tool_call_id"]
|
||||
|
||||
parts = [part]
|
||||
parts = [
|
||||
types.Part.from_function_response(
|
||||
name=message["tool_call_id"],
|
||||
response={
|
||||
"name": message["tool_call_id"],
|
||||
"content": message["content"],
|
||||
},
|
||||
),
|
||||
]
|
||||
append_or_extend(gemini_contents, parts, types.UserContent)
|
||||
|
||||
if gemini_contents and isinstance(gemini_contents[0], types.ModelContent):
|
||||
|
||||
@@ -29,24 +29,10 @@ class GenieTTSProvider(TTSProvider):
|
||||
if not genie:
|
||||
raise ImportError("Please install genie_tts first.")
|
||||
|
||||
self.character_name = provider_config.get("genie_character_name", "mika")
|
||||
language = provider_config.get("genie_language", "Japanese")
|
||||
model_dir = provider_config.get("genie_onnx_model_dir", "")
|
||||
refer_audio_path = provider_config.get("genie_refer_audio_path", "")
|
||||
refer_text = provider_config.get("genie_refer_text", "")
|
||||
self.character_name = provider_config.get("character_name", "mika")
|
||||
|
||||
try:
|
||||
genie.load_character(
|
||||
character_name=self.character_name,
|
||||
language=language,
|
||||
onnx_model_dir=model_dir,
|
||||
)
|
||||
genie.set_reference_audio(
|
||||
character_name=self.character_name,
|
||||
audio_path=refer_audio_path,
|
||||
audio_text=refer_text,
|
||||
language=language,
|
||||
)
|
||||
genie.load_predefined_character(self.character_name)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to load character {self.character_name}: {e}")
|
||||
|
||||
|
||||
@@ -328,29 +328,28 @@ class Context:
|
||||
"""获取所有用于 Embedding 任务的 Provider。"""
|
||||
return self.provider_manager.embedding_provider_insts
|
||||
|
||||
def get_using_provider(self, umo: str | None = None) -> Provider | None:
|
||||
def get_using_provider(self, umo: str | None = None) -> Provider:
|
||||
"""获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。
|
||||
|
||||
Args:
|
||||
umo: unified_message_origin 值,如果传入并且用户启用了提供商会话隔离,
|
||||
则使用该会话偏好的对话模型(提供商)。
|
||||
则使用该会话偏好的提供商。
|
||||
|
||||
Returns:
|
||||
当前使用的对话模型(提供商),如果未设置则返回 None。
|
||||
当前使用的文本生成提供者。
|
||||
|
||||
Raises:
|
||||
ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。
|
||||
ValueError: 返回的提供者不是 Provider 类型。
|
||||
|
||||
Note:
|
||||
通过 /provider 指令可以切换提供者。
|
||||
"""
|
||||
prov = self.provider_manager.get_using_provider(
|
||||
provider_type=ProviderType.CHAT_COMPLETION,
|
||||
umo=umo,
|
||||
)
|
||||
if prov is None:
|
||||
return None
|
||||
if not isinstance(prov, Provider):
|
||||
raise ValueError(
|
||||
f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}"
|
||||
)
|
||||
raise ValueError("返回的 Provider 不是 Provider 类型")
|
||||
return prov
|
||||
|
||||
def get_using_tts_provider(self, umo: str | None = None) -> TTSProvider | None:
|
||||
|
||||
@@ -46,7 +46,6 @@ class ChatRoute(Route):
|
||||
"POST",
|
||||
self.update_session_display_name,
|
||||
),
|
||||
"/chat/search": ("GET", self.search_sessions),
|
||||
"/chat/get_file": ("GET", self.get_file),
|
||||
"/chat/get_attachment": ("GET", self.get_attachment),
|
||||
"/chat/post_file": ("POST", self.post_file),
|
||||
@@ -64,35 +63,6 @@ class ChatRoute(Route):
|
||||
|
||||
self.running_convs: dict[str, bool] = {}
|
||||
|
||||
@staticmethod
|
||||
def _extract_plain_text(content: dict) -> str:
|
||||
if not isinstance(content, dict):
|
||||
return ""
|
||||
message = content.get("message")
|
||||
if isinstance(message, str):
|
||||
return message
|
||||
if not isinstance(message, list):
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
for part in message:
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
part_type = part.get("type")
|
||||
if part_type == "plain" and part.get("text"):
|
||||
parts.append(str(part.get("text")))
|
||||
elif part_type == "reply" and part.get("selected_text"):
|
||||
parts.append(str(part.get("selected_text")))
|
||||
return "\n".join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _build_snippet(text: str, match_index: int, match_length: int, context_len: int):
|
||||
if match_index < 0:
|
||||
return "", 0
|
||||
start = max(match_index - context_len, 0)
|
||||
end = min(match_index + match_length + context_len, len(text))
|
||||
return text[start:end], start
|
||||
|
||||
async def get_file(self):
|
||||
filename = request.args.get("filename")
|
||||
if not filename:
|
||||
@@ -761,57 +731,6 @@ class ChatRoute(Route):
|
||||
|
||||
return Response().ok(data=sessions_data).__dict__
|
||||
|
||||
async def search_sessions(self):
|
||||
"""Search sessions by title or content, with pagination."""
|
||||
username = g.get("username", "guest")
|
||||
query = request.args.get("query", "", type=str).strip()
|
||||
page = max(request.args.get("page", 1, type=int), 1)
|
||||
page_size = min(max(request.args.get("page_size", 10, type=int), 1), 100)
|
||||
context_len = min(max(request.args.get("context", 40, type=int), 0), 200)
|
||||
|
||||
if not query:
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
data={
|
||||
"results": [],
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": 0,
|
||||
"total_pages": 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
|
||||
# Delegate searching to the database implementation for efficiency
|
||||
paged_results, total = await self.db.search_platform_sessions(
|
||||
creator=username,
|
||||
query=query,
|
||||
context_len=context_len,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
data={
|
||||
"results": paged_results,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": total_pages,
|
||||
},
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
|
||||
async def get_session(self):
|
||||
"""Get session information and message history by session_id."""
|
||||
session_id = request.args.get("session_id")
|
||||
|
||||
@@ -7,8 +7,6 @@ from typing import cast
|
||||
import jwt
|
||||
import psutil
|
||||
from flask.json.provider import DefaultJSONProvider
|
||||
from hypercorn.asyncio import serve
|
||||
from hypercorn.config import Config as HyperConfig
|
||||
from psutil._common import addr as psutil_addr
|
||||
from quart import Quart, g, jsonify, request
|
||||
from quart.logging import default_handler
|
||||
@@ -246,22 +244,11 @@ class AstrBotDashboard:
|
||||
|
||||
logger.info(display)
|
||||
|
||||
# 配置 Hypercorn
|
||||
config = HyperConfig()
|
||||
config.bind = [f"{host}:{port}"]
|
||||
|
||||
# 根据配置决定是否禁用访问日志
|
||||
disable_access_log = self.core_lifecycle.astrbot_config.get(
|
||||
"dashboard", {}
|
||||
).get("disable_access_log", True)
|
||||
if disable_access_log:
|
||||
config.accesslog = None
|
||||
else:
|
||||
# 启用访问日志,使用简洁格式
|
||||
config.accesslog = "-"
|
||||
config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s"
|
||||
|
||||
return serve(self.app, config, shutdown_trigger=self.shutdown_trigger)
|
||||
return self.app.run_task(
|
||||
host=host,
|
||||
port=port,
|
||||
shutdown_trigger=self.shutdown_trigger,
|
||||
)
|
||||
|
||||
async def shutdown_trigger(self):
|
||||
await self.shutdown_event.wait()
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
## 更新内容
|
||||
|
||||
### 新功能
|
||||
- 为 ChatUI 添加文件拖拽上传功能 ([#4583](https://github.com/AstrBotDevs/AstrBot/issues/4583))
|
||||
- 实现人格文件夹以进行高级人格管理 ([#4443](https://github.com/AstrBotDevs/AstrBot/issues/4443))
|
||||
- 添加人格文件夹管理以支持层级组织 (db)
|
||||
- 支持 Genie TTS
|
||||
|
||||
### 修复
|
||||
- 增强提供商选择错误处理和日志记录,避免出现 `Provider 不是 Provider 类型` 的错误 ([#4654](https://github.com/AstrBotDevs/AstrBot/issues/4654))
|
||||
- aiocqhttp 适配器中的 Markdown KeyError 或 UnboundLocalError 问题 ([#4656](https://github.com/AstrBotDevs/AstrBot/issues/4656))
|
||||
- 确保 providers 中的 embedding 维度作为整数返回 ([#4547](https://github.com/AstrBotDevs/AstrBot/issues/4547))
|
||||
- 钉钉流式响应问题 ([#4590](https://github.com/AstrBotDevs/AstrBot/issues/4590))
|
||||
- 提供商选择按钮被长模型名称遮挡的问题 ([#4631](https://github.com/AstrBotDevs/AstrBot/issues/4631))
|
||||
- 更新 `web_search_tavily` 处理,避免在非 ChatUI 平台出现信息引用 ([#4633](https://github.com/AstrBotDevs/AstrBot/issues/4633))
|
||||
|
||||
### 性能优化
|
||||
- T2I 模板编辑器预览 ([#4574](https://github.com/AstrBotDevs/AstrBot/issues/4574))
|
||||
|
||||
### 杂项
|
||||
- 移除已弃用的 `tool` 命令
|
||||
@@ -26,7 +26,6 @@
|
||||
@createProject="showCreateProjectDialog"
|
||||
@editProject="showEditProjectDialog"
|
||||
@deleteProject="handleDeleteProject"
|
||||
@openSearch="handleOpenSearch"
|
||||
/>
|
||||
|
||||
<!-- 右侧聊天内容区域 -->
|
||||
@@ -43,99 +42,65 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<ChatSearchView
|
||||
v-if="isSearchActive"
|
||||
@close="handleCloseSearch"
|
||||
<!-- 面包屑导航 -->
|
||||
<div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container">
|
||||
<div class="breadcrumb-content">
|
||||
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
|
||||
<span class="breadcrumb-project" @click="handleSelectProject(currentSessionProject.project_id)">{{ currentSessionProject.title }}</span>
|
||||
<v-icon size="small" class="breadcrumb-separator">mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-list-wrapper" v-if="currSessionId && !selectedProjectId">
|
||||
<MessageList :messages="messages" :isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning"
|
||||
:isLoadingMessages="isLoadingMessages"
|
||||
@openImagePreview="openImagePreview"
|
||||
@replyMessage="handleReplyMessage"
|
||||
@replyWithText="handleReplyWithText"
|
||||
@openRefs="handleOpenRefs"
|
||||
ref="messageList" />
|
||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||
</div>
|
||||
<ProjectView
|
||||
v-else-if="selectedProjectId"
|
||||
:project="currentProject"
|
||||
:sessions="projectSessions"
|
||||
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
|
||||
/>
|
||||
<template v-else>
|
||||
<!-- 面包屑导航 -->
|
||||
<div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container">
|
||||
<div class="breadcrumb-content">
|
||||
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
|
||||
<span class="breadcrumb-project" @click="handleSelectProject(currentSessionProject.project_id)">{{ currentSessionProject.title }}</span>
|
||||
<v-icon size="small" class="breadcrumb-separator">mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-list-wrapper" v-if="currSessionId && !selectedProjectId">
|
||||
<MessageList :messages="messages" :isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning"
|
||||
:isLoadingMessages="isLoadingMessages"
|
||||
@openImagePreview="openImagePreview"
|
||||
@replyMessage="handleReplyMessage"
|
||||
@replyWithText="handleReplyWithText"
|
||||
@openRefs="handleOpenRefs"
|
||||
ref="messageList" />
|
||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||
</div>
|
||||
<ProjectView
|
||||
v-else-if="selectedProjectId"
|
||||
:project="currentProject"
|
||||
:sessions="projectSessions"
|
||||
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
|
||||
@editSessionTitle="showEditTitleDialog"
|
||||
@deleteSession="handleDeleteConversation"
|
||||
>
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</ProjectView>
|
||||
<WelcomeView
|
||||
v-else
|
||||
:isLoading="isLoadingMessages"
|
||||
>
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</WelcomeView>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
@editSessionTitle="showEditTitleDialog"
|
||||
@deleteSession="handleDeleteConversation"
|
||||
>
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</ProjectView>
|
||||
<WelcomeView
|
||||
v-else
|
||||
:isLoading="isLoadingMessages"
|
||||
>
|
||||
<ChatInput
|
||||
v-if="currSessionId && !selectedProjectId"
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
@@ -159,7 +124,34 @@
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</template>
|
||||
</WelcomeView>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<ChatInput
|
||||
v-if="currSessionId && !selectedProjectId"
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +200,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
@@ -218,7 +209,6 @@ import ConversationSidebar from '@/components/chat/ConversationSidebar.vue';
|
||||
import ChatInput from '@/components/chat/ChatInput.vue';
|
||||
import ProjectDialog from '@/components/chat/ProjectDialog.vue';
|
||||
import ProjectView from '@/components/chat/ProjectView.vue';
|
||||
import ChatSearchView from '@/components/chat/ChatSearchView.vue';
|
||||
import WelcomeView from '@/components/chat/WelcomeView.vue';
|
||||
import RefsSidebar from '@/components/chat/message_list_comps/RefsSidebar.vue';
|
||||
import LiveMode from '@/components/chat/LiveMode.vue';
|
||||
@@ -229,7 +219,6 @@ import { useMediaHandling } from '@/composables/useMediaHandling';
|
||||
import { useProjects } from '@/composables/useProjects';
|
||||
import type { Project } from '@/components/chat/ProjectList.vue';
|
||||
import { useRecording } from '@/composables/useRecording';
|
||||
import { useChatSearchStore } from '@/stores/chatSearch';
|
||||
|
||||
interface Props {
|
||||
chatboxMode?: boolean;
|
||||
@@ -244,8 +233,6 @@ const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/chat');
|
||||
const theme = useTheme();
|
||||
const chatSearchStore = useChatSearchStore();
|
||||
const { active: isSearchActive } = storeToRefs(chatSearchStore);
|
||||
|
||||
// UI 状态
|
||||
const isMobile = ref(false);
|
||||
@@ -449,15 +436,12 @@ function handleOpenRefs(refs: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelectConversation(sessionIds: string[], shouldCloseSearch: boolean = true) {
|
||||
async function handleSelectConversation(sessionIds: string[]) {
|
||||
if (!sessionIds[0]) return;
|
||||
|
||||
// 退出项目视图
|
||||
selectedProjectId.value = null;
|
||||
projectSessions.value = [];
|
||||
if (shouldCloseSearch) {
|
||||
chatSearchStore.closeSearch();
|
||||
}
|
||||
|
||||
// 立即更新选中状态,避免需要点击两次
|
||||
currSessionId.value = sessionIds[0];
|
||||
@@ -498,7 +482,6 @@ function handleNewChat() {
|
||||
// 退出项目视图
|
||||
selectedProjectId.value = null;
|
||||
projectSessions.value = [];
|
||||
chatSearchStore.closeSearch();
|
||||
}
|
||||
|
||||
async function handleDeleteConversation(sessionId: string) {
|
||||
@@ -517,7 +500,6 @@ async function handleSelectProject(projectId: string) {
|
||||
const sessions = await getProjectSessions(projectId);
|
||||
projectSessions.value = sessions;
|
||||
messages.value = [];
|
||||
chatSearchStore.closeSearch();
|
||||
|
||||
// 清空当前会话ID,准备在项目中创建新对话
|
||||
currSessionId.value = '';
|
||||
@@ -591,17 +573,6 @@ function closeLiveMode() {
|
||||
liveModeOpen.value = false;
|
||||
}
|
||||
|
||||
function handleOpenSearch() {
|
||||
chatSearchStore.openSearch();
|
||||
if (isMobile.value) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseSearch() {
|
||||
chatSearchStore.closeSearch();
|
||||
}
|
||||
|
||||
async function handleSendMessage() {
|
||||
// 只有引用不能发送,必须有输入内容
|
||||
if (!prompt.value.trim() && stagedFiles.value.length === 0 && !stagedAudioUrl.value) {
|
||||
@@ -676,7 +647,7 @@ watch(
|
||||
if (sessions.value.length > 0) {
|
||||
const session = sessions.value.find(s => s.session_id === pathSessionId);
|
||||
if (session) {
|
||||
handleSelectConversation([pathSessionId], !isSearchActive.value);
|
||||
handleSelectConversation([pathSessionId]);
|
||||
}
|
||||
} else {
|
||||
pendingSessionId.value = pathSessionId;
|
||||
@@ -693,13 +664,13 @@ watch(sessions, (newSessions) => {
|
||||
const session = newSessions.find(s => s.session_id === pendingSessionId.value);
|
||||
if (session) {
|
||||
selectedSessions.value = [pendingSessionId.value];
|
||||
handleSelectConversation([pendingSessionId.value], !isSearchActive.value);
|
||||
handleSelectConversation([pendingSessionId.value]);
|
||||
pendingSessionId.value = null;
|
||||
}
|
||||
} else if (!currSessionId.value && newSessions.length > 0) {
|
||||
const firstSession = newSessions[0];
|
||||
selectedSessions.value = [firstSession.session_id];
|
||||
handleSelectConversation([firstSession.session_id], !isSearchActive.value);
|
||||
handleSelectConversation([firstSession.session_id]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
<template>
|
||||
<div class="chat-search-container fade-in">
|
||||
<div class="chat-search-header">
|
||||
<div class="chat-search-header-info">
|
||||
<h2 class="chat-search-header-title">{{ tm('search.title') }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-search-input">
|
||||
<v-text-field
|
||||
v-model="query"
|
||||
:placeholder="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
rounded="xl"
|
||||
density="comfortable"
|
||||
clearable
|
||||
flat
|
||||
hide-details
|
||||
:loading="isLoading"
|
||||
@keyup.enter="handleSearch"
|
||||
@click:clear="handleClear"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-card flat class="chat-search-results">
|
||||
<v-list v-if="results.length > 0">
|
||||
<v-list-item
|
||||
v-for="item in results"
|
||||
:key="item.session_id"
|
||||
class="chat-search-result-item"
|
||||
rounded="lg"
|
||||
@click="emit('selectSession', item.session_id)"
|
||||
>
|
||||
<v-list-item-title style="font-weight: bold;">
|
||||
{{ item.title || tm('conversation.newConversation') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="chat-search-snippet">
|
||||
<span>{{ getSnippetParts(item).before }}</span>
|
||||
<span class="chat-search-highlight">{{ getSnippetParts(item).match }}</span>
|
||||
<span>{{ getSnippetParts(item).after }}</span>
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="chat-search-meta">
|
||||
<!-- {{ getMatchFieldLabel(item) }} -->
|
||||
<!-- · {{ tm('search.matchPosition') }} {{ item.match_index + 1 }} -->
|
||||
{{ tm('search.createdAt') }} {{ formatDate(item.created_at) }}
|
||||
· {{ tm('search.updatedAt') }} {{ formatDate(item.updated_at) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="chat-search-empty">
|
||||
<v-icon icon="mdi-text-box-search-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<p>
|
||||
{{ searchPerformed ? tm('search.noResults') : tm('search.hint') }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<div v-if="pagination.total > 0" class="chat-search-pagination">
|
||||
<div class="chat-search-page-size">
|
||||
<span class="chat-search-page-label">{{ tm('search.pageSize') }}</span>
|
||||
<v-select
|
||||
v-model="pageSizeProxy"
|
||||
:items="pageSizeOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</div>
|
||||
<v-pagination
|
||||
v-model="pageProxy"
|
||||
:length="pagination.total_pages"
|
||||
:disabled="isLoading"
|
||||
rounded="circle"
|
||||
:total-visible="7"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useChatSearchStore, type ChatSearchResult } from '@/stores/chatSearch';
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
selectSession: [sessionId: string];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/chat');
|
||||
|
||||
const chatSearchStore = useChatSearchStore();
|
||||
const { query, results, pagination, isLoading, searchPerformed } = storeToRefs(chatSearchStore);
|
||||
|
||||
const pageSizeOptions = [10, 20, 50];
|
||||
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const debounceDelay = 400;
|
||||
|
||||
const pageProxy = computed({
|
||||
get: () => pagination.value.page,
|
||||
set: (value) => chatSearchStore.setPage(value)
|
||||
});
|
||||
|
||||
const pageSizeProxy = computed({
|
||||
get: () => pagination.value.page_size,
|
||||
set: (value) => chatSearchStore.setPageSize(value)
|
||||
});
|
||||
|
||||
function handleSearch() {
|
||||
chatSearchStore.runNewSearch();
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
chatSearchStore.search();
|
||||
}
|
||||
|
||||
function scheduleSearch() {
|
||||
if (searchTimeout.value) {
|
||||
clearTimeout(searchTimeout.value);
|
||||
}
|
||||
searchTimeout.value = setTimeout(() => {
|
||||
chatSearchStore.runNewSearch();
|
||||
}, debounceDelay);
|
||||
}
|
||||
|
||||
watch(query, (value) => {
|
||||
if (!value || !value.trim()) {
|
||||
if (searchTimeout.value) {
|
||||
clearTimeout(searchTimeout.value);
|
||||
}
|
||||
chatSearchStore.search();
|
||||
return;
|
||||
}
|
||||
scheduleSearch();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (searchTimeout.value) {
|
||||
clearTimeout(searchTimeout.value);
|
||||
}
|
||||
});
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString();
|
||||
}
|
||||
|
||||
function getSnippetParts(item: ChatSearchResult) {
|
||||
const localIndex = Math.max(0, item.match_index - item.snippet_start);
|
||||
return {
|
||||
before: item.snippet.slice(0, localIndex),
|
||||
match: item.snippet.slice(localIndex, localIndex + item.match_length),
|
||||
after: item.snippet.slice(localIndex + item.match_length)
|
||||
};
|
||||
}
|
||||
|
||||
function getMatchFieldLabel(item: ChatSearchResult) {
|
||||
return item.match_field === 'title' ? tm('search.matchTitle') : tm('search.matchContent');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-search-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-search-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.chat-search-header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-search-header-emoji {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.chat-search-header-title {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-search-header-description {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.chat-search-input {
|
||||
width: 100%;
|
||||
max-width: 730px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-search-results {
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.chat-search-result-item {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 12px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-search-result-item:hover {
|
||||
background-color: rgba(103, 58, 183, 0.05);
|
||||
}
|
||||
|
||||
.chat-search-snippet {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chat-search-highlight {
|
||||
background-color: rgba(255, 204, 102, 0.45);
|
||||
padding: 0 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-search-meta {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-search-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.chat-search-empty p {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chat-search-pagination {
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 4px 0;
|
||||
}
|
||||
|
||||
.chat-search-page-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.chat-search-page-label {
|
||||
font-size: 12px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-search-container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.chat-search-input {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-search-pagination {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -20,15 +20,6 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div style="padding: 8px; padding-bottom: 0px; opacity: 0.6;">
|
||||
<v-btn block variant="text" class="search-chat-btn" @click="$emit('openSearch')"
|
||||
v-if="!sidebarCollapsed || isMobile" prepend-icon="mdi-magnify">
|
||||
{{ t('core.actions.search') }}
|
||||
</v-btn>
|
||||
<v-btn icon="mdi-magnify" rounded="xl" @click="$emit('openSearch')"
|
||||
v-if="sidebarCollapsed && !isMobile" elevation="0"></v-btn>
|
||||
</div>
|
||||
|
||||
<div style="padding: 8px; opacity: 0.6;">
|
||||
<v-btn block variant="text" class="new-chat-btn" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
|
||||
v-if="!sidebarCollapsed || isMobile" prepend-icon="mdi-square-edit-outline">{{ tm('actions.newChat') }}</v-btn>
|
||||
@@ -187,7 +178,6 @@ const emit = defineEmits<{
|
||||
createProject: [];
|
||||
editProject: [project: Project];
|
||||
deleteProject: [projectId: string];
|
||||
openSearch: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -274,13 +264,6 @@ function handleDeleteConversation(session: Session) {
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.search-chat-btn {
|
||||
justify-content: flex-start;
|
||||
background-color: transparent !important;
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
/* margin-bottom: 4px; */
|
||||
border-radius: 20px !important;
|
||||
@@ -376,3 +359,4 @@ function handleDeleteConversation(session: Session) {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
|
||||
{{ tm('providerSelector.notSelected') }}
|
||||
</span>
|
||||
<span v-else class="provider-name-text">
|
||||
<span v-else>
|
||||
{{ modelValue }}
|
||||
</span>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
@@ -228,14 +228,6 @@ function closeProviderDrawer() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.provider-name-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: calc(100% - 80px);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -98,18 +98,6 @@
|
||||
"noSessions": "No conversations in this project",
|
||||
"confirmDelete": "Are you sure you want to delete project \"{title}\"? Conversations in this project will not be deleted."
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
"placeholder": "Enter keywords to search titles or content",
|
||||
"hint": "Enter keywords to start searching",
|
||||
"noResults": "No matching conversations found",
|
||||
"matchTitle": "Title match",
|
||||
"matchContent": "Content match",
|
||||
"matchPosition": "Match position",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Updated",
|
||||
"pageSize": "Items per page"
|
||||
},
|
||||
"time": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday"
|
||||
@@ -145,4 +133,4 @@
|
||||
"sendMessageFailed": "Failed to send message, please try again",
|
||||
"createSessionFailed": "Failed to create session, please refresh the page"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,8 +447,7 @@
|
||||
"description": "Segment Only LLM Results"
|
||||
},
|
||||
"interval_method": {
|
||||
"description": "Interval Method",
|
||||
"hint": "random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。"
|
||||
"description": "Interval Method"
|
||||
},
|
||||
"interval": {
|
||||
"description": "Random Interval Time",
|
||||
@@ -456,15 +455,13 @@
|
||||
},
|
||||
"log_base": {
|
||||
"description": "Logarithm Base",
|
||||
"hint": "Base for logarithmic intervals, defaults to 2.6. Value range: 1.0-10.0."
|
||||
"hint": "Base for logarithmic intervals, defaults to 2.0. Value range: 1.0-10.0."
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"description": "Segmented Reply Word Count Threshold",
|
||||
"hint": "Segmented reply word count threshold. Only messages with less than this number of words will be segmented, and messages with more than this number of words will be sent directly (not segmented)."
|
||||
"description": "Segmented Reply Word Count Threshold"
|
||||
},
|
||||
"split_mode": {
|
||||
"description": "Split Mode",
|
||||
"hint": "Used to segment a message. By default, it will be separated by punctuation marks like period, question mark, etc. For example, filling `[。?!]` will remove all periods, question marks, and exclamation marks. re.findall(r'<regex>', text)",
|
||||
"labels": [
|
||||
"Regex",
|
||||
"Words List"
|
||||
|
||||
@@ -151,11 +151,6 @@
|
||||
"title": "No New Version Detected",
|
||||
"message": "No new version detected for this plugin. Do you want to force reinstall? This will pull the latest code from the remote repository.",
|
||||
"confirm": "Force Update"
|
||||
},
|
||||
"updateAllConfirm": {
|
||||
"title": "Confirm Update All Plugins",
|
||||
"message": "Are you sure you want to update all {count} plugins? This operation may take some time.",
|
||||
"confirm": "Confirm Update"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -222,4 +217,4 @@
|
||||
"pluginChangelog": {
|
||||
"menuTitle": "View Changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,18 +100,6 @@
|
||||
"noSessions": "该项目暂无对话",
|
||||
"confirmDelete": "确定要删除项目 \"{title}\" 吗?项目中的对话不会被删除。"
|
||||
},
|
||||
"search": {
|
||||
"title": "搜索",
|
||||
"placeholder": "输入关键词搜索标题或内容",
|
||||
"hint": "输入关键词开始搜索",
|
||||
"noResults": "没有找到匹配的对话",
|
||||
"matchTitle": "标题匹配",
|
||||
"matchContent": "内容匹配",
|
||||
"matchPosition": "匹配位置",
|
||||
"createdAt": "创建",
|
||||
"updatedAt": "更新",
|
||||
"pageSize": "每页条数"
|
||||
},
|
||||
"time": {
|
||||
"today": "今天",
|
||||
"yesterday": "昨天"
|
||||
@@ -147,4 +135,4 @@
|
||||
"sendMessageFailed": "发送消息失败,请重试",
|
||||
"createSessionFailed": "创建会话失败,请刷新页面重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,8 +445,7 @@
|
||||
"description": "仅对 LLM 结果分段"
|
||||
},
|
||||
"interval_method": {
|
||||
"description": "间隔方法",
|
||||
"hint": "random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。"
|
||||
"description": "间隔方法"
|
||||
},
|
||||
"interval": {
|
||||
"description": "随机间隔时间",
|
||||
@@ -454,15 +453,13 @@
|
||||
},
|
||||
"log_base": {
|
||||
"description": "对数底数",
|
||||
"hint": "对数间隔的底数,默认为 2.6。取值范围为 1.0-10.0。"
|
||||
"hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。"
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"description": "分段回复字数阈值",
|
||||
"hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段),默认为 150。"
|
||||
"description": "分段回复字数阈值"
|
||||
},
|
||||
"split_mode": {
|
||||
"description": "分段模式",
|
||||
"hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.findall(r'<regex>', text)",
|
||||
"labels": [
|
||||
"正则表达式",
|
||||
"分段词列表"
|
||||
|
||||
@@ -151,11 +151,6 @@
|
||||
"title": "未检测到新版本",
|
||||
"message": "当前插件未检测到新版本,是否强制重新安装?这将从远程仓库拉取最新代码。",
|
||||
"confirm": "强制更新"
|
||||
},
|
||||
"updateAllConfirm": {
|
||||
"title": "确认更新全部插件",
|
||||
"message": "确定要更新全部 {count} 个插件吗?此操作可能需要一些时间。",
|
||||
"confirm": "确认更新"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -222,4 +217,4 @@
|
||||
"pluginChangelog": {
|
||||
"menuTitle": "查看更新日志"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
export interface ChatSearchResult {
|
||||
session_id: string;
|
||||
title: string | null;
|
||||
match_field: 'title' | 'content';
|
||||
match_index: number;
|
||||
match_length: number;
|
||||
snippet: string;
|
||||
snippet_start: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ChatSearchPagination {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
const defaultPagination: ChatSearchPagination = {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0,
|
||||
total_pages: 1
|
||||
};
|
||||
|
||||
export const useChatSearchStore = defineStore('chatSearch', () => {
|
||||
const active = ref(false);
|
||||
const query = ref('');
|
||||
const results = ref<ChatSearchResult[]>([]);
|
||||
const pagination = ref<ChatSearchPagination>({ ...defaultPagination });
|
||||
const isLoading = ref(false);
|
||||
const searchPerformed = ref(false);
|
||||
const contextLength = ref(40);
|
||||
|
||||
function openSearch() {
|
||||
active.value = true;
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
active.value = false;
|
||||
}
|
||||
|
||||
async function search() {
|
||||
const trimmedQuery = query.value.trim();
|
||||
if (!trimmedQuery) {
|
||||
results.value = [];
|
||||
pagination.value = { ...defaultPagination };
|
||||
searchPerformed.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
searchPerformed.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/chat/search', {
|
||||
params: {
|
||||
query: trimmedQuery,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.page_size,
|
||||
context: contextLength.value
|
||||
}
|
||||
});
|
||||
|
||||
const data = response.data?.data || {};
|
||||
results.value = data.results || [];
|
||||
pagination.value = data.pagination || { ...defaultPagination };
|
||||
} catch (error) {
|
||||
console.error('Search sessions failed:', error);
|
||||
results.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setPage(page: number) {
|
||||
pagination.value.page = page;
|
||||
await search();
|
||||
}
|
||||
|
||||
async function setPageSize(pageSize: number) {
|
||||
pagination.value.page_size = pageSize;
|
||||
pagination.value.page = 1;
|
||||
await search();
|
||||
}
|
||||
|
||||
async function runNewSearch() {
|
||||
pagination.value.page = 1;
|
||||
await search();
|
||||
}
|
||||
|
||||
return {
|
||||
active,
|
||||
query,
|
||||
results,
|
||||
pagination,
|
||||
isLoading,
|
||||
searchPerformed,
|
||||
contextLength,
|
||||
openSearch,
|
||||
closeSearch,
|
||||
search,
|
||||
setPage,
|
||||
setPageSize,
|
||||
runNewSearch
|
||||
};
|
||||
});
|
||||
@@ -92,11 +92,6 @@ const forceUpdateDialog = reactive({
|
||||
extensionName: "",
|
||||
});
|
||||
|
||||
// 更新全部插件确认对话框
|
||||
const updateAllConfirmDialog = reactive({
|
||||
show: false,
|
||||
});
|
||||
|
||||
// 插件更新日志对话框(复用 ReadmeDialog)
|
||||
const changelogDialog = reactive({
|
||||
show: false,
|
||||
@@ -476,23 +471,6 @@ const updateExtension = async (extension_name, forceUpdate = false) => {
|
||||
};
|
||||
|
||||
// 确认强制更新
|
||||
// 显示更新全部插件确认对话框
|
||||
const showUpdateAllConfirm = () => {
|
||||
if (updatableExtensions.value.length === 0) return;
|
||||
updateAllConfirmDialog.show = true;
|
||||
};
|
||||
|
||||
// 确认更新全部插件
|
||||
const confirmUpdateAll = () => {
|
||||
updateAllConfirmDialog.show = false;
|
||||
updateAllExtensions();
|
||||
};
|
||||
|
||||
// 取消更新全部插件
|
||||
const cancelUpdateAll = () => {
|
||||
updateAllConfirmDialog.show = false;
|
||||
};
|
||||
|
||||
const confirmForceUpdate = () => {
|
||||
const name = forceUpdateDialog.extensionName;
|
||||
forceUpdateDialog.show = false;
|
||||
@@ -1150,7 +1128,7 @@ watch(isListView, (newVal) => {
|
||||
variant="tonal"
|
||||
:disabled="updatableExtensions.length === 0"
|
||||
:loading="updatingAll"
|
||||
@click="showUpdateAllConfirm"
|
||||
@click="updateAllExtensions"
|
||||
>
|
||||
<v-icon>mdi-update</v-icon>
|
||||
{{ tm("buttons.updateAll") }}
|
||||
@@ -2301,34 +2279,6 @@ watch(isListView, (newVal) => {
|
||||
@confirm="handleUninstallConfirm"
|
||||
/>
|
||||
|
||||
<!-- 更新全部插件确认对话框 -->
|
||||
<v-dialog v-model="updateAllConfirmDialog.show" max-width="420">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<v-icon color="warning" class="mr-2">mdi-update</v-icon>
|
||||
{{ tm("dialogs.updateAllConfirm.title") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-1">
|
||||
{{ tm("dialogs.updateAllConfirm.message", { count: updatableExtensions.length }) }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelUpdateAll"
|
||||
>{{ tm("buttons.cancel") }}</v-btn>
|
||||
<v-btn
|
||||
color="warning"
|
||||
variant="flat"
|
||||
@click="confirmUpdateAll"
|
||||
>{{ tm("dialogs.updateAllConfirm.confirm") }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
<!-- 指令冲突提示对话框 -->
|
||||
<v-dialog v-model="conflictDialog.show" max-width="420">
|
||||
<v-card class="rounded-lg">
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.12.4"
|
||||
version = "4.12.3"
|
||||
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