Compare commits
36 Commits
Astrbot_skill
...
v4.14.2
| Author | SHA1 | Date | |
|---|---|---|---|
| d2b9997620 | |||
| 36432c4361 | |||
| 36f0d1f0f9 | |||
| f65b268bb2 | |||
| fe06dfcca3 | |||
| bc9043bc3f | |||
| 430694aae9 | |||
| c643e3c093 | |||
| ff46eef3b2 | |||
| a0c364aa81 | |||
| 0e0f923a49 | |||
| f2d637b935 | |||
| 96e61a4a92 | |||
| e42c1b6da8 | |||
| 387bba093e | |||
| 123cf9cb11 | |||
| 93277ffac9 | |||
| c091053ea8 | |||
| 8b9f2f1e70 | |||
| 25ca7bd71e | |||
| 093b37e04b | |||
| a12e27f9ab | |||
| ae6e0db053 | |||
| cd6bef4d78 | |||
| de1304dc6a | |||
| f835f63542 | |||
| 5deb045e47 | |||
| 42e84afd89 | |||
| a7ed6b8c76 | |||
| ee43b98ce6 | |||
| 681b4747a6 | |||
| a6da4ebe5e | |||
| e35a604b30 | |||
| 19651d24bb | |||
| dba08edd0d | |||
| dc06bc943a |
@@ -34,7 +34,7 @@
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
||||
</div>
|
||||
|
||||
AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主流即时通讯软件,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建生产可用的 AI 应用。
|
||||
AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
|
||||
|
||||

