Spaces:
Running
Running
| import type { OpenClawConfig } from "../../config/config.js"; | |
| import type { GatewayRequestHandlers } from "./types.js"; | |
| import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; | |
| import { installSkill } from "../../agents/skills-install.js"; | |
| import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js"; | |
| import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js"; | |
| import { loadConfig, writeConfigFile } from "../../config/config.js"; | |
| import { getRemoteSkillEligibility } from "../../infra/skills-remote.js"; | |
| import { | |
| ErrorCodes, | |
| errorShape, | |
| formatValidationErrors, | |
| validateSkillsBinsParams, | |
| validateSkillsInstallParams, | |
| validateSkillsStatusParams, | |
| validateSkillsUpdateParams, | |
| } from "../protocol/index.js"; | |
| function listWorkspaceDirs(cfg: OpenClawConfig): string[] { | |
| const dirs = new Set<string>(); | |
| const list = cfg.agents?.list; | |
| if (Array.isArray(list)) { | |
| for (const entry of list) { | |
| if (entry && typeof entry === "object" && typeof entry.id === "string") { | |
| dirs.add(resolveAgentWorkspaceDir(cfg, entry.id)); | |
| } | |
| } | |
| } | |
| dirs.add(resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg))); | |
| return [...dirs]; | |
| } | |
| function collectSkillBins(entries: SkillEntry[]): string[] { | |
| const bins = new Set<string>(); | |
| for (const entry of entries) { | |
| const required = entry.metadata?.requires?.bins ?? []; | |
| const anyBins = entry.metadata?.requires?.anyBins ?? []; | |
| const install = entry.metadata?.install ?? []; | |
| for (const bin of required) { | |
| const trimmed = bin.trim(); | |
| if (trimmed) { | |
| bins.add(trimmed); | |
| } | |
| } | |
| for (const bin of anyBins) { | |
| const trimmed = bin.trim(); | |
| if (trimmed) { | |
| bins.add(trimmed); | |
| } | |
| } | |
| for (const spec of install) { | |
| const specBins = spec?.bins ?? []; | |
| for (const bin of specBins) { | |
| const trimmed = String(bin).trim(); | |
| if (trimmed) { | |
| bins.add(trimmed); | |
| } | |
| } | |
| } | |
| } | |
| return [...bins].toSorted(); | |
| } | |
| export const skillsHandlers: GatewayRequestHandlers = { | |
| "skills.status": ({ params, respond }) => { | |
| if (!validateSkillsStatusParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid skills.status params: ${formatValidationErrors(validateSkillsStatusParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const cfg = loadConfig(); | |
| const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); | |
| const report = buildWorkspaceSkillStatus(workspaceDir, { | |
| config: cfg, | |
| eligibility: { remote: getRemoteSkillEligibility() }, | |
| }); | |
| respond(true, report, undefined); | |
| }, | |
| "skills.bins": ({ params, respond }) => { | |
| if (!validateSkillsBinsParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid skills.bins params: ${formatValidationErrors(validateSkillsBinsParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const cfg = loadConfig(); | |
| const workspaceDirs = listWorkspaceDirs(cfg); | |
| const bins = new Set<string>(); | |
| for (const workspaceDir of workspaceDirs) { | |
| const entries = loadWorkspaceSkillEntries(workspaceDir, { config: cfg }); | |
| for (const bin of collectSkillBins(entries)) { | |
| bins.add(bin); | |
| } | |
| } | |
| respond(true, { bins: [...bins].toSorted() }, undefined); | |
| }, | |
| "skills.install": async ({ params, respond }) => { | |
| if (!validateSkillsInstallParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid skills.install params: ${formatValidationErrors(validateSkillsInstallParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const p = params as { | |
| name: string; | |
| installId: string; | |
| timeoutMs?: number; | |
| }; | |
| const cfg = loadConfig(); | |
| const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); | |
| const result = await installSkill({ | |
| workspaceDir: workspaceDirRaw, | |
| skillName: p.name, | |
| installId: p.installId, | |
| timeoutMs: p.timeoutMs, | |
| config: cfg, | |
| }); | |
| respond( | |
| result.ok, | |
| result, | |
| result.ok ? undefined : errorShape(ErrorCodes.UNAVAILABLE, result.message), | |
| ); | |
| }, | |
| "skills.update": async ({ params, respond }) => { | |
| if (!validateSkillsUpdateParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid skills.update params: ${formatValidationErrors(validateSkillsUpdateParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const p = params as { | |
| skillKey: string; | |
| enabled?: boolean; | |
| apiKey?: string; | |
| env?: Record<string, string>; | |
| }; | |
| const cfg = loadConfig(); | |
| const skills = cfg.skills ? { ...cfg.skills } : {}; | |
| const entries = skills.entries ? { ...skills.entries } : {}; | |
| const current = entries[p.skillKey] ? { ...entries[p.skillKey] } : {}; | |
| if (typeof p.enabled === "boolean") { | |
| current.enabled = p.enabled; | |
| } | |
| if (typeof p.apiKey === "string") { | |
| const trimmed = p.apiKey.trim(); | |
| if (trimmed) { | |
| current.apiKey = trimmed; | |
| } else { | |
| delete current.apiKey; | |
| } | |
| } | |
| if (p.env && typeof p.env === "object") { | |
| const nextEnv = current.env ? { ...current.env } : {}; | |
| for (const [key, value] of Object.entries(p.env)) { | |
| const trimmedKey = key.trim(); | |
| if (!trimmedKey) { | |
| continue; | |
| } | |
| const trimmedVal = value.trim(); | |
| if (!trimmedVal) { | |
| delete nextEnv[trimmedKey]; | |
| } else { | |
| nextEnv[trimmedKey] = trimmedVal; | |
| } | |
| } | |
| current.env = nextEnv; | |
| } | |
| entries[p.skillKey] = current; | |
| skills.entries = entries; | |
| const nextConfig: OpenClawConfig = { | |
| ...cfg, | |
| skills, | |
| }; | |
| await writeConfigFile(nextConfig); | |
| respond(true, { ok: true, skillKey: p.skillKey, config: current }, undefined); | |
| }, | |
| }; | |