Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 113 additions & 9 deletions frontend/server/skills/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,83 @@
from .archive import SkillArchive


def _is_macos_metadata(path: PurePosixPath) -> bool:
return bool(path.parts) and (
path.parts[0] == "__MACOSX"
or path.name == ".DS_Store"
or path.name.startswith("._")
)


class SkillRepositoryError(RuntimeError):
def __init__(self, code: str, message: str, *, status_code: int = 400) -> None:
def __init__(
self,
code: str,
message: str,
*,
status_code: int = 400,
retryable: bool = False,
original_error: BaseException | None = None,
) -> None:
super().__init__(message)
self.code = code
self.status_code = status_code
self.retryable = retryable
self.original_error = original_error

def detail(self) -> dict[str, object]:
return {"code": self.code, "message": str(self), "retryable": False}
detail: dict[str, object] = {
"code": self.code,
"message": str(self),
"retryable": self.retryable,
}
if self.original_error is not None:
detail["originalError"] = {
"type": (
f"{type(self.original_error).__module__}."
f"{type(self.original_error).__qualname__}"
),
"message": str(self.original_error).strip()
or repr(self.original_error),
"repr": repr(self.original_error),
}
return detail


def resolve_skill_response(
client: Any,
*,
space_id: str,
skill_id: str,
version: str | None,
skill_space_name: str | None = None,
skill_name: str | None = None,
) -> Any:
"""Read either a managed Skill version or a legacy SkillSpace Skill."""
from agentkit.sdk.skills import types as skills_types

try:
return client.get_skill_version(
skills_types.GetSkillVersionRequest(Id=skill_id, SkillVersion=version)
)
except Exception as version_error:
if (
"interface type not consistent with skill type"
not in str(version_error).casefold()
or not skill_space_name
or not skill_name
):
raise
try:
return client.get_skill_info(
skills_types.GetSkillInfoRequest(
SkillName=skill_name,
SkillSpaceName=skill_space_name,
SkillSpaceId=space_id,
)
)
except Exception as info_error:
raise info_error from version_error


class AgentKitSkillRepository:
Expand Down Expand Up @@ -160,13 +229,23 @@ def skill_archive(
space_id: str,
skill_id: str,
version: str | None,
skill_space_name: str | None = None,
skill_name: str | None = None,
) -> tuple[bytes, str]:
from agentkit.sdk.skills import types as skills_types

