fix(skills): distinguish external skill install paths and open real location (#463)
This commit is contained in:
@@ -109,7 +109,8 @@ AIタスクを自動的に実行するようスケジュール設定できます
|
|||||||
|
|
||||||
### 🧩 拡張可能なスキルシステム
|
### 🧩 拡張可能なスキルシステム
|
||||||
事前構築されたスキルでAIエージェントを拡張できます。統合スキルパネルからスキルの閲覧、インストール、管理が可能です。パッケージマネージャーは不要です。
|
事前構築されたスキルでAIエージェントを拡張できます。統合スキルパネルからスキルの閲覧、インストール、管理が可能です。パッケージマネージャーは不要です。
|
||||||
ClawX はドキュメント処理スキル(`pdf`、`xlsx`、`docx`、`pptx`)もフル内容で同梱し、起動時に `~/.openclaw/skills` へ自動配備し、初回インストール時に既定で有効化します。追加の同梱スキル(`find-skills`、`self-improving-agent`、`tavily-search`、`brave-web-search`、`bocha-skill`)も既定で有効化されますが、必要な API キーが未設定の場合は OpenClaw が実行時に設定エラーを表示します。
|
ClawX はドキュメント処理スキル(`pdf`、`xlsx`、`docx`、`pptx`)もフル内容で同梱し、起動時に管理スキルディレクトリ(既定 `~/.openclaw/skills`)へ自動配備し、初回インストール時に既定で有効化します。追加の同梱スキル(`find-skills`、`self-improving-agent`、`tavily-search`、`brave-web-search`、`bocha-skill`)も既定で有効化されますが、必要な API キーが未設定の場合は OpenClaw が実行時に設定エラーを表示します。
|
||||||
|
Skills ページでは OpenClaw の複数ソース(管理ディレクトリ、workspace、追加スキルディレクトリ)から検出されたスキルを表示でき、各スキルの実際のパスを確認して実フォルダを直接開けます。
|
||||||
|
|
||||||
主な検索スキルで必要な環境変数:
|
主な検索スキルで必要な環境変数:
|
||||||
- `BRAVE_SEARCH_API_KEY`: `brave-web-search` 用
|
- `BRAVE_SEARCH_API_KEY`: `brave-web-search` 用
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ Schedule AI tasks to run automatically. Define triggers, set intervals, and let
|
|||||||
|
|
||||||
### 🧩 Extensible Skill System
|
### 🧩 Extensible Skill System
|
||||||
Extend your AI agents with pre-built skills. Browse, install, and manage skills through the integrated skill panel—no package managers required.
|
Extend your AI agents with pre-built skills. Browse, install, and manage skills through the integrated skill panel—no package managers required.
|
||||||
ClawX also pre-bundles full document-processing skills (`pdf`, `xlsx`, `docx`, `pptx`), deploys them automatically to `~/.openclaw/skills` on startup, and enables them by default on first install. Additional bundled skills (`find-skills`, `self-improving-agent`, `tavily-search`, `brave-web-search`, `bocha-skill`) are also enabled by default; if required API keys are missing, OpenClaw will surface configuration errors in runtime.
|
ClawX also pre-bundles full document-processing skills (`pdf`, `xlsx`, `docx`, `pptx`), deploys them automatically to the managed skills directory (default `~/.openclaw/skills`) on startup, and enables them by default on first install. Additional bundled skills (`find-skills`, `self-improving-agent`, `tavily-search`, `brave-web-search`, `bocha-skill`) are also enabled by default; if required API keys are missing, OpenClaw will surface configuration errors in runtime.
|
||||||
|
The Skills page can display skills discovered from multiple OpenClaw sources (managed dir, workspace, and extra skill dirs), and now shows each skill's actual location so you can open the real folder directly.
|
||||||
|
|
||||||
Environment variables for bundled search skills:
|
Environment variables for bundled search skills:
|
||||||
- `BRAVE_SEARCH_API_KEY` for `brave-web-search`
|
- `BRAVE_SEARCH_API_KEY` for `brave-web-search`
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们
|
|||||||
|
|
||||||
### 🧩 可扩展技能系统
|
### 🧩 可扩展技能系统
|
||||||
通过预构建的技能扩展 AI 智能体的能力。在集成的技能面板中浏览、安装和管理技能——无需包管理器。
|
通过预构建的技能扩展 AI 智能体的能力。在集成的技能面板中浏览、安装和管理技能——无需包管理器。
|
||||||
ClawX 还会内置预装完整的文档处理技能(`pdf`、`xlsx`、`docx`、`pptx`),在启动时自动部署到 `~/.openclaw/skills`,并在首次安装时默认启用。额外预装技能(`find-skills`、`self-improving-agent`、`tavily-search`、`brave-web-search`、`bocha-skill`)也会默认启用;若缺少必需的 API Key,OpenClaw 会在运行时给出配置错误提示。
|
ClawX 还会内置预装完整的文档处理技能(`pdf`、`xlsx`、`docx`、`pptx`),在启动时自动部署到托管技能目录(默认 `~/.openclaw/skills`),并在首次安装时默认启用。额外预装技能(`find-skills`、`self-improving-agent`、`tavily-search`、`brave-web-search`、`bocha-skill`)也会默认启用;若缺少必需的 API Key,OpenClaw 会在运行时给出配置错误提示。
|
||||||
|
Skills 页面可展示来自多个 OpenClaw 来源的技能(托管目录、workspace、额外技能目录),并显示每个技能的实际路径,便于直接打开真实安装位置。
|
||||||
|
|
||||||
重点搜索技能所需环境变量:
|
重点搜索技能所需环境变量:
|
||||||
- `BRAVE_SEARCH_API_KEY`:用于 `brave-web-search`
|
- `BRAVE_SEARCH_API_KEY`:用于 `brave-web-search`
|
||||||
|
|||||||
@@ -77,8 +77,19 @@ export async function handleSkillRoutes(
|
|||||||
|
|
||||||
if (url.pathname === '/api/clawhub/open-readme' && req.method === 'POST') {
|
if (url.pathname === '/api/clawhub/open-readme' && req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
const body = await parseJsonBody<{ slug?: string; skillKey?: string }>(req);
|
const body = await parseJsonBody<{ slug?: string; skillKey?: string; baseDir?: string }>(req);
|
||||||
await ctx.clawHubService.openSkillReadme(body.skillKey || body.slug || '', body.slug);
|
await ctx.clawHubService.openSkillReadme(body.skillKey || body.slug || '', body.slug, body.baseDir);
|
||||||
|
sendJson(res, 200, { success: true });
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(res, 500, { success: false, error: String(error) });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/api/clawhub/open-path' && req.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const body = await parseJsonBody<{ slug?: string; skillKey?: string; baseDir?: string }>(req);
|
||||||
|
await ctx.clawHubService.openSkillPath(body.skillKey || body.slug || '', body.slug, body.baseDir);
|
||||||
sendJson(res, 200, { success: true });
|
sendJson(res, 200, { success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendJson(res, 500, { success: false, error: String(error) });
|
sendJson(res, 500, { success: false, error: String(error) });
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export interface ClawHubSkillResult {
|
|||||||
stars?: number;
|
stars?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClawHubInstalledSkillResult {
|
||||||
|
slug: string;
|
||||||
|
version: string;
|
||||||
|
source?: string;
|
||||||
|
baseDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ClawHubService {
|
export class ClawHubService {
|
||||||
private workDir: string;
|
private workDir: string;
|
||||||
private cliPath: string;
|
private cliPath: string;
|
||||||
@@ -339,7 +346,7 @@ export class ClawHubService {
|
|||||||
/**
|
/**
|
||||||
* List installed skills
|
* List installed skills
|
||||||
*/
|
*/
|
||||||
async listInstalled(): Promise<Array<{ slug: string; version: string }>> {
|
async listInstalled(): Promise<ClawHubInstalledSkillResult[]> {
|
||||||
try {
|
try {
|
||||||
const output = await this.runCommand(['list']);
|
const output = await this.runCommand(['list']);
|
||||||
if (!output || output.includes('No installed skills')) {
|
if (!output || output.includes('No installed skills')) {
|
||||||
@@ -351,31 +358,41 @@ export class ClawHubService {
|
|||||||
const cleanLine = this.stripAnsi(line);
|
const cleanLine = this.stripAnsi(line);
|
||||||
const match = cleanLine.match(/^(\S+)\s+v?(\d+\.\S+)/);
|
const match = cleanLine.match(/^(\S+)\s+v?(\d+\.\S+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
const slug = match[1];
|
||||||
return {
|
return {
|
||||||
slug: match[1],
|
slug,
|
||||||
version: match[2],
|
version: match[2],
|
||||||
|
source: 'openclaw-managed',
|
||||||
|
baseDir: path.join(this.workDir, 'skills', slug),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).filter((s): s is { slug: string; version: string } => s !== null);
|
}).filter((s): s is ClawHubInstalledSkillResult => s !== null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('ClawHub list error:', error);
|
console.error('ClawHub list error:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private resolveSkillDir(skillKeyOrSlug: string, fallbackSlug?: string, preferredBaseDir?: string): string | null {
|
||||||
* Open skill README/manual in default editor
|
|
||||||
*/
|
|
||||||
async openSkillReadme(skillKeyOrSlug: string, fallbackSlug?: string): Promise<boolean> {
|
|
||||||
const candidates = [skillKeyOrSlug, fallbackSlug]
|
const candidates = [skillKeyOrSlug, fallbackSlug]
|
||||||
.filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
|
.filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
|
||||||
.map(v => v.trim());
|
.map(v => v.trim());
|
||||||
const uniqueCandidates = [...new Set(candidates)];
|
const uniqueCandidates = [...new Set(candidates)];
|
||||||
|
if (preferredBaseDir && preferredBaseDir.trim() && fs.existsSync(preferredBaseDir.trim())) {
|
||||||
|
return preferredBaseDir.trim();
|
||||||
|
}
|
||||||
const directSkillDir = uniqueCandidates
|
const directSkillDir = uniqueCandidates
|
||||||
.map((id) => path.join(this.workDir, 'skills', id))
|
.map((id) => path.join(this.workDir, 'skills', id))
|
||||||
.find((dir) => fs.existsSync(dir));
|
.find((dir) => fs.existsSync(dir));
|
||||||
const skillDir = directSkillDir || this.resolveSkillDirByManifestName(uniqueCandidates);
|
return directSkillDir || this.resolveSkillDirByManifestName(uniqueCandidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open skill README/manual in default editor
|
||||||
|
*/
|
||||||
|
async openSkillReadme(skillKeyOrSlug: string, fallbackSlug?: string, preferredBaseDir?: string): Promise<boolean> {
|
||||||
|
const skillDir = this.resolveSkillDir(skillKeyOrSlug, fallbackSlug, preferredBaseDir);
|
||||||
|
|
||||||
// Try to find documentation file
|
// Try to find documentation file
|
||||||
const possibleFiles = ['SKILL.md', 'README.md', 'skill.md', 'readme.md'];
|
const possibleFiles = ['SKILL.md', 'README.md', 'skill.md', 'readme.md'];
|
||||||
@@ -409,4 +426,19 @@ export class ClawHubService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open skill path in file explorer
|
||||||
|
*/
|
||||||
|
async openSkillPath(skillKeyOrSlug: string, fallbackSlug?: string, preferredBaseDir?: string): Promise<boolean> {
|
||||||
|
const skillDir = this.resolveSkillDir(skillKeyOrSlug, fallbackSlug, preferredBaseDir);
|
||||||
|
if (!skillDir) {
|
||||||
|
throw new Error('Skill directory not found');
|
||||||
|
}
|
||||||
|
const openResult = await shell.openPath(skillDir);
|
||||||
|
if (openResult) {
|
||||||
|
throw new Error(openResult);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,25 @@
|
|||||||
"saveConfig": "Save Configuration",
|
"saveConfig": "Save Configuration",
|
||||||
"configSaved": "Configuration saved",
|
"configSaved": "Configuration saved",
|
||||||
"openManual": "Open Manual",
|
"openManual": "Open Manual",
|
||||||
|
"openActualFolder": "Open Actual Folder",
|
||||||
|
"copyPath": "Copy path",
|
||||||
|
"pathUnavailable": "Path not available",
|
||||||
"configurable": "Configurable",
|
"configurable": "Configurable",
|
||||||
"uninstall": "Uninstall",
|
"uninstall": "Uninstall",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable"
|
"disable": "Disable"
|
||||||
},
|
},
|
||||||
|
"source": {
|
||||||
|
"badge": {
|
||||||
|
"bundled": "Bundled",
|
||||||
|
"managed": "Managed",
|
||||||
|
"workspace": "Workspace",
|
||||||
|
"extra": "Extra dirs",
|
||||||
|
"agentsPersonal": "Personal .agents",
|
||||||
|
"agentsProject": "Project .agents",
|
||||||
|
"unknown": "Unknown source"
|
||||||
|
}
|
||||||
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"enabled": "Skill enabled",
|
"enabled": "Skill enabled",
|
||||||
"disabled": "Skill disabled",
|
"disabled": "Skill disabled",
|
||||||
@@ -66,6 +80,9 @@
|
|||||||
"failedInstall": "Failed to install",
|
"failedInstall": "Failed to install",
|
||||||
"failedUninstall": "Failed to uninstall",
|
"failedUninstall": "Failed to uninstall",
|
||||||
"failedFolderNotFound": "Skills folder does not exist yet. Install a skill first.",
|
"failedFolderNotFound": "Skills folder does not exist yet. Install a skill first.",
|
||||||
|
"copiedPath": "Path copied",
|
||||||
|
"failedCopyPath": "Failed to copy path",
|
||||||
|
"failedOpenActualFolder": "Failed to open actual skill folder",
|
||||||
"searchTimeoutError": "Search timed out, check network. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{{path}}\"",
|
"searchTimeoutError": "Search timed out, check network. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{{path}}\"",
|
||||||
"installTimeoutError": "Installation timed out, check network. You can also download the ZIP from ClawHub.ai and extract it to \"{{path}}\"",
|
"installTimeoutError": "Installation timed out, check network. You can also download the ZIP from ClawHub.ai and extract it to \"{{path}}\"",
|
||||||
"searchRateLimitError": "Search rate limit exceeded. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{{path}}\"",
|
"searchRateLimitError": "Search rate limit exceeded. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{{path}}\"",
|
||||||
|
|||||||
@@ -49,11 +49,25 @@
|
|||||||
"saveConfig": "設定を保存",
|
"saveConfig": "設定を保存",
|
||||||
"configSaved": "設定を保存しました",
|
"configSaved": "設定を保存しました",
|
||||||
"openManual": "マニュアルを開く",
|
"openManual": "マニュアルを開く",
|
||||||
|
"openActualFolder": "実際のフォルダを開く",
|
||||||
|
"copyPath": "パスをコピー",
|
||||||
|
"pathUnavailable": "パスを取得できません",
|
||||||
"configurable": "設定可能",
|
"configurable": "設定可能",
|
||||||
"uninstall": "アンインストール",
|
"uninstall": "アンインストール",
|
||||||
"enable": "有効化",
|
"enable": "有効化",
|
||||||
"disable": "無効化"
|
"disable": "無効化"
|
||||||
},
|
},
|
||||||
|
"source": {
|
||||||
|
"badge": {
|
||||||
|
"bundled": "内蔵",
|
||||||
|
"managed": "管理ディレクトリ",
|
||||||
|
"workspace": "ワークスペース",
|
||||||
|
"extra": "追加ディレクトリ",
|
||||||
|
"agentsPersonal": "個人 .agents",
|
||||||
|
"agentsProject": "プロジェクト .agents",
|
||||||
|
"unknown": "不明なソース"
|
||||||
|
}
|
||||||
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"enabled": "スキルを有効にしました",
|
"enabled": "スキルを有効にしました",
|
||||||
"disabled": "スキルを無効にしました",
|
"disabled": "スキルを無効にしました",
|
||||||
@@ -66,6 +80,9 @@
|
|||||||
"failedInstall": "インストールに失敗しました",
|
"failedInstall": "インストールに失敗しました",
|
||||||
"failedUninstall": "アンインストールに失敗しました",
|
"failedUninstall": "アンインストールに失敗しました",
|
||||||
"failedFolderNotFound": "スキルフォルダがまだ存在しません。先にスキルをインストールしてください。",
|
"failedFolderNotFound": "スキルフォルダがまだ存在しません。先にスキルをインストールしてください。",
|
||||||
|
"copiedPath": "パスをコピーしました",
|
||||||
|
"failedCopyPath": "パスのコピーに失敗しました",
|
||||||
|
"failedOpenActualFolder": "スキルの実際のフォルダを開けませんでした",
|
||||||
"searchTimeoutError": "検索がタイムアウトしました。ClawHub.aiで検索してZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
|
"searchTimeoutError": "検索がタイムアウトしました。ClawHub.aiで検索してZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
|
||||||
"installTimeoutError": "インストールがタイムアウトしました。ClawHub.aiでZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
|
"installTimeoutError": "インストールがタイムアウトしました。ClawHub.aiでZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
|
||||||
"searchRateLimitError": "検索リクエストの制限を超過しました。ClawHub.aiで検索してZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
|
"searchRateLimitError": "検索リクエストの制限を超過しました。ClawHub.aiで検索してZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
|
||||||
|
|||||||
@@ -49,11 +49,25 @@
|
|||||||
"saveConfig": "保存配置",
|
"saveConfig": "保存配置",
|
||||||
"configSaved": "配置已保存",
|
"configSaved": "配置已保存",
|
||||||
"openManual": "打开手册",
|
"openManual": "打开手册",
|
||||||
|
"openActualFolder": "打开实际目录",
|
||||||
|
"copyPath": "复制路径",
|
||||||
|
"pathUnavailable": "路径不可用",
|
||||||
"configurable": "可配置",
|
"configurable": "可配置",
|
||||||
"uninstall": "卸载",
|
"uninstall": "卸载",
|
||||||
"enable": "启用",
|
"enable": "启用",
|
||||||
"disable": "禁用"
|
"disable": "禁用"
|
||||||
},
|
},
|
||||||
|
"source": {
|
||||||
|
"badge": {
|
||||||
|
"bundled": "内置",
|
||||||
|
"managed": "托管目录",
|
||||||
|
"workspace": "工作区",
|
||||||
|
"extra": "额外目录",
|
||||||
|
"agentsPersonal": "个人 .agents",
|
||||||
|
"agentsProject": "项目 .agents",
|
||||||
|
"unknown": "未知来源"
|
||||||
|
}
|
||||||
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"enabled": "技能已启用",
|
"enabled": "技能已启用",
|
||||||
"disabled": "技能已禁用",
|
"disabled": "技能已禁用",
|
||||||
@@ -66,6 +80,9 @@
|
|||||||
"failedInstall": "安装失败",
|
"failedInstall": "安装失败",
|
||||||
"failedUninstall": "卸载失败",
|
"failedUninstall": "卸载失败",
|
||||||
"failedFolderNotFound": "技能文件夹尚不存在,请先安装一个技能。",
|
"failedFolderNotFound": "技能文件夹尚不存在,请先安装一个技能。",
|
||||||
|
"copiedPath": "路径已复制",
|
||||||
|
"failedCopyPath": "复制路径失败",
|
||||||
|
"failedOpenActualFolder": "打开技能实际目录失败",
|
||||||
"searchTimeoutError": "搜索超时,请检查网络。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{{path}}\"",
|
"searchTimeoutError": "搜索超时,请检查网络。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{{path}}\"",
|
||||||
"installTimeoutError": "安装超时,请检查网络。您也可在 ClawHub.ai 下载该技能压缩包,解压到 \"{{path}}\"",
|
"installTimeoutError": "安装超时,请检查网络。您也可在 ClawHub.ai 下载该技能压缩包,解压到 \"{{path}}\"",
|
||||||
"searchRateLimitError": "搜索请求过于频繁。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{{path}}\"",
|
"searchRateLimitError": "搜索请求过于频繁。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{{path}}\"",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
FolderOpen,
|
FolderOpen,
|
||||||
FileCode,
|
FileCode,
|
||||||
Globe,
|
Globe,
|
||||||
|
Copy,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -33,6 +34,7 @@ import { trackUiEvent } from '@/lib/telemetry';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { Skill } from '@/types/skill';
|
import type { Skill } from '@/types/skill';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { TFunction } from 'i18next';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -44,9 +46,25 @@ interface SkillDetailDialogProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onToggle: (enabled: boolean) => void;
|
onToggle: (enabled: boolean) => void;
|
||||||
onUninstall?: (slug: string) => void;
|
onUninstall?: (slug: string) => void;
|
||||||
|
onOpenFolder?: (skill: Skill) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: SkillDetailDialogProps) {
|
function resolveSkillSourceLabel(skill: Skill, t: TFunction<'skills'>): string {
|
||||||
|
const source = (skill.source || '').trim().toLowerCase();
|
||||||
|
if (!source) {
|
||||||
|
if (skill.isBundled) return t('source.badge.bundled', { defaultValue: 'Bundled' });
|
||||||
|
return t('source.badge.unknown', { defaultValue: 'Unknown source' });
|
||||||
|
}
|
||||||
|
if (source === 'openclaw-bundled') return t('source.badge.bundled', { defaultValue: 'Bundled' });
|
||||||
|
if (source === 'openclaw-managed') return t('source.badge.managed', { defaultValue: 'Managed' });
|
||||||
|
if (source === 'openclaw-workspace') return t('source.badge.workspace', { defaultValue: 'Workspace' });
|
||||||
|
if (source === 'openclaw-extra') return t('source.badge.extra', { defaultValue: 'Extra dirs' });
|
||||||
|
if (source === 'agents-skills-personal') return t('source.badge.agentsPersonal', { defaultValue: 'Personal .agents' });
|
||||||
|
if (source === 'agents-skills-project') return t('source.badge.agentsProject', { defaultValue: 'Project .agents' });
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall, onOpenFolder }: SkillDetailDialogProps) {
|
||||||
const { t } = useTranslation('skills');
|
const { t } = useTranslation('skills');
|
||||||
const { fetchSkills } = useSkillsStore();
|
const { fetchSkills } = useSkillsStore();
|
||||||
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
||||||
@@ -86,7 +104,7 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
|
|||||||
try {
|
try {
|
||||||
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-readme', {
|
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-readme', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ skillKey: skill.id, slug: skill.slug }),
|
body: JSON.stringify({ skillKey: skill.id, slug: skill.slug, baseDir: skill.baseDir }),
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(t('toast.openedEditor'));
|
toast.success(t('toast.openedEditor'));
|
||||||
@@ -98,6 +116,16 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopyPath = async () => {
|
||||||
|
if (!skill?.baseDir) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(skill.baseDir);
|
||||||
|
toast.success(t('toast.copiedPath'));
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(t('toast.failedCopyPath') + ': ' + String(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddEnv = () => {
|
const handleAddEnv = () => {
|
||||||
setEnvVars([...envVars, { key: '', value: '' }]);
|
setEnvVars([...envVars, { key: '', value: '' }]);
|
||||||
};
|
};
|
||||||
@@ -192,6 +220,42 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-7 px-1">
|
<div className="space-y-7 px-1">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-[13px] font-bold text-foreground/80">{t('detail.source')}</h3>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Badge variant="secondary" className="font-mono text-[11px] font-medium px-3 py-0.5 rounded-full bg-black/[0.04] dark:bg-white/[0.08] border-0 shadow-none text-foreground/70">
|
||||||
|
{resolveSkillSourceLabel(skill, t)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={skill.baseDir || t('detail.pathUnavailable')}
|
||||||
|
readOnly
|
||||||
|
className="h-[38px] font-mono text-[12px] bg-[#eeece3] dark:bg-muted border-black/10 dark:border-white/10 rounded-xl text-foreground/70"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-[38px] w-[38px] border-black/10 dark:border-white/10"
|
||||||
|
disabled={!skill.baseDir}
|
||||||
|
onClick={handleCopyPath}
|
||||||
|
title={t('detail.copyPath')}
|
||||||
|
>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-[38px] w-[38px] border-black/10 dark:border-white/10"
|
||||||
|
disabled={!skill.baseDir}
|
||||||
|
onClick={() => onOpenFolder?.(skill)}
|
||||||
|
title={t('detail.openActualFolder')}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* API Key Section */}
|
{/* API Key Section */}
|
||||||
{!skill.isCore && (
|
{!skill.isCore && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -471,6 +535,24 @@ export function Skills() {
|
|||||||
}
|
}
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
const handleOpenSkillFolder = useCallback(async (skill: Skill) => {
|
||||||
|
try {
|
||||||
|
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-path', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
skillKey: skill.id,
|
||||||
|
slug: skill.slug,
|
||||||
|
baseDir: skill.baseDir,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to open folder');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(t('toast.failedOpenActualFolder') + ': ' + String(err));
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
const [skillsDirPath, setSkillsDirPath] = useState('~/.openclaw/skills');
|
const [skillsDirPath, setSkillsDirPath] = useState('~/.openclaw/skills');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -698,6 +780,14 @@ export function Skills() {
|
|||||||
<p className="text-[13.5px] text-muted-foreground line-clamp-1 pr-6 leading-relaxed">
|
<p className="text-[13.5px] text-muted-foreground line-clamp-1 pr-6 leading-relaxed">
|
||||||
{skill.description}
|
{skill.description}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-1 flex items-center gap-2 text-[11px] text-foreground/55">
|
||||||
|
<Badge variant="secondary" className="px-1.5 py-0 h-5 text-[10px] font-medium bg-black/5 dark:bg-white/10 border-0 shadow-none">
|
||||||
|
{resolveSkillSourceLabel(skill, t)}
|
||||||
|
</Badge>
|
||||||
|
<span className="truncate font-mono">
|
||||||
|
{skill.baseDir || t('detail.pathUnavailable')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6 shrink-0" onClick={e => e.stopPropagation()}>
|
<div className="flex items-center gap-6 shrink-0" onClick={e => e.stopPropagation()}>
|
||||||
@@ -858,6 +948,7 @@ export function Skills() {
|
|||||||
setSelectedSkill({ ...selectedSkill, enabled });
|
setSelectedSkill({ ...selectedSkill, enabled });
|
||||||
}}
|
}}
|
||||||
onUninstall={handleUninstall}
|
onUninstall={handleUninstall}
|
||||||
|
onOpenFolder={handleOpenSkillFolder}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ type GatewaySkillStatus = {
|
|||||||
config?: Record<string, unknown>;
|
config?: Record<string, unknown>;
|
||||||
bundled?: boolean;
|
bundled?: boolean;
|
||||||
always?: boolean;
|
always?: boolean;
|
||||||
|
source?: string;
|
||||||
|
baseDir?: string;
|
||||||
|
filePath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GatewaySkillsStatusResult = {
|
type GatewaySkillsStatusResult = {
|
||||||
@@ -29,6 +32,8 @@ type GatewaySkillsStatusResult = {
|
|||||||
type ClawHubListResult = {
|
type ClawHubListResult = {
|
||||||
slug: string;
|
slug: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
source?: string;
|
||||||
|
baseDir?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapErrorCodeToSkillErrorKey(
|
function mapErrorCodeToSkillErrorKey(
|
||||||
@@ -120,6 +125,9 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
isCore: s.bundled && s.always,
|
isCore: s.bundled && s.always,
|
||||||
isBundled: s.bundled,
|
isBundled: s.bundled,
|
||||||
|
source: s.source,
|
||||||
|
baseDir: s.baseDir,
|
||||||
|
filePath: s.filePath,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (currentSkills.length > 0) {
|
} else if (currentSkills.length > 0) {
|
||||||
@@ -131,7 +139,15 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
|||||||
if (clawhubResult.success && clawhubResult.results) {
|
if (clawhubResult.success && clawhubResult.results) {
|
||||||
clawhubResult.results.forEach((cs: ClawHubListResult) => {
|
clawhubResult.results.forEach((cs: ClawHubListResult) => {
|
||||||
const existing = combinedSkills.find(s => s.id === cs.slug);
|
const existing = combinedSkills.find(s => s.id === cs.slug);
|
||||||
if (!existing) {
|
if (existing) {
|
||||||
|
if (!existing.baseDir && cs.baseDir) {
|
||||||
|
existing.baseDir = cs.baseDir;
|
||||||
|
}
|
||||||
|
if (!existing.source && cs.source) {
|
||||||
|
existing.source = cs.source;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const directConfig = configResult[cs.slug] || {};
|
const directConfig = configResult[cs.slug] || {};
|
||||||
combinedSkills.push({
|
combinedSkills.push({
|
||||||
id: cs.slug,
|
id: cs.slug,
|
||||||
@@ -145,8 +161,9 @@ export const useSkillsStore = create<SkillsState>((set, get) => ({
|
|||||||
config: directConfig,
|
config: directConfig,
|
||||||
isCore: false,
|
isCore: false,
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
|
source: cs.source || 'openclaw-managed',
|
||||||
|
baseDir: cs.baseDir,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export interface Skill {
|
|||||||
isCore?: boolean;
|
isCore?: boolean;
|
||||||
isBundled?: boolean;
|
isBundled?: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
|
source?: string;
|
||||||
|
baseDir?: string;
|
||||||
|
filePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user