|
||||
|
||||
@@ -50,6 +50,23 @@ AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主
|
||||
7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
|
||||
8. 🌐 国际化(i18n)支持。
|
||||
|
||||
<br>
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>💙 角色扮演 & 情感陪伴</th>
|
||||
<th>✨ 主动式 Agent</th>
|
||||
<th>🚀 通用 Agentic 能力</th>
|
||||
<th>🧩 900+ 社区插件</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
|
||||
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
|
||||
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
|
||||
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 快速开始
|
||||
|
||||
#### Docker 部署(推荐 🥳)
|
||||
@@ -249,6 +266,6 @@ pre-commit install
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
</div
|
||||
陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
|
||||
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ class Main(star.Star):
|
||||
|
||||
yield event.request_llm(
|
||||
prompt=prompt,
|
||||
func_tool_manager=self.context.get_llm_tool_manager(),
|
||||
session_id=event.session_id,
|
||||
conversation=conv,
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ class Main(Star):
|
||||
if p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
try:
|
||||
# 尝试使用 LLM 生成更生动的回复
|
||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
# func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
|
||||
# 获取用户当前的对话信息
|
||||
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
@@ -76,7 +76,6 @@ class Main(Star):
|
||||
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
|
||||
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
|
||||
),
|
||||
func_tool_manager=func_tools_mgr,
|
||||
session_id=curr_cid,
|
||||
contexts=[],
|
||||
system_prompt="",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.13.2"
|
||||
__version__ = "4.14.2"
|
||||
|
||||
@@ -54,6 +54,14 @@ 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)
|
||||
@@ -67,12 +75,22 @@ 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:
|
||||
json_comp = resp.data["chain"].chain[0]
|
||||
if isinstance(json_comp, Json):
|
||||
m = f"🔨 调用工具: {json_comp.data.get('name')}"
|
||||
if tool_info:
|
||||
m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
|
||||
else:
|
||||
m = "🔨 调用工具..."
|
||||
chain = MessageChain(type="tool_call").message(m)
|
||||
|
||||
@@ -12,6 +12,7 @@ from dataclasses import dataclass, field
|
||||
from astrbot.api import sp
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.agent.handoff import HandoffTool
|
||||
from astrbot.core.agent.mcp_client import MCPTool
|
||||
from astrbot.core.agent.message import TextPart
|
||||
from astrbot.core.agent.tool import ToolSet
|
||||
from astrbot.core.astr_agent_context import AgentContextWrapper, AstrAgentContext
|
||||
@@ -19,7 +20,6 @@ from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS
|
||||
from astrbot.core.astr_agent_run_util import AgentRunner
|
||||
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
|
||||
from astrbot.core.astr_main_agent_resources import (
|
||||
CHATUI_EXTRA_PROMPT,
|
||||
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
|
||||
EXECUTE_SHELL_TOOL,
|
||||
FILE_DOWNLOAD_TOOL,
|
||||
@@ -99,6 +99,8 @@ class MainAgentBuildConfig:
|
||||
"""This will inject healthy and safe system prompt into the main agent,
|
||||
to prevent LLM output harmful information"""
|
||||
safety_mode_strategy: str = "system_prompt"
|
||||
computer_use_runtime: str = "local"
|
||||
"""The runtime for agent computer use: none, local, or sandbox."""
|
||||
sandbox_cfg: dict = field(default_factory=dict)
|
||||
add_cron_tools: bool = True
|
||||
"""This will add cron job management tools to the main agent for proactive cron job execution."""
|
||||
@@ -259,6 +261,8 @@ async def _ensure_persona_and_skills(
|
||||
return
|
||||
|
||||
# get persona ID
|
||||
|
||||
# 1. from session service config - highest priority
|
||||
persona_id = (
|
||||
await sp.get_async(
|
||||
scope="umo",
|
||||
@@ -269,14 +273,15 @@ async def _ensure_persona_and_skills(
|
||||
).get("persona_id")
|
||||
|
||||
if not persona_id:
|
||||
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
||||
if persona_id is None or persona_id != "[%None]":
|
||||
default_persona = plugin_context.persona_manager.selected_default_persona_v3
|
||||
if default_persona:
|
||||
persona_id = default_persona["name"]
|
||||
if event.get_platform_name() == "webchat":
|
||||
persona_id = "_chatui_default_"
|
||||
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
||||
# 2. from conversation setting - second priority
|
||||
persona_id = req.conversation.persona_id
|
||||
|
||||
if persona_id == "[%None]":
|
||||
# explicitly set to no persona
|
||||
pass
|
||||
elif persona_id is None:
|
||||
# 3. from config default persona setting - last priority
|
||||
persona_id = cfg.get("default_personality")
|
||||
|
||||
persona = next(
|
||||
builtins.filter(
|
||||
@@ -291,23 +296,18 @@ async def _ensure_persona_and_skills(
|
||||
req.system_prompt += f"\n# Persona Instructions\n\n{prompt}\n"
|
||||
if begin_dialogs := copy.deepcopy(persona.get("_begin_dialogs_processed")):
|
||||
req.contexts[:0] = begin_dialogs
|
||||
else:
|
||||
# special handling for webchat persona
|
||||
if event.get_platform_name() == "webchat" and persona_id != "[%None]":
|
||||
persona_id = "_chatui_default_"
|
||||
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
||||
|
||||
# Inject skills prompt
|
||||
skills_cfg = cfg.get("skills", {})
|
||||
sandbox_cfg = cfg.get("sandbox", {})
|
||||
runtime = cfg.get("computer_use_runtime", "local")
|
||||
skill_manager = SkillManager()
|
||||
runtime = skills_cfg.get("runtime", "local")
|
||||
skills = skill_manager.list_skills(active_only=True, runtime=runtime)
|
||||
|
||||
if runtime == "sandbox" and not sandbox_cfg.get("enable", False):
|
||||
logger.warning(
|
||||
"Skills runtime is set to sandbox, but sandbox mode is disabled, will skip skills prompt injection.",
|
||||
)
|
||||
req.system_prompt += (
|
||||
"\n[Background: User added some skills, and skills runtime is set to sandbox, "
|
||||
"but sandbox mode is disabled. So skills will be unavailable.]\n"
|
||||
)
|
||||
elif skills:
|
||||
if skills:
|
||||
if persona and persona.get("skills") is not None:
|
||||
if not persona["skills"]:
|
||||
skills = []
|
||||
@@ -316,12 +316,12 @@ async def _ensure_persona_and_skills(
|
||||
skills = [skill for skill in skills if skill.name in allowed]
|
||||
if skills:
|
||||
req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
|
||||
|
||||
runtime = skills_cfg.get("runtime", "local")
|
||||
sandbox_enabled = sandbox_cfg.get("enable", False)
|
||||
if runtime == "local" and not sandbox_enabled:
|
||||
_apply_local_env_tools(req)
|
||||
|
||||
if runtime == "none":
|
||||
req.system_prompt += (
|
||||
"User has not enabled the Computer Use feature. "
|
||||
"You cannot use shell or Python to perform skills. "
|
||||
"If you need to use these capabilities, ask the user to enable Computer Use in the AstrBot WebUI -> Config."
|
||||
)
|
||||
tmgr = plugin_context.get_llm_tool_manager()
|
||||
|
||||
# sub agents integration
|
||||
@@ -708,9 +708,18 @@ def _sanitize_context_by_modalities(
|
||||
|
||||
|
||||
def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||||
"""根据事件中的插件设置,过滤请求中的工具列表。
|
||||
|
||||
注意:没有 handler_module_path 的工具(如 MCP 工具)会被保留,
|
||||
因为它们不属于任何插件,不应被插件过滤逻辑影响。
|
||||
"""
|
||||
if event.plugins_name is not None and req.func_tool:
|
||||
new_tool_set = ToolSet()
|
||||
for tool in req.func_tool.tools:
|
||||
if isinstance(tool, MCPTool):
|
||||
# 保留 MCP 工具
|
||||
new_tool_set.add_tool(tool)
|
||||
continue
|
||||
mp = tool.handler_module_path
|
||||
if not mp:
|
||||
continue
|
||||
@@ -905,8 +914,10 @@ async def build_main_agent(
|
||||
if config.llm_safety_mode:
|
||||
_apply_llm_safety_mode(config, req)
|
||||
|
||||
if config.sandbox_cfg.get("enable", False):
|
||||
if config.computer_use_runtime == "sandbox":
|
||||
_apply_sandbox_tools(config, req, req.session_id)
|
||||
elif config.computer_use_runtime == "local":
|
||||
_apply_local_env_tools(req)
|
||||
|
||||
agent_runner = AgentRunner()
|
||||
astr_agent_ctx = AstrAgentContext(
|
||||
@@ -931,7 +942,6 @@ async def build_main_agent(
|
||||
|
||||
if event.get_platform_name() == "webchat":
|
||||
asyncio.create_task(_handle_webchat(event, req, provider))
|
||||
req.system_prompt += f"\n{CHATUI_EXTRA_PROMPT}\n"
|
||||
|
||||
if req.func_tool and req.func_tool.tools:
|
||||
tool_prompt = (
|
||||
|
||||
@@ -78,9 +78,6 @@ CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT = (
|
||||
"You listen more than you speak, respect uncertainty, avoid forcing quick conclusions or grand narratives, "
|
||||
"and prefer clear, restrained language over unnecessary emotional embellishment. At your core, you value "
|
||||
"empathy, clarity, autonomy, and meaning, favoring steady, sustainable progress over judgment or dramatic leaps."
|
||||
)
|
||||
|
||||
CHATUI_EXTRA_PROMPT = (
|
||||
'When you answered, you need to add a follow up question / summarization but do not add "Follow up" words. '
|
||||
"Such as, user asked you to generate codes, you can add: Do you need me to run these codes for you?"
|
||||
)
|
||||
|
||||
@@ -35,12 +35,21 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None:
|
||||
os.remove(zip_path)
|
||||
shutil.make_archive(zip_base, "zip", skills_root)
|
||||
remote_zip = Path(SANDBOX_SKILLS_ROOT) / "skills.zip"
|
||||
logger.info("Uploading skills bundle to sandbox...")
|
||||
await booter.shell.exec(f"mkdir -p {SANDBOX_SKILLS_ROOT}")
|
||||
upload_result = await booter.upload_file(zip_path, str(remote_zip))
|
||||
if not upload_result.get("success", False):
|
||||
raise RuntimeError("Failed to upload skills bundle to sandbox.")
|
||||
# Use -n flag to never overwrite existing files, fallback to Python if unzip unavailable
|
||||
await booter.shell.exec(
|
||||
f"unzip -o {remote_zip} -d {SANDBOX_SKILLS_ROOT} && rm -f {remote_zip}"
|
||||
f"unzip -n {remote_zip} -d {SANDBOX_SKILLS_ROOT} || "
|
||||
f"python3 -c \"import zipfile, os, pathlib; z=zipfile.ZipFile('{remote_zip}'); "
|
||||
f"[z.extract(m, '{SANDBOX_SKILLS_ROOT}') for m in z.namelist() "
|
||||
f"if not os.path.exists(os.path.join('{SANDBOX_SKILLS_ROOT}', m))]\" || "
|
||||
f"python -c \"import zipfile, os, pathlib; z=zipfile.ZipFile('{remote_zip}'); "
|
||||
f"[z.extract(m, '{SANDBOX_SKILLS_ROOT}') for m in z.namelist() "
|
||||
f"if not os.path.exists(os.path.join('{SANDBOX_SKILLS_ROOT}', m))]\"; "
|
||||
f"rm -f {remote_zip}"
|
||||
)
|
||||
finally:
|
||||
if os.path.exists(zip_path):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.13.2"
|
||||
VERSION = "4.14.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -114,15 +114,17 @@ DEFAULT_CONFIG = {
|
||||
"provider": "moonshotai",
|
||||
"moonshotai_api_key": "",
|
||||
},
|
||||
"proactive_capability": {
|
||||
"add_cron_tools": True,
|
||||
},
|
||||
"computer_use_runtime": "local",
|
||||
"sandbox": {
|
||||
"enable": False,
|
||||
"booter": "shipyard",
|
||||
"shipyard_endpoint": "",
|
||||
"shipyard_access_token": "",
|
||||
"shipyard_ttl": 3600,
|
||||
"shipyard_max_sessions": 10,
|
||||
},
|
||||
"skills": {"runtime": "sandbox"},
|
||||
},
|
||||
# SubAgent orchestrator mode:
|
||||
# - main_enable = False: disabled; main LLM mounts tools normally (persona selection).
|
||||
@@ -199,6 +201,7 @@ 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,
|
||||
@@ -2221,15 +2224,12 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"skills": {
|
||||
"proactive_capability": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"enable": {
|
||||
"add_cron_tools": {
|
||||
"type": "bool",
|
||||
},
|
||||
"runtime": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2504,6 +2504,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
"persona": {
|
||||
"description": "人格",
|
||||
"hint": "",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.default_personality": {
|
||||
@@ -2519,6 +2520,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
"knowledgebase": {
|
||||
"description": "知识库",
|
||||
"hint": "",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"kb_names": {
|
||||
@@ -2551,6 +2553,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
"websearch": {
|
||||
"description": "网页搜索",
|
||||
"hint": "",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.web_search": {
|
||||
@@ -2561,6 +2564,9 @@ CONFIG_METADATA_3 = {
|
||||
"description": "网页搜索提供商",
|
||||
"type": "string",
|
||||
"options": ["default", "tavily", "baidu_ai_search"],
|
||||
"condition": {
|
||||
"provider_settings.web_search": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.websearch_tavily_key": {
|
||||
"description": "Tavily API Key",
|
||||
@@ -2569,6 +2575,7 @@ CONFIG_METADATA_3 = {
|
||||
"hint": "可添加多个 Key 进行轮询。",
|
||||
"condition": {
|
||||
"provider_settings.websearch_provider": "tavily",
|
||||
"provider_settings.web_search": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.websearch_baidu_app_builder_key": {
|
||||
@@ -2582,6 +2589,73 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.web_search_link": {
|
||||
"description": "显示来源引用",
|
||||
"type": "bool",
|
||||
"condition": {
|
||||
"provider_settings.web_search": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"agent_computer_use": {
|
||||
"description": "Agent Computer Use",
|
||||
"hint": "",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.computer_use_runtime": {
|
||||
"description": "Computer Use Runtime",
|
||||
"type": "string",
|
||||
"options": ["none", "local", "sandbox"],
|
||||
"labels": ["无", "本地", "沙箱"],
|
||||
"hint": "选择 Computer Use 运行环境。",
|
||||
},
|
||||
"provider_settings.sandbox.booter": {
|
||||
"description": "沙箱环境驱动器",
|
||||
"type": "string",
|
||||
"options": ["shipyard"],
|
||||
"labels": ["Shipyard"],
|
||||
"condition": {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_endpoint": {
|
||||
"description": "Shipyard API Endpoint",
|
||||
"type": "string",
|
||||
"hint": "Shipyard 服务的 API 访问地址。",
|
||||
"condition": {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
"_special": "check_shipyard_connection",
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_access_token": {
|
||||
"description": "Shipyard Access Token",
|
||||
"type": "string",
|
||||
"hint": "用于访问 Shipyard 服务的访问令牌。",
|
||||
"condition": {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_ttl": {
|
||||
"description": "Shipyard Session TTL",
|
||||
"type": "int",
|
||||
"hint": "Shipyard 会话的生存时间(秒)。",
|
||||
"condition": {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_max_sessions": {
|
||||
"description": "Shipyard Max Sessions",
|
||||
"type": "int",
|
||||
"hint": "Shipyard 最大会话数量。",
|
||||
"condition": {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
@@ -2619,78 +2693,15 @@ CONFIG_METADATA_3 = {
|
||||
# "provider_settings.enable": True,
|
||||
# },
|
||||
# },
|
||||
"sandbox": {
|
||||
"description": "Agent 沙箱环境",
|
||||
"hint": "",
|
||||
"proactive_capability": {
|
||||
"description": "主动型 Agent",
|
||||
"hint": "https://docs.astrbot.app/use/proactive-agent.html",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.sandbox.enable": {
|
||||
"description": "启用沙箱环境",
|
||||
"provider_settings.proactive_capability.add_cron_tools": {
|
||||
"description": "启用",
|
||||
"type": "bool",
|
||||
"hint": "启用后,Agent 可以使用沙箱环境中的工具和资源,如 Python 代码执行、Shell 等。",
|
||||
},
|
||||
"provider_settings.sandbox.booter": {
|
||||
"description": "沙箱环境驱动器",
|
||||
"type": "string",
|
||||
"options": ["shipyard"],
|
||||
"labels": ["Shipyard"],
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_endpoint": {
|
||||
"description": "Shipyard API Endpoint",
|
||||
"type": "string",
|
||||
"hint": "Shipyard 服务的 API 访问地址。",
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
"_special": "check_shipyard_connection",
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_access_token": {
|
||||
"description": "Shipyard Access Token",
|
||||
"type": "string",
|
||||
"hint": "用于访问 Shipyard 服务的访问令牌。",
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_ttl": {
|
||||
"description": "Shipyard Session TTL",
|
||||
"type": "int",
|
||||
"hint": "Shipyard 会话的生存时间(秒)。",
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_max_sessions": {
|
||||
"description": "Shipyard Max Sessions",
|
||||
"type": "int",
|
||||
"hint": "Shipyard 最大会话数量。",
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
"provider_settings.agent_runner_type": "local",
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"skills": {
|
||||
"description": "Skills",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.skills.runtime": {
|
||||
"description": "Skill Runtime",
|
||||
"type": "string",
|
||||
"options": ["local", "sandbox"],
|
||||
"labels": ["本地", "沙箱"],
|
||||
"hint": "选择 Skills 运行环境。使用沙箱时需先启用沙箱环境。",
|
||||
"hint": "启用后,将会传递给 Agent 相关工具来实现主动型 Agent。你可以告诉 AstrBot 未来某个时间要做的事情,它将被定时触发然后执行任务。",
|
||||
},
|
||||
},
|
||||
"condition": {
|
||||
@@ -2699,6 +2710,7 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"hint": "",
|
||||
"description": "上下文管理策略",
|
||||
"type": "object",
|
||||
"items": {
|
||||
|
||||
@@ -54,7 +54,6 @@ class EventBus:
|
||||
event (AstrMessageEvent): 事件对象
|
||||
|
||||
"""
|
||||
event.trace.record("event_dispatch", config_name=conf_name)
|
||||
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
||||
if event.get_sender_name():
|
||||
logger.info(
|
||||
|
||||
@@ -9,6 +9,7 @@ from astrbot.core.message.components import (
|
||||
AtAll,
|
||||
BaseMessageComponent,
|
||||
Image,
|
||||
Json,
|
||||
Plain,
|
||||
)
|
||||
|
||||
@@ -117,9 +118,26 @@ class MessageChain:
|
||||
self.use_t2i_ = use_t2i
|
||||
return self
|
||||
|
||||
def get_plain_text(self) -> str:
|
||||
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。"""
|
||||
return " ".join([comp.text for comp in self.chain if isinstance(comp, Plain)])
|
||||
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 squash_plain(self):
|
||||
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
|
||||
|
||||
@@ -92,8 +92,13 @@ class InternalAgentSubStage(Stage):
|
||||
"safety_mode_strategy", "system_prompt"
|
||||
)
|
||||
|
||||
self.computer_use_runtime = settings.get("computer_use_runtime")
|
||||
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(
|
||||
@@ -112,7 +117,9 @@ class InternalAgentSubStage(Stage):
|
||||
dequeue_context_length=self.dequeue_context_length,
|
||||
llm_safety_mode=self.llm_safety_mode,
|
||||
safety_mode_strategy=self.safety_mode_strategy,
|
||||
computer_use_runtime=self.computer_use_runtime,
|
||||
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"),
|
||||
|
||||
@@ -85,6 +85,4 @@ class PipelineScheduler:
|
||||
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
||||
await event.send(None)
|
||||
|
||||
event.trace.record("event_end")
|
||||
|
||||
logger.debug("pipeline 执行完毕。")
|
||||
|
||||
@@ -8,6 +8,7 @@ from time import time
|
||||
from typing import Any
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.agent.tool import ToolSet
|
||||
from astrbot.core.db.po import Conversation
|
||||
from astrbot.core.message.components import (
|
||||
At,
|
||||
@@ -73,9 +74,6 @@ 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
|
||||
@@ -358,6 +356,7 @@ class AstrMessageEvent(abc.ABC):
|
||||
self,
|
||||
prompt: str,
|
||||
func_tool_manager=None,
|
||||
tool_set: ToolSet | None = None,
|
||||
session_id: str = "",
|
||||
image_urls: list[str] | None = None,
|
||||
contexts: list | None = None,
|
||||
@@ -380,7 +379,7 @@ class AstrMessageEvent(abc.ABC):
|
||||
|
||||
contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。
|
||||
|
||||
func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。
|
||||
func_tool_manager: [Deprecated] 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。已过时,请使用 tool_set 参数代替。
|
||||
|
||||
conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
|
||||
|
||||
@@ -396,7 +395,8 @@ class AstrMessageEvent(abc.ABC):
|
||||
prompt=prompt,
|
||||
session_id=session_id,
|
||||
image_urls=image_urls,
|
||||
func_tool=func_tool_manager,
|
||||
# func_tool=func_tool_manager,
|
||||
func_tool=tool_set,
|
||||
contexts=contexts,
|
||||
system_prompt=system_prompt,
|
||||
conversation=conversation,
|
||||
|
||||
@@ -21,3 +21,6 @@ class PlatformMetadata:
|
||||
"""平台是否支持真实流式传输"""
|
||||
support_proactive_message: bool = True
|
||||
"""平台是否支持主动消息推送(非用户触发)"""
|
||||
|
||||
module_path: str | None = None
|
||||
"""注册该适配器的模块路径,用于插件热重载时清理"""
|
||||
|
||||
@@ -37,6 +37,9 @@ def register_platform_adapter(
|
||||
if "id" not in default_config_tmpl:
|
||||
default_config_tmpl["id"] = adapter_name
|
||||
|
||||
# Get the module path of the class being decorated
|
||||
module_path = cls.__module__
|
||||
|
||||
pm = PlatformMetadata(
|
||||
name=adapter_name,
|
||||
description=desc,
|
||||
@@ -45,6 +48,7 @@ def register_platform_adapter(
|
||||
adapter_display_name=adapter_display_name,
|
||||
logo_path=logo_path,
|
||||
support_streaming_message=support_streaming_message,
|
||||
module_path=module_path,
|
||||
)
|
||||
platform_registry.append(pm)
|
||||
platform_cls_map[adapter_name] = cls
|
||||
@@ -52,3 +56,31 @@ def register_platform_adapter(
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str]:
|
||||
"""根据模块路径前缀注销平台适配器。
|
||||
|
||||
在插件热重载时调用,用于清理该插件注册的所有平台适配器。
|
||||
|
||||
Args:
|
||||
module_path_prefix: 模块路径前缀,如 "data.plugins.my_plugin"
|
||||
|
||||
Returns:
|
||||
被注销的平台适配器名称列表
|
||||
"""
|
||||
unregistered = []
|
||||
to_remove = []
|
||||
|
||||
for pm in platform_registry:
|
||||
if pm.module_path and pm.module_path.startswith(module_path_prefix):
|
||||
to_remove.append(pm)
|
||||
unregistered.append(pm.name)
|
||||
|
||||
for pm in to_remove:
|
||||
platform_registry.remove(pm)
|
||||
if pm.name in platform_cls_map:
|
||||
del platform_cls_map[pm.name]
|
||||
logger.debug(f"平台适配器 {pm.name} 已注销 (来自模块 {pm.module_path})")
|
||||
|
||||
return unregistered
|
||||
|
||||
@@ -37,9 +37,9 @@ class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
|
||||
class CustomFilterOr(CustomFilter):
|
||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||
super().__init__()
|
||||
if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
|
||||
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||
raise ValueError(
|
||||
"CustomFilter lass can only operate with other CustomFilter.",
|
||||
"CustomFilter class can only operate with other CustomFilter.",
|
||||
)
|
||||
self.filter1 = filter1
|
||||
self.filter2 = filter2
|
||||
@@ -51,7 +51,7 @@ class CustomFilterOr(CustomFilter):
|
||||
class CustomFilterAnd(CustomFilter):
|
||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||
super().__init__()
|
||||
if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
|
||||
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||
raise ValueError(
|
||||
"CustomFilter lass can only operate with other CustomFilter.",
|
||||
)
|
||||
|
||||
@@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs):
|
||||
if args:
|
||||
raise_error = args[0]
|
||||
|
||||
if not isinstance(custom_filter, CustomFilterAnd | CustomFilterOr):
|
||||
if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
|
||||
custom_filter = custom_filter(raise_error)
|
||||
|
||||
def decorator(awaitable):
|
||||
|
||||
@@ -15,6 +15,7 @@ import yaml
|
||||
from astrbot.core import logger, pip_installer, sp
|
||||
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.platform.register import unregister_platform_adapters_by_module
|
||||
from astrbot.core.provider.register import llm_tools
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_config_path,
|
||||
@@ -842,6 +843,18 @@ class PluginManager:
|
||||
for func_tool in to_remove:
|
||||
llm_tools.func_list.remove(func_tool)
|
||||
|
||||
# Unregister platform adapters registered by this plugin
|
||||
# module_path is like "data.plugins.my_plugin.main", extract prefix like "data.plugins.my_plugin"
|
||||
module_prefix = ".".join(plugin_module_path.split(".")[:-1])
|
||||
if module_prefix:
|
||||
unregistered_adapters = unregister_platform_adapters_by_module(
|
||||
module_prefix
|
||||
)
|
||||
for adapter_name in unregistered_adapters:
|
||||
logger.info(
|
||||
f"移除了插件 {plugin_name} 的平台适配器 {adapter_name}",
|
||||
)
|
||||
|
||||
if plugin is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ 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",
|
||||
|
||||
@@ -31,6 +31,16 @@ 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
|
||||
@@ -106,3 +116,29 @@ 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__
|
||||
|
||||
@@ -315,6 +315,17 @@ class PluginRoute(Route):
|
||||
"display_name": plugin.display_name,
|
||||
"logo": f"/api/file/{logo_url}" if logo_url else None,
|
||||
}
|
||||
# 检查是否为全空的幽灵插件
|
||||
if not any(
|
||||
[
|
||||
plugin.name,
|
||||
plugin.author,
|
||||
plugin.desc,
|
||||
plugin.version,
|
||||
plugin.display_name,
|
||||
]
|
||||
):
|
||||
continue
|
||||
_plugin_resp.append(_t)
|
||||
return (
|
||||
Response()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,14 +24,22 @@ class SkillsRoute(Route):
|
||||
|
||||
async def get_skills(self):
|
||||
try:
|
||||
cfg = self.core_lifecycle.astrbot_config.get("provider_settings", {}).get(
|
||||
"skills", {}
|
||||
provider_settings = self.core_lifecycle.astrbot_config.get(
|
||||
"provider_settings", {}
|
||||
)
|
||||
runtime = cfg.get("runtime", "local")
|
||||
runtime = provider_settings.get("computer_use_runtime", "local")
|
||||
skills = SkillManager().list_skills(
|
||||
active_only=False, runtime=runtime, show_sandbox_path=False
|
||||
)
|
||||
return Response().ok([skill.__dict__ for skill in skills]).__dict__
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"skills": [skill.__dict__ for skill in skills],
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
@@ -60,41 +67,9 @@ class SkillsRoute(Route):
|
||||
temp_path = os.path.join(temp_dir, filename)
|
||||
await file.save(temp_path)
|
||||
|
||||
cfg = self.core_lifecycle.astrbot_config.get("provider_settings", {}).get(
|
||||
"skills", {}
|
||||
)
|
||||
runtime = cfg.get("runtime", "local")
|
||||
if runtime == "sandbox":
|
||||
sandbox_enabled = (
|
||||
self.core_lifecycle.astrbot_config.get("provider_settings", {})
|
||||
.get("sandbox", {})
|
||||
.get("enable", False)
|
||||
)
|
||||
if not sandbox_enabled:
|
||||
return (
|
||||
Response()
|
||||
.error(
|
||||
"Sandbox is not enabled. Please enable sandbox before using sandbox runtime."
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
skill_mgr = SkillManager()
|
||||
skill_name = skill_mgr.install_skill_from_zip(temp_path, overwrite=True)
|
||||
|
||||
if runtime == "sandbox":
|
||||
sb = await get_booter(self.core_lifecycle.star_context, "skills-upload")
|
||||
remote_root = "/home/shared/skills"
|
||||
remote_zip = f"{remote_root}/{skill_name}.zip"
|
||||
await sb.shell.exec(f"mkdir -p {remote_root}")
|
||||
upload_result = await sb.upload_file(temp_path, remote_zip)
|
||||
if not upload_result.get("success", False):
|
||||
return (
|
||||
Response().error("Failed to upload skill to sandbox").__dict__
|
||||
)
|
||||
await sb.shell.exec(
|
||||
f"unzip -o {remote_zip} -d {remote_root} && rm -f {remote_zip}"
|
||||
)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok({"name": skill_name}, "Skill uploaded successfully.")
|
||||
|
||||
@@ -130,19 +130,25 @@ class ToolsRoute(Route):
|
||||
server_data = await request.json
|
||||
|
||||
name = server_data.get("name", "")
|
||||
old_name = server_data.get("oldName") or name
|
||||
|
||||
if not name:
|
||||
return Response().error("服务器名称不能为空").__dict__
|
||||
|
||||
config = self.tool_mgr.load_mcp_config()
|
||||
|
||||
if name not in config["mcpServers"]:
|
||||
return Response().error(f"服务器 {name} 不存在").__dict__
|
||||
if old_name not in config["mcpServers"]:
|
||||
return Response().error(f"服务器 {old_name} 不存在").__dict__
|
||||
|
||||
is_rename = name != old_name
|
||||
|
||||
if name in config["mcpServers"] and is_rename:
|
||||
return Response().error(f"服务器 {name} 已存在").__dict__
|
||||
|
||||
# 获取活动状态
|
||||
active = server_data.get(
|
||||
"active",
|
||||
config["mcpServers"][name].get("active", True),
|
||||
config["mcpServers"][old_name].get("active", True),
|
||||
)
|
||||
|
||||
# 创建新的配置对象
|
||||
@@ -153,7 +159,13 @@ class ToolsRoute(Route):
|
||||
|
||||
# 复制所有配置字段
|
||||
for key, value in server_data.items():
|
||||
if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段
|
||||
if key not in [
|
||||
"name",
|
||||
"active",
|
||||
"tools",
|
||||
"errlogs",
|
||||
"oldName",
|
||||
]: # 排除特殊字段
|
||||
if key == "mcpServers":
|
||||
key_0 = list(server_data["mcpServers"].keys())[
|
||||
0
|
||||
@@ -165,29 +177,42 @@ class ToolsRoute(Route):
|
||||
|
||||
# 如果只更新活动状态,保留原始配置
|
||||
if only_update_active:
|
||||
for key, value in config["mcpServers"][name].items():
|
||||
for key, value in config["mcpServers"][old_name].items():
|
||||
if key != "active": # 除了active之外的所有字段都保留
|
||||
server_config[key] = value
|
||||
|
||||
config["mcpServers"][name] = server_config
|
||||
# config["mcpServers"][name] = server_config
|
||||
if is_rename:
|
||||
config["mcpServers"].pop(old_name)
|
||||
config["mcpServers"][name] = server_config
|
||||
else:
|
||||
config["mcpServers"][name] = server_config
|
||||
|
||||
if self.tool_mgr.save_mcp_config(config):
|
||||
# 处理MCP客户端状态变化
|
||||
if active:
|
||||
if name in self.tool_mgr.mcp_client_dict or not only_update_active:
|
||||
if (
|
||||
old_name in self.tool_mgr.mcp_client_dict
|
||||
or not only_update_active
|
||||
or is_rename
|
||||
):
|
||||
try:
|
||||
await self.tool_mgr.disable_mcp_server(name, timeout=10)
|
||||
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
|
||||
except TimeoutError as e:
|
||||
return (
|
||||
Response()
|
||||
.error(f"启用前停用 MCP 服务器时 {name} 超时: {e!s}")
|
||||
.error(
|
||||
f"启用前停用 MCP 服务器时 {old_name} 超时: {e!s}"
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return (
|
||||
Response()
|
||||
.error(f"启用前停用 MCP 服务器时 {name} 失败: {e!s}")
|
||||
.error(
|
||||
f"启用前停用 MCP 服务器时 {old_name} 失败: {e!s}"
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
try:
|
||||
@@ -208,18 +233,20 @@ class ToolsRoute(Route):
|
||||
.__dict__
|
||||
)
|
||||
# 如果要停用服务器
|
||||
elif name in self.tool_mgr.mcp_client_dict:
|
||||
elif old_name in self.tool_mgr.mcp_client_dict:
|
||||
try:
|
||||
await self.tool_mgr.disable_mcp_server(name, timeout=10)
|
||||
await self.tool_mgr.disable_mcp_server(old_name, timeout=10)
|
||||
except TimeoutError:
|
||||
return (
|
||||
Response().error(f"停用 MCP 服务器 {name} 超时。").__dict__
|
||||
Response()
|
||||
.error(f"停用 MCP 服务器 {old_name} 超时。")
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return (
|
||||
Response()
|
||||
.error(f"停用 MCP 服务器 {name} 失败: {e!s}")
|
||||
.error(f"停用 MCP 服务器 {old_name} 失败: {e!s}")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
## What's Changed - BIG AND BEAUTIFUL VERSION
|
||||
|
||||
> 如果在之前版本使用了 Skill,这次更新之后**需要重新配置** Skill Runtime 相关选项。
|
||||
|
||||
### 新增
|
||||
- 🔥 新增未来任务系统(Future Tasks)。给 AstrBot 布置的未来任务,让 AstrBot 能够在某一时刻自动唤醒,帮你完成任务。详见 [主动任务](https://docs.astrbot.app/use/proactive-agent.html) 。(实验性) ([#4697](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||
- 🔥 新增子代理(SubAgent)编排器。(实验性)([#4697](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||
- 🔥 AstrBot 目前可以直接通过调用 tool 将图片 / 文件推送给用户,大大提高交互效果。
|
||||
- 新增 Computer Use 运行时配置,以融合 Skill 和 Sandbox 配置 ([#4831](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||
- 新增主题自定义功能,可设置主色与辅色
|
||||
- 支持在配置页下人格对话框的编辑人格 ([#4826](https://github.com/AstrBotDevs/AstrBot/issues/4826))
|
||||
- 支持开关 “追踪” 功能;支持在系统配置中设置是否将日志写入 log 文件 ([#4822](https://github.com/AstrBotDevs/AstrBot/issues/4822))
|
||||
|
||||
### 修复
|
||||
- ‼️ 修复 ChatUI 图片、思考等显示异常问题。
|
||||
- ‼️ 修复 Skill 上传到 Sandbox 后未自动解压导致 Agent 无法读取的问题。
|
||||
- ‼️ 修复配置特定插件集时 MCP 工具被过滤的问题 ([#4825](https://github.com/AstrBotDevs/AstrBot/issues/4825))
|
||||
- ‼️ 移除 ChatUI 自带的让 LLM 最后提出问题的 prompt ([#4824](https://github.com/AstrBotDevs/AstrBot/issues/4824))
|
||||
- ‼️ 修复 WebUI 在上传 Skill 失败后仍显示成功消息的 bug ([#4768](https://github.com/AstrBotDevs/AstrBot/issues/4768))
|
||||
- 修复 MCP 服务器无法重命名的问题 ([#4766](https://github.com/AstrBotDevs/AstrBot/issues/4766))
|
||||
- 修复插件的 tool 无法在 WebUI 管理行为中看到来源的问题 ([#4776](https://github.com/AstrBotDevs/AstrBot/issues/4776))
|
||||
- ‼️ 修复 skill-like 的 tool 模式下,调用 tool 失败的问题 ([#4775](https://github.com/AstrBotDevs/AstrBot/issues/4775))
|
||||
|
||||
### 优化
|
||||
|
||||
- WebUI 整体 UI 效果优化
|
||||
- 部分 Dialog 标题样式统一
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
- Introduce CronJob system with one-time tasks and enhanced dashboard management
|
||||
- Add theme customization with primary & secondary color options
|
||||
- Add computer-use runtime config for skills sandbox execution ([#4831](https://github.com/AstrBotDevs/AstrBot/issues/4831))
|
||||
- Add edit button to persona selector dialog ([#4826](https://github.com/AstrBotDevs/AstrBot/issues/4826))
|
||||
- Add trace logging toggle and configuration UI ([#4822](https://github.com/AstrBotDevs/AstrBot/issues/4822))
|
||||
- Add proactive-messaging capability with cron-tool trigger
|
||||
- Implement SubAgent orchestrator with configurable tool-management policies
|
||||
- Support resolving sandbox file paths and auto-download when necessary
|
||||
- Add embedded image & audio styles in MessagePartsRenderer
|
||||
- Introduce i18n foundation
|
||||
- Persist agent-interaction history
|
||||
- Add user notifications for file-download success/removal
|
||||
|
||||
### Bug Fixes
|
||||
- Improve ghost-plugin detection accuracy
|
||||
- Add error handling to prevent ghost-plugin crashes
|
||||
- Prevent skills bundle from overwriting existing files
|
||||
- Fix skills bundle unzip failure inside sandbox
|
||||
- Fix MCP tools being filtered when specific plugin set configured ([#4825](https://github.com/AstrBotDevs/AstrBot/issues/4825))
|
||||
- Merge ChatUI persona pop-up into default persona ([#4824](https://github.com/AstrBotDevs/AstrBot/issues/4824))
|
||||
- Fix reasoning block style
|
||||
- Add missing comma in truncate_and_compress hint
|
||||
- Fix frontend still showing success message ([#4768](https://github.com/AstrBotDevs/AstrBot/issues/4768))
|
||||
- Fix unable to rename MCP server ([#4766](https://github.com/AstrBotDevs/AstrBot/issues/4766))
|
||||
- Remove leftover sandbox runtime handling in skill upload ([#4798](https://github.com/AstrBotDevs/AstrBot/issues/4798))
|
||||
- Fix handler module path construction ([#4776](https://github.com/AstrBotDevs/AstrBot/issues/4776))
|
||||
- Fix skill-like tool invocation error ([#4775](https://github.com/AstrBotDevs/AstrBot/issues/4775))
|
||||
|
||||
### Improvements
|
||||
- Runtime hints & refined UI in skills management
|
||||
- Performance and UX improvements on cron-job page
|
||||
- General WebUI performance boost
|
||||
- Group tools by plugin in dropdown
|
||||
- Consistent dialog titles with padding and text styles
|
||||
- Code formatting unified (ruff format)
|
||||
- Bump version to 4.13.2
|
||||
|
||||
### Others
|
||||
- Remove obsolete reminder code
|
||||
- Extract main-agent module for better architecture
|
||||
- Merge AstrBot_skill branch changes
|
||||
@@ -0,0 +1,7 @@
|
||||
## What's Changed - BIG AND BEAUTIFUL VERSION
|
||||
|
||||
hotfix of v4.14.0
|
||||
|
||||
fixes:
|
||||
|
||||
- 由 `event.request_llm()` 过时导致的群聊上下文感知-主动回复功能可能不可用的问题
|
||||
@@ -0,0 +1,23 @@
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
- 控制台页面新增调试提示和本地化文件 ([#4852](https://github.com/AstrBotDevs/AstrBot/pull/4852))
|
||||
|
||||
### 修复
|
||||
- 修复插件热重载时平台适配器未清理导致注册冲突的问题 ([#4859](https://github.com/AstrBotDevs/AstrBot/pull/4859))
|
||||
|
||||
### 其他
|
||||
- 更新 ruff 版本至 0.15.0
|
||||
- 新增 robots.txt ([#4847](https://github.com/AstrBotDevs/AstrBot/pull/4847))
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
- Add debug hint to console page and localization files ([#4852](https://github.com/AstrBotDevs/AstrBot/pull/4852))
|
||||
|
||||
### Bug Fixes
|
||||
- Fix platform adapter not being cleaned up during plugin hot reload, causing registration conflicts ([#4859](https://github.com/AstrBotDevs/AstrBot/pull/4859))
|
||||
|
||||
### Others
|
||||
- Update ruff version to 0.15.0
|
||||
- Add robots.txt ([#4847](https://github.com/AstrBotDevs/AstrBot/pull/4847))
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="keywords" content="AstrBot Soulter" />
|
||||
<meta name="description" content="AstrBot Dashboard" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"markdown-it": "^14.1.0",
|
||||
"markstream-vue": "^0.0.6",
|
||||
"mermaid": "^11.12.2",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "2.1.6",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"remixicon": "3.5.0",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -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;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<v-container fluid class="pa-0" elevation="0">
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<v-btn color="success" prepend-icon="mdi-upload" class="me-2" variant="tonal"
|
||||
@click="uploadDialog = true">
|
||||
<v-btn color="success" prepend-icon="mdi-upload" class="me-2" variant="tonal" @click="uploadDialog = true">
|
||||
{{ tm('skills.upload') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchSkills">
|
||||
@@ -13,6 +12,10 @@
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<div class="px-2 pb-2">
|
||||
<small style="color: grey;">{{ tm('skills.runtimeHint') }}</small>
|
||||
</div>
|
||||
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<div v-else-if="skills.length === 0" class="text-center pa-8">
|
||||
@@ -40,13 +43,13 @@
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog v-model="uploadDialog" max-width="520px" persistent>
|
||||
<v-dialog v-model="uploadDialog" max-width="520px">
|
||||
<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"
|
||||
variant="outlined" class="mt-4" :multiple="false" />
|
||||
<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">
|
||||
<v-btn variant="text" @click="uploadDialog = false">{{ tm('skills.cancel') }}</v-btn>
|
||||
@@ -110,7 +113,12 @@ export default {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await axios.get("/api/skills");
|
||||
skills.value = res.data.data || [];
|
||||
const payload = res.data?.data || [];
|
||||
if (Array.isArray(payload)) {
|
||||
skills.value = payload;
|
||||
} else {
|
||||
skills.value = payload.skills || [];
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage(tm("skills.loadFailed"), "error");
|
||||
} finally {
|
||||
|
||||
@@ -119,8 +119,17 @@
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="selectedItemId === getItemId(item)"
|
||||
color="primary" size="22">mdi-check-circle</v-icon>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn v-if="showEditButton && !isDefaultItem(item)"
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="handleEditItem(item)"
|
||||
:title="labels.editButton || 'Edit'"
|
||||
/>
|
||||
<v-icon v-if="selectedItemId === getItemId(item)"
|
||||
color="primary" size="22">mdi-check-circle</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -197,6 +206,11 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示编辑按钮
|
||||
showEditButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 默认项(如 "默认人格")
|
||||
defaultItem: {
|
||||
type: Object as PropType<SelectableItem | null>,
|
||||
@@ -221,7 +235,7 @@ export default defineComponent({
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'navigate', 'create'],
|
||||
emits: ['update:modelValue', 'navigate', 'create', 'edit'],
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
@@ -370,6 +384,17 @@ export default defineComponent({
|
||||
cancelSelection() {
|
||||
this.selectedItemId = this.modelValue || '';
|
||||
this.dialog = false;
|
||||
},
|
||||
|
||||
isDefaultItem(item: SelectableItem): boolean {
|
||||
if (this.defaultItem === null) {
|
||||
return false;
|
||||
}
|
||||
return this.getItemId(item) === this.getItemId(this.defaultItem);
|
||||
},
|
||||
|
||||
handleEditItem(item: SelectableItem) {
|
||||
this.$emit('edit', item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -241,6 +241,7 @@ export interface FolderItemSelectorLabels {
|
||||
|
||||
// 按钮
|
||||
createButton?: string;
|
||||
editButton?: string;
|
||||
confirmButton?: string;
|
||||
cancelButton?: string;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ConfigItemRenderer from './ConfigItemRenderer.vue'
|
||||
@@ -24,12 +25,23 @@ const props = defineProps({
|
||||
const { t } = useI18n()
|
||||
const { tm, getRaw } = useModuleI18n('features/config-metadata')
|
||||
|
||||
const hintMarkdown = new MarkdownIt({
|
||||
linkify: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
// 翻译器函数 - 如果是国际化键则翻译,否则原样返回
|
||||
const translateIfKey = (value) => {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return tm(value)
|
||||
}
|
||||
|
||||
const renderHint = (value) => {
|
||||
const text = translateIfKey(value)
|
||||
if (!text) return ''
|
||||
return hintMarkdown.renderInline(text)
|
||||
}
|
||||
|
||||
// 处理labels翻译 - labels可以是数组或国际化键
|
||||
const getTranslatedLabels = (itemMeta) => {
|
||||
if (!itemMeta?.labels) return null
|
||||
@@ -185,7 +197,7 @@ function getSpecialSubtype(value) {
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ translateIfKey(metadata[metadataKey]?.hint) }}
|
||||
<span v-html="renderHint(metadata[metadataKey]?.hint)"></span>
|
||||
</v-list-item-subtitle>
|
||||
</v-card-text>
|
||||
|
||||
@@ -205,7 +217,7 @@ function getSpecialSubtype(value) {
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint">‼️</span>
|
||||
{{ translateIfKey(itemMeta?.hint) }}
|
||||
<span v-html="renderHint(itemMeta?.hint)"></span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
@@ -293,6 +305,12 @@ function getSpecialSubtype(value) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.config-hint :deep(a),
|
||||
.property-hint :deep(a) {
|
||||
color: var(--v-theme-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.metadata-key,
|
||||
.property-key {
|
||||
font-size: 0.85em;
|
||||
|
||||
@@ -530,8 +530,13 @@ export default {
|
||||
try {
|
||||
const response = await axios.get('/api/skills');
|
||||
if (response.data.status === 'ok') {
|
||||
const skills = response.data.data || [];
|
||||
this.availableSkills = skills.filter(skill => skill.active !== false);
|
||||
const payload = response.data.data || [];
|
||||
if (Array.isArray(payload)) {
|
||||
this.availableSkills = payload.filter(skill => skill.active !== false);
|
||||
} else {
|
||||
const skills = payload.skills || [];
|
||||
this.availableSkills = skills.filter(skill => skill.active !== false);
|
||||
}
|
||||
} else {
|
||||
this.$emit('error', response.data.message || 'Failed to load skills');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:items-loading="itemsLoading"
|
||||
:labels="labels"
|
||||
:show-create-button="true"
|
||||
:show-edit-button="true"
|
||||
:default-item="defaultPersona"
|
||||
item-id-field="persona_id"
|
||||
item-name-field="persona_id"
|
||||
@@ -15,15 +16,16 @@
|
||||
:display-value-formatter="formatDisplayValue"
|
||||
@navigate="handleNavigate"
|
||||
@create="openCreatePersona"
|
||||
@edit="openEditPersona"
|
||||
/>
|
||||
|
||||
<!-- 创建人格对话框 -->
|
||||
<!-- 创建/编辑人格对话框 -->
|
||||
<PersonaForm
|
||||
v-model="showCreateDialog"
|
||||
:editing-persona="undefined"
|
||||
v-model="showPersonaDialog"
|
||||
:editing-persona="editingPersona ?? undefined"
|
||||
:current-folder-id="currentFolderId ?? undefined"
|
||||
:current-folder-name="currentFolderName ?? undefined"
|
||||
@saved="handlePersonaCreated"
|
||||
@saved="handlePersonaSaved"
|
||||
@error="handleError" />
|
||||
</template>
|
||||
|
||||
@@ -62,7 +64,8 @@ const folderTree = ref<FolderTreeNode[]>([])
|
||||
const currentPersonas = ref<Persona[]>([])
|
||||
const treeLoading = ref(false)
|
||||
const itemsLoading = ref(false)
|
||||
const showCreateDialog = ref(false)
|
||||
const showPersonaDialog = ref(false)
|
||||
const editingPersona = ref<Persona | null>(null)
|
||||
const currentFolderId = ref<string | null>(null)
|
||||
|
||||
// 默认人格
|
||||
@@ -104,6 +107,7 @@ const labels = computed(() => ({
|
||||
defaultItem: tm('personaSelector.defaultPersona'),
|
||||
noDescription: tm('personaSelector.noDescription'),
|
||||
createButton: tm('personaSelector.createPersona'),
|
||||
editButton: tm('personaSelector.editPersona') || 'Edit',
|
||||
confirmButton: t('core.common.confirm'),
|
||||
cancelButton: t('core.common.cancel'),
|
||||
rootFolder: tm('personaSelector.rootFolder') || '全部人格',
|
||||
@@ -171,13 +175,21 @@ async function handleNavigate(folderId: string | null) {
|
||||
|
||||
// 打开创建人格对话框
|
||||
function openCreatePersona() {
|
||||
showCreateDialog.value = true
|
||||
editingPersona.value = null
|
||||
showPersonaDialog.value = true
|
||||
}
|
||||
|
||||
// 人格创建成功
|
||||
async function handlePersonaCreated(message: string) {
|
||||
console.log('人格创建成功:', message)
|
||||
showCreateDialog.value = false
|
||||
// 打开编辑人格对话框
|
||||
function openEditPersona(persona: Persona) {
|
||||
editingPersona.value = persona
|
||||
showPersonaDialog.value = true
|
||||
}
|
||||
|
||||
// 人格保存成功(创建或编辑)
|
||||
async function handlePersonaSaved(message: string) {
|
||||
console.log('人格保存成功:', message)
|
||||
showPersonaDialog.value = false
|
||||
editingPersona.value = null
|
||||
// 刷新当前文件夹的人格列表
|
||||
await loadPersonasInFolder(currentFolderId.value)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"chat": "Chat",
|
||||
"cron": "Future Tasks",
|
||||
"extension": "Extensions",
|
||||
"extensionTabs": {
|
||||
"installed": "AstrBot Plugins",
|
||||
"market": "Plugin Market",
|
||||
"mcp": "MCP Servers",
|
||||
"skills": "Skills",
|
||||
"components": "Handlers"
|
||||
},
|
||||
"conversation": "Conversations",
|
||||
"sessionManagement": "Custom Rules",
|
||||
"console": "Console",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"websearch_baidu_app_builder_key": {
|
||||
"description": "Baidu Qianfan Smart Cloud APP Builder API Key",
|
||||
"hint": "Reference: https://console.bce.baidu.com/iam/#/iam/apikey/list"
|
||||
"hint": "Reference: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "Display Source Citations"
|
||||
@@ -133,15 +133,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
"description": "Agent Sandbox Env(Beta)",
|
||||
"hint": "https://docs.astrbot.app/en/use/astrbot-agent-sandbox.html",
|
||||
"agent_computer_use": {
|
||||
"description": "Agent Computer Use",
|
||||
"hint": "Allows the AstrBot to access and use your computer or an sandbox environment to perform more complex tasks. See [Sandbox Mode](https://docs.astrbot.app/use/astrbot-agent-sandbox.html), [Skills](https://docs.astrbot.app/use/skills.html)",
|
||||
"provider_settings": {
|
||||
"computer_use_runtime": {
|
||||
"description": "Computer Use Runtime",
|
||||
"hint": "sandbox means running in a sandbox environment, local means running in a local environment, none means disabling Computer Use. If skills are uploaded, choosing none will cause them to not be usable by the Agent."
|
||||
},
|
||||
"sandbox": {
|
||||
"enable": {
|
||||
"description": "Enable Sandbox Env",
|
||||
"hint": "When enabled, Agent can use tools and resources in the sandbox environment, such as Python tool, Shell, etc."
|
||||
},
|
||||
"booter": {
|
||||
"description": "Sandbox Environment Driver"
|
||||
},
|
||||
@@ -164,18 +164,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "Skills",
|
||||
"proactive_capability": {
|
||||
"description": "Proactive Agent",
|
||||
"hint": "AstrBot will wake up, run your tasks, and deliver the results to you. See [Proactive Agent](https://docs.astrbot.app/en/use/proactive-agent.html)",
|
||||
"provider_settings": {
|
||||
"skills": {
|
||||
"runtime": {
|
||||
"description": "Skill Runtime",
|
||||
"hint": "Select the runtime for Skills. Sandbox runtime requires sandbox to be enabled first. In local mode, the Agent CAN FULLY ACCESS the runtime environment through Shell and Python tools, but non-admin users will be automatically prohibited from using it to ensure security."
|
||||
"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": "[Context Management](https://docs.astrbot.app/en/use/context-compress.html)",
|
||||
"description": "Context Management Strategy",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
@@ -426,7 +428,7 @@
|
||||
},
|
||||
"emojis": {
|
||||
"description": "Emoji List (Lark Emoji Enum Names)",
|
||||
"hint": "Emoji enum names reference: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce"
|
||||
"hint": "Emoji enum names reference: [https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -437,7 +439,7 @@
|
||||
},
|
||||
"emojis": {
|
||||
"description": "Emoji List (Unicode)",
|
||||
"hint": "Telegram only supports a fixed reaction set, reference: https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9"
|
||||
"hint": "Telegram only supports a fixed reaction set, reference: [https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -594,15 +596,15 @@
|
||||
},
|
||||
"pypi_index_url": {
|
||||
"description": "PyPI Repository URL",
|
||||
"hint": "PyPI repository URL for installing Python dependencies. Defaults to https://mirrors.aliyun.com/pypi/simple/"
|
||||
"hint": "PyPI repository URL for installing Python dependencies. Defaults to [https://mirrors.aliyun.com/pypi/simple/](https://mirrors.aliyun.com/pypi/simple/)"
|
||||
},
|
||||
"callback_api_base": {
|
||||
"description": "Externally Accessible Callback API Address",
|
||||
"hint": "External services may access AstrBot's backend through callback links generated by AstrBot (such as file download links). Since AstrBot cannot automatically determine the externally accessible host address in the deployment environment, this configuration item is needed to explicitly specify how external services should access AstrBot's address. Examples: http://localhost:6185, https://example.com, etc."
|
||||
"hint": "External services may access AstrBot's backend through callback links generated by AstrBot (such as file download links). Since AstrBot cannot automatically determine the externally accessible host address in the deployment environment, this configuration item is needed to explicitly specify how external services should access AstrBot's address. Examples: [http://localhost:6185](http://localhost:6185), [https://example.com](https://example.com), etc."
|
||||
},
|
||||
"timezone": {
|
||||
"description": "Timezone",
|
||||
"hint": "Timezone setting. Please enter an IANA timezone name, such as Asia/Shanghai. Uses system default timezone when empty. For all timezones, see: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab"
|
||||
"hint": "Timezone setting. Please enter an IANA timezone name, such as Asia/Shanghai. Uses system default timezone when empty. For all timezones, see: [https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab](https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab)"
|
||||
},
|
||||
"http_proxy": {
|
||||
"description": "HTTP Proxy",
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
"mirrorLabel": "Force PyPI repository URL (optional)",
|
||||
"mirrorHint": "Force PyPI repository URL > Config item `PyPI Repository Address`",
|
||||
"installButton": "Install"
|
||||
},
|
||||
"debugHint": {
|
||||
"text": "Debug logs can be enabled in \"Configuration File → System → Console Log Level\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "Future Task Management",
|
||||
"beta": "Beta",
|
||||
"beta": "Experimental",
|
||||
"subtitle": "See scheduled tasks for AstrBot. AstrBot will wake up, run them, and deliver the results.",
|
||||
"proactive": {
|
||||
"supported": "Proactive delivery is available on: {platforms}",
|
||||
@@ -22,6 +22,7 @@
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"cron": "Cron",
|
||||
"session": "Session ID",
|
||||
"nextRun": "Next Run",
|
||||
"lastRun": "Last Run",
|
||||
"note": "Note",
|
||||
@@ -39,6 +40,7 @@
|
||||
},
|
||||
"form": {
|
||||
"title": "New Task",
|
||||
"chatHint": "You can ask AstrBot in chat to create future tasks instead of adding them here.",
|
||||
"runOnce": "One-off task",
|
||||
"name": "Task name",
|
||||
"note": "Task description",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"title": "Extension Management",
|
||||
"subtitle": "Manage and configure system extensions",
|
||||
"tabs": {
|
||||
"installedPlugins": "Installed Plugins",
|
||||
"installedMcpServers": "Installed MCP Servers",
|
||||
"installedPlugins": "AstrBot Plugins",
|
||||
"installedMcpServers": "MCP",
|
||||
"skills": "Skills",
|
||||
"handlersOperation": "Manage Handlers",
|
||||
"market": "Extension Market"
|
||||
"market": "AstrBot Plugin Market"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search extensions...",
|
||||
@@ -210,7 +210,9 @@
|
||||
"deleteTitle": "Delete confirmation",
|
||||
"deleteMessage": "Are you sure you want to delete this Skill?",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"deleteFailed": "Delete failed"
|
||||
"deleteFailed": "Delete failed",
|
||||
"runtimeNoneWarning": "Computer Use runtime is set to None; Skills may not run correctly because no runtime is enabled.",
|
||||
"runtimeHint": "Set the Computer Use runtime to Local or Sandbox in settings so AstrBot can use your Skills."
|
||||
},
|
||||
"card": {
|
||||
"actions": {
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"loadingSkills": "Loading skills...",
|
||||
"allSkillsAvailable": "Use all available Skills",
|
||||
"noSkillsSelected": "No skills selected",
|
||||
"skillsRuntimeNoneWarning": "Computer Use runtime is set to None; Skills may not run correctly because no runtime is enabled.",
|
||||
"createInFolder": "Will be created in \"{folder}\"",
|
||||
"rootFolder": "All Personas"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"subtitle": "Customize theme primary and secondary colors. Changes apply immediately and are stored locally in your browser.",
|
||||
"customize": {
|
||||
"title": "Theme Colors",
|
||||
"primary": "Primary Color",
|
||||
"secondary": "Secondary Color",
|
||||
"reset": "Reset to Default"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "System",
|
||||
"restart": {
|
||||
@@ -119,4 +129,4 @@
|
||||
"ftpHint": "For large backup files, you can also upload directly to the data/backups directory via FTP/SFTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "SubAgent Orchestration",
|
||||
"beta": "Beta",
|
||||
"beta": "Experimental",
|
||||
"subtitle": "The main LLM only chats and delegates; tools live on individual SubAgents."
|
||||
},
|
||||
"actions": {
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
"subagent": "SubAgent 编排",
|
||||
"toolUse": "MCP",
|
||||
"extension": "插件",
|
||||
"extensionTabs": {
|
||||
"installed": "AstrBot 插件",
|
||||
"market": "插件市场",
|
||||
"mcp": "MCP",
|
||||
"skills": "Skills",
|
||||
"components": "管理行为"
|
||||
},
|
||||
"config": "配置文件",
|
||||
"chat": "聊天",
|
||||
"cron": "未来任务",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
},
|
||||
"persona": {
|
||||
"description": "人格",
|
||||
"hint": "赋予 AstrBot 人格。",
|
||||
"provider_settings": {
|
||||
"default_personality": {
|
||||
"description": "默认采用的人格"
|
||||
@@ -78,6 +79,7 @@
|
||||
},
|
||||
"knowledgebase": {
|
||||
"description": "知识库",
|
||||
"hint": "AstrBot 的 “外置大脑”。",
|
||||
"kb_names": {
|
||||
"description": "知识库列表",
|
||||
"hint": "支持多选"
|
||||
@@ -97,6 +99,7 @@
|
||||
},
|
||||
"websearch": {
|
||||
"description": "网页搜索",
|
||||
"hint": "让 AstrBot 能够访问互联网,获悉时讯。",
|
||||
"provider_settings": {
|
||||
"web_search": {
|
||||
"description": "启用网页搜索"
|
||||
@@ -110,7 +113,7 @@
|
||||
},
|
||||
"websearch_baidu_app_builder_key": {
|
||||
"description": "百度千帆智能云 APP Builder API Key",
|
||||
"hint": "参考:https://console.bce.baidu.com/iam/#/iam/apikey/list"
|
||||
"hint": "参考:[https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)"
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "显示来源引用"
|
||||
@@ -133,15 +136,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
"description": "Agent 沙箱环境(Beta)",
|
||||
"hint": "https://docs.astrbot.app/use/astrbot-agent-sandbox.html",
|
||||
"agent_computer_use": {
|
||||
"description": "使用电脑能力",
|
||||
"hint": "让 AstrBot 访问和使用你的电脑或者隔离的沙盒环境,以执行更复杂的任务。详见: [沙盒模式](https://docs.astrbot.app/use/astrbot-agent-sandbox.html), [Skills](https://docs.astrbot.app/use/skills.html)。",
|
||||
"provider_settings": {
|
||||
"computer_use_runtime": {
|
||||
"description": "运行环境",
|
||||
"hint": "sandbox 代表在沙箱环境中运行, local 代表在本地环境中运行, none 代表不启用。如果上传了 skills,选择 none 会导致其无法被 Agent 正常使用。"
|
||||
},
|
||||
"sandbox": {
|
||||
"enable": {
|
||||
"description": "启用沙箱环境",
|
||||
"hint": "启用后,Agent 可以使用沙箱环境中的工具和资源,如 Python 代码执行、Shell 等。"
|
||||
},
|
||||
"booter": {
|
||||
"description": "沙箱环境驱动器"
|
||||
},
|
||||
@@ -164,18 +167,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"description": "Skills",
|
||||
"proactive_capability": {
|
||||
"description": "主动型能力",
|
||||
"hint": "让 AstrBot 能够在某一时刻自动唤醒,帮你完成任务。详见: [主动型 Agent](https://docs.astrbot.app/use/proactive-agent.html)。",
|
||||
"provider_settings": {
|
||||
"skills": {
|
||||
"runtime": {
|
||||
"description": "Skill Runtime",
|
||||
"hint": "选择 Skills 运行环境。使用 sandbox 前需启用沙箱;local 模式下 Agent 可通过 Shell 和 Python 功能完全访问运行环境,非管理员将被自动禁止使用以保证安全。"
|
||||
"proactive_capability": {
|
||||
"add_cron_tools": {
|
||||
"description": "启用",
|
||||
"hint": "启用后,将会传递给 Agent 相关工具来实现主动型 Agent。你可以告诉 AstrBot 未来某个时间要做的事情,它将被定时触发然后执行任务,然后将结果发送给你。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"hint": "AstrBot 如何管理工作记忆。详见: [上下文管理策略](https://docs.astrbot.app/use/context-compress.html)。",
|
||||
"description": "上下文管理策略",
|
||||
"provider_settings": {
|
||||
"max_context_length": {
|
||||
@@ -424,7 +429,7 @@
|
||||
},
|
||||
"emojis": {
|
||||
"description": "表情列表(飞书表情枚举名)",
|
||||
"hint": "表情枚举名参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce"
|
||||
"hint": "表情枚举名参考:[https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -435,7 +440,7 @@
|
||||
},
|
||||
"emojis": {
|
||||
"description": "表情列表(Unicode)",
|
||||
"hint": "Telegram 仅支持固定反应集合,参考:https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9"
|
||||
"hint": "Telegram 仅支持固定反应集合,参考:[https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -592,15 +597,15 @@
|
||||
},
|
||||
"pypi_index_url": {
|
||||
"description": "PyPI 软件仓库地址",
|
||||
"hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/"
|
||||
"hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 [https://mirrors.aliyun.com/pypi/simple/](https://mirrors.aliyun.com/pypi/simple/)"
|
||||
},
|
||||
"callback_api_base": {
|
||||
"description": "对外可达的回调接口地址",
|
||||
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定外部服务如何访问 AstrBot 的地址。如 http://localhost:6185,https://example.com 等。"
|
||||
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定外部服务如何访问 AstrBot 的地址。如 [http://localhost:6185](http://localhost:6185),[https://example.com](https://example.com) 等。"
|
||||
},
|
||||
"timezone": {
|
||||
"description": "时区",
|
||||
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab"
|
||||
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: [https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab](https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab)"
|
||||
},
|
||||
"http_proxy": {
|
||||
"description": "HTTP 代理",
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
"mirrorLabel": "强制 PyPI 软件仓库链接(可选)",
|
||||
"mirrorHint": "强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`",
|
||||
"installButton": "安装"
|
||||
},
|
||||
"debugHint": {
|
||||
"text": "Debug 日志需要在「配置文件 → 系统 → 控制台日志级别」中开启"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "未来任务管理",
|
||||
"beta": "Beta",
|
||||
"subtitle": "查看给 AstrBot 布置的未来任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。",
|
||||
"beta": "实验性",
|
||||
"subtitle": "查看给 AstrBot 布置的未来任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。需要先在配置文件中启用“主动型能力”。",
|
||||
"proactive": {
|
||||
"supported": "主动发送结果仅支持以下平台:{platforms}",
|
||||
"supported": "主动发送结果仅支持以下您已配置的平台:{platforms}",
|
||||
"unsupported": "暂无支持主动消息的平台,请在平台设置中开启。"
|
||||
}
|
||||
},
|
||||
@@ -22,6 +22,7 @@
|
||||
"name": "名称",
|
||||
"type": "类型",
|
||||
"cron": "Cron",
|
||||
"session": "会话 ID",
|
||||
"nextRun": "下一次执行",
|
||||
"lastRun": "最近执行",
|
||||
"note": "说明",
|
||||
@@ -39,6 +40,7 @@
|
||||
},
|
||||
"form": {
|
||||
"title": "新建任务",
|
||||
"chatHint": "你可以直接通过聊天的方式来让 AstrBot 创建未来任务,而不必在此添加。",
|
||||
"runOnce": "一次性任务",
|
||||
"name": "任务名称",
|
||||
"note": "任务说明",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"title": "插件管理",
|
||||
"subtitle": "管理和配置系统插件",
|
||||
"tabs": {
|
||||
"installedPlugins": "已安装的插件",
|
||||
"installedMcpServers": "已安装的 MCP 服务器",
|
||||
"installedPlugins": "AstrBot 插件",
|
||||
"market": "AstrBot 插件市场",
|
||||
"installedMcpServers": "MCP",
|
||||
"skills": "Skills",
|
||||
"handlersOperation": "管理行为",
|
||||
"market": "插件市场"
|
||||
"handlersOperation": "管理行为"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索插件...",
|
||||
@@ -210,7 +210,9 @@
|
||||
"deleteTitle": "删除确认",
|
||||
"deleteMessage": "确定要删除该 Skill 吗?",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteFailed": "删除失败"
|
||||
"deleteFailed": "删除失败",
|
||||
"runtimeNoneWarning": "Computer Use 运行环境为无,Skills 可能无法正确被 Agent 运行,因为没有启用运行环境。",
|
||||
"runtimeHint": "需要在配置的 “使用电脑能力” 中将运行环境设置为 “local” 或 “sandbox” 才能让 AstrBot 正常使用你提供的 Skills。"
|
||||
},
|
||||
"card": {
|
||||
"actions": {
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"loadingSkills": "正在加载 Skills...",
|
||||
"allSkillsAvailable": "使用所有可用 Skills",
|
||||
"noSkillsSelected": "未选择任何 Skills",
|
||||
"skillsRuntimeNoneWarning": "Computer Use 运行环境为无,Skills 可能无法正确被 Agent 运行,因为没有启用运行环境。",
|
||||
"createInFolder": "将在「{folder}」中创建",
|
||||
"rootFolder": "全部人格"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
"custom": "自定义"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题",
|
||||
"subtitle": "自定义主题主色与辅助色。修改后立即生效,并保存在浏览器本地。",
|
||||
"customize": {
|
||||
"title": "主题颜色",
|
||||
"primary": "主色",
|
||||
"secondary": "辅助色",
|
||||
"reset": "恢复默认"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "系统",
|
||||
"restart": {
|
||||
@@ -119,4 +129,4 @@
|
||||
"ftpHint": "对于较大的备份文件,也可以通过 FTP/SFTP 等方式直接上传到 data/backups 目录"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "SubAgent 编排",
|
||||
"beta": "Beta",
|
||||
"beta": "实验性",
|
||||
"subtitle": "主 LLM 只负责聊天与分派(handoff),工具挂载在各个 SubAgent 上。"
|
||||
},
|
||||
"actions": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"nameHint": "建议使用英文小写+下划线,且全局唯一",
|
||||
"providerLabel": "Chat Provider(可选)",
|
||||
"providerHint": "留空表示跟随全局默认 provider。",
|
||||
"personaLabel": "选择 Persona",
|
||||
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。",
|
||||
"personaLabel": "选择人格设定",
|
||||
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。在人格设定页管理和新建人格。",
|
||||
"descriptionLabel": "对主 LLM 的描述(用于决定是否 handoff)",
|
||||
"descriptionHint": "这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
|
||||
},
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
"autoScroll": {
|
||||
"enabled": "自动滚动:开",
|
||||
"disabled": "自动滚动:关"
|
||||
}
|
||||
},
|
||||
"hint": "当前仅记录部分 AstrBot 主 Agent 的模型调用路径,后续会不断完善。",
|
||||
"recording": "记录中",
|
||||
"paused": "已暂停"
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ const changeLanguage = async (langCode: string) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar elevation="0" height="55">
|
||||
<v-app-bar elevation="0" height="50" class="top-header">
|
||||
|
||||
<!-- 桌面端 menu 按钮 - 仅在 bot 模式下显示 -->
|
||||
<v-btn v-if="customizer.viewMode === 'bot' && useCustomizerStore().uiTheme === 'PurpleTheme'" style="margin-left: 16px;"
|
||||
|
||||
@@ -2,23 +2,41 @@
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps({ item: Object, level: Number });
|
||||
const { t } = useI18n();
|
||||
const customizer = useCustomizerStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const lvl = props.level ?? 0;
|
||||
const indent = customizer.mini_sidebar ? '0px' : `${lvl * 24}px`;
|
||||
return { '--indent-padding': indent };
|
||||
});
|
||||
|
||||
const handleGroupClick = () => {
|
||||
if (!props.item || props.item.type === 'external' || !props.item.to) return;
|
||||
router.push(props.item.to);
|
||||
};
|
||||
|
||||
const isItemActive = computed(() => {
|
||||
if (!props.item || props.item.type === 'external' || !props.item.to) return false;
|
||||
if (typeof props.item.to !== 'string') return false;
|
||||
if (props.item.to.includes('#')) {
|
||||
const [path, hash] = props.item.to.split('#');
|
||||
return route.path === path && route.hash === `#${hash}`;
|
||||
}
|
||||
return route.path === props.item.to;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
:style="{ '--indent-padding': '0px' }">
|
||||
:style="{ '--indent-padding': '0px' }" @click="handleGroupClick">
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ t(item.title) }}
|
||||
</v-list-item-title>
|
||||
@@ -31,8 +49,9 @@ const itemStyle = computed(() => {
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''" rounded
|
||||
class="mb-1" color="secondary" :disabled="item.disabled" :target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
|
||||
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''"
|
||||
:active="isItemActive" rounded class="mb-1" color="secondary" :disabled="item.disabled"
|
||||
:target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
@@ -56,4 +75,4 @@ const itemStyle = computed(() => {
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--v-theme-borderLight), 0.04);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -253,16 +253,19 @@ function openChangelogDialog() {
|
||||
</template>
|
||||
</v-list>
|
||||
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="tonal" color="primary" to="/settings">
|
||||
🔧 {{ t('core.navigation.settings') }}
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="tonal" color="primary" to="/settings" prepend-icon="mdi-cog">
|
||||
{{ t('core.navigation.settings') }}
|
||||
</v-btn>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="openChangelogDialog">
|
||||
📝 {{ t('core.navigation.changelog') }}
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-note-text-outline"
|
||||
@click="openChangelogDialog">
|
||||
{{ t('core.navigation.changelog') }}
|
||||
</v-btn>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="toggleIframe">
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-book-open-variant"
|
||||
@click="toggleIframe">
|
||||
{{ t('core.navigation.documentation') }}
|
||||
</v-btn>
|
||||
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
{{ t('core.navigation.github') }}
|
||||
<v-chip
|
||||
v-if="starCount"
|
||||
@@ -366,4 +369,4 @@ function openChangelogDialog() {
|
||||
.leftSidebar .v-navigation-drawer__content {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -36,7 +36,34 @@ const sidebarItem: menu[] = [
|
||||
{
|
||||
title: 'core.navigation.extension',
|
||||
icon: 'mdi-puzzle',
|
||||
to: '/extension'
|
||||
to: '/extension#installed',
|
||||
children: [
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.installed',
|
||||
icon: 'mdi-puzzle',
|
||||
to: '/extension#installed'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.market',
|
||||
icon: 'mdi-store',
|
||||
to: '/extension#market'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.mcp',
|
||||
icon: 'mdi-server-network',
|
||||
to: '/extension#mcp'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.skills',
|
||||
icon: 'mdi-lightning-bolt',
|
||||
to: '/extension#skills'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.components',
|
||||
icon: 'mdi-wrench',
|
||||
to: '/extension#components'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.knowledgeBase',
|
||||
|
||||
+27
-1
@@ -30,6 +30,19 @@ setupI18n().then(() => {
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem('themePrimary');
|
||||
const storedSecondary = localStorage.getItem('themeSecondary');
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('❌ 新i18n系统初始化失败:', error);
|
||||
@@ -49,6 +62,19 @@ setupI18n().then(() => {
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem('themePrimary');
|
||||
const storedSecondary = localStorage.getItem('themeSecondary');
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,4 +105,4 @@ loader.config({
|
||||
paths: {
|
||||
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,10 @@ html {
|
||||
.v-main {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-borderLight), 0.5);
|
||||
}
|
||||
@media (max-width: 1279px) {
|
||||
.v-main {
|
||||
margin: 0 10px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*This is for the logo*/
|
||||
.leftSidebar {
|
||||
border: 0px;
|
||||
border-right: 1px solid rgba(var(--v-theme-borderLight), 0.45);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.listitem {
|
||||
@@ -16,6 +17,9 @@
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
.v-list-item--density-default.v-list-item--one-line {
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题下的侧边栏悬停和选中样式
|
||||
@@ -50,7 +54,7 @@
|
||||
}
|
||||
}
|
||||
.v-list-item--density-default.v-list-item--one-line {
|
||||
min-height: 42px;
|
||||
min-height: 40px;
|
||||
}
|
||||
.leftPadding {
|
||||
margin-left: 4px;
|
||||
@@ -113,6 +117,11 @@
|
||||
max-width: 180px;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.sidebar-footer-btn {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ const PurpleTheme: ThemeTypes = {
|
||||
'carousel-control-size': 10
|
||||
},
|
||||
colors: {
|
||||
primary: '#1e88e5',
|
||||
secondary: '#5e35b1',
|
||||
primary: '#3c96ca',
|
||||
secondary: '#2288b7',
|
||||
info: '#03c9d7',
|
||||
success: '#00c853',
|
||||
accent: '#FFAB91',
|
||||
|
||||
@@ -58,7 +58,7 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
|
||||
|
||||
// 收集所有条目,按 title 建索引
|
||||
defaultItems.forEach(item => {
|
||||
if (item.children) {
|
||||
if (item.children && item.title === 'core.navigation.groups.more') {
|
||||
item.children.forEach(child => {
|
||||
all.set(child.title, cloneItems ? { ...child } : child);
|
||||
defaultMore.push(child.title);
|
||||
|
||||
@@ -10,7 +10,18 @@ const { tm } = useModuleI18n('features/console');
|
||||
<div style="height: 100%;">
|
||||
<div
|
||||
style="background-color: var(--v-theme-surface); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
|
||||
<h4>{{ tm('title') }}</h4>
|
||||
<div>
|
||||
<h4>{{ tm('title') }}</h4>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-2"
|
||||
style="max-width: 600px;"
|
||||
>
|
||||
{{ tm('debugHint.text') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-switch
|
||||
v-model="autoScrollEnabled"
|
||||
@@ -111,4 +122,4 @@ export default {
|
||||
.fade-in {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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">
|
||||
@@ -52,20 +48,18 @@
|
||||
<div class="text-caption text-medium-emphasis">{{ item.timezone || tm('table.timezoneLocal') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.session="{ item }">
|
||||
<div>{{ item.session || tm('table.notAvailable') }}</div>
|
||||
</template>
|
||||
<template #item.next_run_time="{ item }">{{ formatTime(item.next_run_time) }}</template>
|
||||
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
|
||||
<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>
|
||||
@@ -79,43 +73,26 @@
|
||||
<v-dialog v-model="createDialog" max-width="560">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ tm('form.title') }}</v-card-title>
|
||||
<v-card-subtitle class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.chatHint') }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<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>
|
||||
@@ -155,6 +132,7 @@ const headers = computed(() => [
|
||||
{ title: tm('table.headers.name'), key: 'name', minWidth: '200px' },
|
||||
{ title: tm('table.headers.type'), key: 'type', width: 110 },
|
||||
{ title: tm('table.headers.cron'), key: 'cron_expression', minWidth: '160px' },
|
||||
{ title: tm('table.headers.session'), key: 'session', minWidth: '200px' },
|
||||
{ title: tm('table.headers.nextRun'), key: 'next_run_time', minWidth: '160px' },
|
||||
{ title: tm('table.headers.lastRun'), key: 'last_run_at', minWidth: '160px' },
|
||||
{ title: tm('table.headers.note'), key: 'note', minWidth: '220px' },
|
||||
@@ -189,7 +167,11 @@ async function loadJobs() {
|
||||
try {
|
||||
const res = await axios.get('/api/cron/jobs')
|
||||
if (res.data.status === 'ok') {
|
||||
jobs.value = Array.isArray(res.data.data) ? res.data.data : []
|
||||
const data = Array.isArray(res.data.data) ? res.data.data : []
|
||||
jobs.value = data.map((job: any) => ({
|
||||
...job,
|
||||
session: job?.payload?.session || job?.session || ''
|
||||
}))
|
||||
} else {
|
||||
toast(res.data.message || tm('messages.loadFailed'), 'error')
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ import { useI18n, useModuleI18n } from "@/i18n/composables";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
|
||||
import { ref, computed, onMounted, reactive, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const commonStore = useCommonStore();
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const getSelectedGitHubProxy = () => {
|
||||
if (typeof window === "undefined" || !window.localStorage) return "";
|
||||
@@ -54,6 +55,23 @@ const handleConflictConfirm = () => {
|
||||
|
||||
const fileInput = ref(null);
|
||||
const activeTab = ref("installed");
|
||||
const validTabs = ["installed", "market", "mcp", "skills", "components"];
|
||||
const isValidTab = (tab) => validTabs.includes(tab);
|
||||
const getLocationHash = () =>
|
||||
typeof window !== "undefined" ? window.location.hash : "";
|
||||
const extractTabFromHash = (hash) => {
|
||||
const lastHashIndex = (hash || "").lastIndexOf("#");
|
||||
if (lastHashIndex === -1) return "";
|
||||
return hash.slice(lastHashIndex + 1);
|
||||
};
|
||||
const syncTabFromHash = (hash) => {
|
||||
const tab = extractTabFromHash(hash);
|
||||
if (isValidTab(tab)) {
|
||||
activeTab.value = tab;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const extension_data = reactive({
|
||||
data: [],
|
||||
message: "",
|
||||
@@ -995,6 +1013,11 @@ const refreshPluginMarket = async () => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
if (!syncTabFromHash(getLocationHash())) {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.hash = `#${activeTab.value}`;
|
||||
}
|
||||
}
|
||||
await getExtensions();
|
||||
|
||||
// 加载自定义插件源
|
||||
@@ -1051,6 +1074,29 @@ watch(isListView, (newVal) => {
|
||||
localStorage.setItem("pluginListViewMode", String(newVal));
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
const tab = extractTabFromHash(getLocationHash());
|
||||
if (isValidTab(tab) && tab !== activeTab.value) {
|
||||
activeTab.value = tab;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
if (!isValidTab(newTab)) return;
|
||||
const currentTab = extractTabFromHash(getLocationHash());
|
||||
if (currentTab === newTab) return;
|
||||
const hash = getLocationHash();
|
||||
const lastHashIndex = hash.lastIndexOf("#");
|
||||
const nextHash =
|
||||
lastHashIndex > 0 ? `${hash.slice(0, lastHashIndex)}#${newTab}` : `#${newTab}`;
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.hash = nextHash;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1067,6 +1113,10 @@ watch(isListView, (newVal) => {
|
||||
<v-icon class="mr-2">mdi-puzzle</v-icon>
|
||||
{{ tm("tabs.installedPlugins") }}
|
||||
</v-tab>
|
||||
<v-tab value="market">
|
||||
<v-icon class="mr-2">mdi-store</v-icon>
|
||||
{{ tm("tabs.market") }}
|
||||
</v-tab>
|
||||
<v-tab value="mcp">
|
||||
<v-icon class="mr-2">mdi-server-network</v-icon>
|
||||
{{ tm("tabs.installedMcpServers") }}
|
||||
@@ -1075,10 +1125,6 @@ watch(isListView, (newVal) => {
|
||||
<v-icon class="mr-2">mdi-lightning-bolt</v-icon>
|
||||
{{ tm("tabs.skills") }}
|
||||
</v-tab>
|
||||
<v-tab value="market">
|
||||
<v-icon class="mr-2">mdi-store</v-icon>
|
||||
{{ tm("tabs.market") }}
|
||||
</v-tab>
|
||||
<v-tab value="components">
|
||||
<v-icon class="mr-2">mdi-wrench</v-icon>
|
||||
{{ tm("tabs.handlersOperation") }}
|
||||
@@ -2433,7 +2479,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">
|
||||
|
||||
@@ -15,6 +15,41 @@
|
||||
<SidebarCustomizer></SidebarCustomizer>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-subheader>{{ tm('theme.title') }}</v-list-subheader>
|
||||
|
||||
<v-list-item :subtitle="tm('theme.subtitle')" :title="tm('theme.customize.title')">
|
||||
<v-row class="mt-2" dense>
|
||||
<v-col cols="4" sm="2">
|
||||
<v-text-field
|
||||
v-model="primaryColor"
|
||||
type="color"
|
||||
:label="tm('theme.customize.primary')"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="max-width: 220px;"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="4" sm="2 ">
|
||||
<v-text-field
|
||||
v-model="secondaryColor"
|
||||
type="color"
|
||||
:label="tm('theme.customize.secondary')"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="max-width: 220px;"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn size="small" variant="tonal" color="primary" @click="resetThemeColors">
|
||||
<v-icon class="mr-2">mdi-restore</v-icon>
|
||||
{{ tm('theme.customize.reset') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-subheader>{{ tm('system.title') }}</v-list-subheader>
|
||||
|
||||
<v-list-item :subtitle="tm('system.backup.subtitle')" :title="tm('system.backup.title')">
|
||||
@@ -42,7 +77,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
@@ -50,8 +85,52 @@ import MigrationDialog from '@/components/shared/MigrationDialog.vue';
|
||||
import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
|
||||
import BackupDialog from '@/components/shared/BackupDialog.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { PurpleTheme } from '@/theme/LightTheme';
|
||||
|
||||
const { tm } = useModuleI18n('features/settings');
|
||||
const theme = useTheme();
|
||||
|
||||
const getStoredColor = (key, fallback) => {
|
||||
const stored = typeof window !== 'undefined' ? localStorage.getItem(key) : null;
|
||||
return stored || fallback;
|
||||
};
|
||||
|
||||
const primaryColor = ref(getStoredColor('themePrimary', PurpleTheme.colors.primary));
|
||||
const secondaryColor = ref(getStoredColor('themeSecondary', PurpleTheme.colors.secondary));
|
||||
|
||||
const resolveThemes = () => {
|
||||
if (theme?.themes?.value) return theme.themes.value;
|
||||
if (theme?.global?.themes?.value) return theme.global.themes.value;
|
||||
return null;
|
||||
};
|
||||
|
||||
const applyThemeColors = (primary, secondary) => {
|
||||
const themes = resolveThemes();
|
||||
if (!themes) return;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const themeDef = themes[name];
|
||||
if (!themeDef?.colors) return;
|
||||
if (primary) themeDef.colors.primary = primary;
|
||||
if (secondary) themeDef.colors.secondary = secondary;
|
||||
if (primary && themeDef.colors.darkprimary) themeDef.colors.darkprimary = primary;
|
||||
if (secondary && themeDef.colors.darksecondary) themeDef.colors.darksecondary = secondary;
|
||||
});
|
||||
};
|
||||
|
||||
applyThemeColors(primaryColor.value, secondaryColor.value);
|
||||
|
||||
watch(primaryColor, (value) => {
|
||||
if (!value) return;
|
||||
localStorage.setItem('themePrimary', value);
|
||||
applyThemeColors(value, secondaryColor.value);
|
||||
});
|
||||
|
||||
watch(secondaryColor, (value) => {
|
||||
if (!value) return;
|
||||
localStorage.setItem('themeSecondary', value);
|
||||
applyThemeColors(primaryColor.value, value);
|
||||
});
|
||||
|
||||
const wfr = ref(null);
|
||||
const migrationDialog = ref(null);
|
||||
@@ -81,4 +160,12 @@ const openBackupDialog = () => {
|
||||
backupDialog.value.open();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
const resetThemeColors = () => {
|
||||
primaryColor.value = PurpleTheme.colors.primary;
|
||||
secondaryColor.value = PurpleTheme.colors.secondary;
|
||||
localStorage.removeItem('themePrimary');
|
||||
localStorage.removeItem('themeSecondary');
|
||||
applyThemeColors(primaryColor.value, secondaryColor.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,72 @@
|
||||
<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%;">
|
||||
<TraceDisplayer />
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -19,3 +78,38 @@ 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>
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.13.2"
|
||||
version = "4.14.2"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -69,14 +69,14 @@ dev = [
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"ruff>=0.12.8",
|
||||
"ruff>=0.15.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
astrbot = "astrbot.cli.__main__:cli"
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py"]
|
||||
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py", "tests"]
|
||||
line-length = 88
|
||||
target-version = "py310"
|
||||
|
||||
@@ -97,6 +97,7 @@ ignore = [
|
||||
"F405",
|
||||
"E501",
|
||||
"ASYNC230", # TODO: handle ASYNC230 in AstrBot
|
||||
"ASYNC240", # TODO: handle ASYNC240 in AstrBot
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
|
||||
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Auto-generate changelog from git commits using LLM.
|
||||
Usage: python scripts/generate_changelog.py [--version VERSION]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_latest_tag():
|
||||
"""Get the latest git tag."""
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--abbrev=0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_commits_since_tag(tag):
|
||||
"""Get all commit messages since the specified tag."""
|
||||
result = subprocess.run(
|
||||
["git", "log", f"{tag}..HEAD", "--pretty=format:%H|%s|%b"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
commits = []
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("|", 2)
|
||||
if len(parts) >= 2:
|
||||
commit_hash = parts[0]
|
||||
subject = parts[1]
|
||||
body = parts[2] if len(parts) > 2 else ""
|
||||
commits.append({"hash": commit_hash[:7], "subject": subject, "body": body})
|
||||
return commits
|
||||
|
||||
|
||||
def extract_issue_number(text):
|
||||
"""Extract issue number from commit message."""
|
||||
# Match #1234 or (#1234)
|
||||
match = re.search(r"#(\d+)", text)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def call_llm_for_changelog(commits, version):
|
||||
"""Call LLM to generate changelog from commits."""
|
||||
try:
|
||||
# Try to use OpenAI API or other LLM providers
|
||||
import openai
|
||||
|
||||
# Build prompt
|
||||
commits_text = "\n".join([f"- {c['subject']}" for c in commits])
|
||||
|
||||
prompt = f"""Based on the following git commit messages, generate a changelog document in BOTH Chinese and English.
|
||||
|
||||
Commit messages:
|
||||
{commits_text}
|
||||
|
||||
Please organize the changes into these categories:
|
||||
- 新增 (New Features)
|
||||
- 修复 (Bug Fixes)
|
||||
- 优化 (Improvements)
|
||||
- 其他 (Others)
|
||||
|
||||
Format requirements:
|
||||
1. Start with Chinese version under "## What's Changed"
|
||||
2. Follow with English version under "## What's Changed (EN)"
|
||||
3. Use markdown format with proper bullet points
|
||||
4. Keep descriptions concise and user-friendly
|
||||
5. If a commit mentions an issue number (#1234), include it in the format ([#1234](https://github.com/AstrBotDevs/AstrBot/issues/1234))
|
||||
|
||||
Example format:
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
- 支持某某功能 ([#1234](https://github.com/AstrBotDevs/AstrBot/issues/1234))
|
||||
|
||||
### 修复
|
||||
- 修复某某问题
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
- Add support for something ([#1234](https://github.com/AstrBotDevs/AstrBot/issues/1234))
|
||||
|
||||
### Bug Fixes
|
||||
- Fix something
|
||||
"""
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=os.getenv("OPENAI_MODEL", "gpt-4"),
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant that generates well-structured changelogs.",
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
)
|
||||
|
||||
return response.choices[0].message.content
|
||||
|
||||
except ImportError:
|
||||
print(
|
||||
"Warning: openai package not installed. Install it with: pip install openai"
|
||||
)
|
||||
return generate_simple_changelog(commits)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to call LLM API: {e}")
|
||||
print("Falling back to simple changelog generation...")
|
||||
return generate_simple_changelog(commits)
|
||||
|
||||
|
||||
def generate_simple_changelog(commits):
|
||||
"""Generate a simple changelog without LLM."""
|
||||
sections = {
|
||||
"feat": ("新增", "New Features", []),
|
||||
"fix": ("修复", "Bug Fixes", []),
|
||||
"perf": ("优化", "Improvements", []),
|
||||
"docs": ("文档", "Documentation", []),
|
||||
"refactor": ("重构", "Refactoring", []),
|
||||
"test": ("测试", "Tests", []),
|
||||
"chore": ("其他", "Chore", []),
|
||||
"other": ("其他", "Others", []),
|
||||
}
|
||||
|
||||
# Categorize commits by conventional commit type
|
||||
for commit in commits:
|
||||
subject = commit["subject"]
|
||||
issue_num = extract_issue_number(subject)
|
||||
issue_link = (
|
||||
f" ([#{issue_num}](https://github.com/AstrBotDevs/AstrBot/issues/{issue_num}))"
|
||||
if issue_num
|
||||
else ""
|
||||
)
|
||||
|
||||
# Detect conventional commit type
|
||||
matched = False
|
||||
for prefix in ["feat", "fix", "perf", "docs", "refactor", "test", "chore"]:
|
||||
if subject.lower().startswith(f"{prefix}:") or subject.lower().startswith(
|
||||
f"{prefix}("
|
||||
):
|
||||
# Remove prefix for display
|
||||
clean_subject = re.sub(
|
||||
r"^[a-z]+(\([^)]+\))?:\s*", "", subject, flags=re.IGNORECASE
|
||||
)
|
||||
sections[prefix][2].append(f"- {clean_subject}{issue_link}")
|
||||
matched = True
|
||||
break
|
||||
|
||||
if not matched:
|
||||
sections["other"][2].append(f"- {subject}{issue_link}")
|
||||
|
||||
# Build Chinese version
|
||||
changelog_zh = "## What's Changed\n\n"
|
||||
for section_key in ["feat", "fix", "perf", "docs", "refactor", "test", "other"]:
|
||||
zh_title, _, items = sections[section_key]
|
||||
if items:
|
||||
changelog_zh += f"### {zh_title}\n\n"
|
||||
changelog_zh += "\n".join(items) + "\n\n"
|
||||
|
||||
# Build English version
|
||||
changelog_en = "## What's Changed (EN)\n\n"
|
||||
for section_key in ["feat", "fix", "perf", "docs", "refactor", "test", "other"]:
|
||||
_, en_title, items = sections[section_key]
|
||||
if items:
|
||||
changelog_en += f"### {en_title}\n\n"
|
||||
changelog_en += "\n".join(items) + "\n\n"
|
||||
|
||||
return changelog_zh + changelog_en
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate changelog from git commits")
|
||||
parser.add_argument(
|
||||
"--version", help="Version number for the changelog (e.g., v4.13.3)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-llm",
|
||||
action="store_true",
|
||||
help="Use LLM to generate changelog (requires OpenAI API key)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get latest tag
|
||||
try:
|
||||
latest_tag = get_latest_tag()
|
||||
print(f"Latest tag: {latest_tag}")
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: No tags found in repository")
|
||||
sys.exit(1)
|
||||
|
||||
# Get commits since tag
|
||||
commits = get_commits_since_tag(latest_tag)
|
||||
if not commits:
|
||||
print(f"No commits found since {latest_tag}")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Found {len(commits)} commits since {latest_tag}")
|
||||
|
||||
# Determine version
|
||||
if args.version:
|
||||
version = args.version
|
||||
else:
|
||||
# Auto-increment patch version
|
||||
match = re.match(r"v(\d+)\.(\d+)\.(\d+)", latest_tag)
|
||||
if match:
|
||||
major, minor, patch = map(int, match.groups())
|
||||
version = f"v{major}.{minor}.{patch + 1}"
|
||||
else:
|
||||
print(f"Warning: Could not parse version from tag {latest_tag}")
|
||||
version = "vX.X.X"
|
||||
|
||||
print(f"Generating changelog for {version}...")
|
||||
|
||||
# Generate changelog
|
||||
if args.use_llm:
|
||||
changelog_content = call_llm_for_changelog(commits, version)
|
||||
else:
|
||||
changelog_content = generate_simple_changelog(commits)
|
||||
|
||||
# Save to file
|
||||
changelog_dir = Path(__file__).parent.parent / "changelogs"
|
||||
changelog_dir.mkdir(exist_ok=True)
|
||||
changelog_file = changelog_dir / f"{version}.md"
|
||||
|
||||
with open(changelog_file, "w", encoding="utf-8") as f:
|
||||
f.write(changelog_content)
|
||||
|
||||
print(f"\n✓ Changelog generated: {changelog_file}")
|
||||
print("\nPreview:")
|
||||
print("=" * 80)
|
||||
print(changelog_content)
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user