mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-05 05:16:03 +00:00
feat(web): improve skill import flow
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -45,12 +46,10 @@ import type {
|
||||
MCPFormDraft,
|
||||
MCPFormHandle,
|
||||
} from '@/app/home/mcp/components/mcp-form/MCPForm';
|
||||
import SkillForm from '@/app/home/skills/components/skill-form/SkillForm';
|
||||
import type { SkillFormDraft } from '@/app/home/skills/components/skill-form/SkillForm';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type PopoverView = 'menu' | 'mcp' | 'skill' | 'github';
|
||||
type PopoverView = 'menu' | 'mcp' | 'github';
|
||||
|
||||
enum GithubInstallStatus {
|
||||
WAIT_INPUT = 'wait_input',
|
||||
@@ -151,6 +150,7 @@ export default function AddExtensionPage() {
|
||||
|
||||
function AddExtensionContent() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { refreshPlugins, refreshMCPServers, refreshSkills } = useSidebarData();
|
||||
const {
|
||||
addTask,
|
||||
@@ -179,7 +179,6 @@ function AddExtensionContent() {
|
||||
const mcpFormRef = useRef<MCPFormHandle>(null);
|
||||
const [mcpTesting, setMcpTesting] = useState(false);
|
||||
const [mcpDraft, setMcpDraft] = useState<MCPFormDraft | undefined>();
|
||||
const [skillDraft, setSkillDraft] = useState<SkillFormDraft | undefined>();
|
||||
|
||||
// GitHub install state
|
||||
const [githubURL, setGithubURL] = useState('');
|
||||
@@ -374,14 +373,6 @@ function AddExtensionContent() {
|
||||
setPopoverOpen(false);
|
||||
}
|
||||
|
||||
function handleSkillCreated(_skillName: string) {
|
||||
setSkillDraft(undefined);
|
||||
refreshPlugins();
|
||||
refreshSkills();
|
||||
setPopoverView('menu');
|
||||
setPopoverOpen(false);
|
||||
}
|
||||
|
||||
async function checkExtensionsLimit(): Promise<boolean> {
|
||||
const maxExtensions = systemInfo.limitation?.max_extensions ?? -1;
|
||||
if (maxExtensions < 0) return true;
|
||||
@@ -614,8 +605,6 @@ function AddExtensionContent() {
|
||||
switch (popoverView) {
|
||||
case 'mcp':
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[560px]';
|
||||
case 'skill':
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[560px]';
|
||||
case 'github':
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[560px]';
|
||||
default:
|
||||
@@ -729,7 +718,11 @@ function AddExtensionContent() {
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
onClick={() => setPopoverView('skill')}
|
||||
onClick={async () => {
|
||||
if (!(await checkExtensionsLimit())) return;
|
||||
setPopoverOpen(false);
|
||||
navigate('/home/skills?action=create');
|
||||
}}
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
<BookOpen className="size-4" />
|
||||
@@ -803,41 +796,6 @@ function AddExtensionContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== Skill Form View ===== */}
|
||||
{popoverView === 'skill' && (
|
||||
<div className="flex max-h-[min(720px,80vh)] flex-col">
|
||||
<div className="flex items-center gap-2 px-4 pb-1 pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setPopoverView('menu')}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h4 className="text-sm font-medium leading-none">
|
||||
{t('skills.createSkill')}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<SkillForm
|
||||
initSkillName={undefined}
|
||||
initialDraft={skillDraft}
|
||||
onNewSkillCreated={handleSkillCreated}
|
||||
onSkillUpdated={() => {}}
|
||||
onDraftChange={setSkillDraft}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 bg-popover px-4 pb-4 pt-1">
|
||||
<Button type="submit" form="skill-form" size="sm">
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== GitHub Install View ===== */}
|
||||
{popoverView === 'github' && (
|
||||
<div className="flex max-h-[min(720px,80vh)] flex-col">
|
||||
|
||||
@@ -15,12 +15,13 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
FolderSearch,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
File,
|
||||
FileIcon,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderUp,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
@@ -50,6 +51,209 @@ interface FileEntry {
|
||||
size: number | null;
|
||||
}
|
||||
|
||||
interface PreviewSkill extends Skill {
|
||||
source_path?: string;
|
||||
entry_file?: string;
|
||||
}
|
||||
|
||||
type DirectoryFile = File & {
|
||||
webkitRelativePath?: string;
|
||||
};
|
||||
|
||||
interface DirectoryTreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
is_dir: boolean;
|
||||
size: number | null;
|
||||
children: DirectoryTreeNode[];
|
||||
}
|
||||
|
||||
const CRC32_TABLE = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
let value = i;
|
||||
for (let j = 0; j < 8; j += 1) {
|
||||
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
||||
}
|
||||
CRC32_TABLE[i] = value >>> 0;
|
||||
}
|
||||
|
||||
function crc32(bytes: Uint8Array) {
|
||||
let crc = 0xffffffff;
|
||||
for (const byte of bytes) {
|
||||
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function writeUint16(target: Uint8Array, offset: number, value: number) {
|
||||
target[offset] = value & 0xff;
|
||||
target[offset + 1] = (value >>> 8) & 0xff;
|
||||
}
|
||||
|
||||
function writeUint32(target: Uint8Array, offset: number, value: number) {
|
||||
target[offset] = value & 0xff;
|
||||
target[offset + 1] = (value >>> 8) & 0xff;
|
||||
target[offset + 2] = (value >>> 16) & 0xff;
|
||||
target[offset + 3] = (value >>> 24) & 0xff;
|
||||
}
|
||||
|
||||
function dosDateTime(timestamp: number) {
|
||||
const date = new Date(timestamp || Date.now());
|
||||
const year = Math.max(date.getFullYear(), 1980);
|
||||
return {
|
||||
time:
|
||||
(date.getHours() << 11) |
|
||||
(date.getMinutes() << 5) |
|
||||
Math.floor(date.getSeconds() / 2),
|
||||
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
||||
};
|
||||
}
|
||||
|
||||
function concatUint8Arrays(chunks: Uint8Array[]) {
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function createStoredZip(
|
||||
entries: Array<{ name: string; file: File }>,
|
||||
): Promise<Blob> {
|
||||
const encoder = new TextEncoder();
|
||||
const localChunks: Uint8Array[] = [];
|
||||
const centralChunks: Uint8Array[] = [];
|
||||
let localOffset = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const nameBytes = encoder.encode(entry.name);
|
||||
const fileBytes = new Uint8Array(await entry.file.arrayBuffer());
|
||||
const checksum = crc32(fileBytes);
|
||||
const dateTime = dosDateTime(entry.file.lastModified);
|
||||
|
||||
const localHeader = new Uint8Array(30 + nameBytes.length);
|
||||
writeUint32(localHeader, 0, 0x04034b50);
|
||||
writeUint16(localHeader, 4, 20);
|
||||
writeUint16(localHeader, 6, 0);
|
||||
writeUint16(localHeader, 8, 0);
|
||||
writeUint16(localHeader, 10, dateTime.time);
|
||||
writeUint16(localHeader, 12, dateTime.date);
|
||||
writeUint32(localHeader, 14, checksum);
|
||||
writeUint32(localHeader, 18, fileBytes.length);
|
||||
writeUint32(localHeader, 22, fileBytes.length);
|
||||
writeUint16(localHeader, 26, nameBytes.length);
|
||||
writeUint16(localHeader, 28, 0);
|
||||
localHeader.set(nameBytes, 30);
|
||||
localChunks.push(localHeader, fileBytes);
|
||||
|
||||
const centralHeader = new Uint8Array(46 + nameBytes.length);
|
||||
writeUint32(centralHeader, 0, 0x02014b50);
|
||||
writeUint16(centralHeader, 4, 20);
|
||||
writeUint16(centralHeader, 6, 20);
|
||||
writeUint16(centralHeader, 8, 0);
|
||||
writeUint16(centralHeader, 10, 0);
|
||||
writeUint16(centralHeader, 12, dateTime.time);
|
||||
writeUint16(centralHeader, 14, dateTime.date);
|
||||
writeUint32(centralHeader, 16, checksum);
|
||||
writeUint32(centralHeader, 20, fileBytes.length);
|
||||
writeUint32(centralHeader, 24, fileBytes.length);
|
||||
writeUint16(centralHeader, 28, nameBytes.length);
|
||||
writeUint16(centralHeader, 30, 0);
|
||||
writeUint16(centralHeader, 32, 0);
|
||||
writeUint16(centralHeader, 34, 0);
|
||||
writeUint16(centralHeader, 36, 0);
|
||||
writeUint32(centralHeader, 38, 0);
|
||||
writeUint32(centralHeader, 42, localOffset);
|
||||
centralHeader.set(nameBytes, 46);
|
||||
centralChunks.push(centralHeader);
|
||||
|
||||
localOffset += localHeader.length + fileBytes.length;
|
||||
}
|
||||
|
||||
const centralDirectory = concatUint8Arrays(centralChunks);
|
||||
const endRecord = new Uint8Array(22);
|
||||
writeUint32(endRecord, 0, 0x06054b50);
|
||||
writeUint16(endRecord, 4, 0);
|
||||
writeUint16(endRecord, 6, 0);
|
||||
writeUint16(endRecord, 8, entries.length);
|
||||
writeUint16(endRecord, 10, entries.length);
|
||||
writeUint32(endRecord, 12, centralDirectory.length);
|
||||
writeUint32(endRecord, 16, localOffset);
|
||||
writeUint16(endRecord, 20, 0);
|
||||
|
||||
return new Blob([...localChunks, centralDirectory, endRecord] as BlobPart[], {
|
||||
type: 'application/zip',
|
||||
});
|
||||
}
|
||||
|
||||
function pathDirname(path: string) {
|
||||
const normalized = path.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const index = normalized.lastIndexOf('/');
|
||||
return index >= 0 ? normalized.slice(0, index) : '';
|
||||
}
|
||||
|
||||
function pathBasename(path: string) {
|
||||
const normalized = path.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
return normalized.split('/').pop() || normalized;
|
||||
}
|
||||
|
||||
function buildDirectoryTree(
|
||||
entries: Array<{ path: string; size: number | null }>,
|
||||
): DirectoryTreeNode[] {
|
||||
const root: DirectoryTreeNode = {
|
||||
name: '',
|
||||
path: '',
|
||||
is_dir: true,
|
||||
size: null,
|
||||
children: [],
|
||||
};
|
||||
|
||||
for (const entry of entries) {
|
||||
const path = entry.path;
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
let current = root;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
const isLast = index === parts.length - 1;
|
||||
const nodePath = parts.slice(0, index + 1).join('/');
|
||||
let node = current.children.find((child) => child.name === part);
|
||||
if (!node) {
|
||||
node = {
|
||||
name: part,
|
||||
path: nodePath,
|
||||
is_dir: !isLast,
|
||||
size: null,
|
||||
children: [],
|
||||
};
|
||||
current.children.push(node);
|
||||
}
|
||||
if (!isLast) {
|
||||
node.is_dir = true;
|
||||
}
|
||||
if (isLast) {
|
||||
node.size = entry.size;
|
||||
}
|
||||
current = node;
|
||||
});
|
||||
}
|
||||
|
||||
function sortNodes(nodes: DirectoryTreeNode[]) {
|
||||
nodes.sort((a, b) => {
|
||||
if (a.is_dir !== b.is_dir) {
|
||||
return a.is_dir ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
nodes.forEach((node) => sortNodes(node.children));
|
||||
}
|
||||
|
||||
sortNodes(root.children);
|
||||
return root.children;
|
||||
}
|
||||
|
||||
interface FileTreeProps {
|
||||
skillName: string;
|
||||
selectedFile?: string | null;
|
||||
@@ -190,7 +394,7 @@ const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(function FileTree(
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<File className="h-4 w-4 text-gray-500" />
|
||||
<FileIcon className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
<span className="text-sm truncate">{entry.name}</span>
|
||||
{!entry.is_dir && entry.size !== null && (
|
||||
@@ -226,6 +430,125 @@ const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(function FileTree(
|
||||
);
|
||||
});
|
||||
|
||||
interface LocalFileTreeProps {
|
||||
entries: DirectoryTreeNode[];
|
||||
fileMap: Map<string, File>;
|
||||
selectedFile?: string | null;
|
||||
onFileSelect: (path: string, content: string) => void;
|
||||
}
|
||||
|
||||
function collectDirectoryPaths(nodes: DirectoryTreeNode[]): string[] {
|
||||
return nodes.flatMap((node) =>
|
||||
node.is_dir ? [node.path, ...collectDirectoryPaths(node.children)] : [],
|
||||
);
|
||||
}
|
||||
|
||||
function LocalFileTree({
|
||||
entries,
|
||||
fileMap,
|
||||
selectedFile,
|
||||
onFileSelect,
|
||||
}: LocalFileTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPath(selectedFile ?? null);
|
||||
}, [selectedFile]);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedDirs(new Set(collectDirectoryPaths(entries)));
|
||||
}, [entries]);
|
||||
|
||||
const toggleDir = (dirPath: string) => {
|
||||
setExpandedDirs((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(dirPath)) {
|
||||
next.delete(dirPath);
|
||||
} else {
|
||||
next.add(dirPath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileClick = async (filePath: string) => {
|
||||
const file = fileMap.get(filePath);
|
||||
if (!file) return;
|
||||
|
||||
setSelectedPath(filePath);
|
||||
try {
|
||||
onFileSelect(filePath, await file.text());
|
||||
} catch (error) {
|
||||
console.error('Failed to read local file:', error);
|
||||
toast.error(t('skills.readFileError') + String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const renderEntry = (entry: DirectoryTreeNode, depth: number = 0) => {
|
||||
const isExpanded = expandedDirs.has(entry.path);
|
||||
const isSelected = selectedPath === entry.path;
|
||||
|
||||
return (
|
||||
<div key={entry.path}>
|
||||
<div
|
||||
className={`flex items-center gap-1 rounded px-2 py-1 hover:bg-muted ${
|
||||
isSelected ? 'bg-muted' : ''
|
||||
} cursor-pointer`}
|
||||
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||
onClick={() =>
|
||||
entry.is_dir ? toggleDir(entry.path) : handleFileClick(entry.path)
|
||||
}
|
||||
>
|
||||
{entry.is_dir ? (
|
||||
<>
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="h-4 w-4 text-blue-500" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 text-blue-500" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FileIcon className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
<span className="truncate text-sm">{entry.name}</span>
|
||||
{!entry.is_dir && entry.size !== null && (
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{entry.size > 1024
|
||||
? `${Math.round(entry.size / 1024)}KB`
|
||||
: `${entry.size}B`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{entry.is_dir && isExpanded && (
|
||||
<div>
|
||||
{entry.children.map((child) => renderEntry(child, depth + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="max-h-[min(46vh,32rem)] space-y-1 overflow-y-auto overscroll-contain pr-1">
|
||||
{entries.length === 0 && (
|
||||
<div className="py-2 text-sm text-muted-foreground">
|
||||
{t('skills.noFiles')}
|
||||
</div>
|
||||
)}
|
||||
{entries.map((entry) => renderEntry(entry))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const emptySkillDraft: SkillFormDraft = {
|
||||
skill: {
|
||||
name: '',
|
||||
@@ -251,13 +574,24 @@ export default function SkillForm({
|
||||
const [skill, setSkill] = useState<Partial<Skill>>(
|
||||
initialDraftRef.current.skill,
|
||||
);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [importingDirectory, setImportingDirectory] = useState(false);
|
||||
const [installingDirectory, setInstallingDirectory] = useState(false);
|
||||
const [directoryZipFile, setDirectoryZipFile] = useState<File | null>(null);
|
||||
const [directoryPreview, setDirectoryPreview] = useState<PreviewSkill | null>(
|
||||
null,
|
||||
);
|
||||
const [directorySourceName, setDirectorySourceName] = useState('');
|
||||
const [directoryTree, setDirectoryTree] = useState<DirectoryTreeNode[]>([]);
|
||||
const [directoryFileMap, setDirectoryFileMap] = useState<Map<string, File>>(
|
||||
new Map(),
|
||||
);
|
||||
const [showAdvanced, setShowAdvanced] = useState(
|
||||
initialDraftRef.current.showAdvanced,
|
||||
);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
const fileTreeRef = useRef<FileTreeHandle>(null);
|
||||
const directoryInputRef = useRef<HTMLInputElement>(null);
|
||||
const [fileTreeLoading, setFileTreeLoading] = useState(false);
|
||||
|
||||
const loadSkill = useCallback(
|
||||
@@ -275,6 +609,11 @@ export default function SkillForm({
|
||||
[t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
directoryInputRef.current?.setAttribute('webkitdirectory', '');
|
||||
directoryInputRef.current?.setAttribute('directory', '');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initSkillName) {
|
||||
loadSkill(initSkillName);
|
||||
@@ -283,6 +622,11 @@ export default function SkillForm({
|
||||
setSelectedFile(initialDraftRef.current.selectedFile ?? null);
|
||||
setSkill(initialDraftRef.current.skill);
|
||||
setShowAdvanced(initialDraftRef.current.showAdvanced);
|
||||
setDirectoryZipFile(null);
|
||||
setDirectoryPreview(null);
|
||||
setDirectorySourceName('');
|
||||
setDirectoryTree([]);
|
||||
setDirectoryFileMap(new Map());
|
||||
}, [initSkillName, loadSkill]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -294,30 +638,130 @@ export default function SkillForm({
|
||||
});
|
||||
}, [initSkillName, onDraftChange, skill, showAdvanced, selectedFile]);
|
||||
|
||||
async function scanDirectory() {
|
||||
const path = skill.package_root?.trim();
|
||||
if (!path) {
|
||||
toast.error(t('skills.packageRootRequired'));
|
||||
async function handleDirectoryImport(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
const files = Array.from(event.target.files ?? []) as DirectoryFile[];
|
||||
event.target.value = '';
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
setScanning(true);
|
||||
|
||||
const skillMdFiles = files.filter((file) => {
|
||||
const relativePath = file.webkitRelativePath || file.name;
|
||||
return pathBasename(relativePath).toLowerCase() === 'skill.md';
|
||||
});
|
||||
if (skillMdFiles.length === 0) {
|
||||
toast.error(t('skills.noSkillMdInDirectory'));
|
||||
return;
|
||||
}
|
||||
if (skillMdFiles.length > 1) {
|
||||
toast.error(t('skills.multipleSkillMdInDirectory'));
|
||||
return;
|
||||
}
|
||||
|
||||
const skillMdRelativePath =
|
||||
skillMdFiles[0].webkitRelativePath || skillMdFiles[0].name;
|
||||
const skillDir = pathDirname(skillMdRelativePath);
|
||||
const packageName = pathBasename(
|
||||
skillDir || pathDirname(files[0].webkitRelativePath || '') || 'skill',
|
||||
);
|
||||
const prefix = skillDir ? `${skillDir}/` : '';
|
||||
const selectedFiles = files
|
||||
.map((file) => {
|
||||
const relativePath = file.webkitRelativePath || file.name;
|
||||
if (prefix && !relativePath.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
const pathInPackage = prefix
|
||||
? relativePath.slice(prefix.length)
|
||||
: relativePath;
|
||||
if (!pathInPackage || pathInPackage.endsWith('/')) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
path: pathInPackage.replace(/^\/+/, ''),
|
||||
file,
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is { path: string; file: File } => Boolean(entry));
|
||||
const packageFiles = selectedFiles.map((entry) => ({
|
||||
name: `${packageName}/${entry.path}`,
|
||||
file: entry.file,
|
||||
}));
|
||||
|
||||
setImportingDirectory(true);
|
||||
try {
|
||||
const result = await httpClient.scanSkillDirectory(path);
|
||||
setSkill((prev) => ({
|
||||
...prev,
|
||||
name: prev.name || result.name,
|
||||
display_name: prev.display_name || result.display_name || '',
|
||||
description: prev.description || result.description,
|
||||
package_root: result.package_root,
|
||||
instructions: result.instructions,
|
||||
}));
|
||||
setFileContent(result.instructions);
|
||||
toast.success(t('skills.scanSuccess'));
|
||||
const zipBlob = await createStoredZip(packageFiles);
|
||||
const zipFile = new File([zipBlob], `${packageName}.zip`, {
|
||||
type: 'application/zip',
|
||||
});
|
||||
const resp = await httpClient.previewSkillInstallFromUpload(zipFile);
|
||||
const preview = (resp.skills?.[0] ?? null) as PreviewSkill | null;
|
||||
if (!preview) {
|
||||
toast.error(t('skills.noSkillMdInDirectory'));
|
||||
return;
|
||||
}
|
||||
|
||||
setDirectoryZipFile(zipFile);
|
||||
setDirectoryPreview(preview);
|
||||
setDirectorySourceName(packageName);
|
||||
setDirectoryTree(
|
||||
buildDirectoryTree(
|
||||
selectedFiles.map((entry) => ({
|
||||
path: entry.path,
|
||||
size: entry.file.size,
|
||||
})),
|
||||
),
|
||||
);
|
||||
setDirectoryFileMap(
|
||||
new Map(selectedFiles.map((entry) => [entry.path, entry.file])),
|
||||
);
|
||||
setSelectedFile(preview.entry_file || 'SKILL.md');
|
||||
setFileContent(preview.instructions || '');
|
||||
setSkill({
|
||||
name: preview.name || packageName,
|
||||
display_name: preview.display_name || '',
|
||||
description: preview.description || '',
|
||||
instructions: preview.instructions || '',
|
||||
package_root: preview.package_root || '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to scan directory:', error);
|
||||
toast.error(t('skills.scanError') + String(error));
|
||||
console.error('Failed to import local skill directory:', error);
|
||||
toast.error(t('skills.importDirectoryError') + String(error));
|
||||
} finally {
|
||||
setScanning(false);
|
||||
setImportingDirectory(false);
|
||||
}
|
||||
}
|
||||
|
||||
function clearDirectoryPreview() {
|
||||
setDirectoryZipFile(null);
|
||||
setDirectoryPreview(null);
|
||||
setDirectorySourceName('');
|
||||
setDirectoryTree([]);
|
||||
setDirectoryFileMap(new Map());
|
||||
setSelectedFile(null);
|
||||
setFileContent('');
|
||||
setSkill({ ...emptySkillDraft.skill });
|
||||
}
|
||||
|
||||
async function handleDirectoryImportConfirm() {
|
||||
if (!directoryZipFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInstallingDirectory(true);
|
||||
try {
|
||||
const resp = await httpClient.installSkillFromUpload(directoryZipFile);
|
||||
toast.success(t('skills.installSuccess'));
|
||||
onNewSkillCreated(
|
||||
resp.skills[0]?.name || directoryPreview?.name || directorySourceName,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to install local skill directory:', error);
|
||||
toast.error(t('skills.importDirectoryError') + String(error));
|
||||
} finally {
|
||||
setInstallingDirectory(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,6 +782,11 @@ export default function SkillForm({
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!initSkillName && directoryZipFile) {
|
||||
await handleDirectoryImportConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!skill.name?.trim()) {
|
||||
toast.error(t('skills.skillNameRequired'));
|
||||
return;
|
||||
@@ -385,6 +834,7 @@ export default function SkillForm({
|
||||
value={skill.display_name || ''}
|
||||
onChange={(e) => setSkill({ ...skill, display_name: e.target.value })}
|
||||
placeholder={t('skills.displayNamePlaceholder')}
|
||||
disabled={Boolean(directoryPreview)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -401,7 +851,7 @@ export default function SkillForm({
|
||||
}
|
||||
placeholder={t('skills.skillSlugPlaceholder')}
|
||||
className="font-mono"
|
||||
disabled={Boolean(initSkillName)}
|
||||
disabled={Boolean(initSkillName || directoryPreview)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('skills.skillSlugHelp')}
|
||||
@@ -416,6 +866,7 @@ export default function SkillForm({
|
||||
onChange={(e) => setSkill({ ...skill, description: e.target.value })}
|
||||
placeholder={t('skills.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
disabled={Boolean(directoryPreview)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -423,7 +874,7 @@ export default function SkillForm({
|
||||
|
||||
const fileTreeSection = (
|
||||
<>
|
||||
{initSkillName && (
|
||||
{initSkillName ? (
|
||||
<div className="space-y-2">
|
||||
<FileTree
|
||||
skillName={initSkillName}
|
||||
@@ -431,6 +882,17 @@ export default function SkillForm({
|
||||
onFileSelect={handleFileSelect}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
directoryPreview && (
|
||||
<div className="space-y-2">
|
||||
<LocalFileTree
|
||||
entries={directoryTree}
|
||||
fileMap={directoryFileMap}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={handleFileSelect}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -448,7 +910,7 @@ export default function SkillForm({
|
||||
id="instructions"
|
||||
value={fileContent}
|
||||
onChange={(e) => handleInstructionDraftChange(e.target.value)}
|
||||
readOnly={Boolean(initSkillName)}
|
||||
readOnly={Boolean(initSkillName || directoryPreview)}
|
||||
placeholder={t('skills.instructionsPlaceholder')}
|
||||
rows={16}
|
||||
className="min-h-[360px] resize-y font-mono text-sm read-only:cursor-default read-only:bg-muted/30 lg:min-h-[calc(100vh-220px)]"
|
||||
@@ -456,34 +918,44 @@ export default function SkillForm({
|
||||
</div>
|
||||
);
|
||||
|
||||
const advancedSettings = (
|
||||
<div className="space-y-2">
|
||||
<Label>{t('skills.packageRoot')}</Label>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
value={skill.package_root || ''}
|
||||
onChange={(e) => setSkill({ ...skill, package_root: e.target.value })}
|
||||
placeholder={`data/skills/${skill.name || '<skill-name>'}/`}
|
||||
className="flex-1"
|
||||
disabled={Boolean(initSkillName)}
|
||||
/>
|
||||
const localDirectoryImport = (
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<input
|
||||
ref={directoryInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleDirectoryImport}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant={directoryPreview ? 'secondary' : 'outline'}
|
||||
onClick={() => directoryInputRef.current?.click()}
|
||||
disabled={importingDirectory || installingDirectory}
|
||||
className="w-full shrink-0 sm:w-auto"
|
||||
>
|
||||
{importingDirectory ? (
|
||||
<Loader2 className="mr-1.5 size-4 animate-spin" />
|
||||
) : (
|
||||
<FolderUp className="mr-1.5 size-4" />
|
||||
)}
|
||||
{importingDirectory
|
||||
? t('skills.importingDirectory')
|
||||
: directoryPreview
|
||||
? t('skills.chooseAnotherDirectory')
|
||||
: t('skills.chooseSkillDirectory')}
|
||||
</Button>
|
||||
{directoryPreview && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={scanDirectory}
|
||||
disabled={
|
||||
Boolean(initSkillName) || scanning || !skill.package_root?.trim()
|
||||
}
|
||||
className="shrink-0"
|
||||
variant="ghost"
|
||||
onClick={clearDirectoryPreview}
|
||||
disabled={installingDirectory}
|
||||
className="w-full shrink-0 sm:w-auto"
|
||||
>
|
||||
<FolderSearch className="mr-1 h-4 w-4" />
|
||||
{scanning ? t('common.loading') : t('skills.scan')}
|
||||
{t('skills.clearDirectoryPreview')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('skills.packageRootHelp')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -495,46 +967,59 @@ export default function SkillForm({
|
||||
className="flex h-full min-h-0 max-w-full flex-col gap-6 overflow-y-auto lg:flex-row lg:overflow-hidden"
|
||||
>
|
||||
<div className="space-y-4 pb-6 lg:min-h-0 lg:w-[360px] lg:flex-shrink-0 lg:overflow-y-auto lg:overflow-x-hidden xl:w-[400px]">
|
||||
{!initSkillName && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('skills.importLocalDirectory')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{localDirectoryImport}</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('bots.basicInfo')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">{metadataFields}</CardContent>
|
||||
</Card>
|
||||
{initSkillName && (
|
||||
{(initSkillName || directoryPreview) && (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle>{t('skills.files')}</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => fileTreeRef.current?.refresh()}
|
||||
disabled={fileTreeLoading}
|
||||
className="size-8"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${fileTreeLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
{initSkillName && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => fileTreeRef.current?.refresh()}
|
||||
disabled={fileTreeLoading}
|
||||
className="size-8"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${fileTreeLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FileTree
|
||||
ref={fileTreeRef}
|
||||
skillName={initSkillName}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={handleFileSelect}
|
||||
onLoadingChange={setFileTreeLoading}
|
||||
/>
|
||||
{initSkillName ? (
|
||||
<FileTree
|
||||
ref={fileTreeRef}
|
||||
skillName={initSkillName}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={handleFileSelect}
|
||||
onLoadingChange={setFileTreeLoading}
|
||||
/>
|
||||
) : (
|
||||
<LocalFileTree
|
||||
entries={directoryTree}
|
||||
fileMap={directoryFileMap}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={handleFileSelect}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('skills.advancedSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{advancedSettings}</CardContent>
|
||||
</Card>
|
||||
{sideFooter}
|
||||
</div>
|
||||
<div className="hidden w-px shrink-0 bg-border lg:block" />
|
||||
@@ -558,10 +1043,10 @@ export default function SkillForm({
|
||||
|
||||
return (
|
||||
<form id="skill-form" onSubmit={handleSubmit} className="space-y-4">
|
||||
{!initSkillName && localDirectoryImport}
|
||||
{metadataFields}
|
||||
{fileTreeSection}
|
||||
{instructionEditor()}
|
||||
{advancedSettings}
|
||||
{sideFooter}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function SkillsPage() {
|
||||
return <SkillDetailContent id={detailId} />;
|
||||
}
|
||||
|
||||
function handleImportedSkills(skillNames: string[]) {
|
||||
function handleImportedSkills(_skillNames: string[]) {
|
||||
void refreshSkills();
|
||||
setActiveView(null);
|
||||
navigate('/home/add-extension');
|
||||
@@ -54,7 +54,12 @@ export default function SkillsPage() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('skills.createSkill')}</h1>
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h1 className="text-xl font-semibold">{t('skills.createSkill')}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('skills.createSkillDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
|
||||
@@ -1341,6 +1341,8 @@ const enUS = {
|
||||
description:
|
||||
'Create and manage skills that can be activated during conversations',
|
||||
createSkill: 'Create Skill',
|
||||
createSkillDescription:
|
||||
'Import a local directory or create one by filling in details',
|
||||
editSkill: 'Edit Skill',
|
||||
getSkillListError: 'Failed to get skill list: ',
|
||||
skillName: 'Skill Name',
|
||||
@@ -1375,6 +1377,15 @@ const enUS = {
|
||||
packageRoot: 'Package Directory',
|
||||
packageRootHelp:
|
||||
'Optional. Only needed when importing an existing skill directory. Leave empty for new skills. Scanning checks the current directory and subdirectories up to 2 levels deep.',
|
||||
importLocalDirectory: 'Import Local Skill Directory',
|
||||
chooseSkillDirectory: 'Choose SKILL.md Directory',
|
||||
chooseAnotherDirectory: 'Choose Another Directory',
|
||||
importingDirectory: 'Previewing...',
|
||||
clearDirectoryPreview: 'Clear Selected Directory',
|
||||
noSkillMdInDirectory: 'No SKILL.md was found in the selected directory',
|
||||
multipleSkillMdInDirectory:
|
||||
'The selected directory contains multiple SKILL.md files. Choose a single skill directory directly.',
|
||||
importDirectoryError: 'Failed to import directory: ',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
searchSkills: 'Search skills...',
|
||||
selectSkills: 'Select Skills',
|
||||
@@ -1498,7 +1509,7 @@ const enUS = {
|
||||
previewSkill: 'Preview Skill',
|
||||
noSkillPreviewFound: 'No importable Skill found',
|
||||
createSkill: 'Create New Skill',
|
||||
createSkillHint: 'Manually create a new skill extension',
|
||||
createSkillHint: 'Import from a local directory or create manually',
|
||||
unsupportedFileType:
|
||||
'Unsupported file type. Only .zip and .lbpkg files are supported',
|
||||
},
|
||||
|
||||
@@ -238,7 +238,6 @@ const esES = {
|
||||
noLocalModels:
|
||||
'No hay modelos locales. Haz clic en Crear para añadir un modelo.',
|
||||
providerCount: '{{count}} proveedores',
|
||||
// New keys for provider-based structure
|
||||
addModel: 'Añadir modelo',
|
||||
manualAdd: 'Manual',
|
||||
scanAdd: 'Escanear',
|
||||
@@ -613,7 +612,14 @@ const esES = {
|
||||
taskQueue: 'Tareas de instalación',
|
||||
clearCompleted: 'Limpiar completados',
|
||||
noTasks: 'No hay tareas de instalación',
|
||||
titlePlugin: 'Instalando plugin {{name}}',
|
||||
titleMCP: 'Instalando servidor MCP {{name}}',
|
||||
titleSkill: 'Instalando skill {{name}}',
|
||||
installCompletePlugin: 'Plugin instalado correctamente',
|
||||
installCompleteMCP: 'Servidor MCP instalado correctamente',
|
||||
installCompleteSkill: 'Skill instalada correctamente',
|
||||
},
|
||||
uploadPluginOnly: 'Solo se admiten paquetes de plugin .lbpkg',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: 'Buscar plugins...',
|
||||
@@ -685,6 +691,7 @@ const esES = {
|
||||
clearAll: 'Borrar todo',
|
||||
noTags: 'No hay etiquetas disponibles',
|
||||
},
|
||||
installCard: 'Instalar {{name}}',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
@@ -853,6 +860,13 @@ const esES = {
|
||||
enableAllMCPServers: 'Activar todos los servidores MCP',
|
||||
allPluginsEnabled: 'Todos los plugins activados',
|
||||
allMCPServersEnabled: 'Todos los servidores MCP activados',
|
||||
enableAllSkills: 'Activar todas las skills',
|
||||
allSkillsEnabled: 'Todas las skills están activadas',
|
||||
skillsTitle: 'Skills',
|
||||
noSkillsSelected: 'No hay skills seleccionadas',
|
||||
addSkill: 'Añadir skill',
|
||||
selectSkills: 'Seleccionar skills',
|
||||
noSkillsAvailable: 'No hay skills disponibles',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Chat del Pipeline',
|
||||
@@ -1438,6 +1452,121 @@ const esES = {
|
||||
selectFromSidebar: 'Selecciona una página de plugin en la barra lateral',
|
||||
invalidPage: 'Página de plugin no válida',
|
||||
},
|
||||
skills: {
|
||||
title: 'Skills',
|
||||
description:
|
||||
'Crea y gestiona skills que se pueden activar durante las conversaciones',
|
||||
createSkill: 'Crear skill',
|
||||
createSkillDescription:
|
||||
'Importa un directorio local o crea una skill rellenando la información',
|
||||
editSkill: 'Editar skill',
|
||||
getSkillListError: 'Error al obtener la lista de skills: ',
|
||||
skillName: 'Nombre de la skill',
|
||||
displayName: 'Nombre de la skill',
|
||||
displayNamePlaceholder: 'Nombre visible (admite cualquier idioma)',
|
||||
skillSlug: 'Nombre del directorio',
|
||||
skillSlugPlaceholder: 'english-name-only',
|
||||
skillSlugHelp:
|
||||
'Se usa como nombre del directorio de la skill. Solo letras, números, guiones y guiones bajos.',
|
||||
skillDescription: 'Descripción de la skill',
|
||||
skillInstructions: 'Instrucciones',
|
||||
saveSuccess: 'Guardado correctamente',
|
||||
saveError: 'Error al guardar: ',
|
||||
createSuccess: 'Creado correctamente',
|
||||
createError: 'Error al crear: ',
|
||||
deleteSuccess: 'Eliminado correctamente',
|
||||
deleteError: 'Error al eliminar: ',
|
||||
deleteConfirmation: '¿Seguro que quieres eliminar esta skill?',
|
||||
delete: 'Eliminar skill',
|
||||
skillNameRequired: 'El nombre de la skill no puede estar vacío',
|
||||
skillDescriptionRequired: 'La descripción de la skill no puede estar vacía',
|
||||
packageRootRequired: 'La ruta raíz del paquete no puede estar vacía',
|
||||
scan: 'Escanear',
|
||||
scanSuccess: 'Directorio escaneado correctamente',
|
||||
scanError: 'Error al escanear el directorio: ',
|
||||
noSkills: 'No hay skills configuradas',
|
||||
preview: 'Vista previa',
|
||||
previewInstructions: 'Vista previa de instrucciones',
|
||||
instructionsPlaceholder:
|
||||
'Introduce las instrucciones de la skill en formato Markdown...',
|
||||
descriptionPlaceholder:
|
||||
'Breve descripción de lo que hace esta skill (se muestra al LLM)',
|
||||
packageRoot: 'Directorio del paquete',
|
||||
packageRootHelp:
|
||||
'Opcional. Solo se necesita al importar un directorio de skill existente. Déjalo vacío para skills nuevas. El escaneo revisa el directorio actual y subdirectorios hasta 2 niveles.',
|
||||
importLocalDirectory: 'Importar directorio local de skill',
|
||||
chooseSkillDirectory: 'Elegir directorio de SKILL.md',
|
||||
chooseAnotherDirectory: 'Elegir otro directorio',
|
||||
importingDirectory: 'Generando vista previa...',
|
||||
clearDirectoryPreview: 'Borrar directorio seleccionado',
|
||||
noSkillMdInDirectory:
|
||||
'No se encontró SKILL.md en el directorio seleccionado',
|
||||
multipleSkillMdInDirectory:
|
||||
'El directorio seleccionado contiene varios SKILL.md. Elige directamente un único directorio de skill.',
|
||||
importDirectoryError: 'Error al importar el directorio: ',
|
||||
advancedSettings: 'Configuración avanzada',
|
||||
searchSkills: 'Buscar skills...',
|
||||
selectSkills: 'Seleccionar skills',
|
||||
addSkill: 'Añadir skill',
|
||||
builtin: 'Integrada',
|
||||
importFromGithub: 'Instalar skill desde GitHub',
|
||||
createManually: 'Crear manualmente',
|
||||
uploadZip: 'Subir paquete ZIP',
|
||||
uploadZipOnly: 'Solo se admiten paquetes de skill .zip',
|
||||
installSuccess: 'Skill instalada correctamente',
|
||||
installError: 'Error al instalar la skill: ',
|
||||
enterRepoUrl: 'Introduce la URL del repositorio de GitHub',
|
||||
repoUrlPlaceholder: 'p. ej., https://github.com/owner/repo',
|
||||
fetchingReleases: 'Obteniendo releases...',
|
||||
selectRelease: 'Seleccionar release',
|
||||
noReleasesFound: 'No se encontraron releases',
|
||||
fetchReleasesError: 'Error al obtener releases: ',
|
||||
selectAsset: 'Selecciona el archivo para instalar',
|
||||
sourceArchive: 'Código fuente (zip)',
|
||||
noAssetsFound: 'No hay archivos instalables en esta release',
|
||||
fetchAssetsError: 'Error al obtener archivos: ',
|
||||
backToReleases: 'Volver a releases',
|
||||
backToRepoUrl: 'Volver a la URL del repositorio',
|
||||
backToAssets: 'Volver a archivos',
|
||||
releaseTag: 'Etiqueta: {{tag}}',
|
||||
publishedAt: 'Publicado el: {{date}}',
|
||||
prerelease: 'Pre-release',
|
||||
assetSize: 'Tamaño: {{size}}',
|
||||
confirmInstall: 'Confirmar instalación',
|
||||
installing: 'Instalando skill...',
|
||||
loading: 'Cargando...',
|
||||
previewLoadError: 'Error al cargar la vista previa',
|
||||
selectFromSidebar: 'Selecciona una skill en la barra lateral',
|
||||
dangerZone: 'Zona peligrosa',
|
||||
dangerZoneDescription: 'Acciones irreversibles y destructivas',
|
||||
files: 'Archivos',
|
||||
noFiles: 'No se encontraron archivos',
|
||||
loadFilesError: 'Error al cargar archivos: ',
|
||||
readFileError: 'Error al leer el archivo: ',
|
||||
saveFile: 'Guardar archivo',
|
||||
saveFileSuccess: 'Archivo guardado correctamente',
|
||||
saveFileError: 'Error al guardar el archivo: ',
|
||||
},
|
||||
addExtension: {
|
||||
manualAdd: 'Añadir manualmente',
|
||||
uploadExtension: 'Arrastra y suelta o haz clic para subir',
|
||||
uploadHint: 'Admite archivos .zip (skills) y .lbpkg (plugins)',
|
||||
orContinueWith: 'o elige una acción abajo',
|
||||
addMCPServerHint: 'Conectar una extensión de servidor de herramientas MCP',
|
||||
installFromGithub: 'Instalar desde GitHub',
|
||||
installFromGithubHint: 'Paquete de plugin o skill (SKILL.md)',
|
||||
githubUrlHelp: 'Pega una URL de GitHub',
|
||||
githubUrlTooltip:
|
||||
'Plugin: pega una URL de repositorio, Release o Tag. Skill: pega la URL de la página SKILL.md dentro del directorio de la skill.',
|
||||
githubUrlPlaceholder: 'Repositorio de GitHub, Release o enlace SKILL.md',
|
||||
githubUrlRequired: 'Introduce una URL de GitHub',
|
||||
previewSkill: 'Previsualizar skill',
|
||||
noSkillPreviewFound: 'No se encontró ninguna skill importable',
|
||||
createSkill: 'Crear nueva skill',
|
||||
createSkillHint: 'Importar desde un directorio local o crear manualmente',
|
||||
unsupportedFileType:
|
||||
'Tipo de archivo no admitido. Solo se admiten archivos .zip y .lbpkg',
|
||||
},
|
||||
};
|
||||
|
||||
export default esES;
|
||||
|
||||
@@ -604,7 +604,14 @@ const jaJP = {
|
||||
taskQueue: 'インストールタスク',
|
||||
clearCompleted: '完了を消去',
|
||||
noTasks: 'インストールタスクはありません',
|
||||
titlePlugin: 'プラグイン {{name}} をインストール中',
|
||||
titleMCP: 'MCP サーバー {{name}} をインストール中',
|
||||
titleSkill: 'スキル {{name}} をインストール中',
|
||||
installCompletePlugin: 'プラグインをインストールしました',
|
||||
installCompleteMCP: 'MCP サーバーをインストールしました',
|
||||
installCompleteSkill: 'スキルをインストールしました',
|
||||
},
|
||||
uploadPluginOnly: '.lbpkg プラグインパッケージのみ対応しています',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: 'プラグインを検索...',
|
||||
@@ -676,6 +683,7 @@ const jaJP = {
|
||||
deprecated: '非推奨',
|
||||
deprecatedTooltip:
|
||||
'対応する「ナレッジエンジン」プラグインをインストールしてください。',
|
||||
installCard: '{{name}} をインストール',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
@@ -838,6 +846,13 @@ const jaJP = {
|
||||
enableAllMCPServers: 'すべてのMCPサーバーを有効にする',
|
||||
allPluginsEnabled: 'すべてのプラグインが有効になっています',
|
||||
allMCPServersEnabled: 'すべてのMCPサーバーが有効になっています',
|
||||
enableAllSkills: 'すべてのスキルを有効化',
|
||||
allSkillsEnabled: 'すべてのスキルが有効です',
|
||||
skillsTitle: 'スキル',
|
||||
noSkillsSelected: 'スキルが選択されていません',
|
||||
addSkill: 'スキルを追加',
|
||||
selectSkills: 'スキルを選択',
|
||||
noSkillsAvailable: '利用可能なスキルがありません',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'パイプラインのチャット',
|
||||
@@ -1403,6 +1418,14 @@ const jaJP = {
|
||||
createSkillHint: '新しいスキル拡張を手動で作成',
|
||||
unsupportedFileType:
|
||||
'サポートされていないファイルタイプです。.zipと.lbpkgファイルのみサポートされています',
|
||||
githubUrlHelp: 'GitHub URL を貼り付けてください',
|
||||
githubUrlTooltip:
|
||||
'プラグイン: リポジトリ、Release、Tag の URL を貼り付けます。スキル: スキルディレクトリ内の SKILL.md ページ URL を貼り付けます。',
|
||||
githubUrlPlaceholder:
|
||||
'GitHub リポジトリ、Release、または SKILL.md のリンク',
|
||||
githubUrlRequired: 'GitHub URL を入力してください',
|
||||
previewSkill: 'スキルをプレビュー',
|
||||
noSkillPreviewFound: 'インポート可能なスキルが見つかりません',
|
||||
},
|
||||
errorPage: {
|
||||
unexpectedError: 'エラーが発生しました',
|
||||
@@ -1420,6 +1443,97 @@ const jaJP = {
|
||||
selectFromSidebar: 'サイドバーからプラグインページを選択してください',
|
||||
invalidPage: '無効なプラグインページ',
|
||||
},
|
||||
skills: {
|
||||
title: 'スキル',
|
||||
description: '会話中に有効化できるスキルを作成・管理します',
|
||||
createSkill: 'スキルを作成',
|
||||
createSkillDescription:
|
||||
'ローカルディレクトリをインポートするか、情報を入力して作成します',
|
||||
editSkill: 'スキルを編集',
|
||||
getSkillListError: 'スキル一覧の取得に失敗しました: ',
|
||||
skillName: 'スキル名',
|
||||
displayName: 'スキル名',
|
||||
displayNamePlaceholder: '表示名(任意の言語に対応)',
|
||||
skillSlug: 'ディレクトリ名',
|
||||
skillSlugPlaceholder: 'english-name-only',
|
||||
skillSlugHelp:
|
||||
'スキルのディレクトリ名として使用します。英字、数字、ハイフン、アンダースコアのみ使用できます。',
|
||||
skillDescription: 'スキルの説明',
|
||||
skillInstructions: '指示内容',
|
||||
saveSuccess: '保存しました',
|
||||
saveError: '保存に失敗しました: ',
|
||||
createSuccess: '作成しました',
|
||||
createError: '作成に失敗しました: ',
|
||||
deleteSuccess: '削除しました',
|
||||
deleteError: '削除に失敗しました: ',
|
||||
deleteConfirmation: 'このスキルを削除してもよろしいですか?',
|
||||
delete: 'スキルを削除',
|
||||
skillNameRequired: 'スキル名は必須です',
|
||||
skillDescriptionRequired: 'スキルの説明は必須です',
|
||||
packageRootRequired: 'パッケージルートパスは必須です',
|
||||
scan: 'スキャン',
|
||||
scanSuccess: 'ディレクトリをスキャンしました',
|
||||
scanError: 'ディレクトリのスキャンに失敗しました: ',
|
||||
noSkills: '設定済みのスキルはありません',
|
||||
preview: 'プレビュー',
|
||||
previewInstructions: '指示内容のプレビュー',
|
||||
instructionsPlaceholder: 'Markdown 形式でスキルの指示を入力...',
|
||||
descriptionPlaceholder: 'このスキルの概要(LLM に表示されます)',
|
||||
packageRoot: 'パッケージディレクトリ',
|
||||
packageRootHelp:
|
||||
'任意。既存のスキルディレクトリをインポートする場合のみ必要です。新規スキルでは空のままにしてください。スキャンは現在のディレクトリと最大 2 階層下まで確認します。',
|
||||
importLocalDirectory: 'ローカルスキルディレクトリをインポート',
|
||||
chooseSkillDirectory: 'SKILL.md のディレクトリを選択',
|
||||
chooseAnotherDirectory: '別のディレクトリを選択',
|
||||
importingDirectory: 'プレビュー中...',
|
||||
clearDirectoryPreview: '選択したディレクトリをクリア',
|
||||
noSkillMdInDirectory: '選択したディレクトリに SKILL.md が見つかりません',
|
||||
multipleSkillMdInDirectory:
|
||||
'選択したディレクトリに複数の SKILL.md があります。単一のスキルディレクトリを直接選択してください。',
|
||||
importDirectoryError: 'ディレクトリのインポートに失敗しました: ',
|
||||
advancedSettings: '詳細設定',
|
||||
searchSkills: 'スキルを検索...',
|
||||
selectSkills: 'スキルを選択',
|
||||
addSkill: 'スキルを追加',
|
||||
builtin: '組み込み',
|
||||
importFromGithub: 'GitHub からスキルをインストール',
|
||||
createManually: '手動で作成',
|
||||
uploadZip: 'ZIP パッケージをアップロード',
|
||||
uploadZipOnly: '.zip スキルパッケージのみ対応しています',
|
||||
installSuccess: 'スキルをインストールしました',
|
||||
installError: 'スキルのインストールに失敗しました: ',
|
||||
enterRepoUrl: 'GitHub リポジトリ URL を入力',
|
||||
repoUrlPlaceholder: '例: https://github.com/owner/repo',
|
||||
fetchingReleases: 'リリースを取得中...',
|
||||
selectRelease: 'リリースを選択',
|
||||
noReleasesFound: 'リリースが見つかりません',
|
||||
fetchReleasesError: 'リリースの取得に失敗しました: ',
|
||||
selectAsset: 'インストールするファイルを選択',
|
||||
sourceArchive: 'ソースコード (zip)',
|
||||
noAssetsFound: 'このリリースにはインストール可能なファイルがありません',
|
||||
fetchAssetsError: 'ファイルの取得に失敗しました: ',
|
||||
backToReleases: 'リリースへ戻る',
|
||||
backToRepoUrl: 'リポジトリ URL へ戻る',
|
||||
backToAssets: 'ファイル一覧へ戻る',
|
||||
releaseTag: 'タグ: {{tag}}',
|
||||
publishedAt: '公開日時: {{date}}',
|
||||
prerelease: 'プレリリース',
|
||||
assetSize: 'サイズ: {{size}}',
|
||||
confirmInstall: 'インストールを確認',
|
||||
installing: 'スキルをインストール中...',
|
||||
loading: '読み込み中...',
|
||||
previewLoadError: 'プレビューの読み込みに失敗しました',
|
||||
selectFromSidebar: 'サイドバーからスキルを選択してください',
|
||||
dangerZone: '危険な操作',
|
||||
dangerZoneDescription: '元に戻せない破壊的な操作',
|
||||
files: 'ファイル',
|
||||
noFiles: 'ファイルが見つかりません',
|
||||
loadFilesError: 'ファイルの読み込みに失敗しました: ',
|
||||
readFileError: 'ファイルの読み取りに失敗しました: ',
|
||||
saveFile: 'ファイルを保存',
|
||||
saveFileSuccess: 'ファイルを保存しました',
|
||||
saveFileError: 'ファイルの保存に失敗しました: ',
|
||||
},
|
||||
};
|
||||
|
||||
export default jaJP;
|
||||
|
||||
@@ -153,6 +153,7 @@ const ruRU = {
|
||||
more: 'Ещё ({{count}})',
|
||||
less: 'Свернуть',
|
||||
noItems: 'Нет элементов',
|
||||
termsOfService: 'Условия обслуживания',
|
||||
},
|
||||
notFound: {
|
||||
title: 'Страница не найдена',
|
||||
@@ -610,7 +611,14 @@ const ruRU = {
|
||||
taskQueue: 'Задачи установки',
|
||||
clearCompleted: 'Очистить завершённые',
|
||||
noTasks: 'Нет задач установки',
|
||||
titlePlugin: 'Установка плагина {{name}}',
|
||||
titleMCP: 'Установка сервера MCP {{name}}',
|
||||
titleSkill: 'Установка навыка {{name}}',
|
||||
installCompletePlugin: 'Плагин успешно установлен',
|
||||
installCompleteMCP: 'Сервер MCP успешно установлен',
|
||||
installCompleteSkill: 'Навык успешно установлен',
|
||||
},
|
||||
uploadPluginOnly: 'Поддерживаются только пакеты плагинов .lbpkg',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: 'Поиск плагинов...',
|
||||
@@ -681,6 +689,7 @@ const ruRU = {
|
||||
clearAll: 'Очистить всё',
|
||||
noTags: 'Нет доступных тегов',
|
||||
},
|
||||
installCard: 'Установить {{name}}',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
@@ -844,6 +853,13 @@ const ruRU = {
|
||||
enableAllMCPServers: 'Включить все MCP-серверы',
|
||||
allPluginsEnabled: 'Все плагины включены',
|
||||
allMCPServersEnabled: 'Все MCP-серверы включены',
|
||||
enableAllSkills: 'Включить все навыки',
|
||||
allSkillsEnabled: 'Все навыки включены',
|
||||
skillsTitle: 'Навыки',
|
||||
noSkillsSelected: 'Навыки не выбраны',
|
||||
addSkill: 'Добавить навык',
|
||||
selectSkills: 'Выбрать навыки',
|
||||
noSkillsAvailable: 'Нет доступных навыков',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Чат конвейера',
|
||||
@@ -1409,6 +1425,119 @@ const ruRU = {
|
||||
selectFromSidebar: 'Выберите страницу плагина на боковой панели',
|
||||
invalidPage: 'Недопустимая страница плагина',
|
||||
},
|
||||
skills: {
|
||||
title: 'Навыки',
|
||||
description:
|
||||
'Создавайте навыки и управляйте ими для активации во время диалогов',
|
||||
createSkill: 'Создать навык',
|
||||
createSkillDescription:
|
||||
'Импортируйте локальный каталог или создайте навык, заполнив данные',
|
||||
editSkill: 'Редактировать навык',
|
||||
getSkillListError: 'Не удалось получить список навыков: ',
|
||||
skillName: 'Название навыка',
|
||||
displayName: 'Название навыка',
|
||||
displayNamePlaceholder: 'Отображаемое имя (поддерживает любой язык)',
|
||||
skillSlug: 'Имя каталога',
|
||||
skillSlugPlaceholder: 'english-name-only',
|
||||
skillSlugHelp:
|
||||
'Используется как имя каталога навыка. Разрешены только буквы, цифры, дефисы и подчёркивания.',
|
||||
skillDescription: 'Описание навыка',
|
||||
skillInstructions: 'Инструкции',
|
||||
saveSuccess: 'Успешно сохранено',
|
||||
saveError: 'Не удалось сохранить: ',
|
||||
createSuccess: 'Успешно создано',
|
||||
createError: 'Не удалось создать: ',
|
||||
deleteSuccess: 'Успешно удалено',
|
||||
deleteError: 'Не удалось удалить: ',
|
||||
deleteConfirmation: 'Вы уверены, что хотите удалить этот навык?',
|
||||
delete: 'Удалить навык',
|
||||
skillNameRequired: 'Название навыка не может быть пустым',
|
||||
skillDescriptionRequired: 'Описание навыка не может быть пустым',
|
||||
packageRootRequired: 'Корневой путь пакета не может быть пустым',
|
||||
scan: 'Сканировать',
|
||||
scanSuccess: 'Каталог успешно просканирован',
|
||||
scanError: 'Не удалось просканировать каталог: ',
|
||||
noSkills: 'Навыки не настроены',
|
||||
preview: 'Предпросмотр',
|
||||
previewInstructions: 'Предпросмотр инструкций',
|
||||
instructionsPlaceholder: 'Введите инструкции навыка в формате Markdown...',
|
||||
descriptionPlaceholder:
|
||||
'Краткое описание того, что делает этот навык (показывается LLM)',
|
||||
packageRoot: 'Каталог пакета',
|
||||
packageRootHelp:
|
||||
'Необязательно. Нужно только при импорте существующего каталога навыка. Для новых навыков оставьте пустым. Сканирование проверяет текущий каталог и подкаталоги глубиной до 2 уровней.',
|
||||
importLocalDirectory: 'Импортировать локальный каталог навыка',
|
||||
chooseSkillDirectory: 'Выбрать каталог с SKILL.md',
|
||||
chooseAnotherDirectory: 'Выбрать другой каталог',
|
||||
importingDirectory: 'Подготовка предпросмотра...',
|
||||
clearDirectoryPreview: 'Очистить выбранный каталог',
|
||||
noSkillMdInDirectory: 'В выбранном каталоге не найден SKILL.md',
|
||||
multipleSkillMdInDirectory:
|
||||
'В выбранном каталоге найдено несколько файлов SKILL.md. Выберите один каталог навыка напрямую.',
|
||||
importDirectoryError: 'Не удалось импортировать каталог: ',
|
||||
advancedSettings: 'Расширенные настройки',
|
||||
searchSkills: 'Поиск навыков...',
|
||||
selectSkills: 'Выбрать навыки',
|
||||
addSkill: 'Добавить навык',
|
||||
builtin: 'Встроенный',
|
||||
importFromGithub: 'Установить навык из GitHub',
|
||||
createManually: 'Создать вручную',
|
||||
uploadZip: 'Загрузить ZIP-пакет',
|
||||
uploadZipOnly: 'Поддерживаются только ZIP-пакеты навыков',
|
||||
installSuccess: 'Навык успешно установлен',
|
||||
installError: 'Не удалось установить навык: ',
|
||||
enterRepoUrl: 'Введите URL репозитория GitHub',
|
||||
repoUrlPlaceholder: 'например, https://github.com/owner/repo',
|
||||
fetchingReleases: 'Получение релизов...',
|
||||
selectRelease: 'Выбрать релиз',
|
||||
noReleasesFound: 'Релизы не найдены',
|
||||
fetchReleasesError: 'Не удалось получить релизы: ',
|
||||
selectAsset: 'Выберите файл для установки',
|
||||
sourceArchive: 'Исходный код (zip)',
|
||||
noAssetsFound: 'В этом релизе нет устанавливаемых файлов',
|
||||
fetchAssetsError: 'Не удалось получить файлы: ',
|
||||
backToReleases: 'Назад к релизам',
|
||||
backToRepoUrl: 'Назад к URL репозитория',
|
||||
backToAssets: 'Назад к файлам',
|
||||
releaseTag: 'Тег: {{tag}}',
|
||||
publishedAt: 'Опубликовано: {{date}}',
|
||||
prerelease: 'Предварительный релиз',
|
||||
assetSize: 'Размер: {{size}}',
|
||||
confirmInstall: 'Подтвердить установку',
|
||||
installing: 'Установка навыка...',
|
||||
loading: 'Загрузка...',
|
||||
previewLoadError: 'Не удалось загрузить предпросмотр',
|
||||
selectFromSidebar: 'Выберите навык на боковой панели',
|
||||
dangerZone: 'Опасная зона',
|
||||
dangerZoneDescription: 'Необратимые и разрушительные действия',
|
||||
files: 'Файлы',
|
||||
noFiles: 'Файлы не найдены',
|
||||
loadFilesError: 'Не удалось загрузить файлы: ',
|
||||
readFileError: 'Не удалось прочитать файл: ',
|
||||
saveFile: 'Сохранить файл',
|
||||
saveFileSuccess: 'Файл успешно сохранён',
|
||||
saveFileError: 'Не удалось сохранить файл: ',
|
||||
},
|
||||
addExtension: {
|
||||
manualAdd: 'Добавить вручную',
|
||||
uploadExtension: 'Перетащите файл сюда или нажмите для загрузки',
|
||||
uploadHint: 'Поддерживаются файлы .zip (навыки) и .lbpkg (плагины)',
|
||||
orContinueWith: 'или выберите действие ниже',
|
||||
addMCPServerHint: 'Подключить расширение сервера инструментов MCP',
|
||||
installFromGithub: 'Установить из GitHub',
|
||||
installFromGithubHint: 'Пакет плагина или навык (SKILL.md)',
|
||||
githubUrlHelp: 'Вставьте URL GitHub',
|
||||
githubUrlTooltip:
|
||||
'Плагин: вставьте URL репозитория, Release или Tag. Навык: вставьте URL страницы SKILL.md внутри каталога навыка.',
|
||||
githubUrlPlaceholder: 'Репозиторий GitHub, Release или ссылка SKILL.md',
|
||||
githubUrlRequired: 'Введите URL GitHub',
|
||||
previewSkill: 'Предпросмотр навыка',
|
||||
noSkillPreviewFound: 'Импортируемый навык не найден',
|
||||
createSkill: 'Создать новый навык',
|
||||
createSkillHint: 'Импортировать из локального каталога или создать вручную',
|
||||
unsupportedFileType:
|
||||
'Неподдерживаемый тип файла. Поддерживаются только файлы .zip и .lbpkg',
|
||||
},
|
||||
};
|
||||
|
||||
export default ruRU;
|
||||
|
||||
@@ -592,7 +592,14 @@ const thTH = {
|
||||
taskQueue: 'งานติดตั้ง',
|
||||
clearCompleted: 'ล้างที่เสร็จแล้ว',
|
||||
noTasks: 'ไม่มีงานติดตั้ง',
|
||||
titlePlugin: 'กำลังติดตั้งปลั๊กอิน {{name}}',
|
||||
titleMCP: 'กำลังติดตั้งเซิร์ฟเวอร์ MCP {{name}}',
|
||||
titleSkill: 'กำลังติดตั้งสกิล {{name}}',
|
||||
installCompletePlugin: 'ติดตั้งปลั๊กอินสำเร็จ',
|
||||
installCompleteMCP: 'ติดตั้งเซิร์ฟเวอร์ MCP สำเร็จ',
|
||||
installCompleteSkill: 'ติดตั้งสกิลสำเร็จ',
|
||||
},
|
||||
uploadPluginOnly: 'รองรับเฉพาะแพ็กเกจปลั๊กอิน .lbpkg',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: 'ค้นหาปลั๊กอิน...',
|
||||
@@ -662,6 +669,7 @@ const thTH = {
|
||||
clearAll: 'ล้างทั้งหมด',
|
||||
noTags: 'ไม่มีแท็กที่พร้อมใช้งาน',
|
||||
},
|
||||
installCard: 'ติดตั้ง {{name}}',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
@@ -824,6 +832,13 @@ const thTH = {
|
||||
enableAllMCPServers: 'เปิดใช้งานเซิร์ฟเวอร์ MCP ทั้งหมด',
|
||||
allPluginsEnabled: 'เปิดใช้งานปลั๊กอินทั้งหมดแล้ว',
|
||||
allMCPServersEnabled: 'เปิดใช้งานเซิร์ฟเวอร์ MCP ทั้งหมดแล้ว',
|
||||
enableAllSkills: 'เปิดใช้สกิลทั้งหมด',
|
||||
allSkillsEnabled: 'เปิดใช้สกิลทั้งหมดแล้ว',
|
||||
skillsTitle: 'สกิล',
|
||||
noSkillsSelected: 'ยังไม่ได้เลือกสกิล',
|
||||
addSkill: 'เพิ่มสกิล',
|
||||
selectSkills: 'เลือกสกิล',
|
||||
noSkillsAvailable: 'ไม่มีสกิลที่พร้อมใช้งาน',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'แชท Pipeline',
|
||||
@@ -1378,6 +1393,116 @@ const thTH = {
|
||||
selectFromSidebar: 'เลือกหน้าปลั๊กอินจากแถบด้านข้าง',
|
||||
invalidPage: 'หน้าปลั๊กอินไม่ถูกต้อง',
|
||||
},
|
||||
skills: {
|
||||
title: 'สกิล',
|
||||
description: 'สร้างและจัดการสกิลที่สามารถเปิดใช้ระหว่างการสนทนาได้',
|
||||
createSkill: 'สร้างสกิล',
|
||||
createSkillDescription: 'นำเข้าไดเรกทอรีในเครื่องหรือสร้างโดยกรอกข้อมูล',
|
||||
editSkill: 'แก้ไขสกิล',
|
||||
getSkillListError: 'ดึงรายการสกิลไม่สำเร็จ: ',
|
||||
skillName: 'ชื่อสกิล',
|
||||
displayName: 'ชื่อสกิล',
|
||||
displayNamePlaceholder: 'ชื่อที่แสดง (รองรับทุกภาษา)',
|
||||
skillSlug: 'ชื่อไดเรกทอรี',
|
||||
skillSlugPlaceholder: 'english-name-only',
|
||||
skillSlugHelp:
|
||||
'ใช้เป็นชื่อไดเรกทอรีของสกิล รองรับเฉพาะตัวอักษร ตัวเลข ขีดกลาง และขีดล่าง',
|
||||
skillDescription: 'คำอธิบายสกิล',
|
||||
skillInstructions: 'คำสั่ง',
|
||||
saveSuccess: 'บันทึกสำเร็จ',
|
||||
saveError: 'บันทึกไม่สำเร็จ: ',
|
||||
createSuccess: 'สร้างสำเร็จ',
|
||||
createError: 'สร้างไม่สำเร็จ: ',
|
||||
deleteSuccess: 'ลบสำเร็จ',
|
||||
deleteError: 'ลบไม่สำเร็จ: ',
|
||||
deleteConfirmation: 'คุณแน่ใจหรือไม่ว่าต้องการลบสกิลนี้?',
|
||||
delete: 'ลบสกิล',
|
||||
skillNameRequired: 'ชื่อสกิลต้องไม่ว่าง',
|
||||
skillDescriptionRequired: 'คำอธิบายสกิลต้องไม่ว่าง',
|
||||
packageRootRequired: 'เส้นทางรากของแพ็กเกจต้องไม่ว่าง',
|
||||
scan: 'สแกน',
|
||||
scanSuccess: 'สแกนไดเรกทอรีสำเร็จ',
|
||||
scanError: 'สแกนไดเรกทอรีไม่สำเร็จ: ',
|
||||
noSkills: 'ยังไม่มีสกิลที่ตั้งค่าไว้',
|
||||
preview: 'ดูตัวอย่าง',
|
||||
previewInstructions: 'ดูตัวอย่างคำสั่ง',
|
||||
instructionsPlaceholder: 'ป้อนคำสั่งของสกิลในรูปแบบ Markdown...',
|
||||
descriptionPlaceholder:
|
||||
'คำอธิบายสั้น ๆ ว่าสกิลนี้ทำอะไร (แสดงให้ LLM เห็น)',
|
||||
packageRoot: 'ไดเรกทอรีแพ็กเกจ',
|
||||
packageRootHelp:
|
||||
'ไม่บังคับ จำเป็นเฉพาะเมื่อนำเข้าไดเรกทอรีสกิลที่มีอยู่ เว้นว่างไว้สำหรับสกิลใหม่ การสแกนจะตรวจไดเรกทอรีปัจจุบันและไดเรกทอรีย่อยลึกสุด 2 ระดับ',
|
||||
importLocalDirectory: 'นำเข้าไดเรกทอรีสกิลในเครื่อง',
|
||||
chooseSkillDirectory: 'เลือกไดเรกทอรีของ SKILL.md',
|
||||
chooseAnotherDirectory: 'เลือกไดเรกทอรีอื่น',
|
||||
importingDirectory: 'กำลังสร้างตัวอย่าง...',
|
||||
clearDirectoryPreview: 'ล้างไดเรกทอรีที่เลือก',
|
||||
noSkillMdInDirectory: 'ไม่พบ SKILL.md ในไดเรกทอรีที่เลือก',
|
||||
multipleSkillMdInDirectory:
|
||||
'ไดเรกทอรีที่เลือกมี SKILL.md หลายไฟล์ โปรดเลือกไดเรกทอรีสกิลเดียวโดยตรง',
|
||||
importDirectoryError: 'นำเข้าไดเรกทอรีไม่สำเร็จ: ',
|
||||
advancedSettings: 'การตั้งค่าขั้นสูง',
|
||||
searchSkills: 'ค้นหาสกิล...',
|
||||
selectSkills: 'เลือกสกิล',
|
||||
addSkill: 'เพิ่มสกิล',
|
||||
builtin: 'ในตัว',
|
||||
importFromGithub: 'ติดตั้งสกิลจาก GitHub',
|
||||
createManually: 'สร้างด้วยตนเอง',
|
||||
uploadZip: 'อัปโหลดแพ็กเกจ ZIP',
|
||||
uploadZipOnly: 'รองรับเฉพาะแพ็กเกจสกิล .zip',
|
||||
installSuccess: 'ติดตั้งสกิลสำเร็จ',
|
||||
installError: 'ติดตั้งสกิลไม่สำเร็จ: ',
|
||||
enterRepoUrl: 'ป้อน URL รีโพสitory GitHub',
|
||||
repoUrlPlaceholder: 'เช่น https://github.com/owner/repo',
|
||||
fetchingReleases: 'กำลังดึงรีลีส...',
|
||||
selectRelease: 'เลือกรีลีส',
|
||||
noReleasesFound: 'ไม่พบรีลีส',
|
||||
fetchReleasesError: 'ดึงรีลีสไม่สำเร็จ: ',
|
||||
selectAsset: 'เลือกไฟล์ที่จะติดตั้ง',
|
||||
sourceArchive: 'ซอร์สโค้ด (zip)',
|
||||
noAssetsFound: 'ไม่มีไฟล์ที่ติดตั้งได้ในรีลีสนี้',
|
||||
fetchAssetsError: 'ดึงไฟล์ไม่สำเร็จ: ',
|
||||
backToReleases: 'กลับไปที่รีลีส',
|
||||
backToRepoUrl: 'กลับไปที่ URL รีโพสitory',
|
||||
backToAssets: 'กลับไปที่ไฟล์',
|
||||
releaseTag: 'แท็ก: {{tag}}',
|
||||
publishedAt: 'เผยแพร่เมื่อ: {{date}}',
|
||||
prerelease: 'พรีรีลีส',
|
||||
assetSize: 'ขนาด: {{size}}',
|
||||
confirmInstall: 'ยืนยันการติดตั้ง',
|
||||
installing: 'กำลังติดตั้งสกิล...',
|
||||
loading: 'กำลังโหลด...',
|
||||
previewLoadError: 'โหลดตัวอย่างไม่สำเร็จ',
|
||||
selectFromSidebar: 'เลือกสกิลจากแถบด้านข้าง',
|
||||
dangerZone: 'พื้นที่อันตราย',
|
||||
dangerZoneDescription: 'การกระทำที่ย้อนกลับไม่ได้และทำลายข้อมูล',
|
||||
files: 'ไฟล์',
|
||||
noFiles: 'ไม่พบไฟล์',
|
||||
loadFilesError: 'โหลดไฟล์ไม่สำเร็จ: ',
|
||||
readFileError: 'อ่านไฟล์ไม่สำเร็จ: ',
|
||||
saveFile: 'บันทึกไฟล์',
|
||||
saveFileSuccess: 'บันทึกไฟล์สำเร็จ',
|
||||
saveFileError: 'บันทึกไฟล์ไม่สำเร็จ: ',
|
||||
},
|
||||
addExtension: {
|
||||
manualAdd: 'เพิ่มด้วยตนเอง',
|
||||
uploadExtension: 'ลากแล้ววางหรือคลิกเพื่ออัปโหลด',
|
||||
uploadHint: 'รองรับไฟล์ .zip (สกิล) และ .lbpkg (ปลั๊กอิน)',
|
||||
orContinueWith: 'หรือเลือกการทำงานด้านล่าง',
|
||||
addMCPServerHint: 'เชื่อมต่อส่วนขยายเซิร์ฟเวอร์เครื่องมือ MCP',
|
||||
installFromGithub: 'ติดตั้งจาก GitHub',
|
||||
installFromGithubHint: 'แพ็กเกจปลั๊กอินหรือสกิล (SKILL.md)',
|
||||
githubUrlHelp: 'วาง URL GitHub',
|
||||
githubUrlTooltip:
|
||||
'ปลั๊กอิน: วาง URL รีโพสitory, Release หรือ Tag สกิล: วาง URL หน้า SKILL.md ภายในไดเรกทอรีสกิล',
|
||||
githubUrlPlaceholder: 'รีโพสitory GitHub, Release หรือลิงก์ SKILL.md',
|
||||
githubUrlRequired: 'ป้อน URL GitHub',
|
||||
previewSkill: 'ดูตัวอย่างสกิล',
|
||||
noSkillPreviewFound: 'ไม่พบสกิลที่นำเข้าได้',
|
||||
createSkill: 'สร้างสกิลใหม่',
|
||||
createSkillHint: 'นำเข้าจากไดเรกทอรีในเครื่องหรือสร้างด้วยตนเอง',
|
||||
unsupportedFileType: 'ประเภทไฟล์ไม่รองรับ รองรับเฉพาะไฟล์ .zip และ .lbpkg',
|
||||
},
|
||||
};
|
||||
|
||||
export default thTH;
|
||||
|
||||
@@ -606,7 +606,14 @@ const viVN = {
|
||||
taskQueue: 'Tác vụ cài đặt',
|
||||
clearCompleted: 'Xóa đã hoàn thành',
|
||||
noTasks: 'Không có tác vụ cài đặt',
|
||||
titlePlugin: 'Đang cài đặt plugin {{name}}',
|
||||
titleMCP: 'Đang cài đặt máy chủ MCP {{name}}',
|
||||
titleSkill: 'Đang cài đặt kỹ năng {{name}}',
|
||||
installCompletePlugin: 'Đã cài đặt plugin thành công',
|
||||
installCompleteMCP: 'Đã cài đặt máy chủ MCP thành công',
|
||||
installCompleteSkill: 'Đã cài đặt kỹ năng thành công',
|
||||
},
|
||||
uploadPluginOnly: 'Chỉ hỗ trợ gói plugin .lbpkg',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: 'Tìm kiếm plugin...',
|
||||
@@ -676,6 +683,7 @@ const viVN = {
|
||||
clearAll: 'Xóa tất cả',
|
||||
noTags: 'Không có thẻ nào',
|
||||
},
|
||||
installCard: 'Cài đặt {{name}}',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
@@ -838,6 +846,13 @@ const viVN = {
|
||||
enableAllMCPServers: 'Bật tất cả máy chủ MCP',
|
||||
allPluginsEnabled: 'Đã bật tất cả plugin',
|
||||
allMCPServersEnabled: 'Đã bật tất cả máy chủ MCP',
|
||||
enableAllSkills: 'Bật tất cả kỹ năng',
|
||||
allSkillsEnabled: 'Tất cả kỹ năng đã được bật',
|
||||
skillsTitle: 'Kỹ năng',
|
||||
noSkillsSelected: 'Chưa chọn kỹ năng',
|
||||
addSkill: 'Thêm kỹ năng',
|
||||
selectSkills: 'Chọn kỹ năng',
|
||||
noSkillsAvailable: 'Không có kỹ năng khả dụng',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Trò chuyện Pipeline',
|
||||
@@ -1402,6 +1417,120 @@ const viVN = {
|
||||
selectFromSidebar: 'Chọn một trang plugin từ thanh bên',
|
||||
invalidPage: 'Trang plugin không hợp lệ',
|
||||
},
|
||||
skills: {
|
||||
title: 'Kỹ năng',
|
||||
description:
|
||||
'Tạo và quản lý các kỹ năng có thể được kích hoạt trong cuộc trò chuyện',
|
||||
createSkill: 'Tạo kỹ năng',
|
||||
createSkillDescription:
|
||||
'Nhập thư mục cục bộ hoặc tạo bằng cách điền thông tin',
|
||||
editSkill: 'Chỉnh sửa kỹ năng',
|
||||
getSkillListError: 'Không thể lấy danh sách kỹ năng: ',
|
||||
skillName: 'Tên kỹ năng',
|
||||
displayName: 'Tên kỹ năng',
|
||||
displayNamePlaceholder: 'Tên hiển thị (hỗ trợ mọi ngôn ngữ)',
|
||||
skillSlug: 'Tên thư mục',
|
||||
skillSlugPlaceholder: 'english-name-only',
|
||||
skillSlugHelp:
|
||||
'Dùng làm tên thư mục kỹ năng. Chỉ hỗ trợ chữ cái, số, dấu gạch nối và dấu gạch dưới.',
|
||||
skillDescription: 'Mô tả kỹ năng',
|
||||
skillInstructions: 'Hướng dẫn',
|
||||
saveSuccess: 'Đã lưu thành công',
|
||||
saveError: 'Lưu thất bại: ',
|
||||
createSuccess: 'Đã tạo thành công',
|
||||
createError: 'Tạo thất bại: ',
|
||||
deleteSuccess: 'Đã xóa thành công',
|
||||
deleteError: 'Xóa thất bại: ',
|
||||
deleteConfirmation: 'Bạn có chắc muốn xóa kỹ năng này không?',
|
||||
delete: 'Xóa kỹ năng',
|
||||
skillNameRequired: 'Tên kỹ năng không được để trống',
|
||||
skillDescriptionRequired: 'Mô tả kỹ năng không được để trống',
|
||||
packageRootRequired: 'Đường dẫn gốc của gói không được để trống',
|
||||
scan: 'Quét',
|
||||
scanSuccess: 'Đã quét thư mục thành công',
|
||||
scanError: 'Quét thư mục thất bại: ',
|
||||
noSkills: 'Chưa cấu hình kỹ năng nào',
|
||||
preview: 'Xem trước',
|
||||
previewInstructions: 'Xem trước hướng dẫn',
|
||||
instructionsPlaceholder:
|
||||
'Nhập hướng dẫn kỹ năng theo định dạng Markdown...',
|
||||
descriptionPlaceholder:
|
||||
'Mô tả ngắn về chức năng của kỹ năng này (hiển thị cho LLM)',
|
||||
packageRoot: 'Thư mục gói',
|
||||
packageRootHelp:
|
||||
'Tùy chọn. Chỉ cần khi nhập một thư mục kỹ năng hiện có. Để trống với kỹ năng mới. Quá trình quét kiểm tra thư mục hiện tại và thư mục con sâu tối đa 2 cấp.',
|
||||
importLocalDirectory: 'Nhập thư mục kỹ năng cục bộ',
|
||||
chooseSkillDirectory: 'Chọn thư mục chứa SKILL.md',
|
||||
chooseAnotherDirectory: 'Chọn thư mục khác',
|
||||
importingDirectory: 'Đang tạo bản xem trước...',
|
||||
clearDirectoryPreview: 'Xóa thư mục đã chọn',
|
||||
noSkillMdInDirectory: 'Không tìm thấy SKILL.md trong thư mục đã chọn',
|
||||
multipleSkillMdInDirectory:
|
||||
'Thư mục đã chọn chứa nhiều tệp SKILL.md. Vui lòng chọn trực tiếp một thư mục kỹ năng duy nhất.',
|
||||
importDirectoryError: 'Nhập thư mục thất bại: ',
|
||||
advancedSettings: 'Cài đặt nâng cao',
|
||||
searchSkills: 'Tìm kiếm kỹ năng...',
|
||||
selectSkills: 'Chọn kỹ năng',
|
||||
addSkill: 'Thêm kỹ năng',
|
||||
builtin: 'Tích hợp sẵn',
|
||||
importFromGithub: 'Cài đặt kỹ năng từ GitHub',
|
||||
createManually: 'Tạo thủ công',
|
||||
uploadZip: 'Tải lên gói ZIP',
|
||||
uploadZipOnly: 'Chỉ hỗ trợ gói kỹ năng .zip',
|
||||
installSuccess: 'Đã cài đặt kỹ năng thành công',
|
||||
installError: 'Cài đặt kỹ năng thất bại: ',
|
||||
enterRepoUrl: 'Nhập URL kho GitHub',
|
||||
repoUrlPlaceholder: 'ví dụ: https://github.com/owner/repo',
|
||||
fetchingReleases: 'Đang tải release...',
|
||||
selectRelease: 'Chọn release',
|
||||
noReleasesFound: 'Không tìm thấy release',
|
||||
fetchReleasesError: 'Không thể tải release: ',
|
||||
selectAsset: 'Chọn tệp để cài đặt',
|
||||
sourceArchive: 'Mã nguồn (zip)',
|
||||
noAssetsFound: 'Không có tệp có thể cài đặt trong release này',
|
||||
fetchAssetsError: 'Không thể tải tệp: ',
|
||||
backToReleases: 'Quay lại release',
|
||||
backToRepoUrl: 'Quay lại URL kho',
|
||||
backToAssets: 'Quay lại tệp',
|
||||
releaseTag: 'Thẻ: {{tag}}',
|
||||
publishedAt: 'Phát hành lúc: {{date}}',
|
||||
prerelease: 'Bản phát hành trước',
|
||||
assetSize: 'Kích thước: {{size}}',
|
||||
confirmInstall: 'Xác nhận cài đặt',
|
||||
installing: 'Đang cài đặt kỹ năng...',
|
||||
loading: 'Đang tải...',
|
||||
previewLoadError: 'Không thể tải bản xem trước',
|
||||
selectFromSidebar: 'Chọn một kỹ năng từ thanh bên',
|
||||
dangerZone: 'Vùng nguy hiểm',
|
||||
dangerZoneDescription: 'Các thao tác không thể hoàn tác và có tính phá hủy',
|
||||
files: 'Tệp',
|
||||
noFiles: 'Không tìm thấy tệp',
|
||||
loadFilesError: 'Không thể tải tệp: ',
|
||||
readFileError: 'Không thể đọc tệp: ',
|
||||
saveFile: 'Lưu tệp',
|
||||
saveFileSuccess: 'Đã lưu tệp thành công',
|
||||
saveFileError: 'Lưu tệp thất bại: ',
|
||||
},
|
||||
addExtension: {
|
||||
manualAdd: 'Thêm thủ công',
|
||||
uploadExtension: 'Kéo thả hoặc nhấp để tải lên',
|
||||
uploadHint: 'Hỗ trợ tệp .zip (kỹ năng) và .lbpkg (plugin)',
|
||||
orContinueWith: 'hoặc chọn một thao tác bên dưới',
|
||||
addMCPServerHint: 'Kết nối tiện ích máy chủ công cụ MCP',
|
||||
installFromGithub: 'Cài đặt từ GitHub',
|
||||
installFromGithubHint: 'Gói plugin hoặc kỹ năng (SKILL.md)',
|
||||
githubUrlHelp: 'Dán URL GitHub',
|
||||
githubUrlTooltip:
|
||||
'Plugin: dán URL kho, Release hoặc Tag. Kỹ năng: dán URL trang SKILL.md trong thư mục kỹ năng.',
|
||||
githubUrlPlaceholder: 'Kho GitHub, Release hoặc liên kết SKILL.md',
|
||||
githubUrlRequired: 'Nhập URL GitHub',
|
||||
previewSkill: 'Xem trước kỹ năng',
|
||||
noSkillPreviewFound: 'Không tìm thấy kỹ năng có thể nhập',
|
||||
createSkill: 'Tạo kỹ năng mới',
|
||||
createSkillHint: 'Nhập từ thư mục cục bộ hoặc tạo thủ công',
|
||||
unsupportedFileType:
|
||||
'Loại tệp không được hỗ trợ. Chỉ hỗ trợ tệp .zip và .lbpkg',
|
||||
},
|
||||
};
|
||||
|
||||
export default viVN;
|
||||
|
||||
@@ -1286,6 +1286,7 @@ const zhHans = {
|
||||
title: '技能',
|
||||
description: '创建和管理可在对话中激活的技能',
|
||||
createSkill: '创建技能',
|
||||
createSkillDescription: '导入本地目录或手动填写信息创建',
|
||||
editSkill: '编辑技能',
|
||||
getSkillListError: '获取技能列表失败:',
|
||||
skillName: '技能名称',
|
||||
@@ -1318,6 +1319,15 @@ const zhHans = {
|
||||
packageRoot: '技能目录',
|
||||
packageRootHelp:
|
||||
'非必填。仅在导入已有技能目录时需要填写,新建技能可留空。扫描会检查当前目录及两级子目录。',
|
||||
importLocalDirectory: '导入本地技能目录',
|
||||
chooseSkillDirectory: '选择 SKILL.md 所在目录',
|
||||
chooseAnotherDirectory: '重新选择目录',
|
||||
importingDirectory: '正在预览...',
|
||||
clearDirectoryPreview: '清除已选目录',
|
||||
noSkillMdInDirectory: '选择的目录中没有找到 SKILL.md',
|
||||
multipleSkillMdInDirectory:
|
||||
'选择的目录中包含多个 SKILL.md,请直接选择单个技能目录。',
|
||||
importDirectoryError: '导入目录失败:',
|
||||
advancedSettings: '高级设置',
|
||||
searchSkills: '搜索技能...',
|
||||
selectSkills: '选择技能',
|
||||
@@ -1437,7 +1447,7 @@ const zhHans = {
|
||||
previewSkill: '预览技能',
|
||||
noSkillPreviewFound: '未找到可导入的技能',
|
||||
createSkill: '创建新的技能',
|
||||
createSkillHint: '手动创建一个新的技能扩展',
|
||||
createSkillHint: '从本地目录导入或手动创建',
|
||||
unsupportedFileType: '不支持的文件类型,仅支持 .zip 和 .lbpkg 文件',
|
||||
},
|
||||
errorPage: {
|
||||
|
||||
@@ -574,7 +574,14 @@ const zhHant = {
|
||||
taskQueue: '安裝任務',
|
||||
clearCompleted: '清除已完成',
|
||||
noTasks: '暫無安裝任務',
|
||||
titlePlugin: '正在安裝外掛 {{name}}',
|
||||
titleMCP: '正在安裝 MCP 伺服器 {{name}}',
|
||||
titleSkill: '正在安裝技能 {{name}}',
|
||||
installCompletePlugin: '外掛安裝成功',
|
||||
installCompleteMCP: 'MCP 伺服器安裝成功',
|
||||
installCompleteSkill: '技能安裝成功',
|
||||
},
|
||||
uploadPluginOnly: '僅支援 .lbpkg 外掛包',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: '搜尋插件...',
|
||||
@@ -802,6 +809,13 @@ const zhHant = {
|
||||
enableAllMCPServers: '啟用所有 MCP 伺服器',
|
||||
allPluginsEnabled: '已啟用所有插件',
|
||||
allMCPServersEnabled: '已啟用所有 MCP 伺服器',
|
||||
enableAllSkills: '啟用全部技能',
|
||||
allSkillsEnabled: '已啟用全部技能',
|
||||
skillsTitle: '技能',
|
||||
noSkillsSelected: '未選擇技能',
|
||||
addSkill: '新增技能',
|
||||
selectSkills: '選擇技能',
|
||||
noSkillsAvailable: '暫無可用技能',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流程線對話',
|
||||
@@ -1343,7 +1357,7 @@ const zhHant = {
|
||||
previewSkill: '預覽技能',
|
||||
noSkillPreviewFound: '未找到可匯入的技能',
|
||||
createSkill: '建立新的技能',
|
||||
createSkillHint: '手動建立一個新的技能擴充',
|
||||
createSkillHint: '從本地目錄匯入或手動建立',
|
||||
unsupportedFileType: '不支援的檔案類型,僅支援 .zip 和 .lbpkg 檔案',
|
||||
},
|
||||
errorPage: {
|
||||
@@ -1364,6 +1378,7 @@ const zhHant = {
|
||||
title: '技能',
|
||||
description: '創建和管理可在對話中激活的技能',
|
||||
createSkill: '創建技能',
|
||||
createSkillDescription: '匯入本機目錄或手動填寫資訊建立',
|
||||
editSkill: '編輯技能',
|
||||
getSkillListError: '獲取技能列表失敗:',
|
||||
skillName: '技能名稱',
|
||||
@@ -1395,6 +1410,15 @@ const zhHant = {
|
||||
descriptionPlaceholder: '簡短描述此技能的功能',
|
||||
packageRoot: '技能目錄',
|
||||
packageRootHelp: '非必填。僅在導入已有技能目錄時需要填寫。',
|
||||
importLocalDirectory: '匯入本地技能目錄',
|
||||
chooseSkillDirectory: '選擇 SKILL.md 所在目錄',
|
||||
chooseAnotherDirectory: '重新選擇目錄',
|
||||
importingDirectory: '正在預覽...',
|
||||
clearDirectoryPreview: '清除已選目錄',
|
||||
noSkillMdInDirectory: '選擇的目錄中沒有找到 SKILL.md',
|
||||
multipleSkillMdInDirectory:
|
||||
'選擇的目錄中包含多個 SKILL.md,請直接選擇單個技能目錄。',
|
||||
importDirectoryError: '匯入目錄失敗:',
|
||||
advancedSettings: '進階設定',
|
||||
searchSkills: '搜尋技能...',
|
||||
selectSkills: '選擇技能',
|
||||
@@ -1419,6 +1443,24 @@ const zhHant = {
|
||||
backToReleases: '返回版本列表',
|
||||
backToRepoUrl: '返回倉庫地址',
|
||||
selectFromSidebar: '從側邊欄選擇一個技能',
|
||||
backToAssets: '返回檔案列表',
|
||||
releaseTag: '標籤:{{tag}}',
|
||||
publishedAt: '發布時間:{{date}}',
|
||||
prerelease: '預發布',
|
||||
assetSize: '大小:{{size}}',
|
||||
confirmInstall: '確認安裝',
|
||||
installing: '正在安裝技能...',
|
||||
loading: '載入中...',
|
||||
previewLoadError: '載入預覽失敗',
|
||||
dangerZone: '危險區域',
|
||||
dangerZoneDescription: '不可逆且具破壞性的操作',
|
||||
files: '檔案',
|
||||
noFiles: '未找到檔案',
|
||||
loadFilesError: '載入檔案失敗:',
|
||||
readFileError: '讀取檔案失敗:',
|
||||
saveFile: '儲存檔案',
|
||||
saveFileSuccess: '檔案儲存成功',
|
||||
saveFileError: '檔案儲存失敗:',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user