Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8132ce24eb | |||
| 38e99cf65c | |||
| 2cfe4288b2 | |||
| 4924739423 | |||
| ec9f7403d5 | |||
| 0de7ae8481 | |||
| 42e84afd89 | |||
| a7ed6b8c76 | |||
| ee43b98ce6 | |||
| 681b4747a6 | |||
| a6da4ebe5e | |||
| e35a604b30 | |||
| 19651d24bb | |||
| dba08edd0d | |||
| dc06bc943a |
@@ -114,6 +114,9 @@ DEFAULT_CONFIG = {
|
||||
"provider": "moonshotai",
|
||||
"moonshotai_api_key": "",
|
||||
},
|
||||
"proactive_capability": {
|
||||
"add_cron_tools": True,
|
||||
},
|
||||
"sandbox": {
|
||||
"enable": False,
|
||||
"booter": "shipyard",
|
||||
@@ -2232,6 +2235,14 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"proactive_capability": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"add_cron_tools": {
|
||||
"type": "bool",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
@@ -2684,6 +2695,7 @@ CONFIG_METADATA_3 = {
|
||||
"skills": {
|
||||
"description": "Skills",
|
||||
"type": "object",
|
||||
"hint": "",
|
||||
"items": {
|
||||
"provider_settings.skills.runtime": {
|
||||
"description": "Skill Runtime",
|
||||
@@ -2698,7 +2710,24 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"proactive_capability": {
|
||||
"description": "主动型 Agent",
|
||||
"hint": "https://docs.astrbot.app/use/proactive-agent.html",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.proactive_capability.add_cron_tools": {
|
||||
"description": "启用",
|
||||
"type": "bool",
|
||||
"hint": "启用后,将会传递给 Agent 相关工具来实现主动型 Agent。你可以告诉 AstrBot 未来某个时间要做的事情,它将被定时触发然后执行任务。",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"hint": "",
|
||||
"description": "上下文管理策略",
|
||||
"type": "object",
|
||||
"items": {
|
||||
|
||||
@@ -94,6 +94,10 @@ class InternalAgentSubStage(Stage):
|
||||
|
||||
self.sandbox_cfg = settings.get("sandbox", {})
|
||||
|
||||
# Proactive capability configuration
|
||||
proactive_cfg = settings.get("proactive_capability", {})
|
||||
self.add_cron_tools = proactive_cfg.get("add_cron_tools", True)
|
||||
|
||||
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
self.main_agent_cfg = MainAgentBuildConfig(
|
||||
@@ -113,6 +117,7 @@ class InternalAgentSubStage(Stage):
|
||||
llm_safety_mode=self.llm_safety_mode,
|
||||
safety_mode_strategy=self.safety_mode_strategy,
|
||||
sandbox_cfg=self.sandbox_cfg,
|
||||
add_cron_tools=self.add_cron_tools,
|
||||
provider_settings=settings,
|
||||
subagent_orchestrator=conf.get("subagent_orchestrator", {}),
|
||||
timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
@@ -212,15 +213,93 @@ class FunctionToolManager:
|
||||
open(mcp_json_file, encoding="utf-8"),
|
||||
)["mcpServers"]
|
||||
|
||||
for name in mcp_server_json_obj:
|
||||
cfg = mcp_server_json_obj[name]
|
||||
tasks: dict[str, asyncio.Task] = {}
|
||||
ready_futures: dict[str, asyncio.Future] = {}
|
||||
|
||||
for name, cfg in mcp_server_json_obj.items():
|
||||
if cfg.get("active", True):
|
||||
event = asyncio.Event()
|
||||
asyncio.create_task(
|
||||
self._init_mcp_client_task_wrapper(name, cfg, event),
|
||||
ready_future = asyncio.get_running_loop().create_future()
|
||||
task = asyncio.create_task(
|
||||
self._init_mcp_client_task_wrapper(
|
||||
name,
|
||||
cfg,
|
||||
event,
|
||||
ready_future,
|
||||
),
|
||||
)
|
||||
tasks[name] = task
|
||||
ready_futures[name] = ready_future
|
||||
self.mcp_client_event[name] = event
|
||||
|
||||
if ready_futures:
|
||||
logger.info(f"等待 {len(ready_futures)} 个 MCP 服务初始化...")
|
||||
|
||||
_, pending_futures = await asyncio.wait(
|
||||
ready_futures.values(),
|
||||
timeout=20.0,
|
||||
)
|
||||
|
||||
pending_services = {
|
||||
name
|
||||
for name, ready_future in ready_futures.items()
|
||||
if ready_future in pending_futures
|
||||
}
|
||||
|
||||
if pending_services:
|
||||
logger.warning(
|
||||
"MCP 服务初始化超时(20秒),部分服务可能未完全加载。"
|
||||
"建议检查 MCP 服务器配置和网络连接。"
|
||||
)
|
||||
for name in pending_services:
|
||||
task = tasks[name]
|
||||
task.cancel()
|
||||
await asyncio.gather(
|
||||
*(tasks[name] for name in pending_services),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
success_count = 0
|
||||
failed_services: list[str] = []
|
||||
|
||||
for name, ready_future in ready_futures.items():
|
||||
if name in pending_services:
|
||||
logger.error(f"MCP 服务 {name} 初始化超时")
|
||||
failed_services.append(name)
|
||||
self.mcp_client_event.pop(name, None)
|
||||
continue
|
||||
|
||||
if ready_future.cancelled():
|
||||
logger.error(f"MCP 服务 {name} 初始化已取消")
|
||||
failed_services.append(name)
|
||||
self.mcp_client_event.pop(name, None)
|
||||
continue
|
||||
|
||||
exc = ready_future.exception()
|
||||
if exc is not None:
|
||||
logger.error(f"MCP 服务 {name} 初始化失败: {exc}")
|
||||
# 仅在 debug 级别输出完整配置,避免在生产日志中泄露敏感信息
|
||||
cfg = mcp_server_json_obj.get(name, {})
|
||||
if "command" in cfg:
|
||||
logger.debug(f" 命令: {cfg['command']}")
|
||||
if "args" in cfg:
|
||||
logger.debug(f" 参数: {cfg['args']}")
|
||||
elif "url" in cfg:
|
||||
parsed = urllib.parse.urlparse(cfg["url"])
|
||||
logger.debug(f" 主机: {parsed.scheme}://{parsed.netloc}")
|
||||
failed_services.append(name)
|
||||
self.mcp_client_event.pop(name, None)
|
||||
else:
|
||||
success_count += 1
|
||||
|
||||
if failed_services:
|
||||
logger.warning(
|
||||
f"以下 MCP 服务初始化失败: {', '.join(failed_services)}。"
|
||||
f"请检查配置文件 mcp_server.json 和服务器可用性。"
|
||||
)
|
||||
|
||||
logger.info(f"MCP 服务初始化完成: {success_count}/{len(tasks)} 成功")
|
||||
|
||||
async def _init_mcp_client_task_wrapper(
|
||||
self,
|
||||
name: str,
|
||||
@@ -229,20 +308,29 @@ class FunctionToolManager:
|
||||
ready_future: asyncio.Future | None = None,
|
||||
) -> None:
|
||||
"""初始化 MCP 客户端的包装函数,用于捕获异常"""
|
||||
initialized = False
|
||||
try:
|
||||
await self._init_mcp_client(name, cfg)
|
||||
tools = await self.mcp_client_dict[name].list_tools_and_save()
|
||||
initialized = True
|
||||
if ready_future and not ready_future.done():
|
||||
# tell the caller we are ready
|
||||
ready_future.set_result(tools)
|
||||
ready_future.set_result(True)
|
||||
await event.wait()
|
||||
logger.info(f"收到 MCP 客户端 {name} 终止信号")
|
||||
except asyncio.CancelledError:
|
||||
if ready_future and not ready_future.done():
|
||||
ready_future.set_exception(
|
||||
asyncio.TimeoutError("MCP 客户端初始化超时"),
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"初始化 MCP 客户端 {name} 失败", exc_info=True)
|
||||
if ready_future and not ready_future.done():
|
||||
ready_future.set_exception(e)
|
||||
if not initialized:
|
||||
# 初始化阶段失败,记录错误并向上抛出让 task.exception() 捕获
|
||||
logger.error(f"初始化 MCP 客户端 {name} 失败", exc_info=True)
|
||||
raise
|
||||
# 初始化已成功,此处异常来自 event.wait() 被取消,属于正常终止流程
|
||||
finally:
|
||||
# 无论如何都能清理
|
||||
await self._terminate_mcp_client(name)
|
||||
|
||||
async def _init_mcp_client(self, name: str, config: dict) -> None:
|
||||
@@ -340,22 +428,22 @@ class FunctionToolManager:
|
||||
if not event:
|
||||
event = asyncio.Event()
|
||||
if not ready_future:
|
||||
ready_future = asyncio.Future()
|
||||
ready_future = asyncio.get_running_loop().create_future()
|
||||
if name in self.mcp_client_dict:
|
||||
return
|
||||
asyncio.create_task(
|
||||
init_task = asyncio.create_task(
|
||||
self._init_mcp_client_task_wrapper(name, config, event, ready_future),
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(ready_future, timeout=timeout)
|
||||
finally:
|
||||
except asyncio.TimeoutError:
|
||||
init_task.cancel()
|
||||
await asyncio.gather(init_task, return_exceptions=True)
|
||||
self.mcp_client_event.pop(name, None)
|
||||
raise
|
||||
else:
|
||||
self.mcp_client_event[name] = event
|
||||
|
||||
if ready_future.done() and ready_future.exception():
|
||||
exc = ready_future.exception()
|
||||
if exc is not None:
|
||||
raise exc
|
||||
|
||||
async def disable_mcp_server(
|
||||
self,
|
||||
name: str | None = None,
|
||||
|
||||
@@ -274,8 +274,8 @@ class ProviderManager:
|
||||
if not self.curr_tts_provider_inst and self.tts_provider_insts:
|
||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||
|
||||
# 初始化 MCP Client 连接
|
||||
asyncio.create_task(self.llm_tools.init_mcp_clients(), name="init_mcp_clients")
|
||||
# 初始化 MCP Client 连接(等待完成以确保工具可用)
|
||||
await self.llm_tools.init_mcp_clients()
|
||||
|
||||
def dynamic_import_provider(self, type: str):
|
||||
"""动态导入提供商适配器模块
|
||||
|
||||
@@ -4,7 +4,6 @@ import traceback
|
||||
from quart import request
|
||||
|
||||
from astrbot.core import DEMO_MODE, logger
|
||||
from astrbot.core.computer.computer_client import get_booter
|
||||
from astrbot.core.skills.skill_manager import SkillManager
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
@@ -60,41 +59,9 @@ class SkillsRoute(Route):
|
||||
temp_path = os.path.join(temp_dir, filename)
|
||||
await file.save(temp_path)
|
||||
|
||||
cfg = self.core_lifecycle.astrbot_config.get("provider_settings", {}).get(
|
||||
"skills", {}
|
||||
)
|
||||
runtime = cfg.get("runtime", "local")
|
||||
if runtime == "sandbox":
|
||||
sandbox_enabled = (
|
||||
self.core_lifecycle.astrbot_config.get("provider_settings", {})
|
||||
.get("sandbox", {})
|
||||
.get("enable", False)
|
||||
)
|
||||
if not sandbox_enabled:
|
||||
return (
|
||||
Response()
|
||||
.error(
|
||||
"Sandbox is not enabled. Please enable sandbox before using sandbox runtime."
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
skill_mgr = SkillManager()
|
||||
skill_name = skill_mgr.install_skill_from_zip(temp_path, overwrite=True)
|
||||
|
||||
if runtime == "sandbox":
|
||||
sb = await get_booter(self.core_lifecycle.star_context, "skills-upload")
|
||||
remote_root = "/home/shared/skills"
|
||||
remote_zip = f"{remote_root}/{skill_name}.zip"
|
||||
await sb.shell.exec(f"mkdir -p {remote_root}")
|
||||
upload_result = await sb.upload_file(temp_path, remote_zip)
|
||||
if not upload_result.get("success", False):
|
||||
return (
|
||||
Response().error("Failed to upload skill to sandbox").__dict__
|
||||
)
|
||||
await sb.shell.exec(
|
||||
f"unzip -o {remote_zip} -d {remote_root} && rm -f {remote_zip}"
|
||||
)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok({"name": skill_name}, "Skill uploaded successfully.")
|
||||
|
||||
@@ -130,19 +130,25 @@ class ToolsRoute(Route):
|
||||
server_data = await request.json
|
||||
|
||||
name = server_data.get("name", "")
|
||||
old_name = server_data.get("oldName") or name
|
||||
|
||||
if not name:
|
||||
return Response().error("服务器名称不能为空").__dict__
|
||||
|
||||
config = self.tool_mgr.load_mcp_config()
|
||||
|
||||
if name not in config["mcpServers"]:
|
||||
return Response().error(f"服务器 {name} 不存在").__dict__
|
||||
if old_name not in config["mcpServers"]:
|
||||
return Response().error(f"服务器 {old_name} 不存在").__dict__
|
||||
|
||||
is_rename = name != old_name
|
||||
|
||||
if name in config["mcpServers"] and is_rename:
|
||||
return Response().error(f"服务器 {name} 已存在").__dict__
|
||||
|
||||
# 获取活动状态
|
||||
active = server_data.get(
|
||||
"active",
|
||||
config["mcpServers"][name].get("active", True),
|
||||
config["mcpServers"][old_name].get("active", True),
|
||||
)
|
||||
|
||||
# 创建新的配置对象
|
||||
@@ -153,7 +159,13 @@ class ToolsRoute(Route):
|
||||
|
||||
# 复制所有配置字段
|
||||
for key, value in server_data.items():
|
||||
if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段
|
||||
if key not in [
|
||||
"name",
|
||||
"active",
|
||||
"tools",
|
||||
"errlogs",
|
||||
"oldName",
|
||||
]: # 排除特殊字段
|
||||
if key == "mcpServers":
|
||||
key_0 = list(server_data["mcpServers"].keys())[
|
||||
0
|
||||
@@ -165,29 +177,42 @@ class ToolsRoute(Route):
|
||||
|
||||
# 如果只更新活动状态,保留原始配置
|
||||
if only_update_active:
|
||||
for key, value in config["mcpServers"][name].items():
|
||||
for key, value in config["mcpServers"][old_name].items():
|
||||
if key != "active": # 除了active之外的所有字段都保留
|
||||
server_config[key] = value
|
||||
|
||||
config["mcpServers"][name] = server_config
|
||||
# config["mcpServers"][name] = server_config
|
||||
if is_rename:
|
||||
config["mcpServers"].pop(old_name)
|
||||
config["mcpServers"][name] = server_config
|
||||
else:
|
||||
config["mcpServers"][name] = server_config
|
||||
|
||||
if self.tool_mgr.save_mcp_config(config):
|
||||
# 处理MCP客户端状态变化
|
||||
if active:
|
||||
if name in self.tool_mgr.mcp_client_dict or not only_update_active:
|
||||
if (
|
||||
old_name in self.tool_mgr.mcp_client_dict
|
||||
or not only_update_active
|
||||
or is_rename
|
||||
):
|
||||
try:
|
||||
await self.tool_mgr.disable_mcp_server(name, timeout=10)
|
||||
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
|
||||
except TimeoutError as e:
|
||||
return (
|
||||
Response()
|
||||
.error(f"启用前停用 MCP 服务器时 {name} 超时: {e!s}")
|
||||
.error(
|
||||
f"启用前停用 MCP 服务器时 {old_name} 超时: {e!s}"
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return (
|
||||
Response()
|
||||
.error(f"启用前停用 MCP 服务器时 {name} 失败: {e!s}")
|
||||
.error(
|
||||
f"启用前停用 MCP 服务器时 {old_name} 失败: {e!s}"
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
try:
|
||||
@@ -208,18 +233,20 @@ class ToolsRoute(Route):
|
||||
.__dict__
|
||||
)
|
||||
# 如果要停用服务器
|
||||
elif name in self.tool_mgr.mcp_client_dict:
|
||||
elif old_name in self.tool_mgr.mcp_client_dict:
|
||||
try:
|
||||
await self.tool_mgr.disable_mcp_server(name, timeout=10)
|
||||
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
|
||||
except TimeoutError:
|
||||
return (
|
||||
Response().error(f"停用 MCP 服务器 {name} 超时。").__dict__
|
||||
Response()
|
||||
.error(f"停用 MCP 服务器 {old_name} 超时。")
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return (
|
||||
Response()
|
||||
.error(f"停用 MCP 服务器 {name} 失败: {e!s}")
|
||||
.error(f"停用 MCP 服务器 {old_name} 失败: {e!s}")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
<!-- Reasoning Block (Collapsible) - 放在最前面 -->
|
||||
<ReasoningBlock v-if="msg.content.reasoning && msg.content.reasoning.trim()"
|
||||
:reasoning="msg.content.reasoning" :is-dark="isDark"
|
||||
class="mt-2"
|
||||
:initial-expanded="isReasoningExpanded(index)" />
|
||||
|
||||
<MessagePartsRenderer :parts="msg.content.message" :is-dark="isDark"
|
||||
@@ -1203,37 +1204,6 @@ export default {
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.embedded-images {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.embedded-image {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bot-embedded-image {
|
||||
max-width: 55%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.embedded-audio {
|
||||
width: 300px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.embedded-audio .audio-player {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* 文件附件样式 */
|
||||
.file-attachments,
|
||||
.embedded-files {
|
||||
|
||||
@@ -331,4 +331,86 @@ const getRenderParts = (messageParts) => {
|
||||
.tool-call-chevron.rotated {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
|
||||
.embedded-images {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.embedded-image {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bot-embedded-image {
|
||||
max-width: 55%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.embedded-audio {
|
||||
width: 300px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.embedded-audio .audio-player {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* 文件附件样式 */
|
||||
.file-attachments,
|
||||
.embedded-files {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.file-attachment,
|
||||
.embedded-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
/* 文件附件样式 */
|
||||
.file-attachments,
|
||||
.embedded-files {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.file-attachment,
|
||||
.embedded-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.file-link-download {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<template>
|
||||
<div class="mb-3 mt-1.5 border border-gray-200 dark:border-gray-700 rounded-2xl overflow-hidden w-fit"
|
||||
:class="{ 'dark:bg-purple-900/8': isDark, 'bg-purple-50/50': !isDark }">
|
||||
<div class="inline-flex items-center px-2 py-2 cursor-pointer select-none rounded-2xl transition-colors hover:bg-purple-50/80 dark:hover:bg-purple-900/15"
|
||||
@click="toggleExpanded">
|
||||
<v-icon size="small" class="mr-1.5 text-purple-600 dark:text-purple-400 transition-transform"
|
||||
:class="{ 'rotate-90': isExpanded }">
|
||||
<div class="reasoning-block" :class="{ 'reasoning-block--dark': isDark }">
|
||||
<div class="reasoning-header" @click="toggleExpanded">
|
||||
<v-icon size="small" class="reasoning-icon" :class="{ 'rotate-90': isExpanded }">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<span class="text-sm font-medium text-purple-600 dark:text-purple-400 tracking-wide">
|
||||
<span class="reasoning-title">
|
||||
{{ tm('reasoning.thinking') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isExpanded" class="px-3 border-t border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 animate-fade-in italic">
|
||||
<MarkdownRender :content="reasoning" class="reasoning-text markdown-content text-sm leading-relaxed"
|
||||
<div v-if="isExpanded" class="reasoning-content animate-fade-in">
|
||||
<MarkdownRender :content="reasoning" class="reasoning-text markdown-content"
|
||||
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,6 +44,63 @@ const toggleExpanded = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
/* Reasoning 区块样式 */
|
||||
.reasoning-container {
|
||||
margin-bottom: 12px;
|
||||
margin-top: 6px;
|
||||
border: 1px solid var(--v-theme-border);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.reasoning-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.reasoning-header:hover {
|
||||
background-color: rgba(103, 58, 183, 0.08);
|
||||
}
|
||||
|
||||
.reasoning-header.is-dark:hover {
|
||||
background-color: rgba(103, 58, 183, 0.15);
|
||||
}
|
||||
|
||||
.reasoning-icon {
|
||||
margin-right: 6px;
|
||||
color: var(--v-theme-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.reasoning-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--v-theme-secondary);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.reasoning-content {
|
||||
padding: 0px 12px;
|
||||
border-top: 1px solid var(--v-theme-border);
|
||||
color: gray;
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.reasoning-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
@@ -65,9 +119,4 @@ const toggleExpanded = () => {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.reasoning-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,10 +81,10 @@
|
||||
</v-container>
|
||||
|
||||
<!-- 添加/编辑 MCP 服务器对话框 -->
|
||||
<v-dialog v-model="showMcpServerDialog" max-width="750px" persistent>
|
||||
<v-dialog v-model="showMcpServerDialog" max-width="750px">
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3">
|
||||
<v-icon color="white" class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
||||
<v-card-title class="pa-4 pl-6">
|
||||
<v-icon class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
||||
<span>{{ isEditMode ? tm('dialogs.addServer.editTitle') : tm('dialogs.addServer.title') }}</span>
|
||||
</v-card-title>
|
||||
|
||||
@@ -251,6 +251,7 @@ export default {
|
||||
active: true,
|
||||
tools: []
|
||||
},
|
||||
originalServerName: '',
|
||||
save_message_snack: false,
|
||||
save_message: '',
|
||||
save_message_success: 'success'
|
||||
@@ -359,6 +360,9 @@ export default {
|
||||
active: this.currentServer.active,
|
||||
...configObj
|
||||
};
|
||||
if (this.isEditMode && this.originalServerName) {
|
||||
serverData.oldName = this.originalServerName;
|
||||
}
|
||||
const endpoint = this.isEditMode ? '/api/tools/mcp/update' : '/api/tools/mcp/add';
|
||||
axios.post(endpoint, serverData)
|
||||
.then(response => {
|
||||
@@ -402,6 +406,7 @@ export default {
|
||||
active: server.active,
|
||||
tools: server.tools || []
|
||||
};
|
||||
this.originalServerName = server.name;
|
||||
this.serverConfigJson = JSON.stringify(configCopy, null, 2);
|
||||
this.isEditMode = true;
|
||||
this.showMcpServerDialog = true;
|
||||
@@ -461,6 +466,7 @@ export default {
|
||||
this.serverConfigJson = '';
|
||||
this.jsonError = null;
|
||||
this.isEditMode = false;
|
||||
this.originalServerName = '';
|
||||
},
|
||||
showSuccess(message) {
|
||||
this.save_message = message;
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
|
||||
<v-dialog v-model="uploadDialog" max-width="520px" persistent>
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('skills.uploadDialogTitle') }}</v-card-title>
|
||||
<v-card-title class="text-h3 pa-4 pb-0 pl-6">{{ tm('skills.uploadDialogTitle') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<small class="text-grey">{{ tm('skills.uploadHint') }}</small>
|
||||
<v-file-input v-model="uploadFile" accept=".zip" :label="tm('skills.selectFile')" prepend-icon="mdi-file-zip"
|
||||
<v-file-input v-model="uploadFile" accept=".zip" :label="tm('skills.selectFile')" prepend-icon="mdi-folder-zip-outline"
|
||||
variant="outlined" class="mt-4" :multiple="false" />
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"hint": "https://docs.astrbot.app/use/skills.html",
|
||||
"description": "Skills",
|
||||
"provider_settings": {
|
||||
"skills": {
|
||||
@@ -175,7 +176,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"proactive_capability": {
|
||||
"description": "Proactive Agent",
|
||||
"hint": "https://docs.astrbot.app/en/use/proactive-agent.html",
|
||||
"provider_settings": {
|
||||
"proactive_capability": {
|
||||
"add_cron_tools": {
|
||||
"description": "Enable",
|
||||
"hint": "When enabled, related tools will be passed to the Agent to implement proactive Agent capabilities. You can tell AstrBot what to do at a future time, and it will be triggered on schedule to execute the task, and report the result back to you."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"hint": "https://docs.astrbot.app/en/use/context-compress.html",
|
||||
"description": "Context Management Strategy",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"hint": "https://docs.astrbot.app/en/use/skills.html",
|
||||
"description": "Skills",
|
||||
"provider_settings": {
|
||||
"skills": {
|
||||
@@ -175,7 +176,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"proactive_capability": {
|
||||
"description": "主动型 Agent",
|
||||
"hint": "https://docs.astrbot.app/use/proactive-agent.html",
|
||||
"provider_settings": {
|
||||
"proactive_capability": {
|
||||
"add_cron_tools": {
|
||||
"description": "启用",
|
||||
"hint": "启用后,将会传递给 Agent 相关工具来实现主动型 Agent。你可以告诉 AstrBot 未来某个时间要做的事情,它将被定时触发然后执行任务,然后将结果发送给你。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"hint": "https://docs.astrbot.app/use/context-compress.html",
|
||||
"description": "上下文管理策略",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
|
||||
@@ -28,17 +28,13 @@
|
||||
|
||||
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">{{ tm('table.empty') }}</v-alert>
|
||||
|
||||
<v-data-table
|
||||
:items="jobs"
|
||||
:headers="headers"
|
||||
:loading="loading"
|
||||
item-key="job_id"
|
||||
density="comfortable"
|
||||
class="elevation-0"
|
||||
>
|
||||
<v-data-table :items="jobs" :headers="headers" :loading="loading" item-key="job_id" density="comfortable"
|
||||
class="elevation-0">
|
||||
<template #item.name="{ item }">
|
||||
<div class="font-weight-medium">{{ item.name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
|
||||
<div class="py-4">
|
||||
<div class="font-weight-medium">{{ item.name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.type="{ item }">
|
||||
<v-chip size="small" :color="item.run_once ? 'orange' : 'primary'" variant="tonal">
|
||||
@@ -57,15 +53,10 @@
|
||||
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<v-switch
|
||||
v-model="item.enabled"
|
||||
inset
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
@change="toggleJob(item)"
|
||||
/>
|
||||
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete') }}</v-btn>
|
||||
<v-switch v-model="item.enabled" inset density="compact" hide-details color="primary"
|
||||
@change="toggleJob(item)" />
|
||||
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete')
|
||||
}}</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
@@ -83,39 +74,19 @@
|
||||
<v-switch v-model="newJob.run_once" :label="tm('form.runOnce')" inset color="primary" hide-details />
|
||||
<v-text-field v-model="newJob.name" :label="tm('form.name')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.note" :label="tm('form.note')" variant="outlined" density="comfortable" />
|
||||
<v-text-field
|
||||
v-if="!newJob.run_once"
|
||||
v-model="newJob.cron_expression"
|
||||
:label="tm('form.cron')"
|
||||
:placeholder="tm('form.cronPlaceholder')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="newJob.run_at"
|
||||
:label="tm('form.runAt')"
|
||||
type="datetime-local"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="newJob.session"
|
||||
:label="tm('form.session')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="newJob.timezone"
|
||||
:label="tm('form.timezone')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
<v-text-field v-if="!newJob.run_once" v-model="newJob.cron_expression" :label="tm('form.cron')"
|
||||
:placeholder="tm('form.cronPlaceholder')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-else v-model="newJob.run_at" :label="tm('form.runAt')" type="datetime-local"
|
||||
variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.session" :label="tm('form.session')" variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="newJob.timezone" :label="tm('form.timezone')" variant="outlined"
|
||||
density="comfortable" />
|
||||
<v-switch v-model="newJob.enabled" :label="tm('form.enabled')" inset color="primary" hide-details />
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn variant="text" @click="createDialog = false">{{ tm('actions.cancel') }}</v-btn>
|
||||
<v-btn variant="tonal" color="primary" :loading="creating" @click="createJob">{{ tm('actions.submit') }}</v-btn>
|
||||
<v-btn variant="tonal" color="primary" :loading="creating" @click="createJob">{{ tm('actions.submit')
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -2433,7 +2433,9 @@ watch(isListView, (newVal) => {
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
|
||||
<div class="v-card-title text-h5">{{ tm("dialogs.install.title") }}</div>
|
||||
<v-card-title class="text-h3 pa-4 pb-0 pl-6">
|
||||
{{ tm("dialogs.install.title") }}
|
||||
</v-card-title>
|
||||
|
||||
<div class="v-card-text">
|
||||
<v-tabs v-model="uploadTab" color="primary">
|
||||
|
||||
Reference in New Issue
Block a user