Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f8d194ab | |||
| 65cceb2f21 | |||
| c4c356887b | |||
| 42e84afd89 | |||
| a7ed6b8c76 | |||
| ee43b98ce6 | |||
| 681b4747a6 | |||
| a6da4ebe5e | |||
| e35a604b30 | |||
| 19651d24bb | |||
| dba08edd0d | |||
| dc06bc943a |
@@ -54,6 +54,14 @@ async def run_agent(
|
|||||||
return
|
return
|
||||||
if resp.type == "tool_call_result":
|
if resp.type == "tool_call_result":
|
||||||
msg_chain = resp.data["chain"]
|
msg_chain = resp.data["chain"]
|
||||||
|
|
||||||
|
astr_event.trace.record(
|
||||||
|
"agent_tool_result",
|
||||||
|
tool_result=msg_chain.get_plain_text(
|
||||||
|
with_other_comps_mark=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if msg_chain.type == "tool_direct_result":
|
if msg_chain.type == "tool_direct_result":
|
||||||
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
|
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
|
||||||
await astr_event.send(msg_chain)
|
await astr_event.send(msg_chain)
|
||||||
@@ -67,12 +75,22 @@ async def run_agent(
|
|||||||
# 用来标记流式响应需要分节
|
# 用来标记流式响应需要分节
|
||||||
yield MessageChain(chain=[], type="break")
|
yield MessageChain(chain=[], type="break")
|
||||||
|
|
||||||
|
tool_info = None
|
||||||
|
|
||||||
|
if resp.data["chain"].chain:
|
||||||
|
json_comp = resp.data["chain"].chain[0]
|
||||||
|
if isinstance(json_comp, Json):
|
||||||
|
tool_info = json_comp.data
|
||||||
|
astr_event.trace.record(
|
||||||
|
"agent_tool_call",
|
||||||
|
tool_name=tool_info if tool_info else "unknown",
|
||||||
|
)
|
||||||
|
|
||||||
if astr_event.get_platform_name() == "webchat":
|
if astr_event.get_platform_name() == "webchat":
|
||||||
await astr_event.send(resp.data["chain"])
|
await astr_event.send(resp.data["chain"])
|
||||||
elif show_tool_use:
|
elif show_tool_use:
|
||||||
json_comp = resp.data["chain"].chain[0]
|
if tool_info:
|
||||||
if isinstance(json_comp, Json):
|
m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
|
||||||
m = f"🔨 调用工具: {json_comp.data.get('name')}"
|
|
||||||
else:
|
else:
|
||||||
m = "🔨 调用工具..."
|
m = "🔨 调用工具..."
|
||||||
chain = MessageChain(type="tool_call").message(m)
|
chain = MessageChain(type="tool_call").message(m)
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ DEFAULT_CONFIG = {
|
|||||||
"provider": "moonshotai",
|
"provider": "moonshotai",
|
||||||
"moonshotai_api_key": "",
|
"moonshotai_api_key": "",
|
||||||
},
|
},
|
||||||
|
"proactive_capability": {
|
||||||
|
"add_cron_tools": True,
|
||||||
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"enable": False,
|
"enable": False,
|
||||||
"booter": "shipyard",
|
"booter": "shipyard",
|
||||||
@@ -199,6 +202,7 @@ DEFAULT_CONFIG = {
|
|||||||
"log_file_enable": False,
|
"log_file_enable": False,
|
||||||
"log_file_path": "logs/astrbot.log",
|
"log_file_path": "logs/astrbot.log",
|
||||||
"log_file_max_mb": 20,
|
"log_file_max_mb": 20,
|
||||||
|
"trace_enable": False,
|
||||||
"trace_log_enable": False,
|
"trace_log_enable": False,
|
||||||
"trace_log_path": "logs/astrbot.trace.log",
|
"trace_log_path": "logs/astrbot.trace.log",
|
||||||
"trace_log_max_mb": 20,
|
"trace_log_max_mb": 20,
|
||||||
@@ -2232,6 +2236,14 @@ CONFIG_METADATA_2 = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"proactive_capability": {
|
||||||
|
"type": "object",
|
||||||
|
"items": {
|
||||||
|
"add_cron_tools": {
|
||||||
|
"type": "bool",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"provider_stt_settings": {
|
"provider_stt_settings": {
|
||||||
@@ -2684,6 +2696,7 @@ CONFIG_METADATA_3 = {
|
|||||||
"skills": {
|
"skills": {
|
||||||
"description": "Skills",
|
"description": "Skills",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"hint": "",
|
||||||
"items": {
|
"items": {
|
||||||
"provider_settings.skills.runtime": {
|
"provider_settings.skills.runtime": {
|
||||||
"description": "Skill Runtime",
|
"description": "Skill Runtime",
|
||||||
@@ -2698,7 +2711,24 @@ CONFIG_METADATA_3 = {
|
|||||||
"provider_settings.enable": True,
|
"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": {
|
"truncate_and_compress": {
|
||||||
|
"hint": "",
|
||||||
"description": "上下文管理策略",
|
"description": "上下文管理策略",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"items": {
|
"items": {
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class EventBus:
|
|||||||
event (AstrMessageEvent): 事件对象
|
event (AstrMessageEvent): 事件对象
|
||||||
|
|
||||||
"""
|
"""
|
||||||
event.trace.record("event_dispatch", config_name=conf_name)
|
|
||||||
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
||||||
if event.get_sender_name():
|
if event.get_sender_name():
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from astrbot.core.message.components import (
|
|||||||
AtAll,
|
AtAll,
|
||||||
BaseMessageComponent,
|
BaseMessageComponent,
|
||||||
Image,
|
Image,
|
||||||
|
Json,
|
||||||
Plain,
|
Plain,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,9 +118,26 @@ class MessageChain:
|
|||||||
self.use_t2i_ = use_t2i
|
self.use_t2i_ = use_t2i
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_plain_text(self) -> str:
|
def get_plain_text(self, with_other_comps_mark: bool = False) -> str:
|
||||||
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。"""
|
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。
|
||||||
return " ".join([comp.text for comp in self.chain if isinstance(comp, Plain)])
|
|
||||||
|
Args:
|
||||||
|
with_other_comps_mark (bool): 是否在纯文本中标记其他组件的位置
|
||||||
|
"""
|
||||||
|
if not with_other_comps_mark:
|
||||||
|
return " ".join(
|
||||||
|
[comp.text for comp in self.chain if isinstance(comp, Plain)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
texts = []
|
||||||
|
for comp in self.chain:
|
||||||
|
if isinstance(comp, Plain):
|
||||||
|
texts.append(comp.text)
|
||||||
|
elif isinstance(comp, Json):
|
||||||
|
texts.append(f"{comp.data}")
|
||||||
|
else:
|
||||||
|
texts.append(f"[{comp.__class__.__name__}]")
|
||||||
|
return " ".join(texts)
|
||||||
|
|
||||||
def squash_plain(self):
|
def squash_plain(self):
|
||||||
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
|
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ class InternalAgentSubStage(Stage):
|
|||||||
|
|
||||||
self.sandbox_cfg = settings.get("sandbox", {})
|
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.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||||
|
|
||||||
self.main_agent_cfg = MainAgentBuildConfig(
|
self.main_agent_cfg = MainAgentBuildConfig(
|
||||||
@@ -113,6 +117,7 @@ class InternalAgentSubStage(Stage):
|
|||||||
llm_safety_mode=self.llm_safety_mode,
|
llm_safety_mode=self.llm_safety_mode,
|
||||||
safety_mode_strategy=self.safety_mode_strategy,
|
safety_mode_strategy=self.safety_mode_strategy,
|
||||||
sandbox_cfg=self.sandbox_cfg,
|
sandbox_cfg=self.sandbox_cfg,
|
||||||
|
add_cron_tools=self.add_cron_tools,
|
||||||
provider_settings=settings,
|
provider_settings=settings,
|
||||||
subagent_orchestrator=conf.get("subagent_orchestrator", {}),
|
subagent_orchestrator=conf.get("subagent_orchestrator", {}),
|
||||||
timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
|
timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
|
||||||
|
|||||||
@@ -85,6 +85,4 @@ class PipelineScheduler:
|
|||||||
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
||||||
await event.send(None)
|
await event.send(None)
|
||||||
|
|
||||||
event.trace.record("event_end")
|
|
||||||
|
|
||||||
logger.debug("pipeline 执行完毕。")
|
logger.debug("pipeline 执行完毕。")
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ class AstrMessageEvent(abc.ABC):
|
|||||||
self.span = self.trace
|
self.span = self.trace
|
||||||
"""事件级 TraceSpan(别名: span)"""
|
"""事件级 TraceSpan(别名: span)"""
|
||||||
|
|
||||||
self.trace.record("umo", umo=self.unified_msg_origin)
|
|
||||||
self.trace.record("event_created", created_at=self.created_at)
|
|
||||||
|
|
||||||
self._has_send_oper = False
|
self._has_send_oper = False
|
||||||
"""在此次事件中是否有过至少一次发送消息的操作"""
|
"""在此次事件中是否有过至少一次发送消息的操作"""
|
||||||
self.call_llm = False
|
self.call_llm = False
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class TraceSpan:
|
|||||||
self.started_at = time.time()
|
self.started_at = time.time()
|
||||||
|
|
||||||
def record(self, action: str, **fields: Any) -> None:
|
def record(self, action: str, **fields: Any) -> None:
|
||||||
|
# Check if trace recording is enabled
|
||||||
|
if not astrbot_config.get("trace_enable", True):
|
||||||
|
return
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"type": "trace",
|
"type": "trace",
|
||||||
"level": "TRACE",
|
"level": "TRACE",
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ class LogRoute(Route):
|
|||||||
view_func=self.log_history,
|
view_func=self.log_history,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
)
|
)
|
||||||
|
self.app.add_url_rule(
|
||||||
|
"/api/trace/settings",
|
||||||
|
view_func=self.get_trace_settings,
|
||||||
|
methods=["GET"],
|
||||||
|
)
|
||||||
|
self.app.add_url_rule(
|
||||||
|
"/api/trace/settings",
|
||||||
|
view_func=self.update_trace_settings,
|
||||||
|
methods=["POST"],
|
||||||
|
)
|
||||||
|
|
||||||
async def _replay_cached_logs(
|
async def _replay_cached_logs(
|
||||||
self, last_event_id: str
|
self, last_event_id: str
|
||||||
@@ -106,3 +116,29 @@ class LogRoute(Route):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取日志历史失败: {e}")
|
logger.error(f"获取日志历史失败: {e}")
|
||||||
return Response().error(f"获取日志历史失败: {e}").__dict__
|
return Response().error(f"获取日志历史失败: {e}").__dict__
|
||||||
|
|
||||||
|
async def get_trace_settings(self):
|
||||||
|
"""获取 Trace 设置"""
|
||||||
|
try:
|
||||||
|
trace_enable = self.config.get("trace_enable", True)
|
||||||
|
return Response().ok(data={"trace_enable": trace_enable}).__dict__
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取 Trace 设置失败: {e}")
|
||||||
|
return Response().error(f"获取 Trace 设置失败: {e}").__dict__
|
||||||
|
|
||||||
|
async def update_trace_settings(self):
|
||||||
|
"""更新 Trace 设置"""
|
||||||
|
try:
|
||||||
|
data = await request.json
|
||||||
|
if data is None:
|
||||||
|
return Response().error("请求数据为空").__dict__
|
||||||
|
|
||||||
|
trace_enable = data.get("trace_enable")
|
||||||
|
if trace_enable is not None:
|
||||||
|
self.config["trace_enable"] = bool(trace_enable)
|
||||||
|
self.config.save_config()
|
||||||
|
|
||||||
|
return Response().ok(message="Trace 设置已更新").__dict__
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新 Trace 设置失败: {e}")
|
||||||
|
return Response().error(f"更新 Trace 设置失败: {e}").__dict__
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import traceback
|
|||||||
from quart import request
|
from quart import request
|
||||||
|
|
||||||
from astrbot.core import DEMO_MODE, logger
|
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.skills.skill_manager import SkillManager
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
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)
|
temp_path = os.path.join(temp_dir, filename)
|
||||||
await file.save(temp_path)
|
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_mgr = SkillManager()
|
||||||
skill_name = skill_mgr.install_skill_from_zip(temp_path, overwrite=True)
|
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 (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.ok({"name": skill_name}, "Skill uploaded successfully.")
|
.ok({"name": skill_name}, "Skill uploaded successfully.")
|
||||||
|
|||||||
@@ -130,19 +130,25 @@ class ToolsRoute(Route):
|
|||||||
server_data = await request.json
|
server_data = await request.json
|
||||||
|
|
||||||
name = server_data.get("name", "")
|
name = server_data.get("name", "")
|
||||||
|
old_name = server_data.get("oldName") or name
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
return Response().error("服务器名称不能为空").__dict__
|
return Response().error("服务器名称不能为空").__dict__
|
||||||
|
|
||||||
config = self.tool_mgr.load_mcp_config()
|
config = self.tool_mgr.load_mcp_config()
|
||||||
|
|
||||||
if name not in config["mcpServers"]:
|
if old_name not in config["mcpServers"]:
|
||||||
return Response().error(f"服务器 {name} 不存在").__dict__
|
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 = server_data.get(
|
||||||
"active",
|
"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():
|
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":
|
if key == "mcpServers":
|
||||||
key_0 = list(server_data["mcpServers"].keys())[
|
key_0 = list(server_data["mcpServers"].keys())[
|
||||||
0
|
0
|
||||||
@@ -165,29 +177,42 @@ class ToolsRoute(Route):
|
|||||||
|
|
||||||
# 如果只更新活动状态,保留原始配置
|
# 如果只更新活动状态,保留原始配置
|
||||||
if only_update_active:
|
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之外的所有字段都保留
|
if key != "active": # 除了active之外的所有字段都保留
|
||||||
server_config[key] = value
|
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):
|
if self.tool_mgr.save_mcp_config(config):
|
||||||
# 处理MCP客户端状态变化
|
# 处理MCP客户端状态变化
|
||||||
if active:
|
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:
|
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:
|
except TimeoutError as e:
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.error(f"启用前停用 MCP 服务器时 {name} 超时: {e!s}")
|
.error(
|
||||||
|
f"启用前停用 MCP 服务器时 {old_name} 超时: {e!s}"
|
||||||
|
)
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.error(f"启用前停用 MCP 服务器时 {name} 失败: {e!s}")
|
.error(
|
||||||
|
f"启用前停用 MCP 服务器时 {old_name} 失败: {e!s}"
|
||||||
|
)
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -208,18 +233,20 @@ class ToolsRoute(Route):
|
|||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
# 如果要停用服务器
|
# 如果要停用服务器
|
||||||
elif name in self.tool_mgr.mcp_client_dict:
|
elif old_name in self.tool_mgr.mcp_client_dict:
|
||||||
try:
|
try:
|
||||||
await self.tool_mgr.disable_mcp_server(name, timeout=10)
|
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
return (
|
return (
|
||||||
Response().error(f"停用 MCP 服务器 {name} 超时。").__dict__
|
Response()
|
||||||
|
.error(f"停用 MCP 服务器 {old_name} 超时。")
|
||||||
|
.__dict__
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.error(f"停用 MCP 服务器 {name} 失败: {e!s}")
|
.error(f"停用 MCP 服务器 {old_name} 失败: {e!s}")
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
<!-- Reasoning Block (Collapsible) - 放在最前面 -->
|
<!-- Reasoning Block (Collapsible) - 放在最前面 -->
|
||||||
<ReasoningBlock v-if="msg.content.reasoning && msg.content.reasoning.trim()"
|
<ReasoningBlock v-if="msg.content.reasoning && msg.content.reasoning.trim()"
|
||||||
:reasoning="msg.content.reasoning" :is-dark="isDark"
|
:reasoning="msg.content.reasoning" :is-dark="isDark"
|
||||||
|
class="mt-2"
|
||||||
:initial-expanded="isReasoningExpanded(index)" />
|
:initial-expanded="isReasoningExpanded(index)" />
|
||||||
|
|
||||||
<MessagePartsRenderer :parts="msg.content.message" :is-dark="isDark"
|
<MessagePartsRenderer :parts="msg.content.message" :is-dark="isDark"
|
||||||
@@ -1203,37 +1204,6 @@ export default {
|
|||||||
border-radius: 18px;
|
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,
|
.file-attachments,
|
||||||
.embedded-files {
|
.embedded-files {
|
||||||
|
|||||||
@@ -331,4 +331,86 @@ const getRenderParts = (messageParts) => {
|
|||||||
.tool-call-chevron.rotated {
|
.tool-call-chevron.rotated {
|
||||||
transform: rotate(90deg);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3 mt-1.5 border border-gray-200 dark:border-gray-700 rounded-2xl overflow-hidden w-fit"
|
<div class="reasoning-block" :class="{ 'reasoning-block--dark': isDark }">
|
||||||
:class="{ 'dark:bg-purple-900/8': isDark, 'bg-purple-50/50': !isDark }">
|
<div class="reasoning-header" @click="toggleExpanded">
|
||||||
<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"
|
<v-icon size="small" class="reasoning-icon" :class="{ 'rotate-90': isExpanded }">
|
||||||
@click="toggleExpanded">
|
|
||||||
<v-icon size="small" class="mr-1.5 text-purple-600 dark:text-purple-400 transition-transform"
|
|
||||||
:class="{ 'rotate-90': isExpanded }">
|
|
||||||
mdi-chevron-right
|
mdi-chevron-right
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<span class="text-sm font-medium text-purple-600 dark:text-purple-400 tracking-wide">
|
<span class="reasoning-title">
|
||||||
{{ tm('reasoning.thinking') }}
|
{{ tm('reasoning.thinking') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div v-if="isExpanded" class="reasoning-content animate-fade-in">
|
||||||
<MarkdownRender :content="reasoning" class="reasoning-text markdown-content text-sm leading-relaxed"
|
<MarkdownRender :content="reasoning" class="reasoning-text markdown-content"
|
||||||
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
|
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +44,63 @@ const toggleExpanded = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.2s ease-in-out;
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
@@ -65,9 +119,4 @@ const toggleExpanded = () => {
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reasoning-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--v-theme-secondaryText);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -81,10 +81,10 @@
|
|||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<!-- 添加/编辑 MCP 服务器对话框 -->
|
<!-- 添加/编辑 MCP 服务器对话框 -->
|
||||||
<v-dialog v-model="showMcpServerDialog" max-width="750px" persistent>
|
<v-dialog v-model="showMcpServerDialog" max-width="750px">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="bg-primary text-white py-3">
|
<v-card-title class="pa-4 pl-6">
|
||||||
<v-icon color="white" class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
<v-icon class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
||||||
<span>{{ isEditMode ? tm('dialogs.addServer.editTitle') : tm('dialogs.addServer.title') }}</span>
|
<span>{{ isEditMode ? tm('dialogs.addServer.editTitle') : tm('dialogs.addServer.title') }}</span>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
@@ -251,6 +251,7 @@ export default {
|
|||||||
active: true,
|
active: true,
|
||||||
tools: []
|
tools: []
|
||||||
},
|
},
|
||||||
|
originalServerName: '',
|
||||||
save_message_snack: false,
|
save_message_snack: false,
|
||||||
save_message: '',
|
save_message: '',
|
||||||
save_message_success: 'success'
|
save_message_success: 'success'
|
||||||
@@ -359,6 +360,9 @@ export default {
|
|||||||
active: this.currentServer.active,
|
active: this.currentServer.active,
|
||||||
...configObj
|
...configObj
|
||||||
};
|
};
|
||||||
|
if (this.isEditMode && this.originalServerName) {
|
||||||
|
serverData.oldName = this.originalServerName;
|
||||||
|
}
|
||||||
const endpoint = this.isEditMode ? '/api/tools/mcp/update' : '/api/tools/mcp/add';
|
const endpoint = this.isEditMode ? '/api/tools/mcp/update' : '/api/tools/mcp/add';
|
||||||
axios.post(endpoint, serverData)
|
axios.post(endpoint, serverData)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -402,6 +406,7 @@ export default {
|
|||||||
active: server.active,
|
active: server.active,
|
||||||
tools: server.tools || []
|
tools: server.tools || []
|
||||||
};
|
};
|
||||||
|
this.originalServerName = server.name;
|
||||||
this.serverConfigJson = JSON.stringify(configCopy, null, 2);
|
this.serverConfigJson = JSON.stringify(configCopy, null, 2);
|
||||||
this.isEditMode = true;
|
this.isEditMode = true;
|
||||||
this.showMcpServerDialog = true;
|
this.showMcpServerDialog = true;
|
||||||
@@ -461,6 +466,7 @@ export default {
|
|||||||
this.serverConfigJson = '';
|
this.serverConfigJson = '';
|
||||||
this.jsonError = null;
|
this.jsonError = null;
|
||||||
this.isEditMode = false;
|
this.isEditMode = false;
|
||||||
|
this.originalServerName = '';
|
||||||
},
|
},
|
||||||
showSuccess(message) {
|
showSuccess(message) {
|
||||||
this.save_message = message;
|
this.save_message = message;
|
||||||
|
|||||||
@@ -42,10 +42,10 @@
|
|||||||
|
|
||||||
<v-dialog v-model="uploadDialog" max-width="520px" persistent>
|
<v-dialog v-model="uploadDialog" max-width="520px" persistent>
|
||||||
<v-card>
|
<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>
|
<v-card-text>
|
||||||
<small class="text-grey">{{ tm('skills.uploadHint') }}</small>
|
<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" />
|
variant="outlined" class="mt-4" :multiple="false" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="d-flex justify-end">
|
<v-card-actions class="d-flex justify-end">
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
|
"hint": "https://docs.astrbot.app/use/skills.html",
|
||||||
"description": "Skills",
|
"description": "Skills",
|
||||||
"provider_settings": {
|
"provider_settings": {
|
||||||
"skills": {
|
"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": {
|
"truncate_and_compress": {
|
||||||
|
"hint": "https://docs.astrbot.app/en/use/context-compress.html",
|
||||||
"description": "Context Management Strategy",
|
"description": "Context Management Strategy",
|
||||||
"provider_settings": {
|
"provider_settings": {
|
||||||
"max_context_length": {
|
"max_context_length": {
|
||||||
|
|||||||
@@ -3,5 +3,8 @@
|
|||||||
"autoScroll": {
|
"autoScroll": {
|
||||||
"enabled": "Auto-scroll: On",
|
"enabled": "Auto-scroll: On",
|
||||||
"disabled": "Auto-scroll: Off"
|
"disabled": "Auto-scroll: Off"
|
||||||
}
|
},
|
||||||
|
"hint": "Currently only recording partial model call paths from AstrBot main Agent. More coverage will be added.",
|
||||||
|
"recording": "Recording",
|
||||||
|
"paused": "Paused"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
|
"hint": "https://docs.astrbot.app/en/use/skills.html",
|
||||||
"description": "Skills",
|
"description": "Skills",
|
||||||
"provider_settings": {
|
"provider_settings": {
|
||||||
"skills": {
|
"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": {
|
"truncate_and_compress": {
|
||||||
|
"hint": "https://docs.astrbot.app/use/context-compress.html",
|
||||||
"description": "上下文管理策略",
|
"description": "上下文管理策略",
|
||||||
"provider_settings": {
|
"provider_settings": {
|
||||||
"max_context_length": {
|
"max_context_length": {
|
||||||
|
|||||||
@@ -3,5 +3,8 @@
|
|||||||
"autoScroll": {
|
"autoScroll": {
|
||||||
"enabled": "自动滚动:开",
|
"enabled": "自动滚动:开",
|
||||||
"disabled": "自动滚动:关"
|
"disabled": "自动滚动:关"
|
||||||
}
|
},
|
||||||
|
"hint": "当前仅记录部分 AstrBot 主 Agent 的模型调用路径,后续会不断完善。",
|
||||||
|
"recording": "记录中",
|
||||||
|
"paused": "已暂停"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,17 +28,13 @@
|
|||||||
|
|
||||||
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">{{ tm('table.empty') }}</v-alert>
|
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">{{ tm('table.empty') }}</v-alert>
|
||||||
|
|
||||||
<v-data-table
|
<v-data-table :items="jobs" :headers="headers" :loading="loading" item-key="job_id" density="comfortable"
|
||||||
:items="jobs"
|
class="elevation-0">
|
||||||
:headers="headers"
|
|
||||||
:loading="loading"
|
|
||||||
item-key="job_id"
|
|
||||||
density="comfortable"
|
|
||||||
class="elevation-0"
|
|
||||||
>
|
|
||||||
<template #item.name="{ item }">
|
<template #item.name="{ item }">
|
||||||
<div class="font-weight-medium">{{ item.name }}</div>
|
<div class="py-4">
|
||||||
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
|
<div class="font-weight-medium">{{ item.name }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #item.type="{ item }">
|
<template #item.type="{ item }">
|
||||||
<v-chip size="small" :color="item.run_once ? 'orange' : 'primary'" variant="tonal">
|
<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.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
||||||
<template #item.actions="{ item }">
|
<template #item.actions="{ item }">
|
||||||
<div class="d-flex" style="gap: 8px;">
|
<div class="d-flex" style="gap: 8px;">
|
||||||
<v-switch
|
<v-switch v-model="item.enabled" inset density="compact" hide-details color="primary"
|
||||||
v-model="item.enabled"
|
@change="toggleJob(item)" />
|
||||||
inset
|
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete')
|
||||||
density="compact"
|
}}</v-btn>
|
||||||
hide-details
|
|
||||||
color="primary"
|
|
||||||
@change="toggleJob(item)"
|
|
||||||
/>
|
|
||||||
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete') }}</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
@@ -83,39 +74,19 @@
|
|||||||
<v-switch v-model="newJob.run_once" :label="tm('form.runOnce')" inset color="primary" hide-details />
|
<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.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-model="newJob.note" :label="tm('form.note')" variant="outlined" density="comfortable" />
|
||||||
<v-text-field
|
<v-text-field v-if="!newJob.run_once" v-model="newJob.cron_expression" :label="tm('form.cron')"
|
||||||
v-if="!newJob.run_once"
|
:placeholder="tm('form.cronPlaceholder')" variant="outlined" density="comfortable" />
|
||||||
v-model="newJob.cron_expression"
|
<v-text-field v-else v-model="newJob.run_at" :label="tm('form.runAt')" type="datetime-local"
|
||||||
:label="tm('form.cron')"
|
variant="outlined" density="comfortable" />
|
||||||
:placeholder="tm('form.cronPlaceholder')"
|
<v-text-field v-model="newJob.session" :label="tm('form.session')" variant="outlined" density="comfortable" />
|
||||||
variant="outlined"
|
<v-text-field v-model="newJob.timezone" :label="tm('form.timezone')" variant="outlined"
|
||||||
density="comfortable"
|
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-switch v-model="newJob.enabled" :label="tm('form.enabled')" inset color="primary" hide-details />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="justify-end">
|
<v-card-actions class="justify-end">
|
||||||
<v-btn variant="text" @click="createDialog = false">{{ tm('actions.cancel') }}</v-btn>
|
<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-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|||||||
@@ -2433,7 +2433,9 @@ watch(isListView, (newVal) => {
|
|||||||
></v-progress-linear>
|
></v-progress-linear>
|
||||||
</div>
|
</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">
|
<div class="v-card-text">
|
||||||
<v-tabs v-model="uploadTab" color="primary">
|
<v-tabs v-model="uploadTab" color="primary">
|
||||||
|
|||||||
@@ -1,13 +1,72 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TraceDisplayer from '@/components/shared/TraceDisplayer.vue';
|
import TraceDisplayer from '@/components/shared/TraceDisplayer.vue';
|
||||||
import { useModuleI18n } from '@/i18n/composables';
|
import { useModuleI18n } from '@/i18n/composables';
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
const { tm } = useModuleI18n('features/trace');
|
const { tm } = useModuleI18n('features/trace');
|
||||||
|
|
||||||
|
const traceEnabled = ref(true);
|
||||||
|
const loading = ref(false);
|
||||||
|
const traceDisplayerKey = ref(0);
|
||||||
|
|
||||||
|
const fetchTraceSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/trace/settings');
|
||||||
|
if (res.data?.status === 'ok') {
|
||||||
|
traceEnabled.value = res.data.data?.trace_enable ?? true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch trace settings:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTraceSettings = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await axios.post('/api/trace/settings', {
|
||||||
|
trace_enable: traceEnabled.value
|
||||||
|
});
|
||||||
|
// Refresh the TraceDisplayer component to reconnect SSE
|
||||||
|
traceDisplayerKey.value += 1;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update trace settings:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTraceSettings();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="height: 100%;">
|
<div style="height: 100%; display: flex; flex-direction: column;">
|
||||||
<TraceDisplayer />
|
<div class="trace-header">
|
||||||
|
<div class="trace-info">
|
||||||
|
<v-icon size="small" color="info" class="mr-2">mdi-information-outline</v-icon>
|
||||||
|
<span class="trace-hint">{{ tm('hint') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="trace-controls">
|
||||||
|
<v-switch
|
||||||
|
v-model="traceEnabled"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="loading"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
@update:model-value="updateTraceSettings"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="switch-label">{{ traceEnabled ? tm('recording') : tm('paused') }}</span>
|
||||||
|
</template>
|
||||||
|
</v-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-height: 0;">
|
||||||
|
<TraceDisplayer :key="traceDisplayerKey" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -19,3 +78,38 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trace-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
border-bottom: 1px solid rgba(59, 130, 246, 0.1);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user