Compare commits

..

12 Commits

Author SHA1 Message Date
Soulter 6f53e8d805 docs: revise README.md for clarity and feature updates
Updated project description and added details about deployment and features.
2026-02-03 20:23:40 +08:00
Soulter 93277ffac9 fix: improve skills bundle extraction process to prevent overwriting existing files 2026-02-03 16:54:53 +08:00
Soulter c091053ea8 fix: skills bundle unzip failed in sandbox 2026-02-03 16:34:07 +08:00
Soulter 8b9f2f1e70 feat: enhance user experience with runtime hints and improved UI elements in skills management 2026-02-03 16:28:17 +08:00
Soulter 25ca7bd71e fix: add missing newline for code readability in _apply_local_env_tools function 2026-02-03 16:09:17 +08:00
Soulter 093b37e04b feat: add computer use runtime config and handling for skills execution (#4831)
* feat: add computer use runtime configuration and handling for skills execution

* fix: improve user notification for disabled Computer Use feature in skills execution
2026-02-03 16:08:15 +08:00
Soulter a12e27f9ab feat: implement theme customization with primary and secondary color options 2026-02-03 14:41:48 +08:00
Soulter ae6e0db053 perf: webui
Co-authored-by: IGCrystal <IGCrystal@wenturc.com>
2026-02-03 14:40:45 +08:00
SJ cd6bef4d78 fix: MCP tools being filtered out when a specific plugin set is configured in the WebUI (#4825)
* fix: preserve MCP tools in _plugin_tool_fix filter

Tools without handler_module_path (such as MCP tools and built-in tools)
were being incorrectly skipped during plugin-based tool filtering.

This fix ensures that tools without plugin association are preserved,
as they should not be affected by plugin-level filtering logic.

* fix: retain MCP tools in _plugin_tool_fix function

---------

Co-authored-by: idiotsj <idiotsj@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
2026-02-03 10:53:20 +08:00
Copilot de1304dc6a feat: add edit button to persona selector dialog (#4826)
* Initial plan

* feat: add edit persona functionality in chatui selector dialog

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix: address code review feedback - improve null checks and i18n consistency

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-03 10:32:20 +08:00
Soulter f835f63542 feat: add trace settings management and UI for enabling/disabling trace logging (#4822)
* feat: add trace settings management and UI for enabling/disabling trace logging

* feat: enhance trace feature with internationalization support for hints and status messages

* fix: improve tool info extraction in run_agent function
2026-02-03 10:24:41 +08:00
Soulter 5deb045e47 fix: merge chatui pop-up prompt into chatui default persona and improve chatui persona handle (#4824)
* fix: merge chatui pop-up prompt into chatui default persona and improve chatui persona handle

* fix: update webchat persona handling to avoid default assignment for None
2026-02-03 01:29:21 +08:00
39 changed files with 592 additions and 251 deletions
+20 -1
View File
@@ -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 应用。
![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
@@ -50,6 +50,25 @@ 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>
陪伴与能力**从来不应该是**对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人——致敬[ATRI](https://zh.wikipedia.org/zh-cn/ATRI_-My_Dear_Moments-)。
## 快速开始
#### Docker 部署(推荐 🥳)
+39 -29
View File
@@ -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?"
)
+10 -1
View File
@@ -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):
+75 -93
View File
@@ -117,15 +117,14 @@ DEFAULT_CONFIG = {
"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).
@@ -2225,17 +2224,6 @@ CONFIG_METADATA_2 = {
},
},
},
"skills": {
"type": "object",
"items": {
"enable": {
"type": "bool",
},
"runtime": {
"type": "string",
},
},
},
"proactive_capability": {
"type": "object",
"items": {
@@ -2516,6 +2504,7 @@ CONFIG_METADATA_3 = {
},
"persona": {
"description": "人格",
"hint": "",
"type": "object",
"items": {
"provider_settings.default_personality": {
@@ -2531,6 +2520,7 @@ CONFIG_METADATA_3 = {
},
"knowledgebase": {
"description": "知识库",
"hint": "",
"type": "object",
"items": {
"kb_names": {
@@ -2563,6 +2553,7 @@ CONFIG_METADATA_3 = {
},
"websearch": {
"description": "网页搜索",
"hint": "",
"type": "object",
"items": {
"provider_settings.web_search": {
@@ -2573,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",
@@ -2581,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": {
@@ -2594,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": {
@@ -2631,86 +2693,6 @@ CONFIG_METADATA_3 = {
# "provider_settings.enable": True,
# },
# },
"sandbox": {
"description": "Agent 沙箱环境",
"hint": "",
"type": "object",
"items": {
"provider_settings.sandbox.enable": {
"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",
"hint": "",
"items": {
"provider_settings.skills.runtime": {
"description": "Skill Runtime",
"type": "string",
"options": ["local", "sandbox"],
"labels": ["本地", "沙箱"],
"hint": "选择 Skills 运行环境。使用沙箱时需先启用沙箱环境。",
},
},
"condition": {
"provider_settings.agent_runner_type": "local",
"provider_settings.enable": True,
},
},
"proactive_capability": {
"description": "主动型 Agent",
"hint": "https://docs.astrbot.app/use/proactive-agent.html",
@@ -92,6 +92,7 @@ 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
@@ -116,6 +117,7 @@ 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,
+12 -4
View File
@@ -24,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__
@@ -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 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-folder-zip-outline"
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);
}
}
});
+1
View File
@@ -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,21 +164,9 @@
}
}
},
"skills": {
"hint": "https://docs.astrbot.app/use/skills.html",
"description": "Skills",
"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": {
"description": "Proactive Agent",
"hint": "https://docs.astrbot.app/en/use/proactive-agent.html",
"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": {
"proactive_capability": {
"add_cron_tools": {
@@ -189,7 +177,7 @@
}
},
"truncate_and_compress": {
"hint": "https://docs.astrbot.app/en/use/context-compress.html",
"hint": "[Context Management](https://docs.astrbot.app/en/use/context-compress.html)",
"description": "Context Management Strategy",
"provider_settings": {
"max_context_length": {
@@ -440,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)"
}
}
},
@@ -451,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)"
}
}
}
@@ -608,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",
@@ -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}",
@@ -39,6 +39,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": {
@@ -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,21 +167,9 @@
}
}
},
"skills": {
"hint": "https://docs.astrbot.app/en/use/skills.html",
"description": "Skills",
"provider_settings": {
"skills": {
"runtime": {
"description": "Skill Runtime",
"hint": "选择 Skills 运行环境。使用 sandbox 前需启用沙箱;local 模式下 Agent 可通过 Shell 和 Python 功能完全访问运行环境,非管理员将被自动禁止使用以保证安全。"
}
}
}
},
"proactive_capability": {
"description": "主动型 Agent",
"hint": "https://docs.astrbot.app/use/proactive-agent.html",
"description": "主动型能力",
"hint": "让 AstrBot 能够在某一时刻自动唤醒,帮你完成任务。详见: [主动型 Agent](https://docs.astrbot.app/use/proactive-agent.html)。",
"provider_settings": {
"proactive_capability": {
"add_cron_tools": {
@@ -189,7 +180,7 @@
}
},
"truncate_and_compress": {
"hint": "https://docs.astrbot.app/use/context-compress.html",
"hint": "AstrBot 如何管理工作记忆。详见: [上下文管理策略](https://docs.astrbot.app/use/context-compress.html)。",
"description": "上下文管理策略",
"provider_settings": {
"max_context_length": {
@@ -438,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)"
}
}
},
@@ -449,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)"
}
}
}
@@ -606,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 代理",
@@ -1,10 +1,10 @@
{
"page": {
"title": "未来任务管理",
"beta": "Beta",
"subtitle": "查看给 AstrBot 布置的未来任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。",
"beta": "实验性",
"subtitle": "查看给 AstrBot 布置的未来任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。需要先在配置文件中启用“主动型能力”。",
"proactive": {
"supported": "主动发送结果仅支持以下平台:{platforms}",
"supported": "主动发送结果仅支持以下您已配置的平台:{platforms}",
"unsupported": "暂无支持主动消息的平台,请在平台设置中开启。"
}
},
@@ -39,6 +39,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": {
@@ -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
View File
@@ -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;
+10 -1
View File
@@ -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;
}
}
}
}
+2 -2
View File
@@ -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',
+1 -1
View File
@@ -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);
+3
View File
@@ -70,6 +70,9 @@
<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" />
+51 -5
View File
@@ -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") }}
+89 -2
View File
@@ -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>