Compare commits

...

15 Commits

Author SHA1 Message Date
邹永赫 8132ce24eb fix: correctly synchronize MCP client initialization 2026-03-01 21:22:41 +09:00
idiotsj 38e99cf65c fix: restore task cancellation on timeout per review feedback
Pending tasks in asyncio.wait are tasks that have NOT completed
initialization within 20s, so cancelling them is safe and correct.
2026-02-27 22:21:47 +08:00
idiotsj 2cfe4288b2 fix: prevent initialized MCP clients from being cleaned up on timeout
- Do not cancel pending tasks on timeout; let them continue running
  in the background waiting for the termination signal (event.set()),
  so successfully initialized services remain available
- Track initialization state with a flag to distinguish init failures
  from post-init cancellations in _init_mcp_client_task_wrapper
2026-02-27 22:18:55 +08:00
idiotsj 4924739423 refactor: simplify MCP init orchestration and improve log security
- Replace Future-based sync with asyncio.wait + name→task mapping
- Explicitly cancel timed-out tasks after 20s timeout
- Downgrade sensitive config details (command/args/URL) to debug level
- Move urllib.parse import to top-level
2026-02-27 21:59:32 +08:00
idiotsj ec9f7403d5 perf: add timeout and better error handling for MCP initialization
- Add 20-second total timeout to prevent slow MCP servers from blocking startup
- Show detailed configuration info when MCP initialization fails
- List all failed services in a summary warning
- Gracefully handle timeout by using already-completed services

This ensures that even if some MCP servers are slow or unreachable,
the system will start within a reasonable time and provide clear
feedback about which services failed and why.
2026-02-27 20:24:17 +08:00
idiotsj 0de7ae8481 fix: resolve MCP tools race condition causing 'completion 无法解析' error
- Wait for MCP client initialization to complete before accepting requests
- Add Future-based synchronization in init_mcp_clients()
- Prevent tool_calls from being rejected due to empty func_list
- Improve error logging for MCP initialization failures

Fixes race condition where AI attempts to call MCP tools before they are
registered, resulting in 'API 返回的 completion 无法解析' exceptions.

The issue occurred because:
1. MCP clients were initialized asynchronously without waiting
2. System accepted user requests immediately after startup
3. AI received empty tool list and attempted to call non-existent tools
4. Tool matching failed, causing parsing errors

This fix ensures all MCP tools are loaded before the system processes
any requests that might use them.
2026-02-27 20:05:13 +08:00
Soulter 42e84afd89 perf: improve cron job page 2026-02-02 14:13:17 +08:00
Soulter a7ed6b8c76 fix: reasoning block style 2026-02-02 14:11:17 +08:00
Soulter ee43b98ce6 fix: add missing comma in truncate_and_compress hint in config-metadata.json 2026-02-01 23:34:21 +08:00
Soulter 681b4747a6 feat: add proactive capability configuration with cron tools support 2026-02-01 23:33:45 +08:00
Soulter a6da4ebe5e feat: add styles for embedded images and audio in MessagePartsRenderer 2026-02-01 23:29:08 +08:00
Soulter e35a604b30 Merge pull request #4697 from advent259141/Astrbot_skill
feat: implemented proactive agents and subagents orchestrator
2026-02-01 22:57:47 +08:00
Soulter 19651d24bb fix(skills): remove sandbox runtime handling from skill upload process (#4798) 2026-02-01 13:13:27 +08:00
Soulter dba08edd0d style: enhance dialog titles with padding and text styles in MCP and Skills sections 2026-02-01 11:09:32 +08:00
letr dc06bc943a fix(mcp): cannot rename MCP Server (#4766)
* fix(mcp): support renaming when editing MCP servers

When editing the MCP server configuration, you can now change the server name. The frontend will save the original name in edit mode, and the backend will recognize the rename operation through the oldName field.

* fix(mcp): fixed an issue where renaming the MCP server did not check for name conflicts

When renaming an MCP server, add a check to see if the target name already exists. If the name exists and it is a rename operation, return an error message to avoid overwriting the configuration.
2026-02-01 11:01:49 +08:00
15 changed files with 389 additions and 165 deletions
+29
View File
@@ -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"),
+105 -17
View File
@@ -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,
+2 -2
View File
@@ -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):
"""动态导入提供商适配器模块
-33
View File
@@ -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.")
+41 -14
View File
@@ -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__
)
+1 -31
View File
@@ -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": {
+19 -48
View File
@@ -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>
+3 -1
View File
@@ -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">