response = self._client_factory(region).get_skill_version(
skills_types.GetSkillVersionRequest(Id=skill_id, SkillVersion=version)
response = resolve_skill_response(
self._client_factory(region),
space_id=space_id,
skill_id=skill_id,
version=version,
skill_space_name=skill_space_name,
skill_name=skill_name,
)
name = str(
getattr(response, "name", "")
or getattr(response, "skill_name", "")
or skill_name
or skill_id
)
name = str(getattr(response, "name", "") or skill_id)
bucket = str(getattr(response, "bucket_name", "") or "")
path = str(getattr(response, "tos_path", "") or "")
if bucket and path:
Expand All @@ -184,11 +263,27 @@ def skill_archive(
)
with tempfile.TemporaryDirectory(prefix="veadk-skill-view-") as directory:
archive_path = Path(directory) / "skill.zip"
if not _download_legacy_skill_space_skill(remote, archive_path):
try:
downloaded = _download_legacy_skill_space_skill(
remote,
archive_path,
region=region,
raise_on_error=True,
)
except Exception as error:
raise SkillRepositoryError(
"SKILL_ARCHIVE_DOWNLOAD_FAILED",
"暂时无法下载 Skill 文件,请稍后重试。",
status_code=502,
retryable=True,
original_error=error,
) from error
if not downloaded:
raise SkillRepositoryError(
"SKILL_ARCHIVE_DOWNLOAD_FAILED",
"暂时无法下载 Skill 文件,请稍后重试。",
status_code=502,
retryable=True,
)
content = archive_path.read_bytes()
else:
Expand Down Expand Up @@ -218,19 +313,28 @@ def skill_files(
space_id: str,
skill_id: str,
version: str | None,
skill_space_name: str | None = None,
skill_name: str | None = None,
) -> dict[str, object]:
content, filename = self.skill_archive(
region=region,
space_id=space_id,
skill_id=skill_id,
version=version,
skill_space_name=skill_space_name,
skill_name=skill_name,
)
files: list[dict[str, object]] = []
total = 0
seen: set[str] = set()
try:
with zipfile.ZipFile(io.BytesIO(content)) as archive:
infos = [item for item in archive.infolist() if not item.is_dir()]
infos = [
item
for item in archive.infolist()
if not item.is_dir()
and not _is_macos_metadata(PurePosixPath(item.filename))
]
if len(infos) > 100:
raise SkillRepositoryError(
"SKILL_ARCHIVE_FILE_COUNT",
Expand Down
8 changes: 8 additions & 0 deletions frontend/server/skills/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ async def skill_files(
request: Request,
region: str = Query(min_length=1, max_length=64),
version: str | None = Query(default=None, max_length=128),
skill_space_name: str | None = Query(default=None, max_length=256),
skill_name: str | None = Query(default=None, max_length=256),
) -> dict[str, object]:
identity = identity_resolver(request)
return await invoke(
Expand All @@ -228,6 +230,8 @@ async def skill_files(
space_id=space_id,
skill_id=skill_id,
version=(version or "").strip() or None,
skill_space_name=(skill_space_name or "").strip() or None,
skill_name=(skill_name or "").strip() or None,
)
)

Expand All @@ -238,6 +242,8 @@ async def skill_archive(
request: Request,
region: str = Query(min_length=1, max_length=64),
version: str | None = Query(default=None, max_length=128),
skill_space_name: str | None = Query(default=None, max_length=256),
skill_name: str | None = Query(default=None, max_length=256),
) -> Response:
identity = identity_resolver(request)
content, filename = await invoke(
Expand All @@ -247,6 +253,8 @@ async def skill_archive(
space_id=space_id,
skill_id=skill_id,
version=(version or "").strip() or None,
skill_space_name=(skill_space_name or "").strip() or None,
skill_name=(skill_name or "").strip() or None,
)
)
return Response(
Expand Down
8 changes: 8 additions & 0 deletions frontend/server/skills/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,17 @@ def skill_files(
space_id: str,
skill_id: str,
version: str | None,
skill_space_name: str | None = None,
skill_name: str | None = None,
) -> dict[str, object]:
del identity
return self._repository.skill_files(
region=region,
space_id=space_id,
skill_id=skill_id,
version=version,
skill_space_name=skill_space_name,
skill_name=skill_name,
)

def skill_archive(
Expand All @@ -146,13 +150,17 @@ def skill_archive(
space_id: str,
skill_id: str,
version: str | None,
skill_space_name: str | None = None,
skill_name: str | None = None,
) -> tuple[bytes, str]:
del identity
return self._repository.skill_archive(
region=region,
space_id=space_id,
skill_id=skill_id,
version=version,
skill_space_name=skill_space_name,
skill_name=skill_name,
)


Expand Down
8 changes: 8 additions & 0 deletions frontend/src/adk/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,13 @@ export async function getManagedSkillFiles(args: {
skillId: string;
version?: string;
region: string;
skillSpaceName?: string;
skillName?: string;
}): Promise<ManagedSkillFile[]> {
const params = new URLSearchParams({ region: args.region });
if (args.version) params.set("version", args.version);
if (args.skillSpaceName) params.set("skill_space_name", args.skillSpaceName);
if (args.skillName) params.set("skill_name", args.skillName);
const result = await json<{ files: ManagedSkillFile[] }>(
await request(`/spaces/${encodeURIComponent(args.spaceId)}/skills/${encodeURIComponent(args.skillId)}/files?${params}`),
"读取 Skill 文件失败",
Expand All @@ -230,9 +234,13 @@ export async function downloadManagedSkillArchive(args: {
version?: string;
region: string;
fallbackName: string;
skillSpaceName?: string;
skillName?: string;
}): Promise<void> {
const params = new URLSearchParams({ region: args.region });
if (args.version) params.set("version", args.version);
if (args.skillSpaceName) params.set("skill_space_name", args.skillSpaceName);
if (args.skillName) params.set("skill_name", args.skillName);
const response = await request(
`/spaces/${encodeURIComponent(args.spaceId)}/skills/${encodeURIComponent(args.skillId)}/archive?${params}`,
{},
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/create/skills/skillspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,15 @@ export async function getSkillDetail(
version?: string,
region?: string,
project?: string,
skillName?: string,
skillSpaceName?: string,
): Promise<SkillDetail> {
const params: string[] = [];
if (version) params.push(`version=${encodeURIComponent(version)}`);
if (region) params.push(`region=${encodeURIComponent(region)}`);
if (project) params.push(`project=${encodeURIComponent(project)}`);
if (skillName) params.push(`skill_name=${encodeURIComponent(skillName)}`);
if (skillSpaceName) params.push(`skill_space_name=${encodeURIComponent(skillSpaceName)}`);
const q = params.length > 0 ? `?${params.join("&")}` : "";
return jfetch<SkillDetail>(
`/web/skill-spaces/${encodeURIComponent(spaceId)}/skills/${encodeURIComponent(skillId)}${q}`,
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/ui/SkillCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -786,12 +786,16 @@ export function SkillCenterView({
skill.version,
selectedRegion,
selectedSpace.projectName,
skill.skillName,
selectedSpace.name,
),
getManagedSkillFiles({
spaceId: selectedSpace.id,
skillId: skill.skillId,
version: skill.version,
region: selectedRegion,
skillSpaceName: selectedSpace.name,
skillName: skill.skillName,
}),
]);
if (detailRequest.current === request) {
Expand Down Expand Up @@ -1109,6 +1113,8 @@ export function SkillCenterView({
version: detailSkill.version,
region: selectedRegion,
fallbackName: detailSkill.skillName,
skillSpaceName: selectedSpace.name,
skillName: detailSkill.skillName,
}).catch((error: unknown) => setDetailError(normalizeSkillError(error, "下载 Skill 失败")))}
onClose={closeDetail}
/>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/ui/skills/skills.css
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,16 @@ button.skill-file-tree__row:hover { background: hsl(var(--foreground) / .045); c

@media (max-width: 560px) {
.skill-generation__groups { grid-template-columns: 1fr; }
.skill-detail-content--files .skill-file-browser {
height: auto;
min-height: 0;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(128px, 34%) minmax(0, 1fr);
}
.skill-detail-content--files .skill-file-tree {
border-right: 0;
border-bottom: 1px solid hsl(var(--border));
}
}

@media (prefers-reduced-motion: reduce) {
Expand Down
14 changes: 14 additions & 0 deletions frontend/tests/skillCenter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ test("skill details render external markdown with raw HTML disabled", () => {
assert.match(skillCenterSource, /numeric \* 1000/);
assert.match(skillCenterSource, /detailRequest\.current/);
assert.match(skillCenterSource, /selectedSpace\?\.region \|\| defaultCloudRegion\(cloudProvider\)/);
assert.match(
skillStylesSource,
/@media \(max-width: 560px\)[\s\S]*?\.skill-detail-content--files \.skill-file-browser \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\)/,
);
assert.match(
skillStylesSource,
/@media \(max-width: 560px\)[\s\S]*?\.skill-detail-content--files \.skill-file-tree \{[\s\S]*?border-bottom: 1px solid/,
);
});

test("Library navigation replaces Skills immediately below agents", () => {
Expand Down Expand Up @@ -245,6 +253,12 @@ test("managed Skill APIs cover space creation, archives, deletion, and full file
assert.match(managementClientSource, /export async function deleteManagedSkill/);
assert.match(managementClientSource, /export async function getManagedSkillFiles/);
assert.match(managementClientSource, /export async function downloadManagedSkillArchive/);
assert.match(managementClientSource, /params\.set\("skill_space_name", args\.skillSpaceName\)/);
assert.match(managementClientSource, /params\.set\("skill_name", args\.skillName\)/);
assert.match(skillspaceSource, /skill_space_name=\$\{encodeURIComponent\(skillSpaceName\)\}/);
assert.match(skillspaceSource, /skill_name=\$\{encodeURIComponent\(skillName\)\}/);
assert.match(skillCenterSource, /skillSpaceName: selectedSpace\.name/);
assert.match(skillCenterSource, /skillName: skill\.skillName/);
assert.match(managementDialogsSource, /await validateSkillArchive\(selected\)/);
assert.match(managementDialogsSource, /export function EditSkillSpaceDialog/);
assert.match(skillCenterSource, /label: "编辑空间"/);
Expand Down
Loading
Loading