mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 08:54:54 +00:00
* refactor: share talk event metric extraction * refactor: reuse shared coercion helpers * refactor: reuse shared primitive guards * refactor: reuse shared record guard * refactor: reuse shared primitive helpers * refactor: reuse shared string guards * refactor: reuse shared non-empty string guard * refactor: share plugin primitive coercion helpers * refactor: reuse plugin coercion helpers * refactor: reuse plugin coercion helpers in more plugins * refactor: reuse channel coercion helpers * refactor: reuse monitor coercion helpers * refactor: reuse provider coercion helpers * refactor: reuse core coercion helpers * refactor: reuse runtime coercion helpers * refactor: reuse helper coercion in codex paths * refactor: reuse helper coercion in runtime paths * refactor: reuse codex app-server coercion helpers * refactor: reuse codex record helpers * refactor: reuse migration and qa record helpers * refactor: reuse feishu and core helper guards * refactor: reuse browser and policy coercion helpers * refactor: reuse memory wiki record helper * refactor: share boolean coercion helpers * refactor: reuse finite number coercion * refactor: reuse trimmed string list helpers * refactor: reuse string list normalization * refactor: reuse remaining string list helpers * refactor: reuse string entry normalizer * refactor: share sorted string helpers * refactor: share string list normalization * test: preserve command registry browser imports * refactor: reuse trimmed list helpers * refactor: reuse string dedupe helpers * refactor: reuse local dedupe helpers * refactor: reuse more string dedupe helpers * refactor: reuse command string dedupe helpers * refactor: dedupe memory path lists with helper * refactor: expose string dedupe helpers to plugins * refactor: reuse core string dedupe helpers * refactor: reuse shared unique value helpers * refactor: reuse unique helpers in agent utilities * refactor: reuse unique helpers in config plumbing * refactor: reuse unique helpers in extensions * refactor: reuse unique helpers in core utilities * refactor: reuse unique helpers in qa plugins * refactor: reuse unique helpers in memory plugins * refactor: reuse unique helpers in channel plugins * refactor: reuse unique helpers in core tails * refactor: reuse unique helper in comfy workflow * refactor: reuse unique helpers in test utilities * refactor: expose unique value helper to plugins * refactor: reuse unique helpers for numeric lists * refactor: replace index dedupe filters * refactor: reuse string entry normalization * refactor: reuse string normalization in plugin helpers * refactor: reuse string normalization in extension helpers * refactor: reuse string normalization in channel parsers * refactor: reuse string normalization in memory search * refactor: reuse string normalization in provider parsers * refactor: reuse string normalization in qa helpers * refactor: reuse string normalization in infra parsers * refactor: reuse string normalization in messaging parsers * refactor: reuse string normalization in core parsers * refactor: reuse string normalization in extension parsers * refactor: reuse string normalization in remaining parsers * refactor: reuse string normalization in final parser spots * refactor: reuse string normalization in qa media helpers * refactor: reuse normalization in provider and media lists * refactor: reuse normalization for remaining set filters * refactor: reuse normalization in policy allowlists * refactor: reuse normalization in session and owner lists * refactor: centralize primitive string lists * refactor: reuse lowercase entry helpers * refactor: reuse sorted string helpers * refactor: reuse unique trimmed helpers * refactor: reuse string normalization helpers * refactor: reuse catalog string helpers * refactor: reuse remaining string helpers * refactor: simplify remaining list normalization * refactor: reuse codex auth order normalization * chore: refresh plugin sdk api baseline * fix: make shared string sorting deterministic * chore: refresh plugin sdk api baseline * fix: align host env security ordering
198 lines
6.5 KiB
TypeScript
198 lines
6.5 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { normalizeOptionalString as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { Type } from "typebox";
|
|
import { jsonResult, type OpenClawPluginApi } from "../api.js";
|
|
import type { SkillWorkshopConfig } from "./config.js";
|
|
import { applyProposalToWorkspace, normalizeSkillName, writeSupportFile } from "./skills.js";
|
|
import type { SkillChange, SkillProposal, SkillWorkshopStatus } from "./types.js";
|
|
import { applyOrStoreProposal, createStoreForContext, resolveWorkspaceDir } from "./workshop.js";
|
|
|
|
type ToolParams = {
|
|
action?: string;
|
|
id?: string;
|
|
status?: SkillWorkshopStatus;
|
|
skillName?: string;
|
|
title?: string;
|
|
reason?: string;
|
|
description?: string;
|
|
body?: string;
|
|
section?: string;
|
|
oldText?: string;
|
|
newText?: string;
|
|
relativePath?: string;
|
|
apply?: boolean;
|
|
};
|
|
|
|
function buildProposal(params: {
|
|
workspaceDir: string;
|
|
raw: ToolParams;
|
|
source: "tool";
|
|
}): SkillProposal {
|
|
const skillName = normalizeSkillName(readString(params.raw.skillName) ?? "");
|
|
if (!skillName) {
|
|
throw new Error("skillName required");
|
|
}
|
|
const now = Date.now();
|
|
const title = readString(params.raw.title) ?? `Skill update: ${skillName}`;
|
|
const reason = readString(params.raw.reason) ?? "Tool-created skill update";
|
|
const body = readString(params.raw.body);
|
|
const description = readString(params.raw.description) ?? title;
|
|
let change: SkillChange;
|
|
if (params.raw.oldText !== undefined || params.raw.newText !== undefined) {
|
|
const oldText = readString(params.raw.oldText);
|
|
const newText = readString(params.raw.newText);
|
|
if (!oldText || !newText) {
|
|
throw new Error("oldText and newText required for replace");
|
|
}
|
|
change = { kind: "replace", oldText, newText };
|
|
} else if (readString(params.raw.section)) {
|
|
if (!body) {
|
|
throw new Error("body required");
|
|
}
|
|
change = {
|
|
kind: "append",
|
|
section: readString(params.raw.section) ?? "Workflow",
|
|
body,
|
|
description,
|
|
};
|
|
} else {
|
|
if (!body) {
|
|
throw new Error("body required");
|
|
}
|
|
change = { kind: "create", description, body };
|
|
}
|
|
return {
|
|
id: randomUUID(),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
workspaceDir: params.workspaceDir,
|
|
skillName,
|
|
title,
|
|
reason,
|
|
source: params.source,
|
|
status: "pending",
|
|
change,
|
|
};
|
|
}
|
|
|
|
export function createSkillWorkshopTool(params: {
|
|
api: OpenClawPluginApi;
|
|
config: SkillWorkshopConfig;
|
|
ctx: { workspaceDir?: string };
|
|
}) {
|
|
return {
|
|
name: "skill_workshop",
|
|
label: "Skill Workshop",
|
|
description:
|
|
"Create, queue, inspect, approve, or safely apply workspace skill updates for repeatable workflows.",
|
|
parameters: Type.Object({
|
|
action: Type.String({
|
|
enum: [
|
|
"status",
|
|
"list_pending",
|
|
"list_quarantine",
|
|
"inspect",
|
|
"suggest",
|
|
"apply",
|
|
"reject",
|
|
"write_support_file",
|
|
],
|
|
}),
|
|
id: Type.Optional(Type.String()),
|
|
status: Type.Optional(
|
|
Type.String({ enum: ["pending", "applied", "rejected", "quarantined"] }),
|
|
),
|
|
skillName: Type.Optional(Type.String()),
|
|
title: Type.Optional(Type.String()),
|
|
reason: Type.Optional(Type.String()),
|
|
description: Type.Optional(Type.String()),
|
|
body: Type.Optional(Type.String()),
|
|
section: Type.Optional(Type.String()),
|
|
oldText: Type.Optional(Type.String()),
|
|
newText: Type.Optional(Type.String()),
|
|
relativePath: Type.Optional(Type.String()),
|
|
apply: Type.Optional(Type.Boolean()),
|
|
}),
|
|
async execute(_toolCallId: string, rawParams: Record<string, unknown>) {
|
|
const raw = rawParams as ToolParams;
|
|
const action = raw.action ?? "status";
|
|
const workspaceDir = resolveWorkspaceDir(params);
|
|
const store = createStoreForContext(params);
|
|
if (action === "status") {
|
|
const all = await store.list();
|
|
return jsonResult({
|
|
workspaceDir,
|
|
pending: all.filter((item) => item.status === "pending").length,
|
|
quarantined: all.filter((item) => item.status === "quarantined").length,
|
|
applied: all.filter((item) => item.status === "applied").length,
|
|
rejected: all.filter((item) => item.status === "rejected").length,
|
|
});
|
|
}
|
|
if (action === "list_pending") {
|
|
return jsonResult(await store.list(raw.status ?? "pending"));
|
|
}
|
|
if (action === "list_quarantine") {
|
|
return jsonResult(await store.list("quarantined"));
|
|
}
|
|
if (action === "inspect") {
|
|
if (!raw.id) {
|
|
throw new Error("id required");
|
|
}
|
|
return jsonResult(await store.get(raw.id));
|
|
}
|
|
if (action === "suggest") {
|
|
const proposal = buildProposal({ workspaceDir, raw, source: "tool" });
|
|
const result = await applyOrStoreProposal({
|
|
proposal,
|
|
store,
|
|
config: params.config,
|
|
workspaceDir,
|
|
skipAutoApply: raw.apply === false,
|
|
});
|
|
return jsonResult(result);
|
|
}
|
|
if (action === "apply") {
|
|
if (!raw.id) {
|
|
throw new Error("id required");
|
|
}
|
|
const proposal = await store.get(raw.id);
|
|
if (!proposal) {
|
|
throw new Error(`proposal not found: ${raw.id}`);
|
|
}
|
|
if (proposal.status === "quarantined") {
|
|
throw new Error("quarantined proposal cannot be applied");
|
|
}
|
|
const applied = await applyProposalToWorkspace({
|
|
proposal,
|
|
maxSkillBytes: params.config.maxSkillBytes,
|
|
});
|
|
const updated = await store.updateStatus(raw.id, "applied");
|
|
return jsonResult({ status: "applied", skillPath: applied.skillPath, proposal: updated });
|
|
}
|
|
if (action === "reject") {
|
|
if (!raw.id) {
|
|
throw new Error("id required");
|
|
}
|
|
return jsonResult(await store.updateStatus(raw.id, "rejected"));
|
|
}
|
|
if (action === "write_support_file") {
|
|
const skillName = readString(raw.skillName);
|
|
const relativePath = readString(raw.relativePath);
|
|
const body = raw.body;
|
|
if (!skillName || !relativePath || typeof body !== "string") {
|
|
throw new Error("skillName, relativePath, and body required");
|
|
}
|
|
const filePath = await writeSupportFile({
|
|
workspaceDir,
|
|
skillName,
|
|
relativePath,
|
|
content: body,
|
|
maxBytes: params.config.maxSkillBytes,
|
|
});
|
|
return jsonResult({ status: "written", filePath });
|
|
}
|
|
throw new Error(`unknown action: ${action}`);
|
|
},
|
|
};
|
|
}
|