599 lines
18 KiB
TypeScript
599 lines
18 KiB
TypeScript
import { getBackendBaseURL } from "@/core/config";
|
|
import { apiFetch } from "@/core/api";
|
|
|
|
import type { Skill, SkillDisplayConfig, SkillState } from "./type";
|
|
|
|
export interface ListSkillsParams {
|
|
search?: string;
|
|
tagIds?: string[];
|
|
sort?: "time_desc" | "time_asc" | "activity_desc" | "activity_asc" | "usage_desc" | "usage_asc" | "name_asc" | "name_desc";
|
|
/** Keep only skills filed under this publish square (发布广场). */
|
|
square?: string;
|
|
/** ``"mine"`` restricts to the skills the caller's 岗位 (position) makes visible. */
|
|
scope?: "mine";
|
|
}
|
|
|
|
export async function loadSkills(params: ListSkillsParams = {}) {
|
|
const qs = new URLSearchParams();
|
|
if (params.search) qs.set("search", params.search);
|
|
for (const id of params.tagIds ?? []) qs.append("tag_ids", id);
|
|
if (params.sort) qs.set("sort", params.sort);
|
|
if (params.square) qs.set("square", params.square);
|
|
if (params.scope) qs.set("scope", params.scope);
|
|
const query = qs.toString();
|
|
const skills = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills${query ? `?${query}` : ""}`,
|
|
);
|
|
const json = await skills.json();
|
|
return json.skills as Skill[];
|
|
}
|
|
|
|
export async function loadAllSkillsAdmin() {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills?admin=true`,
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to load skills");
|
|
}
|
|
const json = await response.json();
|
|
return json.skills as Skill[];
|
|
}
|
|
|
|
export async function getSkill(skillName: string): Promise<Skill> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}`,
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to load skill");
|
|
}
|
|
return response.json() as Promise<Skill>;
|
|
}
|
|
|
|
export async function enableSkill(skillName: string, enabled: boolean) {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${skillName}`,
|
|
{
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
enabled,
|
|
}),
|
|
},
|
|
);
|
|
return response.json();
|
|
}
|
|
|
|
export async function setSkillAlwaysOn(
|
|
skillName: string,
|
|
alwaysOn: boolean,
|
|
): Promise<Skill> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/always-on`,
|
|
{
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ always_on: alwaysOn }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update skill 常驻 flag");
|
|
}
|
|
return response.json() as Promise<Skill>;
|
|
}
|
|
|
|
export async function setSkillDisplay(
|
|
skillName: string,
|
|
display: SkillDisplayConfig | null,
|
|
sample?: Record<string, unknown> | null,
|
|
): Promise<Skill> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/display`,
|
|
{
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ display, sample }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update skill display adapter");
|
|
}
|
|
return response.json() as Promise<Skill>;
|
|
}
|
|
|
|
export interface SkillDisplaySampleResponse {
|
|
skill_name: string;
|
|
sample: Record<string, unknown>;
|
|
}
|
|
|
|
export interface PersistedSkillDisplaySampleResponse {
|
|
skill_name: string;
|
|
sample: Record<string, unknown> | null;
|
|
}
|
|
|
|
export async function getPersistedSkillDisplaySample(
|
|
skillName: string,
|
|
): Promise<PersistedSkillDisplaySampleResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/display-sample`,
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to get persisted skill display sample");
|
|
}
|
|
return response.json() as Promise<PersistedSkillDisplaySampleResponse>;
|
|
}
|
|
|
|
export async function getSkillDisplaySample(
|
|
skillName: string,
|
|
): Promise<SkillDisplaySampleResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/display-sample`,
|
|
{ method: "POST" },
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to get skill display sample");
|
|
}
|
|
return response.json() as Promise<SkillDisplaySampleResponse>;
|
|
}
|
|
|
|
export interface InstallSkillRequest {
|
|
thread_id: string;
|
|
path: string;
|
|
skill_name?: string;
|
|
}
|
|
|
|
export interface InstallSkillResponse {
|
|
success: boolean;
|
|
skill_name: string;
|
|
message: string;
|
|
overwritten?: boolean;
|
|
}
|
|
|
|
export interface PreviewSkillInstallRequest {
|
|
thread_id: string;
|
|
path: string;
|
|
}
|
|
|
|
export interface PreviewSkillInstallResponse {
|
|
ok: boolean;
|
|
skill_name: string;
|
|
description: string;
|
|
license: string | null;
|
|
allowed_tools: string[];
|
|
can_install_as_is: boolean;
|
|
owned_by_me: boolean;
|
|
owned_by_other: boolean;
|
|
builtin_conflict: boolean;
|
|
owner_username?: string | null;
|
|
owner_user_id?: string | null;
|
|
message: string;
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ValidateSkillUploadResponse {
|
|
ok: boolean;
|
|
skill_name: string;
|
|
description: string;
|
|
license: string | null;
|
|
allowed_tools: string[];
|
|
conflict: boolean;
|
|
conflict_kind: "public" | "custom" | null;
|
|
warnings: string[];
|
|
}
|
|
|
|
export async function installSkill(
|
|
request: InstallSkillRequest,
|
|
): Promise<InstallSkillResponse> {
|
|
const response = await apiFetch(`${getBackendBaseURL()}/api/skills/install`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(request),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Handle HTTP error responses (4xx, 5xx)
|
|
const errorData = await response.json().catch(() => ({}));
|
|
const errorMessage =
|
|
errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`;
|
|
return {
|
|
success: false,
|
|
skill_name: "",
|
|
message: errorMessage,
|
|
};
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export async function previewSkillInstall(
|
|
request: PreviewSkillInstallRequest,
|
|
): Promise<PreviewSkillInstallResponse> {
|
|
const response = await apiFetch(`${getBackendBaseURL()}/api/skills/install-preview`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(request),
|
|
});
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to preview skill install");
|
|
}
|
|
return response.json() as Promise<PreviewSkillInstallResponse>;
|
|
}
|
|
|
|
async function parseDetailedError(response: Response, fallback: string): Promise<Error> {
|
|
const data = await response.json().catch(() => ({}));
|
|
const detail = (data as { detail?: unknown }).detail;
|
|
const message = typeof detail === "string" && detail ? detail : `${fallback} (HTTP ${response.status})`;
|
|
return new Error(message);
|
|
}
|
|
|
|
export async function validateSkillUpload(file: File): Promise<ValidateSkillUploadResponse> {
|
|
const form = new FormData();
|
|
form.append("file", file, file.name);
|
|
const response = await apiFetch(`${getBackendBaseURL()}/api/skills/validate`, {
|
|
method: "POST",
|
|
body: form,
|
|
});
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Validation failed");
|
|
}
|
|
return response.json() as Promise<ValidateSkillUploadResponse>;
|
|
}
|
|
|
|
export async function installSkillUpload(
|
|
file: File,
|
|
options: { overwrite?: boolean; targetCategory?: "public" | "custom" } = {},
|
|
): Promise<InstallSkillResponse> {
|
|
const form = new FormData();
|
|
form.append("file", file, file.name);
|
|
const params = new URLSearchParams();
|
|
if (options.overwrite) params.set("overwrite", "true");
|
|
if (options.targetCategory) params.set("target_category", options.targetCategory);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/install-upload${query}`,
|
|
{ method: "POST", body: form },
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Install failed");
|
|
}
|
|
return response.json() as Promise<InstallSkillResponse>;
|
|
}
|
|
|
|
export async function duplicateSkill(
|
|
sourceName: string,
|
|
newName: string,
|
|
): Promise<InstallSkillResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(sourceName)}/duplicate`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ new_name: newName }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to duplicate skill");
|
|
}
|
|
return response.json() as Promise<InstallSkillResponse>;
|
|
}
|
|
|
|
export interface ReconcileCustomSkillsResponse {
|
|
adopted: string[];
|
|
already_registered: string[];
|
|
}
|
|
|
|
export async function reconcileCustomSkills(): Promise<ReconcileCustomSkillsResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/reconcile-custom`,
|
|
{ method: "POST" },
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to reconcile skills directory");
|
|
}
|
|
return response.json() as Promise<ReconcileCustomSkillsResponse>;
|
|
}
|
|
|
|
export interface SetSkillPublishedResponse {
|
|
name: string;
|
|
owner_user_id: string | null;
|
|
published: boolean;
|
|
square_id?: string;
|
|
}
|
|
|
|
export async function setSkillPublished(
|
|
skillName: string,
|
|
published: boolean,
|
|
squareId?: string,
|
|
): Promise<SetSkillPublishedResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/published`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(
|
|
squareId != null ? { published, square_id: squareId } : { published },
|
|
),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update published state");
|
|
}
|
|
return response.json() as Promise<SetSkillPublishedResponse>;
|
|
}
|
|
|
|
export async function setSkillState(
|
|
skillName: string,
|
|
state: SkillState,
|
|
): Promise<Skill> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/state`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ state }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update skill state");
|
|
}
|
|
return response.json() as Promise<Skill>;
|
|
}
|
|
|
|
export async function setSkillPinned(
|
|
skillName: string,
|
|
pinned: boolean,
|
|
): Promise<Skill> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/pinned`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ pinned }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update skill pinned state");
|
|
}
|
|
return response.json() as Promise<Skill>;
|
|
}
|
|
|
|
export async function setSkillFeatured(
|
|
skillName: string,
|
|
featured: boolean,
|
|
): Promise<Skill> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/featured`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ featured }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update skill featured state");
|
|
}
|
|
return response.json() as Promise<Skill>;
|
|
}
|
|
|
|
export interface SkillContentResponse {
|
|
name: string;
|
|
category: string;
|
|
content: string;
|
|
}
|
|
|
|
export async function getSkillContent(
|
|
skillName: string,
|
|
): Promise<SkillContentResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/content`,
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to load skill content");
|
|
}
|
|
return response.json() as Promise<SkillContentResponse>;
|
|
}
|
|
|
|
export async function deleteCustomSkill(skillName: string): Promise<void> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}`,
|
|
{ method: "DELETE" },
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to delete skill");
|
|
}
|
|
}
|
|
|
|
export interface SkillFileNode {
|
|
name: string;
|
|
type: "file" | "dir";
|
|
path: string;
|
|
size?: number | null;
|
|
mtime?: number | null;
|
|
children?: SkillFileNode[] | null;
|
|
}
|
|
|
|
export interface SkillFilesResponse {
|
|
name: string;
|
|
category: "public" | "custom";
|
|
editable: boolean;
|
|
root: SkillFileNode;
|
|
}
|
|
|
|
export interface SkillFileContentResponse {
|
|
name: string;
|
|
category: "public" | "custom";
|
|
path: string;
|
|
mime: string;
|
|
size: number;
|
|
mtime: number;
|
|
is_text: boolean;
|
|
editable: boolean;
|
|
content?: string | null;
|
|
}
|
|
|
|
export interface SkillFileSaveResponse {
|
|
name: string;
|
|
path: string;
|
|
size: number;
|
|
mtime: number;
|
|
}
|
|
|
|
export async function getSkillFiles(skillName: string): Promise<SkillFilesResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files`,
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to load skill files");
|
|
}
|
|
return response.json() as Promise<SkillFilesResponse>;
|
|
}
|
|
|
|
export async function getSkillFileContent(
|
|
skillName: string,
|
|
path: string,
|
|
): Promise<SkillFileContentResponse> {
|
|
const url =
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/content` +
|
|
`?path=${encodeURIComponent(path)}`;
|
|
const response = await apiFetch(url);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to read skill file");
|
|
}
|
|
return response.json() as Promise<SkillFileContentResponse>;
|
|
}
|
|
|
|
/** URL suitable for direct `<img>` / `<iframe>` / `<a download>` embedding. */
|
|
export function getSkillFileDownloadURL(skillName: string, path: string): string {
|
|
return (
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/download` +
|
|
`?path=${encodeURIComponent(path)}`
|
|
);
|
|
}
|
|
|
|
export async function saveSkillFile(
|
|
skillName: string,
|
|
path: string,
|
|
content: string,
|
|
): Promise<SkillFileSaveResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/content`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ path, content }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to save skill file");
|
|
}
|
|
return response.json() as Promise<SkillFileSaveResponse>;
|
|
}
|
|
|
|
export interface SkillNameZhResponse {
|
|
name: string;
|
|
name_zh: string | null;
|
|
}
|
|
|
|
export interface GenerateMissingSkillNameZhResponse {
|
|
generated: SkillNameZhResponse[];
|
|
failed: string[];
|
|
skipped: number;
|
|
/** Representative model error when some skills failed due to the LLM. */
|
|
model_error?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Set a skill's Chinese display name. An empty string clears it (falls back to
|
|
* the English name).
|
|
*/
|
|
export async function setSkillNameZh(
|
|
skillName: string,
|
|
nameZh: string,
|
|
): Promise<SkillNameZhResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/name-zh`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name_zh: nameZh }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to set skill Chinese name");
|
|
}
|
|
return response.json() as Promise<SkillNameZhResponse>;
|
|
}
|
|
|
|
/** Ask the LLM to generate and persist a Chinese name for a single skill. */
|
|
export async function generateSkillNameZh(
|
|
skillName: string,
|
|
modelName?: string | null,
|
|
): Promise<SkillNameZhResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/name-zh/generate`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ model_name: modelName ?? null }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to generate skill Chinese name");
|
|
}
|
|
return response.json() as Promise<SkillNameZhResponse>;
|
|
}
|
|
|
|
/** One-click (admin): generate Chinese names for every skill that lacks one. */
|
|
export async function generateMissingSkillNameZh(
|
|
modelName?: string | null,
|
|
): Promise<GenerateMissingSkillNameZhResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/name-zh/generate-missing`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ model_name: modelName ?? null }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to bulk-generate skill Chinese names");
|
|
}
|
|
return response.json() as Promise<GenerateMissingSkillNameZhResponse>;
|
|
}
|
|
|
|
export interface SetSkillDetailResponse {
|
|
name: string;
|
|
detail: string | null;
|
|
}
|
|
|
|
/**
|
|
* Update a skill's long-form detail text. Owner/admin for custom skills,
|
|
* admin-only for built-in skills (backend enforces). An empty string clears it.
|
|
*/
|
|
export async function setSkillDetail(
|
|
skillName: string,
|
|
detail: string,
|
|
): Promise<SetSkillDetailResponse> {
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/detail`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ detail }),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw await parseDetailedError(response, "Failed to update skill detail");
|
|
}
|
|
return response.json() as Promise<SetSkillDetailResponse>;
|
|
}
|