@zeyiy/openclaw-channel 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +3 -0
- package/dist/inbound.d.ts +3 -1
- package/dist/inbound.js +199 -6
- package/dist/media.js +2 -2
- package/dist/network.d.ts +9 -0
- package/dist/network.js +61 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +243 -0
- package/dist/portal.js +988 -164
- package/dist/tools.js +154 -113
- package/dist/types.d.ts +59 -28
- package/package.json +1 -1
package/dist/portal.js
CHANGED
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
* Handles requests from portal to manage local openclaw agents, files, and models.
|
|
6
6
|
* Lifecycle is tied to the OpenIM account: starts/stops alongside the account.
|
|
7
7
|
*/
|
|
8
|
-
import { readFile, writeFile, stat, mkdir } from "node:fs/promises";
|
|
9
|
-
import {
|
|
8
|
+
import { readFile, writeFile, stat, mkdir, unlink, rm } from "node:fs/promises";
|
|
9
|
+
import { readdirSync, realpathSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
10
|
+
import { resolve, join, dirname, basename } from "node:path";
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { normalizeAgentId, resolveDefaultAgentId, resolveUserPath, resolveAgentWorkspaceDir, isPathSafe, resolveStateDir, resolveOpenClawConfigPath, hasBinarySync, resolveBundledSkillsDir, resolveCronStorePath, resolveClawHubBaseUrl, loadFullConfig, clearDiskConfigCache, isEnvSatisfied, } from "./paths";
|
|
15
|
+
import { fetchClawHub, downloadArchive, extractTarGz } from "./network";
|
|
10
16
|
const bridges = new Map();
|
|
11
17
|
const RECONNECT_BASE_MS = 2000;
|
|
12
18
|
const RECONNECT_MAX_MS = 60000;
|
|
@@ -31,50 +37,6 @@ function portalLog(api, level, msg) {
|
|
|
31
37
|
function getConfig(api) {
|
|
32
38
|
return api.config ?? globalThis.__openimGatewayConfig ?? {};
|
|
33
39
|
}
|
|
34
|
-
function normalizeAgentId(value) {
|
|
35
|
-
const trimmed = (value ?? "").trim();
|
|
36
|
-
if (!trimmed)
|
|
37
|
-
return "main";
|
|
38
|
-
return trimmed.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 64) || "main";
|
|
39
|
-
}
|
|
40
|
-
function resolveDefaultAgentId(cfg) {
|
|
41
|
-
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
42
|
-
if (agents.length === 0)
|
|
43
|
-
return "main";
|
|
44
|
-
const defaults = agents.filter((a) => a?.default);
|
|
45
|
-
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
|
46
|
-
return normalizeAgentId(chosen || "main");
|
|
47
|
-
}
|
|
48
|
-
/** Expand leading ~ to $HOME, then resolve to absolute path. */
|
|
49
|
-
function resolveUserPath(p) {
|
|
50
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
51
|
-
if (p.startsWith("~/") || p === "~") {
|
|
52
|
-
return resolve(home, p.slice(2));
|
|
53
|
-
}
|
|
54
|
-
return resolve(p);
|
|
55
|
-
}
|
|
56
|
-
function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
57
|
-
const id = normalizeAgentId(agentId);
|
|
58
|
-
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
59
|
-
const entry = agents.find((a) => a?.id && normalizeAgentId(a.id) === id);
|
|
60
|
-
if (entry?.workspace?.trim())
|
|
61
|
-
return resolveUserPath(entry.workspace.trim());
|
|
62
|
-
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
|
63
|
-
const defaultId = resolveDefaultAgentId(cfg);
|
|
64
|
-
const home = process.env.HOME ?? process.cwd();
|
|
65
|
-
if (id === defaultId) {
|
|
66
|
-
if (fallback)
|
|
67
|
-
return resolveUserPath(fallback);
|
|
68
|
-
return resolve(home, ".openclaw", "workspace");
|
|
69
|
-
}
|
|
70
|
-
if (fallback)
|
|
71
|
-
return join(resolveUserPath(fallback), id);
|
|
72
|
-
return resolve(home, ".openclaw", `workspace-${id}`);
|
|
73
|
-
}
|
|
74
|
-
function isPathSafe(workspaceRoot, targetPath) {
|
|
75
|
-
const resolved = resolve(workspaceRoot, targetPath);
|
|
76
|
-
return resolved.startsWith(workspaceRoot + "/") || resolved === workspaceRoot;
|
|
77
|
-
}
|
|
78
40
|
async function statFileSafely(filePath) {
|
|
79
41
|
try {
|
|
80
42
|
const s = await stat(filePath);
|
|
@@ -119,12 +81,13 @@ function handleModelsList(api, params) {
|
|
|
119
81
|
}
|
|
120
82
|
}
|
|
121
83
|
}
|
|
122
|
-
// Resolve active model for the requested agent
|
|
84
|
+
// Resolve active model for the requested agent (read from disk for latest state)
|
|
123
85
|
const rawAgentId = String(params.agentId ?? "").trim();
|
|
124
86
|
let activeModelId;
|
|
125
87
|
if (rawAgentId) {
|
|
88
|
+
const fullCfg = loadFullConfig();
|
|
126
89
|
const agentId = normalizeAgentId(rawAgentId);
|
|
127
|
-
const agents = Array.isArray(
|
|
90
|
+
const agents = Array.isArray(fullCfg.agents?.list) ? fullCfg.agents.list : [];
|
|
128
91
|
const entry = agents.find((a) => a?.id && normalizeAgentId(a.id) === agentId);
|
|
129
92
|
if (entry?.model) {
|
|
130
93
|
// model can be a string like "deepminer/claude-sonnet-4-6" or an object { primary: "..." }
|
|
@@ -375,150 +338,999 @@ function handleBotAgentGet(api, accountId) {
|
|
|
375
338
|
return { agentId: defaultId, ...(defaultEntry?.name ? { name: defaultEntry.name } : {}) };
|
|
376
339
|
}
|
|
377
340
|
/**
|
|
378
|
-
* tools.catalog —
|
|
379
|
-
*
|
|
341
|
+
* tools.catalog — relay to gateway for the live runtime tool catalog.
|
|
342
|
+
*
|
|
343
|
+
* Gateway returns: { agentId, profiles, groups: [{ id, label, source, tools: [...] }] }
|
|
344
|
+
*/
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// tools.catalog: static core tool definitions (mirrors gateway's CORE_TOOL_DEFINITIONS)
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
const CORE_TOOL_SECTION_ORDER = [
|
|
349
|
+
{ id: "fs", label: "Files" },
|
|
350
|
+
{ id: "runtime", label: "Runtime" },
|
|
351
|
+
{ id: "web", label: "Web" },
|
|
352
|
+
{ id: "memory", label: "Memory" },
|
|
353
|
+
{ id: "sessions", label: "Sessions" },
|
|
354
|
+
{ id: "ui", label: "UI" },
|
|
355
|
+
{ id: "messaging", label: "Messaging" },
|
|
356
|
+
{ id: "automation", label: "Automation" },
|
|
357
|
+
{ id: "nodes", label: "Nodes" },
|
|
358
|
+
{ id: "agents", label: "Agents" },
|
|
359
|
+
{ id: "media", label: "Media" },
|
|
360
|
+
];
|
|
361
|
+
const CORE_TOOL_DEFINITIONS = [
|
|
362
|
+
{ id: "read", label: "read", description: "Read file contents", sectionId: "fs", profiles: ["coding"] },
|
|
363
|
+
{ id: "write", label: "write", description: "Create or overwrite files", sectionId: "fs", profiles: ["coding"] },
|
|
364
|
+
{ id: "edit", label: "edit", description: "Make precise edits", sectionId: "fs", profiles: ["coding"] },
|
|
365
|
+
{ id: "apply_patch", label: "apply_patch", description: "Patch files", sectionId: "fs", profiles: ["coding"] },
|
|
366
|
+
{ id: "exec", label: "exec", description: "Run shell commands", sectionId: "runtime", profiles: ["coding"] },
|
|
367
|
+
{ id: "process", label: "process", description: "Manage processes", sectionId: "runtime", profiles: ["coding"] },
|
|
368
|
+
{ id: "code_execution", label: "code_execution", description: "Run sandboxed remote analysis", sectionId: "runtime", profiles: ["coding"] },
|
|
369
|
+
{ id: "web_search", label: "web_search", description: "Search the web", sectionId: "web", profiles: ["coding"] },
|
|
370
|
+
{ id: "web_fetch", label: "web_fetch", description: "Fetch web content", sectionId: "web", profiles: ["coding"] },
|
|
371
|
+
{ id: "x_search", label: "x_search", description: "Search X posts", sectionId: "web", profiles: ["coding"] },
|
|
372
|
+
{ id: "memory_search", label: "memory_search", description: "Semantic search", sectionId: "memory", profiles: ["coding"] },
|
|
373
|
+
{ id: "memory_get", label: "memory_get", description: "Read memory files", sectionId: "memory", profiles: ["coding"] },
|
|
374
|
+
{ id: "sessions_list", label: "sessions_list", description: "List sessions", sectionId: "sessions", profiles: ["coding", "messaging"] },
|
|
375
|
+
{ id: "sessions_history", label: "sessions_history", description: "View session history", sectionId: "sessions", profiles: ["coding", "messaging"] },
|
|
376
|
+
{ id: "sessions_send", label: "sessions_send", description: "Send to session", sectionId: "sessions", profiles: ["coding", "messaging"] },
|
|
377
|
+
{ id: "sessions_spawn", label: "sessions_spawn", description: "Spawn new session", sectionId: "sessions", profiles: ["coding"] },
|
|
378
|
+
{ id: "sessions_yield", label: "sessions_yield", description: "End turn to receive sub-agent results", sectionId: "sessions", profiles: ["coding"] },
|
|
379
|
+
{ id: "subagents", label: "subagents", description: "Manage sub-agents", sectionId: "sessions", profiles: ["coding"] },
|
|
380
|
+
{ id: "session_status", label: "session_status", description: "Session status", sectionId: "sessions", profiles: ["minimal", "coding", "messaging"] },
|
|
381
|
+
{ id: "browser", label: "browser", description: "Control web browser", sectionId: "ui", profiles: [] },
|
|
382
|
+
{ id: "canvas", label: "canvas", description: "Control canvases", sectionId: "ui", profiles: [] },
|
|
383
|
+
{ id: "message", label: "message", description: "Send messages", sectionId: "messaging", profiles: ["messaging"] },
|
|
384
|
+
{ id: "cron", label: "cron", description: "Schedule cron jobs", sectionId: "automation", profiles: ["coding"] },
|
|
385
|
+
{ id: "gateway", label: "gateway", description: "Gateway control", sectionId: "automation", profiles: [] },
|
|
386
|
+
{ id: "nodes", label: "nodes", description: "Nodes + devices", sectionId: "nodes", profiles: [] },
|
|
387
|
+
{ id: "agents_list", label: "agents_list", description: "List agents", sectionId: "agents", profiles: [] },
|
|
388
|
+
{ id: "update_plan", label: "update_plan", description: "Update plan", sectionId: "agents", profiles: ["coding"] },
|
|
389
|
+
{ id: "image", label: "image", description: "Image understanding", sectionId: "media", profiles: ["coding"] },
|
|
390
|
+
{ id: "image_generate", label: "image_generate", description: "Image generation", sectionId: "media", profiles: ["coding"] },
|
|
391
|
+
{ id: "music_generate", label: "music_generate", description: "Music generation", sectionId: "media", profiles: ["coding"] },
|
|
392
|
+
{ id: "video_generate", label: "video_generate", description: "Video generation", sectionId: "media", profiles: ["coding"] },
|
|
393
|
+
{ id: "tts", label: "tts", description: "Text-to-speech conversion", sectionId: "media", profiles: [] },
|
|
394
|
+
];
|
|
395
|
+
const TOOL_CATALOG_PROFILES = [
|
|
396
|
+
{ id: "minimal", label: "Minimal" },
|
|
397
|
+
{ id: "coding", label: "Coding" },
|
|
398
|
+
{ id: "messaging", label: "Messaging" },
|
|
399
|
+
{ id: "full", label: "Full" },
|
|
400
|
+
];
|
|
401
|
+
/**
|
|
402
|
+
* tools.catalog — mirrors gateway's buildToolsCatalogResult.
|
|
403
|
+
*
|
|
404
|
+
* Core groups: hardcoded static CORE_TOOL_DEFINITIONS (identical to gateway source).
|
|
405
|
+
* Plugin groups: read from config.plugins.entries (enabled plugins).
|
|
406
|
+
* Note: individual plugin tool names require loading plugin factories (runtime-only),
|
|
407
|
+
* so plugin groups are listed without their tools array — matching what config can provide.
|
|
380
408
|
*/
|
|
381
|
-
function handleToolsCatalog(api) {
|
|
409
|
+
function handleToolsCatalog(api, params) {
|
|
382
410
|
const cfg = getConfig(api);
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
411
|
+
const rawAgentId = String(params.agentId ?? "").trim();
|
|
412
|
+
const agentId = rawAgentId ? normalizeAgentId(rawAgentId) : resolveDefaultAgentId(cfg);
|
|
413
|
+
// Build core groups (identical to gateway's buildCoreGroups)
|
|
414
|
+
const coreGroups = CORE_TOOL_SECTION_ORDER.map((section) => ({
|
|
415
|
+
id: section.id,
|
|
416
|
+
label: section.label,
|
|
417
|
+
source: "core",
|
|
418
|
+
tools: CORE_TOOL_DEFINITIONS
|
|
419
|
+
.filter((tool) => tool.sectionId === section.id)
|
|
420
|
+
.map((tool) => ({
|
|
421
|
+
id: tool.id,
|
|
422
|
+
label: tool.label,
|
|
423
|
+
description: tool.description,
|
|
424
|
+
source: "core",
|
|
425
|
+
defaultProfiles: tool.profiles,
|
|
426
|
+
})),
|
|
427
|
+
})).filter((section) => section.tools.length > 0);
|
|
428
|
+
// Build plugin groups from config.plugins.entries (best-effort, no factory loading)
|
|
429
|
+
const pluginEntries = cfg.plugins?.entries;
|
|
430
|
+
const pluginGroups = [];
|
|
431
|
+
if (pluginEntries && typeof pluginEntries === "object") {
|
|
432
|
+
for (const [pluginId, entry] of Object.entries(pluginEntries)) {
|
|
433
|
+
// Skip explicitly disabled plugins
|
|
434
|
+
if (entry?.enabled === false)
|
|
389
435
|
continue;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
source: "
|
|
394
|
-
|
|
436
|
+
pluginGroups.push({
|
|
437
|
+
id: `plugin:${pluginId}`,
|
|
438
|
+
label: pluginId,
|
|
439
|
+
source: "plugin",
|
|
440
|
+
pluginId,
|
|
441
|
+
tools: [],
|
|
395
442
|
});
|
|
396
443
|
}
|
|
444
|
+
pluginGroups.sort((a, b) => a.label.localeCompare(b.label));
|
|
397
445
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
446
|
+
return {
|
|
447
|
+
agentId,
|
|
448
|
+
profiles: TOOL_CATALOG_PROFILES,
|
|
449
|
+
groups: [...coreGroups, ...pluginGroups],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Skills: file-based helpers (mirrors openclaw gateway internals)
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
/**
|
|
456
|
+
* Relaxed JSON5 parser: removes trailing commas before } or ].
|
|
457
|
+
* Used for SKILL.md metadata blocks which use JSON5 syntax (trailing commas,
|
|
458
|
+
* multi-line objects) but do NOT contain // or /* comments in practice.
|
|
459
|
+
*
|
|
460
|
+
* NOTE: We intentionally do NOT strip // comments here because that regex
|
|
461
|
+
* would incorrectly match URLs like "https://..." inside string values.
|
|
462
|
+
*/
|
|
463
|
+
function parseJson5Relaxed(text) {
|
|
464
|
+
// Remove trailing commas before } or ] (the main JSON5 feature in SKILL.md)
|
|
465
|
+
const s = text.replace(/,(\s*[}\]])/g, "$1");
|
|
466
|
+
return JSON.parse(s);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Extract the raw YAML block between --- delimiters.
|
|
470
|
+
* Mirrors gateway's extractFrontmatterBlock.
|
|
471
|
+
*/
|
|
472
|
+
function extractFrontmatterBlock(content) {
|
|
473
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
474
|
+
if (!normalized.startsWith("---"))
|
|
475
|
+
return undefined;
|
|
476
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
477
|
+
if (endIndex === -1)
|
|
478
|
+
return undefined;
|
|
479
|
+
return normalized.slice(4, endIndex);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Parse SKILL.md frontmatter to plain string key-value pairs.
|
|
483
|
+
* Handles both single-line and multi-line (indented) values.
|
|
484
|
+
* Mirrors gateway's parseLineFrontmatter + lineFrontmatterToPlain.
|
|
485
|
+
*/
|
|
486
|
+
function parseSkillFrontmatter(content) {
|
|
487
|
+
const block = extractFrontmatterBlock(content);
|
|
488
|
+
if (!block)
|
|
489
|
+
return {};
|
|
490
|
+
const result = {};
|
|
491
|
+
const lines = block.split("\n");
|
|
492
|
+
let i = 0;
|
|
493
|
+
while (i < lines.length) {
|
|
494
|
+
const match = lines[i].match(/^([\w-]+):\s*(.*)$/);
|
|
495
|
+
if (!match) {
|
|
496
|
+
i++;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const key = match[1];
|
|
500
|
+
const inlineValue = match[2].trim();
|
|
501
|
+
// Handle YAML multi-line indicators (| or >) and empty inline values
|
|
502
|
+
const isMultilineIndicator = inlineValue === "|" || inlineValue === ">" || inlineValue === "|-" || inlineValue === ">-";
|
|
503
|
+
if ((!inlineValue || isMultilineIndicator) && i + 1 < lines.length) {
|
|
504
|
+
const nextLine = lines[i + 1];
|
|
505
|
+
if (nextLine.startsWith(" ") || nextLine.startsWith("\t")) {
|
|
506
|
+
// Multi-line: collect all indented lines
|
|
507
|
+
const valueLines = [];
|
|
508
|
+
let j = i + 1;
|
|
509
|
+
while (j < lines.length && (lines[j].startsWith(" ") || lines[j].startsWith("\t") || lines[j] === "")) {
|
|
510
|
+
valueLines.push(lines[j]);
|
|
511
|
+
j++;
|
|
417
512
|
}
|
|
513
|
+
const value = valueLines.join("\n").trim();
|
|
514
|
+
if (value)
|
|
515
|
+
result[key] = value;
|
|
516
|
+
i = j;
|
|
517
|
+
continue;
|
|
418
518
|
}
|
|
419
519
|
}
|
|
520
|
+
if (inlineValue && !isMultilineIndicator) {
|
|
521
|
+
result[key] = inlineValue.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
522
|
+
}
|
|
523
|
+
i++;
|
|
420
524
|
}
|
|
421
|
-
|
|
422
|
-
const openimTools = ["openim_send_text", "openim_send_image", "openim_send_file", "openim_send_video"];
|
|
423
|
-
for (const name of openimTools) {
|
|
424
|
-
tools.push({ name, description: `OpenIM: ${name.replace("openim_", "")}`, source: "plugin", pluginId: "openim" });
|
|
425
|
-
}
|
|
426
|
-
return { tools };
|
|
525
|
+
return result;
|
|
427
526
|
}
|
|
428
527
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
528
|
+
* Extract openclaw manifest from parsed frontmatter.
|
|
529
|
+
* The metadata field is a JSON5 object with key "openclaw".
|
|
530
|
+
* Mirrors gateway's resolveOpenClawManifestBlock + resolveOpenClawMetadata.
|
|
431
531
|
*/
|
|
432
|
-
function
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
532
|
+
function resolveOpenClawManifest(frontmatter) {
|
|
533
|
+
const metadataRaw = frontmatter.metadata;
|
|
534
|
+
if (!metadataRaw)
|
|
535
|
+
return undefined;
|
|
536
|
+
let manifest;
|
|
537
|
+
try {
|
|
538
|
+
const parsed = parseJson5Relaxed(metadataRaw);
|
|
539
|
+
if (!parsed || typeof parsed !== "object")
|
|
540
|
+
return undefined;
|
|
541
|
+
// Try known openclaw manifest keys
|
|
542
|
+
manifest = parsed.openclaw ?? parsed["openclaw-agent"] ?? parsed["claude-code"];
|
|
543
|
+
if (!manifest && typeof parsed === "object") {
|
|
544
|
+
// If no known wrapper key, check if the parsed object itself looks like a manifest
|
|
545
|
+
const keys = Object.keys(parsed);
|
|
546
|
+
if (keys.some(k => ["requires", "install", "always", "emoji"].includes(k)))
|
|
547
|
+
manifest = parsed;
|
|
548
|
+
}
|
|
549
|
+
if (!manifest || typeof manifest !== "object")
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
const normalizeStrList = (v) => {
|
|
556
|
+
if (!v)
|
|
557
|
+
return [];
|
|
558
|
+
if (Array.isArray(v))
|
|
559
|
+
return v.map(String).filter(Boolean);
|
|
560
|
+
if (typeof v === "string" && v.trim())
|
|
561
|
+
return [v.trim()];
|
|
562
|
+
return [];
|
|
563
|
+
};
|
|
564
|
+
const requiresRaw = manifest.requires;
|
|
565
|
+
const requires = {};
|
|
566
|
+
if (requiresRaw && typeof requiresRaw === "object") {
|
|
567
|
+
requires.bins = normalizeStrList(requiresRaw.bins);
|
|
568
|
+
requires.anyBins = normalizeStrList(requiresRaw.anyBins);
|
|
569
|
+
requires.env = normalizeStrList(requiresRaw.env);
|
|
570
|
+
requires.config = normalizeStrList(requiresRaw.config);
|
|
571
|
+
}
|
|
572
|
+
const install = [];
|
|
573
|
+
if (Array.isArray(manifest.install)) {
|
|
574
|
+
for (const spec of manifest.install) {
|
|
575
|
+
if (!spec || typeof spec !== "object")
|
|
440
576
|
continue;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
577
|
+
install.push({
|
|
578
|
+
id: typeof spec.id === "string" ? spec.id : undefined,
|
|
579
|
+
kind: typeof spec.kind === "string" ? spec.kind : typeof spec.type === "string" ? spec.type : undefined,
|
|
580
|
+
bins: normalizeStrList(spec.bins),
|
|
581
|
+
label: typeof spec.label === "string" ? spec.label : undefined,
|
|
582
|
+
formula: typeof spec.formula === "string" ? spec.formula : undefined,
|
|
583
|
+
package: typeof spec.package === "string" ? spec.package : undefined,
|
|
584
|
+
module: typeof spec.module === "string" ? spec.module : undefined,
|
|
585
|
+
os: normalizeStrList(spec.os),
|
|
448
586
|
});
|
|
449
587
|
}
|
|
450
588
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
589
|
+
return {
|
|
590
|
+
always: typeof manifest.always === "boolean" ? manifest.always : undefined,
|
|
591
|
+
emoji: typeof manifest.emoji === "string" ? manifest.emoji : undefined,
|
|
592
|
+
homepage: typeof manifest.homepage === "string" ? manifest.homepage : undefined,
|
|
593
|
+
skillKey: typeof manifest.skillKey === "string" ? manifest.skillKey : undefined,
|
|
594
|
+
primaryEnv: typeof manifest.primaryEnv === "string" ? manifest.primaryEnv : undefined,
|
|
595
|
+
os: normalizeStrList(manifest.os),
|
|
596
|
+
requires: Object.keys(requires).length > 0 ? requires : undefined,
|
|
597
|
+
install: install.length > 0 ? install : undefined,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Load all skills from a directory. Each skill lives in a subdirectory with a SKILL.md file.
|
|
602
|
+
* Mirrors gateway's loadSkillsFromDirSafe.
|
|
603
|
+
*/
|
|
604
|
+
function loadSkillsFromDir(dir, source) {
|
|
605
|
+
let rootReal;
|
|
606
|
+
try {
|
|
607
|
+
rootReal = realpathSync(resolve(dir));
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
let subdirs;
|
|
613
|
+
try {
|
|
614
|
+
subdirs = readdirSync(dir, { withFileTypes: true })
|
|
615
|
+
.filter(e => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
|
|
616
|
+
.map(e => join(dir, e.name))
|
|
617
|
+
.sort();
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
const entries = [];
|
|
623
|
+
for (const skillDir of subdirs) {
|
|
624
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
625
|
+
if (!existsSync(skillMdPath))
|
|
626
|
+
continue;
|
|
627
|
+
let content;
|
|
628
|
+
try {
|
|
629
|
+
content = readFileSync(skillMdPath, "utf-8");
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
635
|
+
const name = (frontmatter.name ?? "").trim() || basename(skillDir);
|
|
636
|
+
const description = (frontmatter.description ?? "").trim();
|
|
637
|
+
if (!name || !description)
|
|
638
|
+
continue;
|
|
639
|
+
entries.push({
|
|
640
|
+
name,
|
|
641
|
+
description,
|
|
642
|
+
filePath: resolve(skillMdPath),
|
|
643
|
+
baseDir: resolve(skillDir),
|
|
644
|
+
source,
|
|
645
|
+
frontmatter,
|
|
646
|
+
metadata: resolveOpenClawManifest(frontmatter),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return entries;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Resolve skill directories registered by enabled plugins.
|
|
653
|
+
* Each plugin's openclaw.plugin.json may declare "skills": ["./skills"]
|
|
654
|
+
* relative to the plugin's root directory.
|
|
655
|
+
*
|
|
656
|
+
* Only plugins explicitly listed in config.plugins.entries with enabled !== false
|
|
657
|
+
* are considered. Unlisted plugins are NOT loaded.
|
|
658
|
+
*/
|
|
659
|
+
function resolvePluginSkillDirs(cfg) {
|
|
660
|
+
const dirs = [];
|
|
661
|
+
const pluginEntries = cfg?.plugins?.entries;
|
|
662
|
+
if (!pluginEntries || typeof pluginEntries !== "object")
|
|
663
|
+
return dirs;
|
|
664
|
+
// Collect explicitly enabled plugin IDs
|
|
665
|
+
const enabledPluginIds = new Set();
|
|
666
|
+
for (const [pluginId, entry] of Object.entries(pluginEntries)) {
|
|
667
|
+
if (entry?.enabled !== false)
|
|
668
|
+
enabledPluginIds.add(pluginId);
|
|
669
|
+
}
|
|
670
|
+
if (enabledPluginIds.size === 0)
|
|
671
|
+
return dirs;
|
|
672
|
+
/** Read skills paths from a plugin manifest file */
|
|
673
|
+
const readManifestSkills = (manifestPath) => {
|
|
674
|
+
try {
|
|
675
|
+
if (!existsSync(manifestPath))
|
|
676
|
+
return [];
|
|
677
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
678
|
+
if (!Array.isArray(manifest?.skills))
|
|
679
|
+
return [];
|
|
680
|
+
const result = [];
|
|
681
|
+
for (const s of manifest.skills) {
|
|
682
|
+
const rel = String(s ?? "").trim();
|
|
683
|
+
if (!rel)
|
|
684
|
+
continue;
|
|
685
|
+
const candidate = resolve(dirname(manifestPath), rel);
|
|
686
|
+
if (existsSync(candidate))
|
|
687
|
+
result.push(candidate);
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
// 1. User-installed plugins (plugins.installs)
|
|
696
|
+
const pluginInstalls = cfg?.plugins?.installs;
|
|
697
|
+
if (pluginInstalls && typeof pluginInstalls === "object") {
|
|
698
|
+
for (const [pluginId, install] of Object.entries(pluginInstalls)) {
|
|
699
|
+
if (!enabledPluginIds.has(pluginId))
|
|
456
700
|
continue;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
enabled: chanCfg?.enabled !== false,
|
|
462
|
-
configCheck: "ok",
|
|
463
|
-
});
|
|
701
|
+
const installPath = String(install?.installPath ?? "").trim();
|
|
702
|
+
if (!installPath)
|
|
703
|
+
continue;
|
|
704
|
+
dirs.push(...readManifestSkills(join(resolveUserPath(installPath), "openclaw.plugin.json")));
|
|
464
705
|
}
|
|
465
706
|
}
|
|
466
|
-
|
|
707
|
+
// 2. Bundled extensions (only for enabled plugins)
|
|
708
|
+
const bundledDir = resolveBundledSkillsDir();
|
|
709
|
+
if (bundledDir) {
|
|
710
|
+
const extensionsDir = resolve(bundledDir, "..", "dist", "extensions");
|
|
711
|
+
try {
|
|
712
|
+
if (existsSync(extensionsDir)) {
|
|
713
|
+
for (const pluginId of enabledPluginIds) {
|
|
714
|
+
const extManifest = join(extensionsDir, pluginId, "openclaw.plugin.json");
|
|
715
|
+
dirs.push(...readManifestSkills(extManifest));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch { /* skip */ }
|
|
720
|
+
}
|
|
721
|
+
return dirs;
|
|
467
722
|
}
|
|
468
723
|
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
724
|
+
* Load all skill entries visible in a workspace.
|
|
725
|
+
* Sources (later overrides earlier):
|
|
726
|
+
* extra (config extraDirs + plugin skills) → bundled → installed → workspace
|
|
471
727
|
*/
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
|
|
728
|
+
function loadAllSkillEntries(workspaceDir, cfg) {
|
|
729
|
+
const stateDir = resolveStateDir();
|
|
730
|
+
const merged = new Map();
|
|
731
|
+
const addAll = (entries) => { for (const e of entries)
|
|
732
|
+
merged.set(e.name, e); };
|
|
733
|
+
// 1. Extra dirs: config extraDirs + plugin-registered skill dirs
|
|
734
|
+
const extraDirs = [];
|
|
735
|
+
if (Array.isArray(cfg?.skills?.load?.extraDirs)) {
|
|
736
|
+
for (const d of cfg.skills.load.extraDirs) {
|
|
737
|
+
const s = String(d ?? "").trim();
|
|
738
|
+
if (s)
|
|
739
|
+
extraDirs.push(resolveUserPath(s));
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const pluginSkillDirs = resolvePluginSkillDirs(cfg);
|
|
743
|
+
for (const dir of [...extraDirs, ...pluginSkillDirs])
|
|
744
|
+
addAll(loadSkillsFromDir(dir, "extra"));
|
|
745
|
+
// 2. Bundled (openclaw package built-in)
|
|
746
|
+
const bundledDir = resolveBundledSkillsDir();
|
|
747
|
+
if (bundledDir)
|
|
748
|
+
addAll(loadSkillsFromDir(bundledDir, "builtin"));
|
|
749
|
+
// 3. Installed (~/.openclaw/skills/)
|
|
750
|
+
addAll(loadSkillsFromDir(join(stateDir, "skills"), "installed"));
|
|
751
|
+
// 4. Workspace skills ({workspace}/skills/) — highest priority
|
|
752
|
+
addAll(loadSkillsFromDir(join(resolve(workspaceDir), "skills"), "workspace"));
|
|
753
|
+
return Array.from(merged.values());
|
|
475
754
|
}
|
|
476
755
|
/**
|
|
477
|
-
*
|
|
478
|
-
* Placeholder: returns basic info from local config if the skill is installed.
|
|
756
|
+
* Build runtime status for a single skill entry.
|
|
479
757
|
*/
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
758
|
+
function buildSkillStatus(entry, cfg) {
|
|
759
|
+
// Read skills config from disk to ensure we have skills.entries
|
|
760
|
+
const fullCfg = loadFullConfig();
|
|
761
|
+
const skillKey = entry.metadata?.skillKey ?? entry.name;
|
|
762
|
+
const skillCfg = fullCfg?.skills?.entries?.[skillKey] ?? {};
|
|
763
|
+
const disabled = skillCfg.enabled === false;
|
|
764
|
+
const always = entry.metadata?.always === true;
|
|
765
|
+
const bundled = entry.source === "builtin";
|
|
766
|
+
const requiresBins = entry.metadata?.requires?.bins ?? [];
|
|
767
|
+
const requiresAnyBins = entry.metadata?.requires?.anyBins ?? [];
|
|
768
|
+
const requiresEnv = entry.metadata?.requires?.env ?? [];
|
|
769
|
+
const requiresConfig = entry.metadata?.requires?.config ?? [];
|
|
770
|
+
const missingBins = always ? [] : requiresBins.filter(b => !hasBinarySync(b));
|
|
771
|
+
const missingAnyBins = always ? [] :
|
|
772
|
+
(requiresAnyBins.length > 0 && !requiresAnyBins.some(b => hasBinarySync(b)) ? requiresAnyBins : []);
|
|
773
|
+
const missingEnv = always ? [] : requiresEnv.filter(e => !isEnvSatisfied(e, skillCfg, entry.metadata?.primaryEnv));
|
|
774
|
+
const isConfigPathSatisfied = (configPath) => {
|
|
775
|
+
// configPath like "channels.discord.token" → check fullCfg.channels.discord.token
|
|
776
|
+
const parts = configPath.split(".");
|
|
777
|
+
let current = fullCfg;
|
|
778
|
+
for (const part of parts) {
|
|
779
|
+
if (!current || typeof current !== "object")
|
|
780
|
+
return false;
|
|
781
|
+
current = current[part];
|
|
782
|
+
}
|
|
783
|
+
return current !== undefined && current !== null && current !== "";
|
|
784
|
+
};
|
|
785
|
+
const configChecks = requiresConfig.map(p => ({ path: p, satisfied: isConfigPathSatisfied(p) }));
|
|
786
|
+
const missingConfig = always ? [] : configChecks.filter(c => !c.satisfied).map(c => c.path);
|
|
787
|
+
const requirementsSatisfied = missingBins.length === 0 && missingAnyBins.length === 0 &&
|
|
788
|
+
missingEnv.length === 0 && missingConfig.length === 0;
|
|
789
|
+
return {
|
|
790
|
+
name: entry.name,
|
|
791
|
+
description: entry.description,
|
|
792
|
+
source: entry.source,
|
|
793
|
+
bundled,
|
|
794
|
+
filePath: entry.filePath,
|
|
795
|
+
baseDir: entry.baseDir,
|
|
796
|
+
skillKey,
|
|
797
|
+
...(entry.metadata?.primaryEnv ? { primaryEnv: entry.metadata.primaryEnv } : {}),
|
|
798
|
+
...(entry.metadata?.emoji ? { emoji: entry.metadata.emoji } : {}),
|
|
799
|
+
...(entry.metadata?.homepage ? { homepage: entry.metadata.homepage } : {}),
|
|
800
|
+
always,
|
|
801
|
+
disabled,
|
|
802
|
+
blockedByAllowlist: false, // simplified: bundled allowlist check omitted
|
|
803
|
+
eligible: !disabled && requirementsSatisfied,
|
|
804
|
+
requirements: { bins: requiresBins, anyBins: requiresAnyBins, env: requiresEnv, config: requiresConfig, os: entry.metadata?.os ?? [] },
|
|
805
|
+
missing: { bins: missingBins, anyBins: missingAnyBins, env: missingEnv, config: missingConfig, os: [] },
|
|
806
|
+
configChecks,
|
|
807
|
+
install: (entry.metadata?.install ?? []).map((spec, idx) => ({
|
|
808
|
+
id: spec.id ?? `${spec.kind ?? "install"}-${idx}`,
|
|
809
|
+
kind: spec.kind ?? "",
|
|
810
|
+
label: spec.label ?? `Install (${spec.kind ?? "unknown"})`,
|
|
811
|
+
bins: spec.bins ?? [],
|
|
812
|
+
})),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
// ---------------------------------------------------------------------------
|
|
816
|
+
// Skills method handlers (file-based, no gateway relay)
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
/**
|
|
819
|
+
* skills.status — read skill status from disk, grouped by source category.
|
|
820
|
+
*
|
|
821
|
+
* Categories:
|
|
822
|
+
* extra — skills.load.extraDirs → "Extra Skills"
|
|
823
|
+
* builtin — openclaw 安装包内置 → "Built-in Skills"
|
|
824
|
+
* installed — ~/.openclaw/skills/ → "Installed Skills"
|
|
825
|
+
* workspace — {workspace}/skills/ → "Workspace Skills"
|
|
826
|
+
*/
|
|
827
|
+
async function handleSkillsStatus(api, params) {
|
|
484
828
|
const cfg = getConfig(api);
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
829
|
+
const rawAgentId = String(params.agentId ?? "").trim();
|
|
830
|
+
const agentId = rawAgentId ? normalizeAgentId(rawAgentId) : resolveDefaultAgentId(cfg);
|
|
831
|
+
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
832
|
+
const entries = loadAllSkillEntries(workspaceDir, cfg);
|
|
833
|
+
const categoryLabels = {
|
|
834
|
+
extra: "Extra Skills",
|
|
835
|
+
builtin: "Built-in Skills",
|
|
836
|
+
installed: "Installed Skills",
|
|
837
|
+
workspace: "Workspace Skills",
|
|
838
|
+
};
|
|
839
|
+
const grouped = {};
|
|
840
|
+
for (const e of entries) {
|
|
841
|
+
const cat = e.source;
|
|
842
|
+
if (!grouped[cat])
|
|
843
|
+
grouped[cat] = [];
|
|
844
|
+
grouped[cat].push(buildSkillStatus(e, cfg));
|
|
497
845
|
}
|
|
498
|
-
|
|
846
|
+
const categories = Object.entries(grouped).map(([source, skills]) => ({
|
|
847
|
+
source,
|
|
848
|
+
label: categoryLabels[source] ?? source,
|
|
849
|
+
skills,
|
|
850
|
+
}));
|
|
851
|
+
portalLog(api, "info", `skills.status: agentId=${agentId} workspace=${workspaceDir} count=${entries.length}`);
|
|
852
|
+
return { agentId, workspaceDir, categories };
|
|
499
853
|
}
|
|
500
854
|
/**
|
|
501
|
-
*
|
|
855
|
+
* Resolve agent-level skill filter (whitelist).
|
|
856
|
+
* Returns undefined if no filter is configured (all skills enabled for this agent).
|
|
857
|
+
* Returns a Set of allowed skill names if a filter is configured.
|
|
502
858
|
*/
|
|
503
|
-
function
|
|
859
|
+
function resolveAgentSkillFilter(cfg, agentId) {
|
|
860
|
+
const normalizedId = normalizeAgentId(agentId);
|
|
861
|
+
const agentEntry = (cfg.agents?.list ?? []).find((e) => normalizeAgentId(e?.id ?? "") === normalizedId);
|
|
862
|
+
// Agent-level skills field takes precedence
|
|
863
|
+
if (agentEntry && agentEntry.skills !== undefined) {
|
|
864
|
+
const list = Array.isArray(agentEntry.skills) ? agentEntry.skills : [];
|
|
865
|
+
return new Set(list.map((s) => String(s ?? "").trim()).filter(Boolean));
|
|
866
|
+
}
|
|
867
|
+
// Fallback to defaults
|
|
868
|
+
const defaultSkills = cfg.agents?.defaults?.skills;
|
|
869
|
+
if (defaultSkills !== undefined) {
|
|
870
|
+
const list = Array.isArray(defaultSkills) ? defaultSkills : [];
|
|
871
|
+
return new Set(list.map((s) => String(s ?? "").trim()).filter(Boolean));
|
|
872
|
+
}
|
|
873
|
+
// No filter — all skills enabled for this agent
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* agent.skills.status — same as skills.status but with an additional
|
|
878
|
+
* `agent_disabled` field per skill indicating whether the skill is disabled
|
|
879
|
+
* for this specific agent (via agent-level skill filter).
|
|
880
|
+
*/
|
|
881
|
+
async function handleAgentSkillsStatus(api, params) {
|
|
882
|
+
const rawAgentId = String(params.agentId ?? "").trim();
|
|
883
|
+
if (!rawAgentId)
|
|
884
|
+
throw { code: 400, message: "agentId is required" };
|
|
504
885
|
const cfg = getConfig(api);
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
886
|
+
const fullCfg = loadFullConfig();
|
|
887
|
+
const agentId = normalizeAgentId(rawAgentId);
|
|
888
|
+
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
889
|
+
const entries = loadAllSkillEntries(workspaceDir, cfg);
|
|
890
|
+
const skillFilter = resolveAgentSkillFilter(fullCfg, agentId);
|
|
891
|
+
const categoryLabels = {
|
|
892
|
+
extra: "Extra Skills",
|
|
893
|
+
builtin: "Built-in Skills",
|
|
894
|
+
installed: "Installed Skills",
|
|
895
|
+
workspace: "Workspace Skills",
|
|
896
|
+
};
|
|
897
|
+
const grouped = {};
|
|
898
|
+
for (const e of entries) {
|
|
899
|
+
const cat = e.source;
|
|
900
|
+
if (!grouped[cat])
|
|
901
|
+
grouped[cat] = [];
|
|
902
|
+
const status = buildSkillStatus(e, cfg);
|
|
903
|
+
// agent_disabled: true if a skill filter exists and this skill is NOT in the whitelist
|
|
904
|
+
const agentDisabled = skillFilter !== undefined && !skillFilter.has(e.name);
|
|
905
|
+
grouped[cat].push({ ...status, agent_disabled: agentDisabled });
|
|
906
|
+
}
|
|
907
|
+
const categories = Object.entries(grouped).map(([source, skills]) => ({
|
|
908
|
+
source,
|
|
909
|
+
label: categoryLabels[source] ?? source,
|
|
910
|
+
skills,
|
|
911
|
+
}));
|
|
912
|
+
portalLog(api, "info", `agent.skills.status: agentId=${agentId} workspace=${workspaceDir} count=${entries.length} filter=${skillFilter ? `[${skillFilter.size}]` : "none"}`);
|
|
913
|
+
return { agentId, workspaceDir, categories };
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* skills.search — search ClawHub registry directly.
|
|
917
|
+
*
|
|
918
|
+
* GET https://clawhub.ai/api/v1/search?q={query}&limit={limit}
|
|
919
|
+
* Mirrors gateway's searchSkillsFromClawHub.
|
|
920
|
+
*/
|
|
921
|
+
async function handleSkillsSearch(api, params) {
|
|
922
|
+
const query = String(params.query ?? "").trim();
|
|
923
|
+
if (!query)
|
|
924
|
+
return { results: [] };
|
|
925
|
+
try {
|
|
926
|
+
const searchParams = { q: query };
|
|
927
|
+
if (params.limit)
|
|
928
|
+
searchParams.limit = String(params.limit);
|
|
929
|
+
const result = await fetchClawHub(resolveClawHubBaseUrl(), "/api/v1/search", searchParams);
|
|
930
|
+
const results = result?.results ?? [];
|
|
931
|
+
portalLog(api, "info", `skills.search: query="${query}" got ${results.length} results`);
|
|
932
|
+
return { results };
|
|
933
|
+
}
|
|
934
|
+
catch (err) {
|
|
935
|
+
portalLog(api, "warn", `skills.search: ClawHub error: ${err?.message ?? String(err)}`);
|
|
936
|
+
return { results: [] };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* skills.install — install a skill into an agent's workspace.
|
|
941
|
+
*
|
|
942
|
+
* Two modes:
|
|
943
|
+
* source=clawhub (default): download from ClawHub by slug
|
|
944
|
+
* source=url: download a zip from the given URL
|
|
945
|
+
*
|
|
946
|
+
* Both extract into {workspace}/skills/{skillKey}/
|
|
947
|
+
*/
|
|
948
|
+
async function handleSkillsInstall(api, params) {
|
|
949
|
+
const source = String(params.source ?? "clawhub").trim();
|
|
950
|
+
const cfg = getConfig(api);
|
|
951
|
+
const rawAgentId = String(params.agentId ?? "").trim();
|
|
952
|
+
const agentId = rawAgentId ? normalizeAgentId(rawAgentId) : resolveDefaultAgentId(cfg);
|
|
953
|
+
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
954
|
+
const execFileAsync = promisify(execFile);
|
|
955
|
+
if (source === "url") {
|
|
956
|
+
// --- URL install: download zip and extract ---
|
|
957
|
+
const url = String(params.url ?? "").trim();
|
|
958
|
+
if (!url)
|
|
959
|
+
throw { code: 400, message: "url is required for source=url" };
|
|
960
|
+
const skillKey = String(params.skillKey ?? params.name ?? "").trim();
|
|
961
|
+
if (!skillKey) {
|
|
962
|
+
throw { code: 400, message: "skillKey (or name) is required — the directory name under skills/" };
|
|
963
|
+
}
|
|
964
|
+
// Download
|
|
965
|
+
let archiveBytes;
|
|
966
|
+
try {
|
|
967
|
+
archiveBytes = await downloadArchive(url);
|
|
520
968
|
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
throw { code: 503, message: `skills.install: download failed: ${err.message}` };
|
|
971
|
+
}
|
|
972
|
+
const targetDir = join(resolve(workspaceDir), "skills", skillKey);
|
|
973
|
+
const isZip = url.endsWith(".zip") || archiveBytes[0] === 0x50 && archiveBytes[1] === 0x4b; // PK magic
|
|
974
|
+
const tmpFile = join(tmpdir(), `openclaw-skill-${skillKey}-${Date.now()}${isZip ? ".zip" : ".tar.gz"}`);
|
|
975
|
+
try {
|
|
976
|
+
await writeFile(tmpFile, archiveBytes);
|
|
977
|
+
// Clean existing dir and recreate
|
|
978
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
979
|
+
await mkdir(targetDir, { recursive: true });
|
|
980
|
+
if (isZip) {
|
|
981
|
+
await execFileAsync("unzip", ["-o", tmpFile, "-d", targetDir]);
|
|
982
|
+
// If zip contains a single root directory, move contents up
|
|
983
|
+
try {
|
|
984
|
+
const items = readdirSync(targetDir);
|
|
985
|
+
if (items.length === 1) {
|
|
986
|
+
const innerDir = join(targetDir, items[0]);
|
|
987
|
+
const innerStat = statSync(innerDir);
|
|
988
|
+
if (innerStat.isDirectory()) {
|
|
989
|
+
const innerItems = readdirSync(innerDir);
|
|
990
|
+
for (const item of innerItems) {
|
|
991
|
+
await execFileAsync("mv", [join(innerDir, item), targetDir]);
|
|
992
|
+
}
|
|
993
|
+
await execFileAsync("rm", ["-rf", innerDir]);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch { /* best effort flatten */ }
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
await extractTarGz(tmpFile, targetDir);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
throw { code: 500, message: `skills.install: extraction failed: ${err.message}` };
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
try {
|
|
1008
|
+
await unlink(tmpFile);
|
|
1009
|
+
}
|
|
1010
|
+
catch { /* cleanup */ }
|
|
1011
|
+
}
|
|
1012
|
+
portalLog(api, "info", `skills.install: source=url agentId=${agentId} skillKey="${skillKey}" targetDir="${targetDir}"`);
|
|
1013
|
+
return { ok: true, message: `Installed ${skillKey} from URL`, agentId, skillKey, targetDir };
|
|
521
1014
|
}
|
|
1015
|
+
if (source === "clawhub") {
|
|
1016
|
+
// --- ClawHub install: download by slug ---
|
|
1017
|
+
const slug = String(params.slug ?? params.name ?? "").trim();
|
|
1018
|
+
if (!slug)
|
|
1019
|
+
throw { code: 400, message: "slug (or name) is required for source=clawhub" };
|
|
1020
|
+
let detail;
|
|
1021
|
+
try {
|
|
1022
|
+
detail = await fetchClawHub(resolveClawHubBaseUrl(), `/api/v1/skills/${encodeURIComponent(slug)}`);
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
throw { code: 503, message: `skills.install: ClawHub fetch failed: ${err.message}` };
|
|
1026
|
+
}
|
|
1027
|
+
if (!detail?.skill)
|
|
1028
|
+
throw { code: 404, message: `Skill "${slug}" not found on ClawHub` };
|
|
1029
|
+
const version = String(params.version ?? detail.latestVersion?.version ?? "").trim();
|
|
1030
|
+
if (!version)
|
|
1031
|
+
throw { code: 400, message: `Skill "${slug}" has no installable version` };
|
|
1032
|
+
let archiveBytes;
|
|
1033
|
+
try {
|
|
1034
|
+
const searchParams = { version };
|
|
1035
|
+
const archiveUrl = resolveClawHubBaseUrl()
|
|
1036
|
+
+ `/api/v1/packages/${encodeURIComponent(slug)}/download?` + new URLSearchParams(searchParams);
|
|
1037
|
+
archiveBytes = await downloadArchive(archiveUrl, 60000);
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
throw { code: 503, message: `skills.install: download failed: ${err.message}` };
|
|
1041
|
+
}
|
|
1042
|
+
const targetDir = join(resolve(workspaceDir), "skills", slug);
|
|
1043
|
+
const tmpArchive = join(tmpdir(), `openclaw-skill-${slug}-${Date.now()}.tar.gz`);
|
|
1044
|
+
try {
|
|
1045
|
+
await writeFile(tmpArchive, archiveBytes);
|
|
1046
|
+
// Clean existing dir and recreate
|
|
1047
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
1048
|
+
await mkdir(targetDir, { recursive: true });
|
|
1049
|
+
await extractTarGz(tmpArchive, targetDir);
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
throw { code: 500, message: `skills.install: extraction failed: ${err.message}` };
|
|
1053
|
+
}
|
|
1054
|
+
finally {
|
|
1055
|
+
try {
|
|
1056
|
+
await unlink(tmpArchive);
|
|
1057
|
+
}
|
|
1058
|
+
catch { /* cleanup */ }
|
|
1059
|
+
}
|
|
1060
|
+
portalLog(api, "info", `skills.install: source=clawhub agentId=${agentId} slug="${slug}" version="${version}" targetDir="${targetDir}"`);
|
|
1061
|
+
return { ok: true, message: `Installed ${slug}@${version}`, agentId, slug, version, targetDir };
|
|
1062
|
+
}
|
|
1063
|
+
throw { code: 400, message: `unsupported source: "${source}". Use "clawhub" or "url"` };
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* skills.set — set a skill's enabled/disabled state globally.
|
|
1067
|
+
*
|
|
1068
|
+
* Only modifies the `enabled` field in skills.entries[skillKey],
|
|
1069
|
+
* preserving all other fields (env, apiKey, etc.).
|
|
1070
|
+
* If enabled is true, the `enabled` key is removed (default is enabled).
|
|
1071
|
+
*/
|
|
1072
|
+
async function handleSkillsSet(api, params) {
|
|
1073
|
+
const skillKey = String(params.skillKey ?? params.name ?? "").trim();
|
|
1074
|
+
if (!skillKey)
|
|
1075
|
+
throw { code: 400, message: "skillKey (or name) is required" };
|
|
1076
|
+
if (typeof params.enabled !== "boolean") {
|
|
1077
|
+
throw { code: 400, message: "enabled (boolean) is required" };
|
|
1078
|
+
}
|
|
1079
|
+
const enabled = params.enabled;
|
|
1080
|
+
const configPath = resolveOpenClawConfigPath();
|
|
1081
|
+
// Read existing config
|
|
1082
|
+
let cfg = {};
|
|
1083
|
+
try {
|
|
1084
|
+
const raw = await readFile(configPath, "utf-8");
|
|
1085
|
+
cfg = JSON.parse(raw);
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
if (err.code !== "ENOENT") {
|
|
1089
|
+
throw { code: 500, message: `skills.set: failed to read config: ${err.message}` };
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// Ensure skills.entries structure exists
|
|
1093
|
+
if (!cfg.skills || typeof cfg.skills !== "object")
|
|
1094
|
+
cfg.skills = {};
|
|
1095
|
+
if (!cfg.skills.entries || typeof cfg.skills.entries !== "object")
|
|
1096
|
+
cfg.skills.entries = {};
|
|
1097
|
+
// Get or create the entry, preserving existing fields
|
|
1098
|
+
const current = cfg.skills.entries[skillKey] && typeof cfg.skills.entries[skillKey] === "object"
|
|
1099
|
+
? { ...cfg.skills.entries[skillKey] }
|
|
1100
|
+
: {};
|
|
1101
|
+
if (enabled) {
|
|
1102
|
+
// Enabled is the default — remove the key to keep config clean
|
|
1103
|
+
delete current.enabled;
|
|
1104
|
+
// If entry is now empty, remove it entirely
|
|
1105
|
+
if (Object.keys(current).length === 0) {
|
|
1106
|
+
delete cfg.skills.entries[skillKey];
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
cfg.skills.entries[skillKey] = current;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
else {
|
|
1113
|
+
current.enabled = false;
|
|
1114
|
+
cfg.skills.entries[skillKey] = current;
|
|
1115
|
+
}
|
|
1116
|
+
// Write back
|
|
1117
|
+
try {
|
|
1118
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1119
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1120
|
+
}
|
|
1121
|
+
catch (err) {
|
|
1122
|
+
throw { code: 500, message: `skills.set: failed to write config: ${err.message}` };
|
|
1123
|
+
}
|
|
1124
|
+
// Invalidate disk config cache
|
|
1125
|
+
clearDiskConfigCache();
|
|
1126
|
+
portalLog(api, "info", `skills.set: skillKey=${skillKey} enabled=${enabled}`);
|
|
1127
|
+
return { ok: true, skillKey, enabled };
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* agent.skills.set — enable or disable a skill for a specific agent.
|
|
1131
|
+
*
|
|
1132
|
+
* Agent skill config is a whitelist array on agents.list[].skills:
|
|
1133
|
+
* - undefined (not configured) → all skills enabled
|
|
1134
|
+
* - [] (empty array) → all skills disabled
|
|
1135
|
+
* - ["a", "b"] → only a and b enabled
|
|
1136
|
+
*
|
|
1137
|
+
* When disabling the first skill (no whitelist exists yet), we populate
|
|
1138
|
+
* the whitelist with ALL currently available skill names minus the target.
|
|
1139
|
+
* When enabling, we add the skill to the existing whitelist.
|
|
1140
|
+
* When disabling, we remove it from the whitelist.
|
|
1141
|
+
*/
|
|
1142
|
+
async function handleAgentSkillsSet(api, params) {
|
|
1143
|
+
const rawAgentId = String(params.agentId ?? "").trim();
|
|
1144
|
+
const skillName = String(params.skillKey ?? params.name ?? "").trim();
|
|
1145
|
+
if (!rawAgentId)
|
|
1146
|
+
throw { code: 400, message: "agentId is required" };
|
|
1147
|
+
if (!skillName)
|
|
1148
|
+
throw { code: 400, message: "skillKey (or name) is required" };
|
|
1149
|
+
if (typeof params.enabled !== "boolean")
|
|
1150
|
+
throw { code: 400, message: "enabled (boolean) is required" };
|
|
1151
|
+
const enabled = params.enabled;
|
|
1152
|
+
const agentId = normalizeAgentId(rawAgentId);
|
|
1153
|
+
const configPath = resolveOpenClawConfigPath();
|
|
1154
|
+
// Read config from disk
|
|
1155
|
+
let cfg = {};
|
|
1156
|
+
try {
|
|
1157
|
+
const raw = await readFile(configPath, "utf-8");
|
|
1158
|
+
cfg = JSON.parse(raw);
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
if (err.code !== "ENOENT") {
|
|
1162
|
+
throw { code: 500, message: `agent.skills.set: failed to read config: ${err.message}` };
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// Find the agent entry in agents.list
|
|
1166
|
+
if (!cfg.agents)
|
|
1167
|
+
cfg.agents = {};
|
|
1168
|
+
if (!Array.isArray(cfg.agents.list))
|
|
1169
|
+
cfg.agents.list = [];
|
|
1170
|
+
let agentEntry = cfg.agents.list.find((a) => a?.id && normalizeAgentId(a.id) === agentId);
|
|
1171
|
+
if (!agentEntry) {
|
|
1172
|
+
throw { code: 400, message: `agent "${agentId}" not found in config` };
|
|
1173
|
+
}
|
|
1174
|
+
const hasWhitelist = Array.isArray(agentEntry.skills);
|
|
1175
|
+
if (enabled) {
|
|
1176
|
+
// Enable the skill
|
|
1177
|
+
if (!hasWhitelist) {
|
|
1178
|
+
// No whitelist → all skills already enabled, nothing to do
|
|
1179
|
+
portalLog(api, "info", `agent.skills.set: agentId=${agentId} skill=${skillName} already enabled (no whitelist)`);
|
|
1180
|
+
return { ok: true, agentId, skillKey: skillName, enabled: true, skills: undefined };
|
|
1181
|
+
}
|
|
1182
|
+
// Add to whitelist if not present
|
|
1183
|
+
const currentList = agentEntry.skills;
|
|
1184
|
+
if (!currentList.includes(skillName)) {
|
|
1185
|
+
currentList.push(skillName);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
// Disable the skill
|
|
1190
|
+
if (!hasWhitelist) {
|
|
1191
|
+
// No whitelist yet → populate with all skill names minus the target
|
|
1192
|
+
const runtimeCfg = getConfig(api);
|
|
1193
|
+
const workspaceDir = resolveAgentWorkspaceDir(runtimeCfg, agentId);
|
|
1194
|
+
const allEntries = loadAllSkillEntries(workspaceDir, runtimeCfg);
|
|
1195
|
+
const allNames = allEntries.map(e => e.name);
|
|
1196
|
+
agentEntry.skills = allNames.filter(n => n !== skillName);
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1199
|
+
// Remove from existing whitelist
|
|
1200
|
+
agentEntry.skills = agentEntry.skills.filter((s) => s !== skillName);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
// Write back config
|
|
1204
|
+
try {
|
|
1205
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1206
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1207
|
+
}
|
|
1208
|
+
catch (err) {
|
|
1209
|
+
throw { code: 500, message: `agent.skills.set: failed to write config: ${err.message}` };
|
|
1210
|
+
}
|
|
1211
|
+
clearDiskConfigCache();
|
|
1212
|
+
const resultSkills = agentEntry.skills ?? undefined;
|
|
1213
|
+
portalLog(api, "info", `agent.skills.set: agentId=${agentId} skill=${skillName} enabled=${enabled} skills=${JSON.stringify(resultSkills)}`);
|
|
1214
|
+
return { ok: true, agentId, skillKey: skillName, enabled, skills: resultSkills };
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* agent.model.set — switch the model for a specific agent.
|
|
1218
|
+
*
|
|
1219
|
+
* Writes the model string (e.g. "deepminer/claude-sonnet-4-6") to
|
|
1220
|
+
* agents.list[agentId].model in openclaw.json.
|
|
1221
|
+
*/
|
|
1222
|
+
async function handleAgentModelSet(api, params) {
|
|
1223
|
+
const rawAgentId = String(params.agentId ?? "").trim();
|
|
1224
|
+
const model = String(params.model ?? "").trim();
|
|
1225
|
+
if (!rawAgentId)
|
|
1226
|
+
throw { code: 400, message: "agentId is required" };
|
|
1227
|
+
if (!model)
|
|
1228
|
+
throw { code: 400, message: "model is required" };
|
|
1229
|
+
const agentId = normalizeAgentId(rawAgentId);
|
|
1230
|
+
const configPath = resolveOpenClawConfigPath();
|
|
1231
|
+
let cfg = {};
|
|
1232
|
+
try {
|
|
1233
|
+
const raw = await readFile(configPath, "utf-8");
|
|
1234
|
+
cfg = JSON.parse(raw);
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
if (err.code !== "ENOENT") {
|
|
1238
|
+
throw { code: 500, message: `agent.model.set: failed to read config: ${err.message}` };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (!cfg.agents)
|
|
1242
|
+
cfg.agents = {};
|
|
1243
|
+
if (!Array.isArray(cfg.agents.list))
|
|
1244
|
+
cfg.agents.list = [];
|
|
1245
|
+
const agentEntry = cfg.agents.list.find((a) => a?.id && normalizeAgentId(a.id) === agentId);
|
|
1246
|
+
if (!agentEntry) {
|
|
1247
|
+
throw { code: 400, message: `agent "${agentId}" not found in config` };
|
|
1248
|
+
}
|
|
1249
|
+
agentEntry.model = model;
|
|
1250
|
+
try {
|
|
1251
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1252
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1253
|
+
}
|
|
1254
|
+
catch (err) {
|
|
1255
|
+
throw { code: 500, message: `agent.model.set: failed to write config: ${err.message}` };
|
|
1256
|
+
}
|
|
1257
|
+
clearDiskConfigCache();
|
|
1258
|
+
portalLog(api, "info", `agent.model.set: agentId=${agentId} model=${model}`);
|
|
1259
|
+
return { ok: true, agentId, model };
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* cron.list — read jobs directly from the persisted cron store (jobs.json).
|
|
1263
|
+
*
|
|
1264
|
+
* Supports filtering by: agentId, includeDisabled, query
|
|
1265
|
+
* Supports sorting: nextRunAtMs | updatedAtMs | name, asc | desc
|
|
1266
|
+
* Supports pagination: offset, limit
|
|
1267
|
+
*/
|
|
1268
|
+
async function handleCronList(api, params) {
|
|
1269
|
+
const storePath = resolveCronStorePath(api);
|
|
1270
|
+
let raw;
|
|
1271
|
+
try {
|
|
1272
|
+
raw = await readFile(storePath, "utf-8");
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
if (err?.code === "ENOENT") {
|
|
1276
|
+
portalLog(api, "info", `cron.list: store not found at ${storePath}`);
|
|
1277
|
+
return { jobs: [] };
|
|
1278
|
+
}
|
|
1279
|
+
throw { code: 500, message: `cron.list: failed to read store: ${err.message}` };
|
|
1280
|
+
}
|
|
1281
|
+
let store;
|
|
1282
|
+
try {
|
|
1283
|
+
store = JSON.parse(raw);
|
|
1284
|
+
}
|
|
1285
|
+
catch {
|
|
1286
|
+
throw { code: 500, message: "cron.list: invalid JSON in cron store" };
|
|
1287
|
+
}
|
|
1288
|
+
let jobs = Array.isArray(store.jobs) ? store.jobs : [];
|
|
1289
|
+
// Filter: agentId
|
|
1290
|
+
const agentId = String(params.agentId ?? "").trim();
|
|
1291
|
+
if (agentId) {
|
|
1292
|
+
jobs = jobs.filter((j) => j.agentId === agentId);
|
|
1293
|
+
}
|
|
1294
|
+
// Filter: enabled (default: exclude disabled)
|
|
1295
|
+
const includeDisabled = params.includeDisabled === true;
|
|
1296
|
+
if (!includeDisabled) {
|
|
1297
|
+
jobs = jobs.filter((j) => j.enabled !== false);
|
|
1298
|
+
}
|
|
1299
|
+
// Filter: name query
|
|
1300
|
+
const query = String(params.query ?? "").trim().toLowerCase();
|
|
1301
|
+
if (query) {
|
|
1302
|
+
jobs = jobs.filter((j) => j.name?.toLowerCase().includes(query));
|
|
1303
|
+
}
|
|
1304
|
+
// Sort
|
|
1305
|
+
const sortBy = String(params.sortBy ?? "nextRunAtMs");
|
|
1306
|
+
const sortDir = String(params.sortDir ?? "asc");
|
|
1307
|
+
jobs = [...jobs].sort((a, b) => {
|
|
1308
|
+
let av = 0;
|
|
1309
|
+
let bv = 0;
|
|
1310
|
+
if (sortBy === "updatedAtMs") {
|
|
1311
|
+
av = a.updatedAtMs ?? 0;
|
|
1312
|
+
bv = b.updatedAtMs ?? 0;
|
|
1313
|
+
}
|
|
1314
|
+
else if (sortBy === "name") {
|
|
1315
|
+
av = a.name ?? "";
|
|
1316
|
+
bv = b.name ?? "";
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
// default: nextRunAtMs
|
|
1320
|
+
av = a.state?.nextRunAtMs ?? 0;
|
|
1321
|
+
bv = b.state?.nextRunAtMs ?? 0;
|
|
1322
|
+
}
|
|
1323
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
1324
|
+
return sortDir === "desc" ? -cmp : cmp;
|
|
1325
|
+
});
|
|
1326
|
+
// Pagination
|
|
1327
|
+
const offset = Math.max(0, Number(params.offset) || 0);
|
|
1328
|
+
const limit = Number(params.limit) || 0;
|
|
1329
|
+
if (offset > 0)
|
|
1330
|
+
jobs = jobs.slice(offset);
|
|
1331
|
+
if (limit > 0)
|
|
1332
|
+
jobs = jobs.slice(0, limit);
|
|
1333
|
+
portalLog(api, "info", `cron.list: storePath=${storePath} agentId=${agentId || "*"} count=${jobs.length}`);
|
|
522
1334
|
return { jobs };
|
|
523
1335
|
}
|
|
524
1336
|
// ---------------------------------------------------------------------------
|
|
@@ -552,19 +1364,31 @@ async function handlePortalRequest(api, accountId, request) {
|
|
|
552
1364
|
result = await handleAgentsCreate(api, params ?? {});
|
|
553
1365
|
break;
|
|
554
1366
|
case "tools.catalog":
|
|
555
|
-
result = handleToolsCatalog(api);
|
|
1367
|
+
result = await handleToolsCatalog(api, params ?? {});
|
|
556
1368
|
break;
|
|
557
1369
|
case "skills.status":
|
|
558
|
-
result = handleSkillsStatus(api, params ?? {});
|
|
1370
|
+
result = await handleSkillsStatus(api, params ?? {});
|
|
1371
|
+
break;
|
|
1372
|
+
case "agent.skills.status":
|
|
1373
|
+
result = await handleAgentSkillsStatus(api, params ?? {});
|
|
1374
|
+
break;
|
|
1375
|
+
case "agent.skills.set":
|
|
1376
|
+
result = await handleAgentSkillsSet(api, params ?? {});
|
|
1377
|
+
break;
|
|
1378
|
+
case "agent.model.set":
|
|
1379
|
+
result = await handleAgentModelSet(api, params ?? {});
|
|
559
1380
|
break;
|
|
560
1381
|
case "skills.search":
|
|
561
|
-
result = handleSkillsSearch(api, params ?? {});
|
|
1382
|
+
result = await handleSkillsSearch(api, params ?? {});
|
|
1383
|
+
break;
|
|
1384
|
+
case "skills.install":
|
|
1385
|
+
result = await handleSkillsInstall(api, params ?? {});
|
|
562
1386
|
break;
|
|
563
|
-
case "skills.
|
|
564
|
-
result =
|
|
1387
|
+
case "skills.set":
|
|
1388
|
+
result = await handleSkillsSet(api, params ?? {});
|
|
565
1389
|
break;
|
|
566
1390
|
case "cron.list":
|
|
567
|
-
result = handleCronList(api);
|
|
1391
|
+
result = await handleCronList(api, params ?? {});
|
|
568
1392
|
break;
|
|
569
1393
|
case "ping":
|
|
570
1394
|
result = { pong: true };
|