mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
fix: refine extension ui and backend errors
This commit is contained in:
@@ -97,12 +97,11 @@ class SkillToolLoader(loader.ToolLoader):
|
|||||||
if skill_data is None:
|
if skill_data is None:
|
||||||
visible_skills = getattr(skill_mgr, 'skills', {})
|
visible_skills = getattr(skill_mgr, 'skills', {})
|
||||||
available_names = ', '.join(sorted(visible_skills.keys())) or 'none'
|
available_names = ', '.join(sorted(visible_skills.keys())) or 'none'
|
||||||
raise ValueError(
|
raise ValueError(f'Skill "{skill_name}" not found. Available skills: {available_names}')
|
||||||
f'Skill "{skill_name}" not found. Available skills: {available_names}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register activated skill for sandbox mount path resolution
|
# Register activated skill for sandbox mount path resolution
|
||||||
from . import skill as skill_loader
|
from . import skill as skill_loader
|
||||||
|
|
||||||
skill_loader.register_activated_skill(query, skill_data)
|
skill_loader.register_activated_skill(query, skill_data)
|
||||||
|
|
||||||
# Return SKILL.md content as Tool Result (injects into context)
|
# Return SKILL.md content as Tool Result (injects into context)
|
||||||
@@ -112,17 +111,17 @@ class SkillToolLoader(loader.ToolLoader):
|
|||||||
|
|
||||||
# Build Tool Result content
|
# Build Tool Result content
|
||||||
result_content = f'<command-message>The "{skill_name}" skill is activated</command-message>\n'
|
result_content = f'<command-message>The "{skill_name}" skill is activated</command-message>\n'
|
||||||
result_content += f'<skill-activation>\n'
|
result_content += '<skill-activation>\n'
|
||||||
result_content += f'<skill-name>{skill_name}</skill-name>\n'
|
result_content += f'<skill-name>{skill_name}</skill-name>\n'
|
||||||
result_content += f'<mount-path>{mount_path}</mount-path>\n'
|
result_content += f'<mount-path>{mount_path}</mount-path>\n'
|
||||||
result_content += f'<package-root>{package_root}</package-root>\n'
|
result_content += f'<package-root>{package_root}</package-root>\n'
|
||||||
result_content += f'\n## Instructions\n{instructions}\n'
|
result_content += f'\n## Instructions\n{instructions}\n'
|
||||||
result_content += f'\n## Runtime Context\n'
|
result_content += '\n## Runtime Context\n'
|
||||||
result_content += f'The skill package is mounted at {mount_path}. Use the standard tools to interact with it:\n'
|
result_content += f'The skill package is mounted at {mount_path}. Use the standard tools to interact with it:\n'
|
||||||
result_content += f'- Use `read` to inspect files under {mount_path}\n'
|
result_content += f'- Use `read` to inspect files under {mount_path}\n'
|
||||||
result_content += f'- Use `exec` with workdir set to {mount_path} to run commands in that package\n'
|
result_content += f'- Use `exec` with workdir set to {mount_path} to run commands in that package\n'
|
||||||
result_content += f'- Use `write` and `edit` on that path when the instructions require updating files\n'
|
result_content += '- Use `write` and `edit` on that path when the instructions require updating files\n'
|
||||||
result_content += f'</skill-activation>\n'
|
result_content += '</skill-activation>\n'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'activated': True,
|
'activated': True,
|
||||||
@@ -190,7 +189,7 @@ class SkillToolLoader(loader.ToolLoader):
|
|||||||
if not normalized_path.startswith('/workspace'):
|
if not normalized_path.startswith('/workspace'):
|
||||||
raise ValueError('path must be under /workspace')
|
raise ValueError('path must be under /workspace')
|
||||||
|
|
||||||
relative = normalized_path[len('/workspace'):].lstrip('/')
|
relative = normalized_path[len('/workspace') :].lstrip('/')
|
||||||
host_root = os.path.realpath(workspace_root)
|
host_root = os.path.realpath(workspace_root)
|
||||||
host_path = os.path.realpath(os.path.join(host_root, relative))
|
host_path = os.path.realpath(os.path.join(host_root, relative))
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ class SkillToolLoader(loader.ToolLoader):
|
|||||||
'properties': {
|
'properties': {
|
||||||
'skill_name': {
|
'skill_name': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The skill name to activate (no arguments). E.g., "pdf" or "create-skill"',
|
'description': 'The skill name to activate (no arguments). E.g., "pdf" or "data-analysis"',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'required': ['skill_name'],
|
'required': ['skill_name'],
|
||||||
@@ -227,7 +226,7 @@ class SkillToolLoader(loader.ToolLoader):
|
|||||||
name=REGISTER_SKILL_TOOL_NAME,
|
name=REGISTER_SKILL_TOOL_NAME,
|
||||||
human_desc='Register a skill from sandbox',
|
human_desc='Register a skill from sandbox',
|
||||||
description=(
|
description=(
|
||||||
'Register a skill package from a directory under /workspace into LangBot\'s skill store. '
|
"Register a skill package from a directory under /workspace into LangBot's skill store. "
|
||||||
'Use this after creating or preparing a skill in the sandbox with exec/read/write/edit. '
|
'Use this after creating or preparing a skill in the sandbox with exec/read/write/edit. '
|
||||||
'The directory must contain a SKILL.md file. '
|
'The directory must contain a SKILL.md file. '
|
||||||
'After registration, the skill can be activated with the activate tool.'
|
'After registration, the skill can be activated with the activate tool.'
|
||||||
@@ -276,15 +275,15 @@ class SkillToolLoader(loader.ToolLoader):
|
|||||||
available_skills_lines = ['<available_skills>']
|
available_skills_lines = ['<available_skills>']
|
||||||
for skill_name, skill_data in sorted(skills.items()):
|
for skill_name, skill_data in sorted(skills.items()):
|
||||||
description = skill_data.get('description', '')
|
description = skill_data.get('description', '')
|
||||||
available_skills_lines.append(f'<skill>')
|
available_skills_lines.append('<skill>')
|
||||||
available_skills_lines.append(f'<name>{skill_name}</name>')
|
available_skills_lines.append(f'<name>{skill_name}</name>')
|
||||||
available_skills_lines.append(f'<description>{description}</description>')
|
available_skills_lines.append(f'<description>{description}</description>')
|
||||||
available_skills_lines.append(f'</skill>')
|
available_skills_lines.append('</skill>')
|
||||||
available_skills_lines.append('</available_skills>')
|
available_skills_lines.append('</available_skills>')
|
||||||
|
|
||||||
available_skills_block = '\n'.join(available_skills_lines)
|
available_skills_block = '\n'.join(available_skills_lines)
|
||||||
|
|
||||||
return f'''Activate a skill within the main conversation.
|
return f"""Activate a skill within the main conversation.
|
||||||
|
|
||||||
<skills_instructions>
|
<skills_instructions>
|
||||||
When users ask you to perform tasks, check if any of the available skills
|
When users ask you to perform tasks, check if any of the available skills
|
||||||
@@ -299,7 +298,7 @@ The skill is activated
|
|||||||
- The skill's instructions will be provided in the tool result
|
- The skill's instructions will be provided in the tool result
|
||||||
- Examples:
|
- Examples:
|
||||||
- skill_name: "pdf" - invoke the pdf skill
|
- skill_name: "pdf" - invoke the pdf skill
|
||||||
- skill_name: "create-skill" - invoke the create-skill skill for creating new skills
|
- skill_name: "data-analysis" - invoke the data-analysis skill
|
||||||
|
|
||||||
Important:
|
Important:
|
||||||
- Only use skills listed in <available_skills> below
|
- Only use skills listed in <available_skills> below
|
||||||
@@ -307,4 +306,4 @@ Important:
|
|||||||
- To create a new skill: prepare it in /workspace, then use register_skill tool
|
- To create a new skill: prepare it in /workspace, then use register_skill tool
|
||||||
</skills_instructions>
|
</skills_instructions>
|
||||||
|
|
||||||
{available_skills_block}'''
|
{available_skills_block}"""
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from ..core import app
|
from ..core import app
|
||||||
@@ -16,9 +15,7 @@ if typing.TYPE_CHECKING:
|
|||||||
class SkillManager:
|
class SkillManager:
|
||||||
"""Skill manager backed by filesystem packages.
|
"""Skill manager backed by filesystem packages.
|
||||||
|
|
||||||
Skills are loaded from two sources:
|
Skills are loaded from data/skills/ and managed by users.
|
||||||
1. Builtin skills: templates/skills/ (shipped with LangBot)
|
|
||||||
2. User skills: data/skills/ (created by users)
|
|
||||||
|
|
||||||
Skills are activated through the `activate` tool (Tool Call mechanism),
|
Skills are activated through the `activate` tool (Tool Call mechanism),
|
||||||
aligned with Claude Code's design. This protects KV Cache and follows
|
aligned with Claude Code's design. This protects KV Cache and follows
|
||||||
@@ -38,8 +35,7 @@ class SkillManager:
|
|||||||
async def reload_skills(self):
|
async def reload_skills(self):
|
||||||
"""Reload all skills.
|
"""Reload all skills.
|
||||||
|
|
||||||
Builtin skills (templates/skills/) are copied to data/skills/ on first run,
|
All skills are loaded from data/skills/.
|
||||||
then all skills are loaded from data/skills/.
|
|
||||||
|
|
||||||
NOTE: This performs a full scan. For registering a single new skill,
|
NOTE: This performs a full scan. For registering a single new skill,
|
||||||
consider adding it directly to self.skills instead of reloading all.
|
consider adding it directly to self.skills instead of reloading all.
|
||||||
@@ -51,25 +47,6 @@ class SkillManager:
|
|||||||
managed_root = self.get_managed_skills_root()
|
managed_root = self.get_managed_skills_root()
|
||||||
os.makedirs(managed_root, exist_ok=True)
|
os.makedirs(managed_root, exist_ok=True)
|
||||||
|
|
||||||
# Copy builtin skills to data/skills/ if not already present
|
|
||||||
builtin_root = self.get_builtin_skills_root()
|
|
||||||
if os.path.isdir(builtin_root):
|
|
||||||
for package_root, entry_file in self._discover_skill_directories(builtin_root):
|
|
||||||
skill_data = {
|
|
||||||
'package_root': package_root,
|
|
||||||
'entry_file': entry_file,
|
|
||||||
}
|
|
||||||
if not self._load_skill_file(skill_data):
|
|
||||||
continue
|
|
||||||
|
|
||||||
skill_name = skill_data['name']
|
|
||||||
target_path = os.path.join(managed_root, skill_name)
|
|
||||||
|
|
||||||
# Only copy if target doesn't exist (preserve user modifications)
|
|
||||||
if not os.path.exists(target_path):
|
|
||||||
shutil.copytree(package_root, target_path)
|
|
||||||
self.ap.logger.info(f'Copied builtin skill "{skill_name}" to data/skills/')
|
|
||||||
|
|
||||||
# Load all skills from data/skills/
|
# Load all skills from data/skills/
|
||||||
if os.path.isdir(managed_root):
|
if os.path.isdir(managed_root):
|
||||||
for package_root, entry_file in self._discover_skill_directories(managed_root):
|
for package_root, entry_file in self._discover_skill_directories(managed_root):
|
||||||
@@ -105,11 +82,6 @@ class SkillManager:
|
|||||||
"""Get the root directory for managed user skills."""
|
"""Get the root directory for managed user skills."""
|
||||||
return paths.get_data_path('skills')
|
return paths.get_data_path('skills')
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_builtin_skills_root() -> str:
|
|
||||||
"""Get the root directory for builtin skills (templates/skills/)."""
|
|
||||||
return paths.get_resource_path('templates/skills')
|
|
||||||
|
|
||||||
def _discover_skill_directories(self, root_path: str, max_depth: int = 6) -> list[tuple[str, str]]:
|
def _discover_skill_directories(self, root_path: str, max_depth: int = 6) -> list[tuple[str, str]]:
|
||||||
"""Discover all skill directories under root_path."""
|
"""Discover all skill directories under root_path."""
|
||||||
discovered: list[tuple[str, str]] = []
|
discovered: list[tuple[str, str]] = []
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
name: create-skill
|
|
||||||
description: Create new skills for LangBot. Use when users want to create, modify, or register a skill. Helps draft SKILL.md with proper frontmatter and register skills from sandbox directories.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Create Skill
|
|
||||||
|
|
||||||
A skill for creating new LangBot skills.
|
|
||||||
|
|
||||||
## Skill Structure
|
|
||||||
|
|
||||||
A skill is a directory containing, at minimum, a `SKILL.md` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
skill-name/
|
|
||||||
├── SKILL.md # Required: metadata + instructions
|
|
||||||
├── scripts/ # Optional: helper scripts
|
|
||||||
├── references/ # Optional: documentation
|
|
||||||
├── assets/ # Optional: templates, resources
|
|
||||||
```
|
|
||||||
|
|
||||||
## SKILL.md Format
|
|
||||||
|
|
||||||
SKILL.md must contain YAML frontmatter followed by Markdown content:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
name: skill-name
|
|
||||||
display_name: Skill Display Name
|
|
||||||
description: What this skill does and when to use it.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Skill Title
|
|
||||||
|
|
||||||
Instructions for how to use this skill...
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
- Example 1
|
|
||||||
- Example 2
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontmatter Fields
|
|
||||||
|
|
||||||
| Field | Required | Description |
|
|
||||||
|-------|----------|-------------|
|
|
||||||
| `name` | Yes | Skill identifier. 1-64 chars, lowercase letters, numbers, hyphens only. Must not start or end with hyphen. |
|
|
||||||
| `display_name` | No | Human-readable name shown in UI. |
|
|
||||||
| `description` | Yes | What the skill does and when to use it. Max 1024 chars. Include keywords for triggering. |
|
|
||||||
| `license` | No | License name or reference. |
|
|
||||||
| `metadata` | No | Additional key-value metadata. |
|
|
||||||
|
|
||||||
### Body Content
|
|
||||||
|
|
||||||
The Markdown body contains skill instructions. Recommended sections:
|
|
||||||
- Step-by-step instructions
|
|
||||||
- Examples
|
|
||||||
- Common edge cases
|
|
||||||
- Guidelines
|
|
||||||
|
|
||||||
Keep SKILL.md under 500 lines. Move detailed content to `references/` directory.
|
|
||||||
|
|
||||||
## Creating a Skill
|
|
||||||
|
|
||||||
1. **Understand requirements**: Ask what the skill should do
|
|
||||||
2. **Draft SKILL.md**: Create frontmatter + instructions
|
|
||||||
3. **Create in sandbox**: Write files to `/workspace/{skill-name}/`
|
|
||||||
4. **Register**: Use `register_skill` tool to register to LangBot store
|
|
||||||
|
|
||||||
### Name Rules
|
|
||||||
|
|
||||||
- Lowercase letters, numbers, hyphens only
|
|
||||||
- Cannot start or end with hyphen
|
|
||||||
- No consecutive hyphens (`--`)
|
|
||||||
- 1-64 characters
|
|
||||||
|
|
||||||
Valid: `pdf-processing`, `data-analysis`, `code-review`
|
|
||||||
Invalid: `PDF-Processing`, `-pdf`, `pdf--processing`
|
|
||||||
|
|
||||||
### Description Tips
|
|
||||||
|
|
||||||
Good description describes both what and when:
|
|
||||||
```yaml
|
|
||||||
description: Extracts text from PDF files. Use when working with PDF documents or when user mentions PDFs.
|
|
||||||
```
|
|
||||||
|
|
||||||
Poor description:
|
|
||||||
```yaml
|
|
||||||
description: Helps with PDFs.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow Example
|
|
||||||
|
|
||||||
1. User: "Create a skill for generating reports"
|
|
||||||
2. Ask clarifying questions about report format, templates, etc.
|
|
||||||
3. Create `/workspace/report-generator/SKILL.md`
|
|
||||||
4. Optionally create helper scripts in `/workspace/report-generator/scripts/`
|
|
||||||
5. Call `register_skill(path="/workspace/report-generator", name="report-generator")`
|
|
||||||
6. Skill is now available via `activate(skill_name="report-generator")`
|
|
||||||
|
|
||||||
## After Registration
|
|
||||||
|
|
||||||
The skill package is copied to `data/skills/` and loaded by LangBot.
|
|
||||||
User can activate it immediately with the `activate` tool.
|
|
||||||
@@ -76,18 +76,26 @@ export default function HomeLayout({
|
|||||||
|
|
||||||
// Auto-redirect to wizard on first visit (wizard not yet completed on this instance)
|
// Auto-redirect to wizard on first visit (wizard not yet completed on this instance)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
const checkWizard = async () => {
|
const checkWizard = async () => {
|
||||||
try {
|
try {
|
||||||
// Always re-fetch to ensure we have the latest wizard_status from backend
|
// Always re-fetch to ensure we have the latest wizard_status from backend
|
||||||
await initializeSystemInfo();
|
await initializeSystemInfo({ throwOnError: true });
|
||||||
if (systemInfo.wizard_status === 'none') {
|
if (!cancelled && systemInfo.wizard_status === 'none') {
|
||||||
navigate('/wizard');
|
navigate('/wizard', { replace: true });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// If fetching system info fails, don't redirect
|
if (!cancelled) {
|
||||||
|
navigate('/backend-unavailable', { replace: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkWizard();
|
checkWizard();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -50,17 +50,6 @@ export default function ExtensionCardComponent({
|
|||||||
cardVO.iconURL || httpClient.getPluginIconURL(cardVO.author, cardVO.name);
|
cardVO.iconURL || httpClient.getPluginIconURL(cardVO.author, cardVO.name);
|
||||||
const showFallback = iconFailed || !iconSrc;
|
const showFallback = iconFailed || !iconSrc;
|
||||||
|
|
||||||
const getTypeBadgeColor = (type: ExtensionType) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'mcp':
|
|
||||||
return 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300';
|
|
||||||
case 'skill':
|
|
||||||
return 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300';
|
|
||||||
default:
|
|
||||||
return 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTypeLabel = (type: ExtensionType) => {
|
const getTypeLabel = (type: ExtensionType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'mcp':
|
case 'mcp':
|
||||||
@@ -72,6 +61,30 @@ export default function ExtensionCardComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTypeIcon = (type: ExtensionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'mcp':
|
||||||
|
return Server;
|
||||||
|
case 'skill':
|
||||||
|
return Sparkles;
|
||||||
|
default:
|
||||||
|
return Puzzle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTypeBadge = (type: ExtensionType) => {
|
||||||
|
const TypeIcon = getTypeIcon(type);
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="flex-shrink-0 gap-1.5 border-blue-200 bg-blue-50/60 text-[0.7rem] text-blue-700 dark:border-blue-500/40 dark:bg-blue-500/10 dark:text-blue-300"
|
||||||
|
>
|
||||||
|
<TypeIcon className="size-3.5" />
|
||||||
|
{getTypeLabel(type)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderPluginContent = () => (
|
const renderPluginContent = () => (
|
||||||
<>
|
<>
|
||||||
<div className="text-[0.7rem] text-muted-foreground truncate w-full">
|
<div className="text-[0.7rem] text-muted-foreground truncate w-full">
|
||||||
@@ -84,12 +97,7 @@ export default function ExtensionCardComponent({
|
|||||||
<Badge variant="outline" className="text-[0.7rem] flex-shrink-0">
|
<Badge variant="outline" className="text-[0.7rem] flex-shrink-0">
|
||||||
v{cardVO.version}
|
v{cardVO.version}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
{renderTypeBadge(cardVO.type)}
|
||||||
variant="outline"
|
|
||||||
className={`text-[0.7rem] flex-shrink-0 ${getTypeBadgeColor(cardVO.type)}`}
|
|
||||||
>
|
|
||||||
{getTypeLabel(cardVO.type)}
|
|
||||||
</Badge>
|
|
||||||
{cardVO.debug && (
|
{cardVO.debug && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -143,12 +151,7 @@ export default function ExtensionCardComponent({
|
|||||||
<div className="text-[1.2rem] text-foreground truncate max-w-[10rem]">
|
<div className="text-[1.2rem] text-foreground truncate max-w-[10rem]">
|
||||||
{cardVO.label}
|
{cardVO.label}
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
{renderTypeBadge('mcp')}
|
||||||
variant="outline"
|
|
||||||
className={`text-[0.7rem] flex-shrink-0 ${getTypeBadgeColor('mcp')}`}
|
|
||||||
>
|
|
||||||
MCP
|
|
||||||
</Badge>
|
|
||||||
{cardVO.mode && (
|
{cardVO.mode && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -169,12 +172,10 @@ export default function ExtensionCardComponent({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[0.8rem] text-muted-foreground line-clamp-2 w-full">
|
<div className="text-[0.8rem] text-muted-foreground line-clamp-2 w-full">
|
||||||
{cardVO.description || t('mcp.noToolsFound')}
|
{cardVO.description ||
|
||||||
{cardVO.tools !== undefined && cardVO.tools > 0 && (
|
(cardVO.tools !== undefined && cardVO.tools > 0
|
||||||
<span className="ml-1">
|
? t('mcp.toolCount', { count: cardVO.tools })
|
||||||
{t('mcp.toolCount', { count: cardVO.tools })}
|
: t('mcp.noToolsFound'))}
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -188,12 +189,7 @@ export default function ExtensionCardComponent({
|
|||||||
<div className="text-[1.2rem] text-foreground truncate max-w-[10rem]">
|
<div className="text-[1.2rem] text-foreground truncate max-w-[10rem]">
|
||||||
{cardVO.label}
|
{cardVO.label}
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
{renderTypeBadge('skill')}
|
||||||
variant="outline"
|
|
||||||
className={`text-[0.7rem] flex-shrink-0 ${getTypeBadgeColor('skill')}`}
|
|
||||||
>
|
|
||||||
{t('common.skill')}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[0.8rem] text-muted-foreground line-clamp-2 w-full">
|
<div className="text-[0.8rem] text-muted-foreground line-clamp-2 w-full">
|
||||||
{cardVO.description}
|
{cardVO.description}
|
||||||
|
|||||||
@@ -90,12 +90,17 @@ export const getCloudServiceClientSync = (): CloudServiceClient => {
|
|||||||
* 手动初始化系统信息
|
* 手动初始化系统信息
|
||||||
* 可以在应用启动时调用此方法预先获取系统信息
|
* 可以在应用启动时调用此方法预先获取系统信息
|
||||||
*/
|
*/
|
||||||
export const initializeSystemInfo = async (): Promise<void> => {
|
export const initializeSystemInfo = async (options?: {
|
||||||
|
throwOnError?: boolean;
|
||||||
|
}): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
Object.assign(systemInfo, await backendClient.getSystemInfo());
|
Object.assign(systemInfo, await backendClient.getSystemInfo());
|
||||||
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
|
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize system info:', error);
|
console.error('Failed to initialize system info:', error);
|
||||||
|
if (options?.throwOnError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
47
web/src/components/BackendUnavailablePage.tsx
Normal file
47
web/src/components/BackendUnavailablePage.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { AlertCircle, Home, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
export default function BackendUnavailablePage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
|
<div className="mx-auto flex max-w-md flex-col items-center text-center">
|
||||||
|
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
|
||||||
|
<AlertCircle className="h-8 w-8 text-destructive" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mb-2 text-sm font-medium text-destructive">
|
||||||
|
{t('errorPage.backendUnavailableStatus')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 className="text-2xl font-semibold text-foreground">
|
||||||
|
{t('common.loginLoadError')}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{t('common.loginLoadErrorDesc')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
>
|
||||||
|
<Home className="h-4 w-4" />
|
||||||
|
{t('errorPage.backToLogin')}
|
||||||
|
</Button>
|
||||||
|
<Button className="gap-2" onClick={() => window.location.reload()}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
{t('common.retry')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1500,8 +1500,10 @@ const enUS = {
|
|||||||
notFound: 'Page not found',
|
notFound: 'Page not found',
|
||||||
notFoundDescription:
|
notFoundDescription:
|
||||||
'The page you are looking for does not exist or has been moved.',
|
'The page you are looking for does not exist or has been moved.',
|
||||||
|
backendUnavailableStatus: 'Backend unavailable',
|
||||||
goBack: 'Go Back',
|
goBack: 'Go Back',
|
||||||
backToHome: 'Back to Home',
|
backToHome: 'Back to Home',
|
||||||
|
backToLogin: 'Back to Login',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'Select a plugin page from the sidebar',
|
selectFromSidebar: 'Select a plugin page from the sidebar',
|
||||||
|
|||||||
@@ -1429,8 +1429,10 @@ const esES = {
|
|||||||
'Ocurrió un error inesperado. Por favor, inténtelo de nuevo más tarde.',
|
'Ocurrió un error inesperado. Por favor, inténtelo de nuevo más tarde.',
|
||||||
notFound: 'Página no encontrada',
|
notFound: 'Página no encontrada',
|
||||||
notFoundDescription: 'La página que buscas no existe o ha sido movida.',
|
notFoundDescription: 'La página que buscas no existe o ha sido movida.',
|
||||||
|
backendUnavailableStatus: 'Backend no disponible',
|
||||||
goBack: 'Volver',
|
goBack: 'Volver',
|
||||||
backToHome: 'Ir al inicio',
|
backToHome: 'Ir al inicio',
|
||||||
|
backToLogin: 'Volver al inicio de sesión',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'Selecciona una página de plugin en la barra lateral',
|
selectFromSidebar: 'Selecciona una página de plugin en la barra lateral',
|
||||||
|
|||||||
@@ -1411,8 +1411,10 @@ const jaJP = {
|
|||||||
notFound: 'ページが見つかりません',
|
notFound: 'ページが見つかりません',
|
||||||
notFoundDescription:
|
notFoundDescription:
|
||||||
'お探しのページは存在しないか、移動された可能性があります。',
|
'お探しのページは存在しないか、移動された可能性があります。',
|
||||||
|
backendUnavailableStatus: 'バックエンドを利用できません',
|
||||||
goBack: '戻る',
|
goBack: '戻る',
|
||||||
backToHome: 'ホームに戻る',
|
backToHome: 'ホームに戻る',
|
||||||
|
backToLogin: 'ログインに戻る',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'サイドバーからプラグインページを選択してください',
|
selectFromSidebar: 'サイドバーからプラグインページを選択してください',
|
||||||
|
|||||||
@@ -1393,6 +1393,18 @@ const ruRU = {
|
|||||||
backToWorkbench: 'Вернуться к рабочей панели',
|
backToWorkbench: 'Вернуться к рабочей панели',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
errorPage: {
|
||||||
|
unexpectedError: 'Что-то пошло не так',
|
||||||
|
unexpectedErrorDescription:
|
||||||
|
'Произошла непредвиденная ошибка. Повторите попытку позже.',
|
||||||
|
notFound: 'Страница не найдена',
|
||||||
|
notFoundDescription:
|
||||||
|
'Страница, которую вы ищете, не существует или была перемещена.',
|
||||||
|
backendUnavailableStatus: 'Бэкенд недоступен',
|
||||||
|
goBack: 'Назад',
|
||||||
|
backToHome: 'На главную',
|
||||||
|
backToLogin: 'Вернуться к входу',
|
||||||
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'Выберите страницу плагина на боковой панели',
|
selectFromSidebar: 'Выберите страницу плагина на боковой панели',
|
||||||
invalidPage: 'Недопустимая страница плагина',
|
invalidPage: 'Недопустимая страница плагина',
|
||||||
|
|||||||
@@ -1369,8 +1369,10 @@ const thTH = {
|
|||||||
'เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่อีกครั้งในภายหลัง',
|
'เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่อีกครั้งในภายหลัง',
|
||||||
notFound: 'ไม่พบหน้า',
|
notFound: 'ไม่พบหน้า',
|
||||||
notFoundDescription: 'หน้าที่คุณกำลังมองหาไม่มีอยู่หรือถูกย้ายแล้ว',
|
notFoundDescription: 'หน้าที่คุณกำลังมองหาไม่มีอยู่หรือถูกย้ายแล้ว',
|
||||||
|
backendUnavailableStatus: 'แบ็กเอนด์ไม่พร้อมใช้งาน',
|
||||||
goBack: 'ย้อนกลับ',
|
goBack: 'ย้อนกลับ',
|
||||||
backToHome: 'กลับหน้าหลัก',
|
backToHome: 'กลับหน้าหลัก',
|
||||||
|
backToLogin: 'กลับไปหน้าเข้าสู่ระบบ',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'เลือกหน้าปลั๊กอินจากแถบด้านข้าง',
|
selectFromSidebar: 'เลือกหน้าปลั๊กอินจากแถบด้านข้าง',
|
||||||
|
|||||||
@@ -1393,8 +1393,10 @@ const viVN = {
|
|||||||
notFound: 'Không tìm thấy trang',
|
notFound: 'Không tìm thấy trang',
|
||||||
notFoundDescription:
|
notFoundDescription:
|
||||||
'Trang bạn tìm kiếm không tồn tại hoặc đã được di chuyển.',
|
'Trang bạn tìm kiếm không tồn tại hoặc đã được di chuyển.',
|
||||||
|
backendUnavailableStatus: 'Backend không khả dụng',
|
||||||
goBack: 'Quay lại',
|
goBack: 'Quay lại',
|
||||||
backToHome: 'Về trang chủ',
|
backToHome: 'Về trang chủ',
|
||||||
|
backToLogin: 'Quay lại đăng nhập',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: 'Chọn một trang plugin từ thanh bên',
|
selectFromSidebar: 'Chọn một trang plugin từ thanh bên',
|
||||||
|
|||||||
@@ -1436,8 +1436,10 @@ const zhHans = {
|
|||||||
unexpectedErrorDescription: '发生了意外错误,请稍后重试。',
|
unexpectedErrorDescription: '发生了意外错误,请稍后重试。',
|
||||||
notFound: '页面未找到',
|
notFound: '页面未找到',
|
||||||
notFoundDescription: '你访问的页面不存在或已被移动。',
|
notFoundDescription: '你访问的页面不存在或已被移动。',
|
||||||
|
backendUnavailableStatus: '后端服务不可用',
|
||||||
goBack: '返回上页',
|
goBack: '返回上页',
|
||||||
backToHome: '返回首页',
|
backToHome: '返回首页',
|
||||||
|
backToLogin: '返回登录',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: '从侧边栏选择一个插件页面',
|
selectFromSidebar: '从侧边栏选择一个插件页面',
|
||||||
|
|||||||
@@ -1343,8 +1343,10 @@ const zhHant = {
|
|||||||
unexpectedErrorDescription: '發生了意外錯誤,請稍後重試。',
|
unexpectedErrorDescription: '發生了意外錯誤,請稍後重試。',
|
||||||
notFound: '頁面未找到',
|
notFound: '頁面未找到',
|
||||||
notFoundDescription: '你訪問的頁面不存在或已被移動。',
|
notFoundDescription: '你訪問的頁面不存在或已被移動。',
|
||||||
|
backendUnavailableStatus: '後端服務不可用',
|
||||||
goBack: '返回上頁',
|
goBack: '返回上頁',
|
||||||
backToHome: '返回首頁',
|
backToHome: '返回首頁',
|
||||||
|
backToLogin: '返回登入',
|
||||||
},
|
},
|
||||||
pluginPages: {
|
pluginPages: {
|
||||||
selectFromSidebar: '從側邊欄選擇一個插件頁面',
|
selectFromSidebar: '從側邊欄選擇一個插件頁面',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import MCPPage from '@/app/home/mcp/page';
|
|||||||
import KnowledgePage from '@/app/home/knowledge/page';
|
import KnowledgePage from '@/app/home/knowledge/page';
|
||||||
import SkillsPage from '@/app/home/skills/page';
|
import SkillsPage from '@/app/home/skills/page';
|
||||||
import ErrorPage from '@/components/ErrorPage';
|
import ErrorPage from '@/components/ErrorPage';
|
||||||
|
import BackendUnavailablePage from '@/components/BackendUnavailablePage';
|
||||||
import PluginPagesPage from '@/app/home/plugin-pages/page';
|
import PluginPagesPage from '@/app/home/plugin-pages/page';
|
||||||
|
|
||||||
const Loading = () => <div>Loading...</div>;
|
const Loading = () => <div>Loading...</div>;
|
||||||
@@ -65,6 +66,10 @@ export const router = createBrowserRouter([
|
|||||||
path: '/wizard',
|
path: '/wizard',
|
||||||
element: <WizardPage />,
|
element: <WizardPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/backend-unavailable',
|
||||||
|
element: <BackendUnavailablePage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/auth/space/callback',
|
path: '/auth/space/callback',
|
||||||
element: <SpaceCallbackPage />,
|
element: <SpaceCallbackPage />,
|
||||||
|
|||||||
Reference in New Issue
Block a user