feat: filesystem grep, read, write, edit file and workspace support (#7402)

* feat: filesystem grep, read, edit file

* feat: add file write tool and enhance file read functionality

* feat: enhance tool prompt formatting and add escaped text decoding for file editing

* feat: remove redundant safe path tests from security restrictions

* feat: implement file read tool with support for text and image files, including validation for large files

* feat: add file read utilities and integrate with filesystem tools

* refactor: move computer tools to builtin tools registry

* refactor: remove unused plugin_context parameter from _apply_sandbox_tools

* feat: supports to display enabled builtin tools in configs

* feat: add tooltip for disabled builtin tools and update localization strings

* feat: add workspace extra prompt handling in message processing

* feat: add ripgrep installation to Dockerfile

* perf: shell executed in workspace dir in local env

* feat: enhance file reading capabilities to support PDF and DOCX parsing, including workspace storage for long documents

* feat: update converted text notice to suggest using grep for large files

* feat: implement handling for large tool results with overflow file writing and read tool integration

* fix: test

* feat: enhance onboarding steps to include computer access configuration and related help information

* feat: add support for additional temporary path in restricted environment checks

* feat: update computer access hints and add detailed configuration instructions
This commit is contained in:
Soulter
2026-04-11 17:01:54 +08:00
committed by GitHub
parent b8ccfe3f64
commit 2472a12671
59 changed files with 3995 additions and 705 deletions
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import type { ToolItem } from '../types';
import type { BuiltinToolConfigTag, ToolConfigCondition, ToolItem } from '../types';
const { tm: tmTool } = useModuleI18n('features/tooluse');
@@ -15,7 +15,7 @@ const emit = defineEmits<{
}>();
const toolHeaders = computed(() => [
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '240px' },
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '320px' },
{ title: tmTool('functionTools.description'), key: 'description' },
{ title: tmTool('functionTools.table.origin'), key: 'origin', sortable: false, width: '120px' },
{ title: tmTool('functionTools.table.originName'), key: 'origin_name', sortable: false, width: '160px' },
@@ -23,6 +23,52 @@ const toolHeaders = computed(() => [
]);
const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.properties || {});
const formatConfigValue = (value: unknown) => {
if (Array.isArray(value)) {
return value.map(item => String(item)).join(', ');
}
if (typeof value === 'boolean') {
return value ? 'true' : 'false';
}
if (value === null || value === undefined || value === '') {
return '-';
}
return String(value);
};
const formatCondition = (condition: ToolConfigCondition) => {
if (condition.message) {
return condition.message;
}
switch (condition.operator) {
case 'truthy':
return tmTool('functionTools.configTags.conditions.truthy', {
key: condition.key
});
case 'equals':
return tmTool('functionTools.configTags.conditions.equals', {
key: condition.key,
expected: formatConfigValue(condition.expected)
});
case 'in':
return tmTool('functionTools.configTags.conditions.in', {
key: condition.key,
expected: formatConfigValue(condition.expected)
});
default:
return tmTool('functionTools.configTags.conditions.fallback', {
key: condition.key,
actual: formatConfigValue(condition.actual)
});
}
};
const enabledConfigTags = (tool: ToolItem): BuiltinToolConfigTag[] => {
if (tool.origin !== 'builtin') return [];
return (tool.builtin_config_tags || []).filter(tag => tag.enabled);
};
</script>
<template>
@@ -38,7 +84,39 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro
>
<template #item.name="{ item }">
<div class="py-2">
<div class="tool-name text-body-2 font-weight-medium">{{ item.name }}</div>
<div class="d-flex flex-wrap align-center ga-1">
<div class="tool-name text-body-2 font-weight-medium">{{ item.name }}</div>
<v-tooltip
v-for="tag in enabledConfigTags(item)"
:key="`${item.name}-${tag.conf_id}`"
location="top"
>
<template #activator="{ props: tooltipProps }">
<v-chip
v-bind="tooltipProps"
size="x-small"
variant="tonal"
color="secondary"
class="text-caption font-weight-medium"
>
{{ tag.conf_name }}
</v-chip>
</template>
<div class="tool-config-tooltip">
<div class="text-body-2 font-weight-medium mb-2">
{{ tmTool('functionTools.configTags.tooltipTitle', { config: tag.conf_name }) }}
</div>
<div
v-for="(condition, index) in tag.matched_conditions"
:key="`${tag.conf_id}-${index}-${condition.key}`"
class="text-body-2 text-medium-emphasis mb-1"
>
{{ formatCondition(condition) }}
</div>
</div>
</v-tooltip>
</div>
</div>
</template>
@@ -135,4 +213,16 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro
font-size: 0.9rem;
line-height: 1.35;
}
.tool-config-tooltip {
max-width: 360px;
padding: 4px 0;
color: rgba(255, 255, 255, 0.92);
}
.tool-config-tooltip :deep(.text-body-2),
.tool-config-tooltip :deep(.text-medium-emphasis),
.tool-config-tooltip :deep(.font-weight-medium) {
color: inherit !important;
}
</style>
@@ -89,6 +89,23 @@ export interface ToolParameter {
description?: string;
}
export interface ToolConfigCondition {
key: string;
operator: 'truthy' | 'equals' | 'in' | 'custom' | string;
expected?: unknown;
actual?: unknown;
matched: boolean;
message?: string | null;
}
export interface BuiltinToolConfigTag {
conf_id: string;
conf_name: string;
enabled: boolean;
matched_conditions: ToolConfigCondition[];
failed_conditions: ToolConfigCondition[];
}
/** MCP/函数工具对象 */
export interface ToolItem {
name: string;
@@ -100,4 +117,6 @@ export interface ToolItem {
};
origin?: string;
origin_name?: string;
builtin_config_statuses?: BuiltinToolConfigTag[];
builtin_config_tags?: BuiltinToolConfigTag[];
}
+85 -29
View File
@@ -90,31 +90,52 @@
<div v-if="filteredTools.length > 0" class="tools-selection">
<v-virtual-scroll :items="filteredTools" height="300" item-height="72">
<template v-slot:default="{ item }">
<v-list-item :key="item.name" density="comfortable"
@click="toggleTool(item.name)">
<template v-slot:prepend>
<v-checkbox-btn :model-value="isToolSelected(item.name)"
@click.stop="toggleTool(item.name)" />
<v-tooltip
:disabled="!isBuiltinTool(item)"
location="top"
>
<template v-slot:activator="{ props: tooltipProps }">
<div v-bind="tooltipProps">
<v-list-item
:key="item.name"
density="comfortable"
:disabled="isBuiltinTool(item)"
@click="toggleTool(item.name)"
>
<template v-slot:prepend>
<v-checkbox-btn
v-if="!isBuiltinTool(item)"
:model-value="isToolSelected(item.name)"
@click.stop="toggleTool(item.name)"
/>
<div
v-else
class="builtin-tool-checkbox-placeholder"
/>
</template>
<v-list-item-title>
{{ item.name }}
<v-chip v-if="item.origin" size="x-small" color="info" class="mr-2"
variant="tonal">
{{ item.origin }}
</v-chip>
<v-chip v-if="item.origin_name" size="x-small" color="info"
variant="outlined">
{{ item.origin_name }}
</v-chip>
</v-list-item-title>
<v-list-item-subtitle v-if="item.description">
{{ truncateText(item.description, 100) }}
</v-list-item-subtitle>
</v-list-item>
</div>
</template>
<v-list-item-title>
{{ item.name }}
<v-chip v-if="item.origin" size="x-small" color="info" class="mr-2"
variant="tonal">
{{ item.origin }}
</v-chip>
<v-chip v-if="item.origin_name" size="x-small" color="info"
variant="outlined">
{{ item.origin_name }}
</v-chip>
</v-list-item-title>
<v-list-item-subtitle v-if="item.description">
{{ truncateText(item.description, 100) }}
</v-list-item-subtitle>
</v-list-item>
<span>{{ tm('form.builtinToolDisabledHint') }}</span>
</v-tooltip>
</template>
</v-virtual-scroll>
</div>
@@ -155,11 +176,26 @@
</h4>
<div v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
<v-chip v-for="toolName in personaForm.tools" :key="toolName" size="small"
color="primary" variant="tonal" closable
@click:close="removeTool(toolName)">
{{ toolName }}
</v-chip>
<v-tooltip
v-for="toolName in personaForm.tools"
:key="toolName"
:disabled="!isBuiltinToolName(toolName)"
location="top"
>
<template v-slot:activator="{ props: tooltipProps }">
<v-chip
v-bind="tooltipProps"
size="small"
color="primary"
variant="tonal"
:closable="!isBuiltinToolName(toolName)"
@click:close="removeTool(toolName)"
>
{{ toolName }}
</v-chip>
</template>
<span>{{ tm('form.builtinToolDisabledHint') }}</span>
</v-tooltip>
</div>
<div v-else class="text-body-2 text-medium-emphasis">
{{ tm('form.noToolsSelected') }}
@@ -712,6 +748,9 @@ export default {
},
toggleTool(toolName) {
if (this.isBuiltinToolName(toolName)) {
return;
}
// 如果当前是全选状态,需要先转换为具体的工具列表
if (this.personaForm.tools === null) {
// 如果是全选状态,点击某个工具表示要取消选择该工具
@@ -735,6 +774,9 @@ export default {
},
removeTool(toolName) {
if (this.isBuiltinToolName(toolName)) {
return;
}
// 如果当前是全选状态,需要先转换为具体的工具列表
if (this.personaForm.tools === null) {
// 创建一个包含所有工具的数组,然后移除指定工具
@@ -784,6 +826,14 @@ export default {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
},
isBuiltinTool(tool) {
return tool?.origin === 'builtin' || tool?.readonly === true;
},
isBuiltinToolName(toolName) {
return this.availableTools.some(tool => tool.name === toolName && this.isBuiltinTool(tool));
},
getDialogRules(index) {
const dialogType = index % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage');
return [
@@ -859,6 +909,12 @@ export default {
overflow-y: auto;
}
.builtin-tool-checkbox-placeholder {
width: 40px;
height: 40px;
flex: 0 0 40px;
}
.skills-selection {
max-height: 300px;
overflow-y: auto;
@@ -117,7 +117,9 @@ const defaultPersonaData = {
const normalizedTools = computed(() => (Array.isArray(personaData.value?.tools) ? personaData.value.tools : []))
const normalizedSkills = computed(() => (Array.isArray(personaData.value?.skills) ? personaData.value.skills : []))
const allToolsCount = computed(() => Object.keys(toolMetaMap.value).length)
const allToolsCount = computed(() =>
Object.values(toolMetaMap.value).filter((tool) => tool.origin !== 'builtin').length
)
const allSkillsCount = computed(() => availableSkills.value.length)
const resolvedTools = computed(() =>
normalizedTools.value.map((toolName) => {
@@ -152,11 +152,11 @@
},
"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)",
"hint": "Allows the AstrBot to access and use your computer or an sandbox environment to perform more complex tasks. See: [Computer Use](https://docs.astrbot.app/use/computer.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."
"hint": "Environment allowed for Agent usage. `local` means the local machine environment, `sandbox` means the sandbox environment, and `none` means no environment is allowed."
},
"computer_use_require_admin": {
"description": "Require AstrBot Admin Permission",
@@ -1641,4 +1641,4 @@
"helpMiddle": "or",
"helpSuffix": "."
}
}
}
@@ -35,6 +35,7 @@
"mcpServersQuickSelect": "MCP Servers Quick Select",
"searchTools": "Search Tools",
"selectedTools": "Selected Tools",
"builtinToolDisabledHint": "Builtin tools cannot be enabled or disabled here yet. Please enable or disable the corresponding config items in the config file.",
"noToolsAvailable": "No tools available",
"noToolsFound": "No matching tools found",
"loadingTools": "Loading tools...",
@@ -47,6 +47,15 @@
"originName": "Origin Name",
"readonly": "Read-only",
"actions": "Actions"
},
"configTags": {
"tooltipTitle": "This tool is enabled in config file {config} because:",
"conditions": {
"truthy": "{key} is enabled",
"equals": "{key} = {expected}",
"in": "{key} matched {expected}",
"fallback": "{key} is currently {actual}"
}
}
},
"marketplace": {
@@ -12,11 +12,21 @@
"onboard": {
"title": "Quick Onboarding",
"subtitle": "Complete initialization directly on the welcome page.",
"step1Title": "Configure Platform Bot",
"step1Desc": "Connect AstrBot to IM platforms like QQ, Lark, Slack, Telegram, etc.",
"step2Title": "Configure AI Model",
"step2Desc": "Configure AI models for AstrBot.",
"step1Title": "Configure AI Model",
"step1Desc": "Configure AI models for AstrBot.",
"step2Title": "Configure Platform Bot",
"step2Desc": "Connect AstrBot to IM platforms like QQ, Lark, Slack, Telegram, etc.",
"step3Title": "Allow Agent to Use the Computer",
"step3Desc": "Set whether the Agent can access and use this computer.",
"step3HelpTitle": "Access Details",
"step3HelpItem1": "The Agent can access and use the workspace directory, which is isolated for each user, as well as Skills to handle more complex tasks.",
"step3HelpItem2": "For AstrBot administrators, the Agent is additionally allowed to execute shell commands, run Python code, and access all local directories on this machine.",
"step3HelpClose": "Close",
"step3SelectLabel": "Computer Access",
"step3Allow": "Allow",
"step3Deny": "Disallow",
"configure": "Configure",
"save": "Save",
"skip": "Skip",
"pending": "Pending",
"completed": "Completed",
@@ -24,7 +34,11 @@
"platformLoadFailed": "Failed to load platform configuration",
"providerLoadFailed": "Failed to load provider configuration",
"providerUpdateFailed": "Failed to update default chat provider in config file \"default\"",
"providerDefaultUpdated": "Default chat provider in config file \"default\" has been set to {id}"
"providerDefaultUpdated": "Default chat provider in config file \"default\" has been set to {id}",
"computerAccessUpdateFailed": "Failed to update Agent computer access",
"computerAccessAllowed": "Agent is now allowed to access and use the computer",
"computerAccessDenied": "Agent is no longer allowed to access and use the computer",
"step3HelpItem3": "For more detailed control, configure the Computer Access options under Settings -> General -> Computer Access."
},
"resources": {
"title": "Resources",
@@ -152,11 +152,11 @@
},
"agent_computer_use": {
"description": "Использование компьютера (Agent Computer Use)",
"hint": "Позволяет AstrBot получать доступ к вашему компьютеру или песочнице для выполнения сложных задач. См. [Режим песочницы](https://docs.astrbot.app/use/astrbot-agent-sandbox.html), [Навыки](https://docs.astrbot.app/use/skills.html)",
"hint": "Позволяет AstrBot получать доступ к вашему компьютеру или песочнице для выполнения сложных задач. Подробнее: [Использование компьютера](https://docs.astrbot.app/use/computer.html).",
"provider_settings": {
"computer_use_runtime": {
"description": "Среда выполнения (Runtime)",
"hint": "'sandbox' означает запуск в изолированной среде, 'local' — локально на вашем ПК, 'none' — отключено. Если навыки загружены, выбор 'none' сделает их недоступными для агента."
"hint": "Среда, к которой разрешён доступ Agent: `local` — локальная среда компьютера, `sandbox` — изолированная песочница, `none` — запретить доступ к любым средам."
},
"computer_use_require_admin": {
"description": "Требовать права администратора AstrBot",
@@ -35,6 +35,7 @@
"mcpServersQuickSelect": "Быстрый выбор MCP серверов",
"searchTools": "Поиск инструментов",
"selectedTools": "Выбранные инструменты",
"builtinToolDisabledHint": "Встроенные инструменты пока нельзя включать или выключать здесь. Измените соответствующие параметры в файле конфигурации.",
"noToolsAvailable": "Нет доступных инструментов",
"noToolsFound": "Инструменты не найдены",
"loadingTools": "Загрузка инструментов...",
@@ -143,4 +144,4 @@
"success": "Объект перемещен",
"error": "Ошибка перемещения"
}
}
}
@@ -47,6 +47,15 @@
"originName": "Имя источника",
"readonly": "Только чтение",
"actions": "Действия"
},
"configTags": {
"tooltipTitle": "Этот инструмент включен в файле конфигурации {config}, потому что:",
"conditions": {
"truthy": "{key} включен",
"equals": "{key} = {expected}",
"in": "{key} соответствует {expected}",
"fallback": "Текущее значение {key}: {actual}"
}
}
},
"marketplace": {
@@ -1,37 +1,51 @@
{
"greeting": {
"morning": "Доброе утро, добро пожаловать в AstrBot",
"afternoon": "Добрый день, добро пожаловать в AstrBot",
"evening": "Добрый вечер, добро пожаловать в AstrBot",
"newYear": "С Новым Годом!"
},
"subtitle": "Сначала пройдите базовое руководство. Настройку платформ и провайдеров моделей можно завершить позже.",
"announcement": {
"title": "Объявление"
},
"onboard": {
"title": "Быстрый старт",
"subtitle": "Вы можете выполнить первичную настройку прямо здесь.",
"step1Title": "Настройка платформ",
"step1Desc": "Подключите AstrBot к QQ, Lark, WeChat, Telegram и другим мессенджерам.",
"step2Title": "Настройка AI моделей",
"step2Desc": "Выберите и настройте AI провайдеров для AstrBot.",
"configure": "Настроить",
"skip": "Пропустить",
"pending": "Ожидает",
"completed": "Готово",
"skipped": "Пропущено",
"platformLoadFailed": "Ошибка загрузки конфигурации платформ",
"providerLoadFailed": "Ошибка загрузки конфигурации провайдеров",
"providerUpdateFailed": "Ошибка обновления провайдера по умолчанию в файле default",
"providerDefaultUpdated": "Провайдер {id} установлен по умолчанию в файле default"
},
"resources": {
"title": "Ресурсы",
"githubDesc": "Поставьте нам звезду на GitHub!",
"docsTitle": "Документация",
"docsDesc": "Официальная документация AstrBot.",
"afdianTitle": "Afdian",
"afdianDesc": "Поддержите команду AstrBot через Afdian."
}
}
{
"greeting": {
"morning": "Доброе утро, добро пожаловать в AstrBot",
"afternoon": "Добрый день, добро пожаловать в AstrBot",
"evening": "Добрый вечер, добро пожаловать в AstrBot",
"newYear": "С Новым Годом!"
},
"subtitle": "Сначала пройдите базовое руководство. Настройку платформ и провайдеров моделей можно завершить позже.",
"announcement": {
"title": "Объявление"
},
"onboard": {
"title": "Быстрый старт",
"subtitle": "Вы можете выполнить первичную настройку прямо здесь.",
"step1Title": "Настройка AI моделей",
"step1Desc": "Выберите и настройте AI провайдеров для AstrBot.",
"step2Title": "Настройка платформ",
"step2Desc": "Подключите AstrBot к QQ, Lark, WeChat, Telegram и другим мессенджерам.",
"step3Title": "Разрешить Agent использовать компьютер",
"step3Desc": "Укажите, может ли Agent получать доступ к этому компьютеру и использовать его.",
"step3HelpTitle": "Сведения о доступе",
"step3HelpItem1": "Agent сможет получать доступ к каталогу рабочего пространства и использовать его, при этом рабочие каталоги разных пользователей изолированы друг от друга, а также использовать Skills для выполнения более сложных задач.",
"step3HelpItem2": "Для администраторов AstrBot Agent дополнительно сможет выполнять shell-команды, запускать Python-код и получать доступ ко всем локальным каталогам на этом компьютере.",
"step3HelpClose": "Закрыть",
"step3SelectLabel": "Доступ к компьютеру",
"step3Allow": "Разрешить",
"step3Deny": "Запретить",
"configure": "Настроить",
"save": "Сохранить",
"skip": "Пропустить",
"pending": "Ожидает",
"completed": "Готово",
"skipped": "Пропущено",
"platformLoadFailed": "Ошибка загрузки конфигурации платформ",
"providerLoadFailed": "Ошибка загрузки конфигурации провайдеров",
"providerUpdateFailed": "Ошибка обновления провайдера по умолчанию в файле default",
"providerDefaultUpdated": "Провайдер {id} установлен по умолчанию в файле default",
"computerAccessUpdateFailed": "Не удалось обновить доступ Agent к компьютеру",
"computerAccessAllowed": "Agent теперь может получать доступ к компьютеру и использовать его",
"computerAccessDenied": "Agent больше не может получать доступ к компьютеру и использовать его",
"step3HelpItem3": "Для более тонкой настройки перейдите в «Конфигурация → Основные настройки → Доступ к компьютеру»."
},
"resources": {
"title": "Ресурсы",
"githubDesc": "Поставьте нам звезду на GitHub!",
"docsTitle": "Документация",
"docsDesc": "Официальная документация AstrBot.",
"afdianTitle": "Afdian",
"afdianDesc": "Поддержите команду AstrBot через Afdian."
}
}
@@ -154,11 +154,11 @@
},
"agent_computer_use": {
"description": "使用电脑能力",
"hint": "让 AstrBot 访问和使用你的电脑或者隔离的沙盒环境,以执行更复杂的任务。详见: [沙盒模式](https://docs.astrbot.app/use/astrbot-agent-sandbox.html), [Skills](https://docs.astrbot.app/use/skills.html)。",
"hint": "让 AstrBot 访问和使用本机环境或者隔离的沙盒环境,以执行更复杂的任务。详见: [电脑使用](https://docs.astrbot.app/use/computer.html)。",
"provider_settings": {
"computer_use_runtime": {
"description": "运行环境",
"hint": "sandbox 代表在沙箱环境中运行, local 代表在本地环境中运行, none 代表不启用。如果上传了 skills,选择 none 会导致其无法被 Agent 正常使用。"
"hint": "允许 Agent 访问的环境。local 为本机环境,sandbox 为沙箱环境,none 为不允许任何环境。"
},
"computer_use_require_admin": {
"description": "需要 AstrBot 管理员权限",
@@ -1119,22 +1119,22 @@
"hint": "仅在使用 qwen3-rerank 模型时生效。建议使用英文撰写。"
},
"nvidia_rerank_api_base": {
"description": "API Base URL"
"description": "API Base URL"
},
"nvidia_rerank_api_key": {
"description": "API Key"
"description": "API Key"
},
"nvidia_rerank_model": {
"description": "重排序模型名称",
"hint": "请参照NVIDIA Docs中模型名称填写。"
"description": "重排序模型名称",
"hint": "请参照NVIDIA Docs中模型名称填写。"
},
"nvidia_rerank_model_endpoint": {
"description": "自定义模型端点",
"hint": "自定义URL末尾端点,默认为 /reranking"
"description": "自定义模型端点",
"hint": "自定义URL末尾端点,默认为 /reranking"
},
"nvidia_rerank_truncate": {
"description": "文本截断策略",
"hint": "当输入文本过长时,是否截断输入以适应模型的最大上下文长度。"
"description": "文本截断策略",
"hint": "当输入文本过长时,是否截断输入以适应模型的最大上下文长度。"
},
"launch_model_if_not_running": {
"description": "模型未运行时自动启动",
@@ -1643,4 +1643,4 @@
"helpMiddle": "或",
"helpSuffix": "。"
}
}
}
@@ -35,6 +35,7 @@
"mcpServersQuickSelect": "MCP 服务器快速选择",
"searchTools": "搜索工具",
"selectedTools": "已选择的工具",
"builtinToolDisabledHint": "暂不支持在这里启用和停用内置工具,请在配置文件中启用和停用工具对应的配置项。",
"noToolsAvailable": "暂无可用工具",
"noToolsFound": "未找到匹配的工具",
"loadingTools": "正在加载工具...",
@@ -47,6 +47,15 @@
"originName": "来源名称",
"readonly": "只读",
"actions": "操作"
},
"configTags": {
"tooltipTitle": "该工具在配置文件 {config} 中启用,因为:",
"conditions": {
"truthy": "启用了 {key}",
"equals": "{key} = {expected}",
"in": "{key} 命中了 {expected}",
"fallback": "{key} 当前值为 {actual}"
}
}
},
"marketplace": {
@@ -12,11 +12,21 @@
"onboard": {
"title": "快速引导",
"subtitle": "欢迎页可直接完成初始化。",
"step1Title": "配置平台机器人",
"step1Desc": "将 AstrBot 连接到 QQ、飞书、企业微信、Telegram 等 IM 平台。",
"step2Title": "配置 AI 模型",
"step2Desc": "为 AstrBot 配置 AI 模型。",
"step1Title": "配置 AI 模型",
"step1Desc": "为 AstrBot 配置 AI 模型。",
"step2Title": "配置平台机器人",
"step2Desc": "将 AstrBot 连接到 QQ、飞书、企业微信、Telegram 等 IM 平台。",
"step3Title": "允许 Agent 使用电脑",
"step3Desc": "设置 Agent 是否可以访问和使用当前电脑。",
"step3HelpTitle": "允许 Agent 使用电脑",
"step3HelpItem1": "Agent 将可以访问和使用工作区目录(每个用户的工作区目录相互独立)以及 Skills 来执行更多复杂任务。",
"step3HelpItem2": "对于 AstrBot 管理员,会额外允许 Agent 执行 Shell 命令、Python 代码,以及访问本机的所有目录。",
"step3HelpClose": "关闭",
"step3SelectLabel": "电脑访问权限",
"step3Allow": "允许",
"step3Deny": "不允许",
"configure": "去配置",
"save": "保存",
"skip": "跳过",
"pending": "待处理",
"completed": "已完成",
@@ -24,7 +34,11 @@
"platformLoadFailed": "加载平台配置失败",
"providerLoadFailed": "加载提供商配置失败",
"providerUpdateFailed": "更新 default 配置文件默认对话提供商失败",
"providerDefaultUpdated": "已将 default 配置文件的默认对话提供商设置为 {id}"
"providerDefaultUpdated": "已将 default 配置文件的默认对话提供商设置为 {id}",
"computerAccessUpdateFailed": "更新 Agent 电脑访问权限失败",
"computerAccessAllowed": "已允许 Agent 访问和使用电脑",
"computerAccessDenied": "已禁止 Agent 访问和使用电脑",
"step3HelpItem3": "如需更多细化权限与能力配置,可前往 配置文件 -> 普通配置 -> 使用电脑能力 继续设置。"
},
"resources": {
"title": "相关资源",
+165 -18
View File
@@ -20,11 +20,28 @@
</div>
<v-timeline align="start" side="end" density="compact" class="welcome-timeline" truncate-line="both">
<v-timeline-item :dot-color="platformStepState === 'completed' ? 'success' : 'primary'"
:icon="platformStepState === 'completed' ? 'mdi-check' : 'mdi-numeric-1'" fill-dot size="small">
<v-timeline-item :dot-color="providerStepState === 'completed' ? 'success' : 'primary'"
icon="mdi-numeric-1" fill-dot size="small">
<div class="pl-2">
<div class="text-h6 font-weight-bold mb-1">{{ tm('onboard.step1Title') }}</div>
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step1Desc') }}</p>
<div class="d-flex align-center">
<v-btn color="primary" variant="flat" rounded="pill" class="px-6" @click="openProviderDialog">
{{ tm('onboard.configure') }}
</v-btn>
<div v-if="providerStepState === 'completed'"
class="text-success d-flex align-center text-body-2 font-weight-medium ml-3">
{{ tm('onboard.completed') }}
</div>
</div>
</div>
</v-timeline-item>
<v-timeline-item :dot-color="platformStepState === 'completed' ? 'success' : 'primary'"
icon="mdi-numeric-2" fill-dot size="small">
<div class="pl-2">
<div class="text-h6 font-weight-bold mb-1">{{ tm('onboard.step2Title') }}</div>
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step2Desc') }}</p>
<div class="d-flex align-center">
<v-btn color="primary" variant="flat" rounded="pill" class="px-6" :loading="loadingPlatformDialog"
@click="openPlatformDialog">
@@ -38,22 +55,37 @@
</div>
</v-timeline-item>
<v-timeline-item :dot-color="providerStepState === 'completed' ? 'success' : 'primary'"
:icon="providerStepState === 'completed' ? 'mdi-check' : 'mdi-numeric-2'" fill-dot size="small">
<v-timeline-item :dot-color="computerAccessStepState === 'completed' ? 'success' : 'primary'"
icon="mdi-numeric-3" fill-dot size="small">
<div class="pl-2">
<div class="text-h6 font-weight-bold mb-1"
:class="{ 'text-medium-emphasis': platformStepState !== 'completed' }">{{ tm('onboard.step2Title')
}}
</div>
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step2Desc') }}</p>
<div class="d-flex align-center">
<v-btn color="primary" variant="flat" rounded="pill" class="px-6" @click="openProviderDialog">
{{ tm('onboard.configure') }}
<div class="d-flex align-center mb-1">
<div class="text-h6 font-weight-bold">{{ tm('onboard.step3Title') }}</div>
<v-btn
icon
variant="text"
density="comfortable"
size="small"
class="ml-1"
@click="showComputerAccessHelpDialog = true"
>
<span class="text-body-2 font-weight-bold">?</span>
</v-btn>
<div v-if="providerStepState === 'completed'"
class="text-success d-flex align-center text-body-2 font-weight-medium ml-3">
{{ tm('onboard.completed') }}
</div>
</div>
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step3Desc') }}</p>
<div class="d-flex flex-wrap align-center ga-3">
<v-select
v-model="computerAccessRuntime"
:items="computerAccessOptions"
item-title="title"
item-value="value"
:label="tm('onboard.step3SelectLabel')"
:loading="savingComputerAccess"
:disabled="savingComputerAccess"
hide-details
density="comfortable"
variant="outlined"
class="computer-access-select"
/>
</div>
</div>
</v-timeline-item>
@@ -136,6 +168,26 @@
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="platformMetadata" :config_data="platformConfigData"
@refresh-config="loadPlatformConfigBase" />
<ProviderConfigDialog v-model="showProviderDialog" />
<v-dialog v-model="showComputerAccessHelpDialog" max-width="640">
<v-card>
<v-card-title class="text-h3 font-weight-bold pa-4">
{{ tm('onboard.step3HelpTitle') }}
</v-card-title>
<v-card-text>
<ol class="computer-access-help-list">
<li>{{ tm('onboard.step3HelpItem1') }}</li>
<li>{{ tm('onboard.step3HelpItem2') }}</li>
<li>{{ tm('onboard.step3HelpItem3') }}</li>
</ol>
</v-card-text>
<v-card-actions class="px-6 pb-4">
<v-spacer />
<v-btn color="primary" variant="text" @click="showComputerAccessHelpDialog = false">
{{ tm('onboard.step3HelpClose') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
@@ -151,6 +203,7 @@ import 'markstream-vue/index.css';
import 'highlight.js/styles/github.css';
type StepState = 'pending' | 'completed' | 'skipped';
type ComputerAccessRuntime = 'local' | 'none';
const { tm } = useModuleI18n('features/welcome');
const { locale } = useI18n();
@@ -158,6 +211,7 @@ const { success: showSuccess, error: showError } = useToast();
const showAddPlatformDialog = ref(false);
const showProviderDialog = ref(false);
const showComputerAccessHelpDialog = ref(false);
const loadingPlatformDialog = ref(false);
const platformMetadata = ref<Record<string, any>>({});
@@ -167,6 +221,10 @@ const providerCountBeforeOpen = ref(0);
const platformStepState = ref<StepState>('pending');
const providerStepState = ref<StepState>('pending');
const computerAccessStepState = ref<StepState>('pending');
const computerAccessRuntime = ref<ComputerAccessRuntime>('none');
const savedComputerAccessRuntime = ref<ComputerAccessRuntime>('none');
const savingComputerAccess = ref(false);
const welcomeAnnouncementRaw = ref<unknown>(null);
function resolveWelcomeAnnouncement(raw: unknown, currentLocale: string) {
@@ -276,6 +334,11 @@ async function loadPlatformConfigBase() {
platformConfigData.value = res.data.data.config || {};
}
async function fetchDefaultConfig() {
const res = await axios.get('/api/config/abconf', { params: { id: 'default' } });
return res.data?.data?.config || {};
}
function getChatProvidersFromTemplatePayload(payload: any) {
const providers = payload?.providers || [];
const sources = payload?.provider_sources || [];
@@ -315,8 +378,7 @@ async function syncDefaultConfigProviderIfNeeded() {
const targetProviderId = pickDefaultProviderId(providers);
if (!targetProviderId) return;
const configRes = await axios.get('/api/config/abconf', { params: { id: 'default' } });
const configData = configRes.data?.data?.config || {};
const configData = await fetchDefaultConfig();
if (!configData.provider_settings) {
configData.provider_settings = {};
}
@@ -336,6 +398,62 @@ async function syncDefaultConfigProviderIfNeeded() {
showSuccess(tm('onboard.providerDefaultUpdated', { id: targetProviderId }));
}
function normalizeComputerAccessRuntime(runtime: unknown): ComputerAccessRuntime {
return runtime === 'local' || runtime === 'sandbox' ? 'local' : 'none';
}
function syncComputerAccessRuntime(configData: any) {
const providerSettings = configData?.provider_settings || {};
const currentRuntime = providerSettings?.computer_use_runtime;
const normalizedRuntime = normalizeComputerAccessRuntime(currentRuntime);
computerAccessRuntime.value = normalizedRuntime;
savedComputerAccessRuntime.value = normalizedRuntime;
computerAccessStepState.value =
currentRuntime === 'local' || currentRuntime === 'none' || currentRuntime === 'sandbox'
? 'completed'
: 'pending';
}
const computerAccessOptions = computed(() => [
{ title: tm('onboard.step3Allow'), value: 'local' },
{ title: tm('onboard.step3Deny'), value: 'none' }
]);
async function saveComputerAccessRuntime() {
savingComputerAccess.value = true;
try {
const configData = await fetchDefaultConfig();
if (!configData.provider_settings) {
configData.provider_settings = {};
}
configData.provider_settings.computer_use_runtime = computerAccessRuntime.value;
const updateRes = await axios.post('/api/config/astrbot/update', {
conf_id: 'default',
config: configData
});
if (updateRes.data.status !== 'ok') {
throw new Error(updateRes.data.message || tm('onboard.computerAccessUpdateFailed'));
}
savedComputerAccessRuntime.value = computerAccessRuntime.value;
computerAccessStepState.value = 'completed';
showSuccess(
tm(
computerAccessRuntime.value === 'local'
? 'onboard.computerAccessAllowed'
: 'onboard.computerAccessDenied'
)
);
} catch (err: any) {
showError(err?.response?.data?.message || err?.message || tm('onboard.computerAccessUpdateFailed'));
} finally {
savingComputerAccess.value = false;
}
}
async function loadWelcomeAnnouncement() {
try {
const res = await axios.get('https://cloud.astrbot.app/api/v1/announcement');
@@ -366,6 +484,13 @@ onMounted(async () => {
} catch (e) {
console.error(e);
}
try {
const defaultConfig = await fetchDefaultConfig();
syncComputerAccessRuntime(defaultConfig);
} catch (e) {
console.error(e);
}
});
async function openPlatformDialog() {
@@ -416,6 +541,18 @@ watch(showProviderDialog, async (visible, wasVisible) => {
showError(err?.response?.data?.message || err?.message || tm('onboard.providerUpdateFailed'));
}
});
watch(computerAccessRuntime, async (value, oldValue) => {
if (value === oldValue) return;
if (value === savedComputerAccessRuntime.value) return;
if (savingComputerAccess.value) return;
try {
await saveComputerAccessRuntime();
} catch {
computerAccessRuntime.value = savedComputerAccessRuntime.value;
}
});
</script>
<style scoped>
@@ -430,4 +567,14 @@ watch(showProviderDialog, async (visible, wasVisible) => {
.welcome-announcement-markdown {
line-height: 1.7;
}
.computer-access-select {
max-width: 240px;
min-width: 220px;
}
.computer-access-help-list {
margin: 0;
padding-left: 1.25rem;
}
</style>