mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-14 22:40:59 +00:00
fix: import github skill directories
This commit is contained in:
@@ -8,7 +8,7 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import quote, unquote, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import yaml
|
import yaml
|
||||||
@@ -429,7 +429,7 @@ class SkillService:
|
|||||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
async def _install_github_skill_md(self, asset_url: str, *, owner: str, repo: str, data: dict) -> list[dict]:
|
async def _install_github_skill_md(self, asset_url: str, *, owner: str, repo: str, data: dict) -> list[dict]:
|
||||||
zip_bytes, filename, package_name = await self._download_github_skill_md_as_zip(
|
zip_bytes, filename, package_name = await self._download_github_skill_directory_as_zip(
|
||||||
asset_url,
|
asset_url,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
repo=repo,
|
repo=repo,
|
||||||
@@ -464,7 +464,7 @@ class SkillService:
|
|||||||
return await self._resolve_installed_skills(scanned)
|
return await self._resolve_installed_skills(scanned)
|
||||||
|
|
||||||
async def _preview_github_skill_md(self, asset_url: str, *, owner: str, repo: str) -> list[dict]:
|
async def _preview_github_skill_md(self, asset_url: str, *, owner: str, repo: str) -> list[dict]:
|
||||||
zip_bytes, _filename, package_name = await self._download_github_skill_md_as_zip(
|
zip_bytes, _filename, package_name = await self._download_github_skill_directory_as_zip(
|
||||||
asset_url,
|
asset_url,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
repo=repo,
|
repo=repo,
|
||||||
@@ -572,30 +572,85 @@ class SkillService:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.content
|
return resp.content
|
||||||
|
|
||||||
async def _download_github_skill_md_as_zip(
|
async def _download_github_skill_directory_as_zip(
|
||||||
self, asset_url: str, *, owner: str, repo: str
|
self, asset_url: str, *, owner: str, repo: str
|
||||||
) -> tuple[bytes, str, str]:
|
) -> tuple[bytes, str, str]:
|
||||||
info = self._parse_github_skill_md_url(asset_url, owner=owner, repo=repo)
|
info = self._parse_github_skill_md_url(asset_url, owner=owner, repo=repo)
|
||||||
content = await self._download_github_skill_md(info['raw_url'])
|
archive_url = f'https://codeload.github.com/{owner}/{repo}/zip/{quote(info["ref"], safe="/")}'
|
||||||
package_name = self._resolve_github_skill_md_package_name(content, info['package_name'])
|
archive_bytes = await self._download_github_asset(archive_url)
|
||||||
|
|
||||||
buffer = io.BytesIO()
|
|
||||||
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
||||||
zf.writestr(f'{package_name}/SKILL.md', content)
|
|
||||||
return buffer.getvalue(), f'{package_name}.zip', package_name
|
|
||||||
|
|
||||||
async def _download_github_skill_md(self, raw_url: str) -> str:
|
|
||||||
async with httpx.AsyncClient(follow_redirects=True, timeout=60) as client:
|
|
||||||
try:
|
|
||||||
resp = await client.get(raw_url)
|
|
||||||
resp.raise_for_status()
|
|
||||||
except httpx.HTTPError as exc:
|
|
||||||
raise ValueError(f'Failed to download SKILL.md from GitHub: {exc}') from exc
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return resp.content.decode('utf-8')
|
source_archive = zipfile.ZipFile(io.BytesIO(archive_bytes), 'r')
|
||||||
except UnicodeDecodeError as exc:
|
except zipfile.BadZipFile as exc:
|
||||||
raise ValueError('GitHub SKILL.md must be valid UTF-8 text') from exc
|
raise ValueError('GitHub repository archive must be a valid .zip archive') from exc
|
||||||
|
|
||||||
|
with source_archive as source_zip:
|
||||||
|
skill_entry = self._find_github_skill_archive_entry(source_zip, info['file_path'])
|
||||||
|
try:
|
||||||
|
skill_md_content = source_zip.read(skill_entry).decode('utf-8')
|
||||||
|
except UnicodeDecodeError as exc:
|
||||||
|
raise ValueError('GitHub SKILL.md must be valid UTF-8 text') from exc
|
||||||
|
|
||||||
|
package_name = self._resolve_github_skill_md_package_name(skill_md_content, info['package_name'])
|
||||||
|
source_skill_dir = posixpath.dirname(posixpath.normpath(skill_entry.filename))
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as target_zip:
|
||||||
|
self._copy_github_skill_directory_to_zip(source_zip, target_zip, source_skill_dir, package_name)
|
||||||
|
return buffer.getvalue(), f'{package_name}.zip', package_name
|
||||||
|
|
||||||
|
def _find_github_skill_archive_entry(self, archive: zipfile.ZipFile, file_path: str) -> zipfile.ZipInfo:
|
||||||
|
normalized_file_path = posixpath.normpath(file_path).lower()
|
||||||
|
for member in archive.infolist():
|
||||||
|
if member.is_dir():
|
||||||
|
continue
|
||||||
|
normalized_member = posixpath.normpath(member.filename)
|
||||||
|
path_parts = normalized_member.split('/', 1)
|
||||||
|
if len(path_parts) != 2:
|
||||||
|
continue
|
||||||
|
archive_relative_path = path_parts[1].lower()
|
||||||
|
if archive_relative_path == normalized_file_path:
|
||||||
|
return member
|
||||||
|
raise ValueError(f'GitHub archive does not contain requested SKILL.md: {file_path}')
|
||||||
|
|
||||||
|
def _copy_github_skill_directory_to_zip(
|
||||||
|
self,
|
||||||
|
source_zip: zipfile.ZipFile,
|
||||||
|
target_zip: zipfile.ZipFile,
|
||||||
|
source_skill_dir: str,
|
||||||
|
package_name: str,
|
||||||
|
) -> None:
|
||||||
|
normalized_source_dir = posixpath.normpath(source_skill_dir)
|
||||||
|
source_prefix = f'{normalized_source_dir}/'
|
||||||
|
copied_files = 0
|
||||||
|
|
||||||
|
for member in source_zip.infolist():
|
||||||
|
normalized_member = posixpath.normpath(member.filename)
|
||||||
|
if normalized_member != normalized_source_dir and not normalized_member.startswith(source_prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
relative_path = posixpath.relpath(normalized_member, normalized_source_dir)
|
||||||
|
if relative_path in ('', '.'):
|
||||||
|
continue
|
||||||
|
if relative_path.startswith('../') or relative_path == '..' or posixpath.isabs(relative_path):
|
||||||
|
raise ValueError(f'GitHub archive contains an unsafe skill path: {member.filename}')
|
||||||
|
|
||||||
|
target_name = f'{package_name}/{relative_path}'
|
||||||
|
if member.is_dir() and not target_name.endswith('/'):
|
||||||
|
target_name = f'{target_name}/'
|
||||||
|
target_info = zipfile.ZipInfo(target_name, date_time=member.date_time)
|
||||||
|
target_info.external_attr = member.external_attr
|
||||||
|
target_info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
|
||||||
|
if member.is_dir():
|
||||||
|
target_zip.writestr(target_info, b'')
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_zip.writestr(target_info, source_zip.read(member))
|
||||||
|
copied_files += 1
|
||||||
|
|
||||||
|
if copied_files == 0:
|
||||||
|
raise ValueError('GitHub skill directory is empty')
|
||||||
|
|
||||||
def _extract_uploaded_skill_to_temp(self, file_bytes: bytes, tmp_dir: str) -> str:
|
def _extract_uploaded_skill_to_temp(self, file_bytes: bytes, tmp_dir: str) -> str:
|
||||||
extract_dir = os.path.join(tmp_dir, 'extracted')
|
extract_dir = os.path.join(tmp_dir, 'extracted')
|
||||||
@@ -758,31 +813,38 @@ class SkillService:
|
|||||||
raise ValueError('asset_url must be a valid HTTPS GitHub SKILL.md URL')
|
raise ValueError('asset_url must be a valid HTTPS GitHub SKILL.md URL')
|
||||||
|
|
||||||
host = parsed.netloc.lower()
|
host = parsed.netloc.lower()
|
||||||
path_parts = [part for part in (parsed.path or '').split('/') if part]
|
path_parts = [unquote(part) for part in (parsed.path or '').split('/') if part]
|
||||||
if host == 'github.com':
|
if host == 'github.com':
|
||||||
if len(path_parts) < 5 or path_parts[0] != owner or path_parts[1] != repo or path_parts[2] != 'blob':
|
if (
|
||||||
|
len(path_parts) < 5
|
||||||
|
or path_parts[0] != owner
|
||||||
|
or path_parts[1] != repo
|
||||||
|
or path_parts[2]
|
||||||
|
not in (
|
||||||
|
'blob',
|
||||||
|
'raw',
|
||||||
|
)
|
||||||
|
):
|
||||||
raise ValueError('GitHub SKILL.md URL must point to the requested owner/repo blob path')
|
raise ValueError('GitHub SKILL.md URL must point to the requested owner/repo blob path')
|
||||||
ref = path_parts[3]
|
ref = path_parts[3]
|
||||||
file_path = '/'.join(path_parts[4:])
|
file_path = '/'.join(path_parts[4:])
|
||||||
raw_url = f'https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{file_path}'
|
|
||||||
elif host == 'raw.githubusercontent.com':
|
elif host == 'raw.githubusercontent.com':
|
||||||
if len(path_parts) < 4 or path_parts[0] != owner or path_parts[1] != repo:
|
if len(path_parts) < 4 or path_parts[0] != owner or path_parts[1] != repo:
|
||||||
raise ValueError('GitHub SKILL.md URL must point to the requested owner/repo raw path')
|
raise ValueError('GitHub SKILL.md URL must point to the requested owner/repo raw path')
|
||||||
ref = path_parts[2]
|
ref = path_parts[2]
|
||||||
file_path = '/'.join(path_parts[3:])
|
file_path = '/'.join(path_parts[3:])
|
||||||
raw_url = parsed.geturl()
|
|
||||||
else:
|
else:
|
||||||
raise ValueError('asset_url must point to a GitHub SKILL.md file')
|
raise ValueError('asset_url must point to a GitHub SKILL.md file')
|
||||||
|
|
||||||
normalized_file_path = posixpath.normpath(file_path).lower()
|
normalized_file_path = posixpath.normpath(file_path)
|
||||||
if normalized_file_path != 'skill.md' and not normalized_file_path.endswith('/skill.md'):
|
normalized_file_path_lower = normalized_file_path.lower()
|
||||||
|
if normalized_file_path_lower != 'skill.md' and not normalized_file_path_lower.endswith('/skill.md'):
|
||||||
raise ValueError('GitHub skill import requires a URL ending with SKILL.md')
|
raise ValueError('GitHub skill import requires a URL ending with SKILL.md')
|
||||||
|
|
||||||
parent_dir = posixpath.basename(posixpath.dirname(file_path)) or repo
|
parent_dir = posixpath.basename(posixpath.dirname(normalized_file_path)) or repo
|
||||||
return {
|
return {
|
||||||
'raw_url': raw_url,
|
|
||||||
'ref': ref,
|
'ref': ref,
|
||||||
'file_path': file_path,
|
'file_path': normalized_file_path,
|
||||||
'package_name': self._uploaded_skill_target_stem(parent_dir),
|
'package_name': self._uploaded_skill_target_stem(parent_dir),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
CircleHelp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -855,9 +861,23 @@ function AddExtensionContent() {
|
|||||||
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
|
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
|
||||||
{githubInstallStatus === GithubInstallStatus.WAIT_INPUT && (
|
{githubInstallStatus === GithubInstallStatus.WAIT_INPUT && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
{t('addExtension.githubUrlHelp')}
|
<span>{t('addExtension.githubUrlHelp')}</span>
|
||||||
</p>
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex size-4 items-center justify-center rounded-full transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||||
|
aria-label={t('addExtension.githubUrlTooltip')}
|
||||||
|
>
|
||||||
|
<CircleHelp className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[280px]">
|
||||||
|
{t('addExtension.githubUrlTooltip')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('addExtension.githubUrlPlaceholder')}
|
placeholder={t('addExtension.githubUrlPlaceholder')}
|
||||||
value={githubURL}
|
value={githubURL}
|
||||||
@@ -866,9 +886,6 @@ function AddExtensionContent() {
|
|||||||
if (e.key === 'Enter') handleGithubAddressSubmit();
|
if (e.key === 'Enter') handleGithubAddressSubmit();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
|
||||||
{t('addExtension.skillMdUrlHelp')}
|
|
||||||
</p>
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleGithubAddressSubmit}
|
onClick={handleGithubAddressSubmit}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(function FileTree(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
<div className="max-h-[min(46vh,32rem)] space-y-1 overflow-y-auto overscroll-contain pr-1">
|
||||||
{rootEntries.length === 0 && !loading && (
|
{rootEntries.length === 0 && !loading && (
|
||||||
<div className="text-sm text-muted-foreground py-2">
|
<div className="text-sm text-muted-foreground py-2">
|
||||||
{t('skills.noFiles')}
|
{t('skills.noFiles')}
|
||||||
@@ -330,24 +330,9 @@ export default function SkillForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContentChange = (content: string) => {
|
const handleInstructionDraftChange = (content: string) => {
|
||||||
setFileContent(content);
|
setFileContent(content);
|
||||||
// If editing SKILL.md, sync to skill.instructions
|
setSkill((prev) => ({ ...prev, instructions: content }));
|
||||||
if (selectedFile === 'SKILL.md' || selectedFile?.endsWith('/SKILL.md')) {
|
|
||||||
setSkill((prev) => ({ ...prev, instructions: content }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveFile = async () => {
|
|
||||||
if (!initSkillName || !selectedFile) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await httpClient.writeSkillFile(initSkillName, selectedFile, fileContent);
|
|
||||||
toast.success(t('skills.saveFileSuccess'));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save file:', error);
|
|
||||||
toast.error(t('skills.saveFileError') + String(error));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
@@ -462,23 +447,12 @@ export default function SkillForm({
|
|||||||
<Textarea
|
<Textarea
|
||||||
id="instructions"
|
id="instructions"
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
onChange={(e) => handleContentChange(e.target.value)}
|
onChange={(e) => handleInstructionDraftChange(e.target.value)}
|
||||||
|
readOnly={Boolean(initSkillName)}
|
||||||
placeholder={t('skills.instructionsPlaceholder')}
|
placeholder={t('skills.instructionsPlaceholder')}
|
||||||
rows={16}
|
rows={16}
|
||||||
className="min-h-[360px] resize-y font-mono text-sm lg:min-h-[calc(100vh-220px)]"
|
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)]"
|
||||||
/>
|
/>
|
||||||
{selectedFile &&
|
|
||||||
selectedFile !== 'SKILL.md' &&
|
|
||||||
!selectedFile.endsWith('/SKILL.md') && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSaveFile}
|
|
||||||
>
|
|
||||||
{t('skills.saveFile')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1487,16 +1487,13 @@ const enUS = {
|
|||||||
uploadHint: 'Supports .zip (skills) and .lbpkg (plugins) files',
|
uploadHint: 'Supports .zip (skills) and .lbpkg (plugins) files',
|
||||||
orContinueWith: 'or choose an action below',
|
orContinueWith: 'or choose an action below',
|
||||||
addMCPServerHint: 'Connect an MCP tool server extension',
|
addMCPServerHint: 'Connect an MCP tool server extension',
|
||||||
installFromGithub: 'Install Plugin or Skill from GitHub',
|
installFromGithub: 'Install from GitHub',
|
||||||
installFromGithubHint:
|
installFromGithubHint: 'Plugin package or Skill (SKILL.md)',
|
||||||
'Supports GitHub Release plugin packages and direct GitHub SKILL.md imports',
|
githubUrlHelp: 'Paste a GitHub URL',
|
||||||
githubUrlHelp:
|
githubUrlTooltip:
|
||||||
'Paste a GitHub repository URL to install a plugin. To install a Skill, paste the GitHub SKILL.md file URL.',
|
'Plugin: paste a repository, Release, or Tag URL. Skill: paste the SKILL.md page URL inside the skill directory.',
|
||||||
githubUrlPlaceholder:
|
githubUrlPlaceholder: 'GitHub repository, Release, or SKILL.md link',
|
||||||
'e.g. https://github.com/owner/repo or https://github.com/owner/repo/blob/main/path/SKILL.md',
|
|
||||||
githubUrlRequired: 'Enter a GitHub URL',
|
githubUrlRequired: 'Enter a GitHub URL',
|
||||||
skillMdUrlHelp:
|
|
||||||
'For Skills, copy the exact SKILL.md file link from the skill directory in GitHub.',
|
|
||||||
previewSkill: 'Preview Skill',
|
previewSkill: 'Preview Skill',
|
||||||
noSkillPreviewFound: 'No importable Skill found',
|
noSkillPreviewFound: 'No importable Skill found',
|
||||||
createSkill: 'Create New Skill',
|
createSkill: 'Create New Skill',
|
||||||
|
|||||||
@@ -1426,18 +1426,15 @@ const zhHans = {
|
|||||||
uploadHint: '支持 .zip(技能)和 .lbpkg(插件)文件',
|
uploadHint: '支持 .zip(技能)和 .lbpkg(插件)文件',
|
||||||
orContinueWith: '或选择以下操作',
|
orContinueWith: '或选择以下操作',
|
||||||
addMCPServerHint: '连接一个 MCP 工具服务器扩展',
|
addMCPServerHint: '连接一个 MCP 工具服务器扩展',
|
||||||
installFromGithub: '从 GitHub 安装插件或 Skill',
|
installFromGithub: '从 GitHub 安装',
|
||||||
installFromGithubHint:
|
installFromGithubHint: '插件包或技能(SKILL.md)',
|
||||||
'支持 GitHub Release 插件包,也支持直接导入 GitHub 上的 SKILL.md',
|
githubUrlHelp: '粘贴 GitHub 地址',
|
||||||
githubUrlHelp:
|
githubUrlTooltip:
|
||||||
'粘贴 GitHub 仓库地址安装插件;如果要安装 Skill,请粘贴 GitHub 上的 SKILL.md 文件地址。',
|
'插件:粘贴仓库、Release 或 Tag 地址。技能:粘贴技能目录里的 SKILL.md 页面地址。',
|
||||||
githubUrlPlaceholder:
|
githubUrlPlaceholder: 'GitHub 仓库、Release 或 SKILL.md 链接',
|
||||||
'例如 https://github.com/owner/repo 或 https://github.com/owner/repo/blob/main/path/SKILL.md',
|
|
||||||
githubUrlRequired: '请输入 GitHub 地址',
|
githubUrlRequired: '请输入 GitHub 地址',
|
||||||
skillMdUrlHelp:
|
previewSkill: '预览技能',
|
||||||
'Skill 需要复制具体文件链接,例如仓库中某个技能目录下的 SKILL.md 页面地址。',
|
noSkillPreviewFound: '未找到可导入的技能',
|
||||||
previewSkill: '预览 Skill',
|
|
||||||
noSkillPreviewFound: '未找到可导入的 Skill',
|
|
||||||
createSkill: '创建新的技能',
|
createSkill: '创建新的技能',
|
||||||
createSkillHint: '手动创建一个新的技能扩展',
|
createSkillHint: '手动创建一个新的技能扩展',
|
||||||
unsupportedFileType: '不支持的文件类型,仅支持 .zip 和 .lbpkg 文件',
|
unsupportedFileType: '不支持的文件类型,仅支持 .zip 和 .lbpkg 文件',
|
||||||
|
|||||||
@@ -1332,18 +1332,15 @@ const zhHant = {
|
|||||||
uploadHint: '支援 .zip(技能)和 .lbpkg(插件)檔案',
|
uploadHint: '支援 .zip(技能)和 .lbpkg(插件)檔案',
|
||||||
orContinueWith: '或選擇以下操作',
|
orContinueWith: '或選擇以下操作',
|
||||||
addMCPServerHint: '連接一個 MCP 工具伺服器擴充',
|
addMCPServerHint: '連接一個 MCP 工具伺服器擴充',
|
||||||
installFromGithub: '從 GitHub 安裝插件或 Skill',
|
installFromGithub: '從 GitHub 安裝',
|
||||||
installFromGithubHint:
|
installFromGithubHint: '插件包或技能(SKILL.md)',
|
||||||
'支援 GitHub Release 插件包,也支援直接匯入 GitHub 上的 SKILL.md',
|
githubUrlHelp: '貼上 GitHub 地址',
|
||||||
githubUrlHelp:
|
githubUrlTooltip:
|
||||||
'貼上 GitHub 倉庫地址安裝插件;如果要安裝 Skill,請貼上 GitHub 上的 SKILL.md 檔案地址。',
|
'插件:貼上倉庫、Release 或 Tag 地址。技能:貼上技能目錄裡的 SKILL.md 頁面地址。',
|
||||||
githubUrlPlaceholder:
|
githubUrlPlaceholder: 'GitHub 倉庫、Release 或 SKILL.md 連結',
|
||||||
'例如 https://github.com/owner/repo 或 https://github.com/owner/repo/blob/main/path/SKILL.md',
|
|
||||||
githubUrlRequired: '請輸入 GitHub 地址',
|
githubUrlRequired: '請輸入 GitHub 地址',
|
||||||
skillMdUrlHelp:
|
previewSkill: '預覽技能',
|
||||||
'Skill 需要複製具體檔案連結,例如倉庫中某個技能目錄下的 SKILL.md 頁面地址。',
|
noSkillPreviewFound: '未找到可匯入的技能',
|
||||||
previewSkill: '預覽 Skill',
|
|
||||||
noSkillPreviewFound: '未找到可匯入的 Skill',
|
|
||||||
createSkill: '建立新的技能',
|
createSkill: '建立新的技能',
|
||||||
createSkillHint: '手動建立一個新的技能擴充',
|
createSkillHint: '手動建立一個新的技能擴充',
|
||||||
unsupportedFileType: '不支援的檔案類型,僅支援 .zip 和 .lbpkg 檔案',
|
unsupportedFileType: '不支援的檔案類型,僅支援 .zip 和 .lbpkg 檔案',
|
||||||
|
|||||||
Reference in New Issue
Block a user