release: v0.69.0 - upgrade aster-rust to v0.13.0, skill content dialog

This commit is contained in:
coso
2026-02-18 18:35:35 +08:00
parent bd6d487d17
commit 97e8ac6d4a
14 changed files with 507 additions and 38 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.67.0",
"version": "0.69.0",
"type": "module",
"repository": {
"type": "git",
+24 -23
View File
@@ -211,12 +211,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "aster"
version = "0.11.0"
source = "git+https://github.com/astercloud/aster-rust?tag=v0.11.0#c4a9c40b48bcb0c77375b10ccb1313366fb4e7ba"
name = "aster-core"
version = "0.13.0"
source = "git+https://github.com/astercloud/aster-rust?tag=v0.13.0#4422f761c3701d2e3af1b0e6db5d4394f80d9c54"
dependencies = [
"ahash",
"anyhow",
"aster-models",
"async-stream",
"async-trait",
"aws-config",
@@ -304,8 +305,8 @@ dependencies = [
[[package]]
name = "aster-models"
version = "0.12.0"
source = "git+https://github.com/astercloud/aster-rust?tag=v0.12.0#f6c1ad342eb58accb61524985fa78ea7f4b8be2b"
version = "0.13.0"
source = "git+https://github.com/astercloud/aster-rust?tag=v0.13.0#4422f761c3701d2e3af1b0e6db5d4394f80d9c54"
dependencies = [
"serde",
"serde_json",
@@ -6684,11 +6685,11 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"anyhow",
"arboard",
"aster",
"aster-core",
"async-stream",
"async-trait",
"axum 0.7.9",
@@ -6782,9 +6783,9 @@ dependencies = [
[[package]]
name = "proxycast-agent"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"aster",
"aster-core",
"async-trait",
"chrono",
"dirs 5.0.1",
@@ -6805,7 +6806,7 @@ dependencies = [
[[package]]
name = "proxycast-config"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"async-trait",
"parking_lot",
@@ -6821,7 +6822,7 @@ dependencies = [
[[package]]
name = "proxycast-core"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"aster-models",
"async-trait",
@@ -6861,7 +6862,7 @@ dependencies = [
[[package]]
name = "proxycast-credential"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -6896,7 +6897,7 @@ dependencies = [
[[package]]
name = "proxycast-infra"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"chrono",
"dashmap 5.5.3",
@@ -6916,7 +6917,7 @@ dependencies = [
[[package]]
name = "proxycast-mcp"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"async-trait",
"glob",
@@ -6947,7 +6948,7 @@ dependencies = [
[[package]]
name = "proxycast-processor"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"async-trait",
"parking_lot",
@@ -6966,7 +6967,7 @@ dependencies = [
[[package]]
name = "proxycast-providers"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"anyhow",
"async-stream",
@@ -7018,7 +7019,7 @@ dependencies = [
[[package]]
name = "proxycast-server"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"async-stream",
"axum 0.7.9",
@@ -7060,7 +7061,7 @@ dependencies = [
[[package]]
name = "proxycast-server-utils"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"axum 0.7.9",
"futures",
@@ -7075,10 +7076,10 @@ dependencies = [
[[package]]
name = "proxycast-services"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"anyhow",
"aster",
"aster-core",
"async-trait",
"base64 0.22.1",
"chrono",
@@ -7116,7 +7117,7 @@ dependencies = [
[[package]]
name = "proxycast-skills"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -7132,7 +7133,7 @@ dependencies = [
[[package]]
name = "proxycast-terminal"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -7159,7 +7160,7 @@ dependencies = [
[[package]]
name = "proxycast-websocket"
version = "0.68.0"
version = "0.69.0"
dependencies = [
"axum 0.7.9",
"chrono",
+6 -6
View File
@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.68.0"
version = "0.69.0"
edition = "2021"
authors = ["you"]
repository = "https://github.com/aiclientproxy/proxycast"
@@ -118,11 +118,11 @@ enigo = "0.3"
# Aster Agent Framework
# 开发时使用本地 aster-rust,CI/CD 使用远程 GitHub 仓库
# 本地开发: path = "../../../astercloud/aster-rust/crates/aster" (相对 src-tauri/)
# CI/CD: git = "https://github.com/astercloud/aster-rust", tag = "v0.11.0"
# aster = { path = "../../../astercloud/aster-rust/crates/aster" }
aster = { git = "https://github.com/astercloud/aster-rust", tag = "v0.11.0" }
# CI/CD: git = "https://github.com/astercloud/aster-rust", tag = "v0.13.0"
# aster = { package = "aster-core", path = "../../../astercloud/aster-rust/crates/aster" }
aster = { package = "aster-core", git = "https://github.com/astercloud/aster-rust", tag = "v0.13.0" }
# 本地开发: aster-models = { path = "../../../astercloud/aster-rust/crates/aster-models" }
aster-models = { git = "https://github.com/astercloud/aster-rust", tag = "v0.12.0" }
aster-models = { git = "https://github.com/astercloud/aster-rust", tag = "v0.13.0" }
# MCP (Model Context Protocol)
rmcp = { version = "0.12.0", features = ["client", "transport-io", "transport-child-process"] }
@@ -185,7 +185,7 @@ version = "2.4"
[package]
name = "proxycast"
version = "0.68.0"
version = "0.69.0"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
+1
View File
@@ -756,6 +756,7 @@ pub fn run() {
commands::skill_cmd::add_skill_repo,
commands::skill_cmd::remove_skill_repo,
commands::skill_cmd::get_installed_proxycast_skills,
commands::skill_cmd::get_local_skill_content,
// Skill Execution commands
commands::skill_exec_cmd::execute_skill,
commands::skill_exec_cmd::list_executable_skills,
+142 -1
View File
@@ -5,7 +5,7 @@ use crate::models::app_type::AppType;
use crate::models::skill_model::{Skill, SkillRepo, SkillState};
use chrono::Utc;
use proxycast_services::skill_service::SkillService;
use std::path::Path;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use tauri::State;
@@ -42,6 +42,75 @@ pub fn scan_installed_skills(skills_dir: &Path) -> Vec<String> {
skills
}
fn get_skills_dir(app_type: &AppType) -> Result<PathBuf, String> {
let home = dirs::home_dir().ok_or_else(|| "Failed to get home directory".to_string())?;
let skills_dir = match app_type {
AppType::Claude => home.join(".claude").join("skills"),
AppType::Codex => home.join(".codex").join("skills"),
AppType::Gemini => home.join(".gemini").join("skills"),
AppType::ProxyCast => home.join(".proxycast").join("skills"),
};
Ok(skills_dir)
}
fn validate_skill_directory(directory: &str) -> Result<(), String> {
if directory.trim().is_empty() {
return Err("Skill directory is required".to_string());
}
if directory.contains("..") || directory.contains('/') || directory.contains('\\') {
return Err("Invalid skill directory".to_string());
}
let mut components = Path::new(directory).components();
let first = components
.next()
.ok_or_else(|| "Skill directory is required".to_string())?;
if components.next().is_some() {
return Err("Invalid skill directory".to_string());
}
match first {
Component::Normal(_) => Ok(()),
_ => Err("Invalid skill directory".to_string()),
}
}
fn read_local_skill_content(skills_dir: &Path, directory: &str) -> Result<String, String> {
validate_skill_directory(directory)?;
if !skills_dir.exists() {
return Err("Skills directory not found".to_string());
}
let canonical_skills_dir = skills_dir
.canonicalize()
.map_err(|e| format!("Failed to resolve skills directory: {e}"))?;
let skill_dir = skills_dir.join(directory);
if !skill_dir.exists() {
return Err(format!("Skill not found: {directory}"));
}
let canonical_skill_dir = skill_dir
.canonicalize()
.map_err(|e| format!("Failed to resolve skill directory: {e}"))?;
if !canonical_skill_dir.starts_with(&canonical_skills_dir) {
return Err("Invalid skill directory path".to_string());
}
let skill_md_path = canonical_skill_dir.join("SKILL.md");
if !skill_md_path.is_file() {
return Err(format!("SKILL.md not found for skill: {directory}"));
}
std::fs::read_to_string(&skill_md_path).map_err(|e| format!("Failed to read SKILL.md: {e}"))
}
/// 获取已安装的 ProxyCast Skills 目录列表
///
/// 扫描 ~/.proxycast/skills/ 目录,返回包含 SKILL.md 的子目录名列表。
@@ -57,6 +126,24 @@ pub async fn get_installed_proxycast_skills() -> Result<Vec<String>, String> {
Ok(scan_installed_skills(&skills_dir))
}
/// 获取本地已安装 Skill 的 SKILL.md 内容
///
/// 仅支持读取本地 Skills 目录下的文件,包含目录合法性与路径穿越防护。
///
/// # Arguments
/// - `app`: 应用类型(proxycast/claude/codex/gemini)
/// - `directory`: Skill 目录名
///
/// # Returns
/// - `Ok(String)`: SKILL.md 的文本内容
/// - `Err(String)`: 错误信息
#[tauri::command]
pub fn get_local_skill_content(app: String, directory: String) -> Result<String, String> {
let app_type: AppType = app.parse().map_err(|e: String| e)?;
let skills_dir = get_skills_dir(&app_type)?;
read_local_skill_content(&skills_dir, &directory)
}
pub struct SkillServiceState(pub Arc<SkillService>);
fn get_skill_key(app_type: &AppType, directory: &str) -> String {
@@ -374,4 +461,58 @@ mod tests {
assert_eq!(discovered.len(), 1);
assert!(discovered.contains(&"valid-skill".to_string()));
}
#[test]
fn test_read_local_skill_content_success() {
let temp_dir = TempDir::new().unwrap();
let skills_dir = temp_dir.path().join("skills");
let skill_dir = skills_dir.join("demo-skill");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(skill_dir.join("SKILL.md"), "# Demo Skill\ncontent").unwrap();
let content = read_local_skill_content(&skills_dir, "demo-skill").unwrap();
assert!(content.contains("# Demo Skill"));
assert!(content.contains("content"));
}
#[test]
fn test_read_local_skill_content_rejects_traversal() {
let temp_dir = TempDir::new().unwrap();
let skills_dir = temp_dir.path().join("skills");
std::fs::create_dir_all(&skills_dir).unwrap();
let err = read_local_skill_content(&skills_dir, "../outside").unwrap_err();
assert!(err.contains("Invalid skill directory"));
}
#[test]
fn test_read_local_skill_content_missing_skill_md() {
let temp_dir = TempDir::new().unwrap();
let skills_dir = temp_dir.path().join("skills");
let skill_dir = skills_dir.join("no-skill-md");
std::fs::create_dir_all(&skill_dir).unwrap();
let err = read_local_skill_content(&skills_dir, "no-skill-md").unwrap_err();
assert!(err.contains("SKILL.md not found"));
}
#[cfg(unix)]
#[test]
fn test_read_local_skill_content_rejects_symlink_escape() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let skills_dir = temp_dir.path().join("skills");
std::fs::create_dir_all(&skills_dir).unwrap();
let outside_dir = temp_dir.path().join("outside-skill");
std::fs::create_dir_all(&outside_dir).unwrap();
std::fs::write(outside_dir.join("SKILL.md"), "# Outside").unwrap();
let symlink_dir = skills_dir.join("escape-skill");
symlink(&outside_dir, &symlink_dir).unwrap();
let err = read_local_skill_content(&skills_dir, "escape-skill").unwrap_err();
assert!(err.contains("Invalid skill directory path"));
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyCast",
"version": "0.67.0",
"version": "0.69.0",
"identifier": "com.proxycast.app",
"build": {
"beforeDevCommand": "npm run dev",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyCast",
"version": "0.67.0",
"version": "0.69.0",
"identifier": "com.proxycast.app",
"build": {
"beforeDevCommand": "npm run dev",
+35 -2
View File
@@ -7,10 +7,14 @@
* **Validates: Requirements 5.1, 5.2**
*/
import { describe, expect } from "vitest";
import { describe, expect, it } from "vitest";
import { test } from "@fast-check/vitest";
import * as fc from "fast-check";
import { getSkillSource, type SkillSource } from "./SkillCard";
import {
canViewLocalSkillContent,
getSkillSource,
type SkillSource,
} from "./SkillCard";
import type { Skill } from "@/lib/api/skills";
/**
@@ -130,3 +134,32 @@ describe("getSkillSource", () => {
);
});
});
describe("canViewLocalSkillContent", () => {
it("本地且已安装 skill 应可查看内容", () => {
const skill = createSkill({
installed: true,
repoOwner: undefined,
repoName: undefined,
});
expect(canViewLocalSkillContent(skill)).toBe(true);
});
it("本地但未安装 skill 不可查看内容", () => {
const skill = createSkill({
installed: false,
repoOwner: undefined,
repoName: undefined,
});
expect(canViewLocalSkillContent(skill)).toBe(false);
});
it("非本地已安装 skill 不可查看内容", () => {
const skill = createSkill({
installed: true,
repoOwner: "proxycast",
repoName: "skills",
});
expect(canViewLocalSkillContent(skill)).toBe(false);
});
});
+45 -1
View File
@@ -6,13 +6,21 @@
* - 显示 Skill 基本信息(名称、描述、来源)
* - 安装/卸载操作按钮
* - 执行按钮(仅已安装的 Skill 显示)
* - 查看内容按钮(仅本地且已安装的 Skill 显示)
* - GitHub 链接按钮
*
* @module components/skills
* @requirements 6.1, 6.3
*/
import { Download, Trash2, ExternalLink, Loader2, Play } from "lucide-react";
import {
Download,
Trash2,
ExternalLink,
Loader2,
Play,
FileText,
} from "lucide-react";
import type { Skill } from "@/lib/api/skills";
/**
@@ -45,6 +53,19 @@ export function getSkillSource(skill: Skill): SkillSource {
return "community";
}
/**
* 是否可查看本地 Skill 内容
*
* 仅本地且已安装的 Skill 支持查看 SKILL.md。
*
* @param skill - Skill 对象
* @returns 是否显示查看内容入口
*/
// eslint-disable-next-line react-refresh/only-export-components
export function canViewLocalSkillContent(skill: Skill): boolean {
return skill.installed && getSkillSource(skill) === "local";
}
/**
* 来源标签配置
*/
@@ -90,6 +111,7 @@ interface SkillCardProps {
onInstall: (directory: string) => void;
onUninstall: (directory: string) => void;
onExecute?: (skill: Skill) => void;
onViewContent?: (skill: Skill) => void;
installing: boolean;
}
@@ -99,6 +121,7 @@ interface SkillCardProps {
* 展示单个 Skill 的信息和操作按钮,包括:
* - 安装/卸载按钮
* - 执行按钮(仅已安装的 Skill 显示)
* - 查看内容按钮(仅本地且已安装的 Skill 显示)
* - GitHub 链接按钮
*
* @param props - 组件属性
@@ -111,6 +134,7 @@ export function SkillCard({
onInstall,
onUninstall,
onExecute,
onViewContent,
installing,
}: SkillCardProps) {
const handleAction = () => {
@@ -138,7 +162,14 @@ export function SkillCard({
}
};
const handleViewContent = () => {
if (onViewContent && canViewLocalSkillContent(skill)) {
onViewContent(skill);
}
};
const source = getSkillSource(skill);
const showViewContent = Boolean(onViewContent && canViewLocalSkillContent(skill));
return (
<div className="rounded-lg border bg-card p-4 hover:shadow-md transition-shadow">
@@ -210,6 +241,19 @@ export function SkillCard({
</button>
)}
{/* 查看内容按钮 - 仅本地且已安装的 Skill 显示 */}
{showViewContent && (
<button
onClick={handleViewContent}
disabled={installing}
className="flex items-center justify-center gap-2 rounded-lg border border-amber-500 px-3 py-2 text-sm font-medium text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-950/30 disabled:opacity-50 disabled:cursor-not-allowed"
title="查看 SKILL.md"
>
<FileText className="h-4 w-4" />
查看内容
</button>
)}
{skill.readmeUrl && (
<button
onClick={openGithub}
@@ -0,0 +1,76 @@
import { act, type ComponentProps } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { SkillContentDialog } from "./SkillContentDialog";
interface RenderResult {
container: HTMLDivElement;
root: Root;
}
const mountedRoots: RenderResult[] = [];
function renderDialog(
overrides: Partial<ComponentProps<typeof SkillContentDialog>> = {},
): RenderResult {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(
<SkillContentDialog
skillName="test-skill"
open={true}
onOpenChange={() => {}}
content=""
loading={false}
error={null}
{...overrides}
/>,
);
});
const rendered = { container, root };
mountedRoots.push(rendered);
return rendered;
}
beforeEach(() => {
(
globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (mountedRoots.length > 0) {
const mounted = mountedRoots.pop();
if (!mounted) {
break;
}
act(() => {
mounted.root.unmount();
});
mounted.container.remove();
}
});
describe("SkillContentDialog", () => {
it("加载中时应显示加载提示", () => {
renderDialog({ loading: true });
expect(document.body.textContent).toContain("正在读取 SKILL.md...");
});
it("出错时应显示错误信息", () => {
renderDialog({ error: "读取失败: 文件不存在" });
expect(document.body.textContent).toContain("读取失败: 文件不存在");
});
it("有内容时应渲染 markdown 文本", () => {
renderDialog({ content: "# 标题\n正文内容" });
expect(document.body.textContent).toContain("标题");
expect(document.body.textContent).toContain("正文内容");
});
});
@@ -0,0 +1,89 @@
/**
* @file SkillContentDialog.tsx
* @description Skill 内容查看弹窗,展示本地 SKILL.md 的 Markdown 预览
*
* 功能:
* - 展示 Skill 名称和说明
* - 加载中状态
* - 错误状态
* - Markdown 内容预览
*
* @module components/skills
*/
import { Loader2, AlertCircle, FileText } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { MarkdownPreview } from "@/components/preview/MarkdownPreview";
import "@/components/preview/preview.css";
export interface SkillContentDialogProps {
/** Skill 名称 */
skillName: string;
/** 是否打开 */
open: boolean;
/** 打开状态变化 */
onOpenChange: (open: boolean) => void;
/** SKILL.md 内容 */
content: string;
/** 是否正在加载 */
loading: boolean;
/** 错误信息 */
error: string | null;
}
export function SkillContentDialog({
skillName,
open,
onOpenChange,
content,
loading,
error,
}: SkillContentDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent maxWidth="max-w-5xl" className="h-[80vh] p-0 overflow-hidden">
<DialogHeader className="border-b px-6 py-4">
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5 text-muted-foreground" />
{skillName}
</DialogTitle>
<DialogDescription>查看本地 SKILL.md 内容</DialogDescription>
</DialogHeader>
<div className="h-[calc(80vh-88px)] overflow-auto [--terminal-bg:#ffffff] [--terminal-fg:#111827] [--terminal-border:#e5e7eb] [--terminal-muted:#6b7280] [--terminal-accent:#2563eb] [--terminal-tab-bg:#f8fafc]">
{loading && (
<div className="flex h-full items-center justify-center gap-2 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
正在读取 SKILL.md...
</div>
)}
{!loading && error && (
<div className="flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-red-600">
<AlertCircle className="h-8 w-8" />
<p className="text-sm">{error}</p>
</div>
)}
{!loading && !error && (
<>
{content.trim() ? (
<MarkdownPreview content={content} />
) : (
<div className="flex h-full items-center justify-center px-6 text-sm text-muted-foreground">
SKILL.md 内容为空
</div>
)}
</>
)}
</div>
</DialogContent>
</Dialog>
);
}
+71 -2
View File
@@ -1,11 +1,12 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import { useState, forwardRef, useImperativeHandle, useRef } from "react";
import { RefreshCw, Search, Settings } from "lucide-react";
import { useSkills } from "@/hooks/useSkills";
import { SkillCard } from "./SkillCard";
import { RepoManagerPanel } from "./RepoManagerPanel";
import { SkillExecutionDialog } from "./SkillExecutionDialog";
import { SkillContentDialog } from "./SkillContentDialog";
import { HelpTip } from "@/components/HelpTip";
import type { AppType, Skill } from "@/lib/api/skills";
import { skillsApi, type AppType, type Skill } from "@/lib/api/skills";
interface SkillsPageProps {
initialApp?: AppType;
@@ -32,6 +33,14 @@ export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
const [executionDialogOpen, setExecutionDialogOpen] = useState(false);
const [selectedSkillForExecution, setSelectedSkillForExecution] =
useState<Skill | null>(null);
// 内容查看对话框状态
const [contentDialogOpen, setContentDialogOpen] = useState(false);
const [selectedSkillForContent, setSelectedSkillForContent] =
useState<Skill | null>(null);
const [skillContent, setSkillContent] = useState("");
const [contentLoading, setContentLoading] = useState(false);
const [contentError, setContentError] = useState<string | null>(null);
const contentRequestIdRef = useRef(0);
const {
skills,
@@ -102,6 +111,53 @@ export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
}
};
/**
* 处理查看内容按钮点击
* 读取本地 SKILL.md 并打开预览弹窗
*/
const handleViewContent = async (skill: Skill) => {
const requestId = ++contentRequestIdRef.current;
setSelectedSkillForContent(skill);
setContentDialogOpen(true);
setSkillContent("");
setContentError(null);
setContentLoading(true);
try {
const content = await skillsApi.getLocalSkillContent(skill.directory, app);
if (requestId !== contentRequestIdRef.current) {
return;
}
setSkillContent(content);
} catch (e) {
if (requestId !== contentRequestIdRef.current) {
return;
}
setContentError(
`读取失败: ${e instanceof Error ? e.message : String(e)}`,
);
} finally {
if (requestId === contentRequestIdRef.current) {
setContentLoading(false);
}
}
};
/**
* 处理内容查看对话框关闭
*/
const handleContentDialogClose = (open: boolean) => {
setContentDialogOpen(open);
if (!open) {
contentRequestIdRef.current += 1;
setSelectedSkillForContent(null);
setSkillContent("");
setContentError(null);
setContentLoading(false);
}
};
const filteredSkills = skills.filter((skill) => {
const matchesSearch =
skill.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -252,6 +308,7 @@ export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
onInstall={handleInstall}
onUninstall={handleUninstall}
onExecute={handleExecute}
onViewContent={handleViewContent}
installing={installingSkills.has(skill.directory)}
/>
))}
@@ -277,6 +334,18 @@ export const SkillsPage = forwardRef<SkillsPageRef, SkillsPageProps>(
onOpenChange={handleExecutionDialogClose}
/>
)}
{/* Skill 内容查看对话框 */}
{selectedSkillForContent && (
<SkillContentDialog
skillName={selectedSkillForContent.name}
open={contentDialogOpen}
onOpenChange={handleContentDialogClose}
content={skillContent}
loading={contentLoading}
error={contentError}
/>
)}
</div>
);
},
+1
View File
@@ -11,3 +11,4 @@ export {
SkillExecutionDialog,
type SkillExecutionDialogProps,
} from "./SkillExecutionDialog";
export { SkillContentDialog, type SkillContentDialogProps } from "./SkillContentDialog";
+14
View File
@@ -63,4 +63,18 @@ export const skillsApi = {
async getInstalledProxyCastSkills(): Promise<string[]> {
return safeInvoke("get_installed_proxycast_skills");
},
/**
* 获取本地已安装 Skill 的 SKILL.md 内容
*
* @param directory Skill 目录名
* @param app 应用类型
* @returns SKILL.md 的文本内容
*/
async getLocalSkillContent(
directory: string,
app: AppType = "proxycast",
): Promise<string> {
return safeInvoke("get_local_skill_content", { app, directory });
},
};