mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-12 23:01:05 +00:00
feat(explore): add --sort parameter for hot/newest/downloads ordering
- Use /api/v1/skills (compat API) when query is empty to get full stats - Map sort options: hot=rating, newest=newest, downloads=downloads - Remove vim-style k/j navigation to fix 'k' key input issue
This commit is contained in:
parent
ffdf8ae30e
commit
c86d99f227
4 changed files with 98 additions and 22 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@motovis/skillhub",
|
||||
"name": "@iflytek/skillhub",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ async function fetchSkillDetail(client: ApiClient, namespace: string, name: stri
|
|||
|
||||
async function runInteractiveSearch(
|
||||
client: ApiClient,
|
||||
initialQuery: string = ""
|
||||
initialQuery: string = "",
|
||||
sort: string = "newest"
|
||||
): Promise<string | null> {
|
||||
const MAX_VISIBLE = 8;
|
||||
let query = initialQuery;
|
||||
|
|
@ -136,7 +137,7 @@ async function runInteractiveSearch(
|
|||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
results = await searchSkills(client, q);
|
||||
results = await searchSkills(client, q, 10, sort);
|
||||
selectedIndex = 0;
|
||||
} catch {
|
||||
results = [];
|
||||
|
|
@ -179,13 +180,13 @@ async function runInteractiveSearch(
|
|||
return;
|
||||
}
|
||||
|
||||
if (key.name === "up" || key.name === "k") {
|
||||
if (key.name === "up") {
|
||||
selectedIndex = Math.max(0, selectedIndex - 1);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
if (key.name === "down") {
|
||||
selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1);
|
||||
render();
|
||||
return;
|
||||
|
|
@ -219,14 +220,17 @@ export function registerExplore(program: Command) {
|
|||
.description("Browse or search skills from the registry")
|
||||
.argument("[query]", "Search query for finding skills")
|
||||
.option("-n, --limit <n>", "Max results", "20")
|
||||
.action(async (query: string | undefined, opts: { limit: string }) => {
|
||||
.option("-s, --sort <sort>", "Sort by: hot, newest, downloads (default: interactive mode)")
|
||||
.action(async (query: string | undefined, opts: { limit: string; sort?: string }) => {
|
||||
const config = loadConfig();
|
||||
const token = await readToken();
|
||||
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
|
||||
const sortMap: Record<string, string> = { hot: "rating", newest: "newest", downloads: "downloads" };
|
||||
const apiSort = sortMap[opts.sort || "newest"] || "newest";
|
||||
|
||||
try {
|
||||
if (!query) {
|
||||
const selected = await runInteractiveSearch(client, "");
|
||||
if (!query && !opts.sort) {
|
||||
const selected = await runInteractiveSearch(client, "", apiSort);
|
||||
if (!selected) {
|
||||
console.log("\nCancelled.");
|
||||
return;
|
||||
|
|
@ -236,14 +240,14 @@ export function registerExplore(program: Command) {
|
|||
return;
|
||||
}
|
||||
|
||||
const results = await searchSkills(client, query, parseInt(opts.limit, 10));
|
||||
const results = await searchSkills(client, query || "", parseInt(opts.limit, 10), apiSort);
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log(`${DIM}No skills found for "${query}"${RESET}`);
|
||||
console.log(`${DIM}No skills found${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const maxResults = Math.min(results.length, 6);
|
||||
const maxResults = Math.min(results.length, parseInt(opts.limit, 10));
|
||||
|
||||
const detailPromises = results.slice(0, maxResults).map((s) =>
|
||||
fetchSkillDetail(client, s.namespace, s.name)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ApiClient } from "./api-client.js";
|
||||
import { ApiRoutes, SearchResponse } from "../schema/routes.js";
|
||||
import { ApiRoutes, SearchResponse, SkillsListResponse } from "../schema/routes.js";
|
||||
import * as readline from "readline";
|
||||
import { dim, info } from "../utils/logger.js";
|
||||
|
||||
|
|
@ -59,17 +59,56 @@ async function fetchSkillDetail(client: ApiClient, namespace: string, name: stri
|
|||
export async function searchSkills(
|
||||
client: ApiClient,
|
||||
query: string,
|
||||
limit: number = 10
|
||||
limit: number = 10,
|
||||
sort?: string
|
||||
): Promise<SearchSkill[]> {
|
||||
if (!query) {
|
||||
const params = new URLSearchParams({ limit: limit.toString() });
|
||||
if (sort && sort !== "newest") {
|
||||
params.set("sort", sort);
|
||||
}
|
||||
const result = await client.get<SkillsListResponse>(
|
||||
`${ApiRoutes.skills}?${params.toString()}`
|
||||
);
|
||||
if (!result.items || result.items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const skills = result.items.map((s) => {
|
||||
const { namespace, name } = parseNamespace(s.slug);
|
||||
return {
|
||||
name,
|
||||
slug: s.slug,
|
||||
namespace,
|
||||
version: s.latestVersion?.version || "",
|
||||
summary: s.summary,
|
||||
installs: s.stats?.downloads || 0,
|
||||
stars: s.stats?.stars || 0,
|
||||
rating: 0,
|
||||
updatedAt: s.updatedAt || 0,
|
||||
};
|
||||
});
|
||||
if (sort === "downloads") {
|
||||
return skills.sort((a, b) => b.installs - a.installs);
|
||||
} else if (sort === "rating") {
|
||||
return skills.sort((a, b) => b.rating - a.rating || b.stars - a.stars);
|
||||
} else {
|
||||
return skills.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ q: query, limit: limit.toString() });
|
||||
if (sort) {
|
||||
params.set("sort", sort);
|
||||
}
|
||||
const result = await client.get<SearchResponse>(
|
||||
`${ApiRoutes.search}?q=${encodeURIComponent(query)}&limit=${limit}`
|
||||
`${ApiRoutes.search}?${params.toString()}`
|
||||
);
|
||||
|
||||
if (!result.results || result.results.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.results.map((s) => {
|
||||
const skills = result.results.map((s) => {
|
||||
const { namespace, name } = parseNamespace(s.slug);
|
||||
return {
|
||||
name,
|
||||
|
|
@ -77,14 +116,26 @@ export async function searchSkills(
|
|||
namespace,
|
||||
version: s.version,
|
||||
summary: s.summary,
|
||||
installs: (s as any).installCount || 0,
|
||||
installs: s.downloadCount || 0,
|
||||
stars: s.starCount || 0,
|
||||
rating: s.ratingAvg || 0,
|
||||
updatedAt: s.updatedAt ? new Date(s.updatedAt).getTime() : 0,
|
||||
};
|
||||
}).sort((a, b) => (b.installs || 0) - (a.installs || 0));
|
||||
});
|
||||
|
||||
if (sort === "downloads") {
|
||||
return skills.sort((a, b) => b.installs - a.installs);
|
||||
} else if (sort === "rating") {
|
||||
return skills.sort((a, b) => b.rating - a.rating || b.stars - a.stars);
|
||||
} else {
|
||||
return skills.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runInteractiveSearch(
|
||||
client: ApiClient,
|
||||
initialQuery: string = ""
|
||||
initialQuery: string = "",
|
||||
sort: string = "newest"
|
||||
): Promise<string | null> {
|
||||
const MAX_VISIBLE = 8;
|
||||
let query = initialQuery;
|
||||
|
|
@ -172,7 +223,7 @@ export async function runInteractiveSearch(
|
|||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
results = await searchSkills(client, q);
|
||||
results = await searchSkills(client, q, 10, sort);
|
||||
selectedIndex = 0;
|
||||
} catch {
|
||||
results = [];
|
||||
|
|
@ -215,13 +266,13 @@ export async function runInteractiveSearch(
|
|||
return;
|
||||
}
|
||||
|
||||
if (key.name === "up" || key.name === "k") {
|
||||
if (key.name === "up") {
|
||||
selectedIndex = Math.max(0, selectedIndex - 1);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
if (key.name === "down") {
|
||||
selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1);
|
||||
render();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,27 @@ export interface SearchResponse {
|
|||
displayName: string;
|
||||
summary: string;
|
||||
version: string;
|
||||
namespace?: string; // Namespace where the skill is published
|
||||
namespace?: string;
|
||||
downloadCount?: number;
|
||||
starCount?: number;
|
||||
ratingAvg?: number;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SkillsListResponse {
|
||||
items: Array<{
|
||||
slug: string;
|
||||
displayName: string;
|
||||
summary: string;
|
||||
updatedAt: number;
|
||||
stats: {
|
||||
downloads?: number;
|
||||
stars?: number;
|
||||
};
|
||||
latestVersion?: {
|
||||
version: string;
|
||||
};
|
||||
}>;
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue