Compare commits

..

1 Commits

Author SHA1 Message Date
Soulter c36dab5de9 feat: remove ASYNC_TASK_new.md as part of project restructuring 2026-02-01 22:35:19 +08:00
24 changed files with 162 additions and 487 deletions
-18
View File
@@ -1,18 +0,0 @@
我需要让 Agent 能够在未来提醒自己去做某些事情,这样 Agent 能够主动地去完成一些任务,而不是等用户主动来下达命令。
你需要实现一个 CronJob 系统,允许 Agent 创建未来任务,并且在未来的某个时间点自动触发这些任务的执行.
CronJob 系统分为 BasicCronJob 和 ActiveAgentCronJob 两种类型。前者只是简单的提供一个定时任务功能(给插件用),而后者则允许 Agent 主动地去完成一些任务。BasicCronJob 不必多说,就是定时执行某个函数。对于 ActiveAgentCronJobAgent 应该可以主动管理(比如通过Tool来管理)这些 CronJobs,当添加的时候,Agent 可以给 CronJob 捎一段文字,以说明未来的自己需要做什么事情。比如说,Agent 在听到用户 “每天早上都给我整理一份今日早报” 之后,应该可以创建 Cron Job,并且自己写脚本来完成这个任务,并且注册 cron job。Agent 给未来的自己捎去的信息应该只是呈现为一段文字,这样可以保持设计简约。当触发后, CronJobManager 会调用 MainAgent 的一轮循环,MainAgent 通过上下文知道这是一个定时任务触发的循环,从而执行相应的操作。
此外,我还有一个需求,后台长任务。需要给当前的 FunctionTool 类增加一个属性,is_background_task: bool = False,插件可以通过这个属性来声明这是一个异步任务。这是为了解决一些 Tool 需要长时间运行的问题,比如 Deep Search tool 需要长时间搜索网页内容、Sub Agent 需要长时间运行来完成一个复杂任务。
基于上面的讨论,我觉得,应该:
1. 需要给当前的 FunctionTool 类增加一个属性is_background_task: bool = Falsetool runner 在执行这个 tool 的时候,如果发现是后台任务,就不等待结果返回,而是直接返回一个任务 ID (已经创建成功提示)的结果,tool runner 在后台继续执行这个任务。当任务完成之后,任务的结果回传给 MainAgent(其实就是再执行一次 main agent loop,但是上下文应该是最新的),并且 MainAgent 此时应该有 send_message_to_user 的工具,通过这个工具可以选择是否主动通知用户任务完成的结果。
2. 增加一个 CronJobManager 类,负责管理所有的定时任务。Agent 可以通过调用这个类的方法来创建、删除、修改定时任务。通过 cron expression 来定义触发条件。
3. CronJobManager 除了管理普通的定时任务(比如插件可能有一些自己的定时任务),还有一种特殊的任务类型,就是上面提到的主动型 Agent 任务。用户提需求,MainAgent 选择性地调用 CronJobManager 的方法来创建这些任务,并且在任务触发时,CronJobManager 的回调就是执行 MainAgent 的一轮循环(需要加 send_message_to_user tool),MainAgent 通过上下文知道这是一个定时任务触发的循环,从而执行相应的操作。
4. WebUI 需要增加 Cron Job 管理界面,用户可以在界面上查看、创建、修改、删除定时任务。对于主动型 Agent 任务,用户可以看到任务的描述、触发条件等信息。
5. 除此之外,现在的代码中已经有了 subagent 的管理。WebUI 可以创建 SubAgent,但是还没写完。除了结合上面我说的之外,你还需要将 SubAgent 与 Persona 结合起来——因为 Persona 是一个包含了 tool、skills、name、description 的完整体,所以 SubAgent 应该直接继承 Persona 的定义,而不是单独定义 SubAgent。SubAgent 本质上就是一个有特定角色和能力的 Persona!多么美妙的设计啊!
6. 为了实现大一统,is_background_task = True 的时候,后台任务也挂到 CronJobManager 上去管理,只不过这个是立即触发的任务,不需要等到未来某个时间点才触发罢了。
我希望设计尽可能简单,但是强大。
+3 -21
View File
@@ -54,14 +54,6 @@ async def run_agent(
return
if resp.type == "tool_call_result":
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":
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
await astr_event.send(msg_chain)
@@ -75,22 +67,12 @@ async def run_agent(
# 用来标记流式响应需要分节
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":
await astr_event.send(resp.data["chain"])
elif show_tool_use:
if tool_info:
m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
json_comp = resp.data["chain"].chain[0]
if isinstance(json_comp, Json):
m = f"🔨 调用工具: {json_comp.data.get('name')}"
else:
m = "🔨 调用工具..."
chain = MessageChain(type="tool_call").message(m)
-30
View File
@@ -114,9 +114,6 @@ DEFAULT_CONFIG = {
"provider": "moonshotai",
"moonshotai_api_key": "",
},
"proactive_capability": {
"add_cron_tools": True,
},
"sandbox": {
"enable": False,
"booter": "shipyard",
@@ -202,7 +199,6 @@ DEFAULT_CONFIG = {
"log_file_enable": False,
"log_file_path": "logs/astrbot.log",
"log_file_max_mb": 20,
"trace_enable": False,
"trace_log_enable": False,
"trace_log_path": "logs/astrbot.trace.log",
"trace_log_max_mb": 20,
@@ -2236,14 +2232,6 @@ CONFIG_METADATA_2 = {
},
},
},
"proactive_capability": {
"type": "object",
"items": {
"add_cron_tools": {
"type": "bool",
},
},
},
},
},
"provider_stt_settings": {
@@ -2696,7 +2684,6 @@ CONFIG_METADATA_3 = {
"skills": {
"description": "Skills",
"type": "object",
"hint": "",
"items": {
"provider_settings.skills.runtime": {
"description": "Skill Runtime",
@@ -2711,24 +2698,7 @@ 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": {
+1
View File
@@ -54,6 +54,7 @@ class EventBus:
event (AstrMessageEvent): 事件对象
"""
event.trace.record("event_dispatch", config_name=conf_name)
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
if event.get_sender_name():
logger.info(
+3 -21
View File
@@ -9,7 +9,6 @@ from astrbot.core.message.components import (
AtAll,
BaseMessageComponent,
Image,
Json,
Plain,
)
@@ -118,26 +117,9 @@ class MessageChain:
self.use_t2i_ = use_t2i
return self
def get_plain_text(self, with_other_comps_mark: bool = False) -> str:
"""获取纯文本消息。这个方法将获取 chain 中所有 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 get_plain_text(self) -> str:
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。"""
return " ".join([comp.text for comp in self.chain if isinstance(comp, Plain)])
def squash_plain(self):
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
@@ -94,10 +94,6 @@ 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(
@@ -117,7 +113,6 @@ 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"),
+2
View File
@@ -85,4 +85,6 @@ class PipelineScheduler:
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
await event.send(None)
event.trace.record("event_end")
logger.debug("pipeline 执行完毕。")
@@ -73,6 +73,9 @@ class AstrMessageEvent(abc.ABC):
self.span = self.trace
"""事件级 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.call_llm = False
-4
View File
@@ -50,10 +50,6 @@ class TraceSpan:
self.started_at = time.time()
def record(self, action: str, **fields: Any) -> None:
# Check if trace recording is enabled
if not astrbot_config.get("trace_enable", True):
return
payload = {
"type": "trace",
"level": "TRACE",
-36
View File
@@ -31,16 +31,6 @@ class LogRoute(Route):
view_func=self.log_history,
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(
self, last_event_id: str
@@ -116,29 +106,3 @@ class LogRoute(Route):
except Exception as e:
logger.error(f"获取日志历史失败: {e}")
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__
+33
View File
@@ -4,6 +4,7 @@ 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
@@ -59,9 +60,41 @@ 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.")
+14 -41
View File
@@ -130,25 +130,19 @@ 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 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__
if name not in config["mcpServers"]:
return Response().error(f"服务器 {name} 不存在").__dict__
# 获取活动状态
active = server_data.get(
"active",
config["mcpServers"][old_name].get("active", True),
config["mcpServers"][name].get("active", True),
)
# 创建新的配置对象
@@ -159,13 +153,7 @@ class ToolsRoute(Route):
# 复制所有配置字段
for key, value in server_data.items():
if key not in [
"name",
"active",
"tools",
"errlogs",
"oldName",
]: # 排除特殊字段
if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段
if key == "mcpServers":
key_0 = list(server_data["mcpServers"].keys())[
0
@@ -177,42 +165,29 @@ class ToolsRoute(Route):
# 如果只更新活动状态,保留原始配置
if only_update_active:
for key, value in config["mcpServers"][old_name].items():
for key, value in config["mcpServers"][name].items():
if key != "active": # 除了active之外的所有字段都保留
server_config[key] = value
# config["mcpServers"][name] = server_config
if is_rename:
config["mcpServers"].pop(old_name)
config["mcpServers"][name] = server_config
else:
config["mcpServers"][name] = server_config
config["mcpServers"][name] = server_config
if self.tool_mgr.save_mcp_config(config):
# 处理MCP客户端状态变化
if active:
if (
old_name in self.tool_mgr.mcp_client_dict
or not only_update_active
or is_rename
):
if name in self.tool_mgr.mcp_client_dict or not only_update_active:
try:
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
await self.tool_mgr.disable_mcp_server(name, timeout=10)
except TimeoutError as e:
return (
Response()
.error(
f"启用前停用 MCP 服务器时 {old_name} 超时: {e!s}"
)
.error(f"启用前停用 MCP 服务器时 {name} 超时: {e!s}")
.__dict__
)
except Exception as e:
logger.error(traceback.format_exc())
return (
Response()
.error(
f"启用前停用 MCP 服务器时 {old_name} 失败: {e!s}"
)
.error(f"启用前停用 MCP 服务器时 {name} 失败: {e!s}")
.__dict__
)
try:
@@ -233,20 +208,18 @@ class ToolsRoute(Route):
.__dict__
)
# 如果要停用服务器
elif old_name in self.tool_mgr.mcp_client_dict:
elif name in self.tool_mgr.mcp_client_dict:
try:
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
await self.tool_mgr.disable_mcp_server(name, timeout=10)
except TimeoutError:
return (
Response()
.error(f"停用 MCP 服务器 {old_name} 超时。")
.__dict__
Response().error(f"停用 MCP 服务器 {name} 超时。").__dict__
)
except Exception as e:
logger.error(traceback.format_exc())
return (
Response()
.error(f"停用 MCP 服务器 {old_name} 失败: {e!s}")
.error(f"停用 MCP 服务器 {name} 失败: {e!s}")
.__dict__
)
+31 -1
View File
@@ -92,7 +92,6 @@
<!-- 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"
@@ -1204,6 +1203,37 @@ 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,86 +331,4 @@ 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,15 +1,18 @@
<template>
<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 }">
<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 }">
mdi-chevron-right
</v-icon>
<span class="reasoning-title">
<span class="text-sm font-medium text-purple-600 dark:text-purple-400 tracking-wide">
{{ tm('reasoning.thinking') }}
</span>
</div>
<div v-if="isExpanded" class="reasoning-content animate-fade-in">
<MarkdownRender :content="reasoning" class="reasoning-text markdown-content"
<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"
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
</div>
</div>
@@ -44,63 +47,6 @@ 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;
}
@@ -119,4 +65,9 @@ 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">
<v-dialog v-model="showMcpServerDialog" max-width="750px" persistent>
<v-card>
<v-card-title class="pa-4 pl-6">
<v-icon class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" 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,7 +251,6 @@ export default {
active: true,
tools: []
},
originalServerName: '',
save_message_snack: false,
save_message: '',
save_message_success: 'success'
@@ -360,9 +359,6 @@ 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 => {
@@ -406,7 +402,6 @@ 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;
@@ -466,7 +461,6 @@ 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 class="text-h3 pa-4 pb-0 pl-6">{{ tm('skills.uploadDialogTitle') }}</v-card-title>
<v-card-title>{{ 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-folder-zip-outline"
<v-file-input v-model="uploadFile" accept=".zip" :label="tm('skills.selectFile')" prepend-icon="mdi-file-zip"
variant="outlined" class="mt-4" :multiple="false" />
</v-card-text>
<v-card-actions class="d-flex justify-end">
@@ -165,7 +165,6 @@
}
},
"skills": {
"hint": "https://docs.astrbot.app/use/skills.html",
"description": "Skills",
"provider_settings": {
"skills": {
@@ -176,20 +175,7 @@
}
}
},
"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": {
@@ -3,8 +3,5 @@
"autoScroll": {
"enabled": "Auto-scroll: On",
"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,7 +165,6 @@
}
},
"skills": {
"hint": "https://docs.astrbot.app/en/use/skills.html",
"description": "Skills",
"provider_settings": {
"skills": {
@@ -176,20 +175,7 @@
}
}
},
"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": {
@@ -3,8 +3,5 @@
"autoScroll": {
"enabled": "自动滚动:开",
"disabled": "自动滚动:关"
},
"hint": "当前仅记录部分 AstrBot 主 Agent 的模型调用路径,后续会不断完善。",
"recording": "记录中",
"paused": "已暂停"
}
}
+48 -19
View File
@@ -28,13 +28,17 @@
<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="py-4">
<div class="font-weight-medium">{{ item.name }}</div>
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
</div>
<div class="font-weight-medium">{{ item.name }}</div>
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
</template>
<template #item.type="{ item }">
<v-chip size="small" :color="item.run_once ? 'orange' : 'primary'" variant="tonal">
@@ -53,10 +57,15 @@
<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>
@@ -74,19 +83,39 @@
<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>
+1 -3
View File
@@ -2433,9 +2433,7 @@ watch(isListView, (newVal) => {
></v-progress-linear>
</div>
<v-card-title class="text-h3 pa-4 pb-0 pl-6">
{{ tm("dialogs.install.title") }}
</v-card-title>
<div class="v-card-title text-h5">{{ tm("dialogs.install.title") }}</div>
<div class="v-card-text">
<v-tabs v-model="uploadTab" color="primary">
+2 -96
View File
@@ -1,72 +1,13 @@
<script setup>
import TraceDisplayer from '@/components/shared/TraceDisplayer.vue';
import { useModuleI18n } from '@/i18n/composables';
import { ref, onMounted } from 'vue';
import axios from 'axios';
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>
<template>
<div style="height: 100%; display: flex; flex-direction: column;">
<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 style="height: 100%;">
<TraceDisplayer />
</div>
</template>
@@ -78,38 +19,3 @@ export default {
}
};
</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>