ocsmarttools 0.1.2
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/CHANGELOG.md +35 -0
- package/README.md +169 -0
- package/openclaw.plugin.json +48 -0
- package/package.json +33 -0
- package/src/commands/chat.ts +123 -0
- package/src/commands/cli.ts +130 -0
- package/src/commands/operations.ts +370 -0
- package/src/index.ts +70 -0
- package/src/lib/bootstrap.ts +114 -0
- package/src/lib/invoke.ts +202 -0
- package/src/lib/metrics-store.ts +177 -0
- package/src/lib/plugin-config.ts +143 -0
- package/src/lib/refs.ts +68 -0
- package/src/lib/result-shaper.ts +237 -0
- package/src/lib/result-store.ts +72 -0
- package/src/lib/tool-catalog.ts +339 -0
- package/src/tools/tool-batch.ts +374 -0
- package/src/tools/tool-dispatch.ts +157 -0
- package/src/tools/tool-result-get.ts +65 -0
- package/src/tools/tool-search.ts +72 -0
- package/src/types/openclaw-plugin-sdk.d.ts +78 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { AdvToolsSettings } from "./plugin-config.js";
|
|
2
|
+
import type { ResultStore } from "./result-store.js";
|
|
3
|
+
|
|
4
|
+
type ShapeParams = {
|
|
5
|
+
toolName: string;
|
|
6
|
+
value: unknown;
|
|
7
|
+
settings: AdvToolsSettings;
|
|
8
|
+
maxChars?: number;
|
|
9
|
+
store?: ResultStore;
|
|
10
|
+
allowStore?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const PRIORITY_KEYS = [
|
|
14
|
+
"status",
|
|
15
|
+
"error",
|
|
16
|
+
"message",
|
|
17
|
+
"summary",
|
|
18
|
+
"title",
|
|
19
|
+
"name",
|
|
20
|
+
"url",
|
|
21
|
+
"id",
|
|
22
|
+
"query",
|
|
23
|
+
"count",
|
|
24
|
+
"results",
|
|
25
|
+
"items",
|
|
26
|
+
"text",
|
|
27
|
+
"content",
|
|
28
|
+
"output",
|
|
29
|
+
"data",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
33
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
34
|
+
? (value as Record<string, unknown>)
|
|
35
|
+
: {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clipMiddle(value: string, keep: number): { truncated: boolean; text: string } {
|
|
39
|
+
if (value.length <= keep) {
|
|
40
|
+
return { truncated: false, text: value };
|
|
41
|
+
}
|
|
42
|
+
const budget = Math.max(60, keep);
|
|
43
|
+
const head = Math.floor(budget * 0.7);
|
|
44
|
+
const tail = Math.max(0, budget - head - 8);
|
|
45
|
+
return {
|
|
46
|
+
truncated: true,
|
|
47
|
+
text: `${value.slice(0, head)}\n...snip...\n${value.slice(value.length - tail)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summarizeString(value: string, maxLen: number): unknown {
|
|
52
|
+
const clipped = clipMiddle(value, maxLen);
|
|
53
|
+
if (!clipped.truncated) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
truncated: true,
|
|
58
|
+
chars: value.length,
|
|
59
|
+
preview: clipped.text,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarizeValue(params: {
|
|
64
|
+
value: unknown;
|
|
65
|
+
depth: number;
|
|
66
|
+
maxDepth: number;
|
|
67
|
+
sampleItems: number;
|
|
68
|
+
maxString: number;
|
|
69
|
+
objectKeys: number;
|
|
70
|
+
}): unknown {
|
|
71
|
+
const { value, depth, maxDepth, sampleItems, maxString, objectKeys } = params;
|
|
72
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
return summarizeString(value, maxString);
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
if (depth >= maxDepth) {
|
|
80
|
+
return { type: "array", length: value.length, sample: value.slice(0, Math.min(sampleItems, value.length)) };
|
|
81
|
+
}
|
|
82
|
+
const sample = value.slice(0, Math.min(sampleItems, value.length)).map((item) =>
|
|
83
|
+
summarizeValue({
|
|
84
|
+
value: item,
|
|
85
|
+
depth: depth + 1,
|
|
86
|
+
maxDepth,
|
|
87
|
+
sampleItems,
|
|
88
|
+
maxString,
|
|
89
|
+
objectKeys,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
type: "array",
|
|
94
|
+
length: value.length,
|
|
95
|
+
sample,
|
|
96
|
+
omitted: Math.max(0, value.length - sample.length),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const obj = asRecord(value);
|
|
100
|
+
const keys = Object.keys(obj);
|
|
101
|
+
const prioritized = PRIORITY_KEYS.filter((k) => keys.includes(k));
|
|
102
|
+
const rest = keys.filter((k) => !prioritized.includes(k)).sort((a, b) => a.localeCompare(b));
|
|
103
|
+
const selected = [...prioritized, ...rest].slice(0, objectKeys);
|
|
104
|
+
|
|
105
|
+
if (depth >= maxDepth) {
|
|
106
|
+
return {
|
|
107
|
+
type: "object",
|
|
108
|
+
keys: selected,
|
|
109
|
+
omittedKeys: Math.max(0, keys.length - selected.length),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const out: Record<string, unknown> = {};
|
|
114
|
+
for (const key of selected) {
|
|
115
|
+
out[key] = summarizeValue({
|
|
116
|
+
value: obj[key],
|
|
117
|
+
depth: depth + 1,
|
|
118
|
+
maxDepth,
|
|
119
|
+
sampleItems,
|
|
120
|
+
maxString,
|
|
121
|
+
objectKeys,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (keys.length > selected.length) {
|
|
125
|
+
out._omittedKeys = keys.length - selected.length;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function shapeWebSearch(value: unknown, sampleItems: number): unknown {
|
|
131
|
+
const obj = asRecord(value);
|
|
132
|
+
const resultsRaw = Array.isArray(obj.results) ? obj.results : [];
|
|
133
|
+
const results = resultsRaw.slice(0, Math.min(sampleItems, resultsRaw.length)).map((entry) => {
|
|
134
|
+
const r = asRecord(entry);
|
|
135
|
+
const desc = typeof r.description === "string" ? clipMiddle(r.description, 320).text : undefined;
|
|
136
|
+
return {
|
|
137
|
+
title: typeof r.title === "string" ? r.title : undefined,
|
|
138
|
+
url: typeof r.url === "string" ? r.url : undefined,
|
|
139
|
+
description: desc,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
query: typeof obj.query === "string" ? obj.query : undefined,
|
|
144
|
+
count: typeof obj.count === "number" ? obj.count : resultsRaw.length,
|
|
145
|
+
results,
|
|
146
|
+
omittedResults: Math.max(0, resultsRaw.length - results.length),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function shapeWebFetch(value: unknown): unknown {
|
|
151
|
+
const obj = asRecord(value);
|
|
152
|
+
const textCandidate =
|
|
153
|
+
typeof obj.text === "string"
|
|
154
|
+
? obj.text
|
|
155
|
+
: typeof obj.content === "string"
|
|
156
|
+
? obj.content
|
|
157
|
+
: typeof obj.markdown === "string"
|
|
158
|
+
? obj.markdown
|
|
159
|
+
: undefined;
|
|
160
|
+
return {
|
|
161
|
+
url: typeof obj.url === "string" ? obj.url : undefined,
|
|
162
|
+
title: typeof obj.title === "string" ? obj.title : undefined,
|
|
163
|
+
text: typeof textCandidate === "string" ? summarizeString(textCandidate, 4000) : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function enforceHardLimit(value: unknown, maxChars: number): unknown {
|
|
168
|
+
const serialized = JSON.stringify(value);
|
|
169
|
+
if (!serialized || serialized.length <= maxChars) {
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
const clipped = clipMiddle(serialized, Math.max(200, maxChars - 80));
|
|
173
|
+
return {
|
|
174
|
+
truncated: true,
|
|
175
|
+
originalChars: serialized.length,
|
|
176
|
+
maxChars,
|
|
177
|
+
preview: clipped.text,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function shapeToolResult(params: ShapeParams): unknown {
|
|
182
|
+
const maxChars = Math.max(500, Math.min(500000, Math.trunc(params.maxChars ?? params.settings.maxResultChars)));
|
|
183
|
+
const serialized = JSON.stringify(params.value);
|
|
184
|
+
if (!serialized || serialized.length <= maxChars) {
|
|
185
|
+
return params.value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let summary: unknown;
|
|
189
|
+
if (params.toolName === "web_search") {
|
|
190
|
+
summary = shapeWebSearch(params.value, params.settings.resultSampleItems);
|
|
191
|
+
} else if (params.toolName === "web_fetch") {
|
|
192
|
+
summary = shapeWebFetch(params.value);
|
|
193
|
+
} else {
|
|
194
|
+
summary = summarizeValue({
|
|
195
|
+
value: params.value,
|
|
196
|
+
depth: 0,
|
|
197
|
+
maxDepth: 3,
|
|
198
|
+
sampleItems: params.settings.resultSampleItems,
|
|
199
|
+
maxString: 2000,
|
|
200
|
+
objectKeys: 18,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let handleMeta:
|
|
205
|
+
| {
|
|
206
|
+
handle: string;
|
|
207
|
+
ttlSec: number;
|
|
208
|
+
expiresAt: number;
|
|
209
|
+
}
|
|
210
|
+
| undefined;
|
|
211
|
+
if (params.allowStore !== false && params.settings.storeLargeResults && params.store) {
|
|
212
|
+
handleMeta = params.store.put({
|
|
213
|
+
value: params.value,
|
|
214
|
+
ttlSec: params.settings.resultStoreTtlSec,
|
|
215
|
+
toolName: params.toolName,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const shaped = {
|
|
220
|
+
reduced: true,
|
|
221
|
+
strategy: "adaptive-shape-v1",
|
|
222
|
+
tool: params.toolName,
|
|
223
|
+
originalChars: serialized.length,
|
|
224
|
+
maxChars,
|
|
225
|
+
...(handleMeta
|
|
226
|
+
? {
|
|
227
|
+
handle: handleMeta.handle,
|
|
228
|
+
handleTtlSec: handleMeta.ttlSec,
|
|
229
|
+
handleExpiresAt: new Date(handleMeta.expiresAt).toISOString(),
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
summary,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return enforceHardLimit(shaped, maxChars);
|
|
236
|
+
}
|
|
237
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
type Entry = {
|
|
4
|
+
value: unknown;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
toolName?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_ENTRIES = 512;
|
|
11
|
+
|
|
12
|
+
export type StoredResultMeta = {
|
|
13
|
+
handle: string;
|
|
14
|
+
ttlSec: number;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class ResultStore {
|
|
19
|
+
private readonly data = new Map<string, Entry>();
|
|
20
|
+
|
|
21
|
+
constructor(private readonly maxEntries: number = DEFAULT_MAX_ENTRIES) {}
|
|
22
|
+
|
|
23
|
+
put(params: { value: unknown; ttlSec: number; toolName?: string }): StoredResultMeta {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const ttlSec = Math.max(60, Math.min(86400, Math.floor(params.ttlSec)));
|
|
26
|
+
const handle = `rs_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
27
|
+
const expiresAt = now + ttlSec * 1000;
|
|
28
|
+
|
|
29
|
+
this.prune(now);
|
|
30
|
+
if (this.data.size >= this.maxEntries) {
|
|
31
|
+
const oldestKey = this.data.keys().next().value as string | undefined;
|
|
32
|
+
if (oldestKey) {
|
|
33
|
+
this.data.delete(oldestKey);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.data.set(handle, {
|
|
38
|
+
value: params.value,
|
|
39
|
+
createdAt: now,
|
|
40
|
+
expiresAt,
|
|
41
|
+
toolName: params.toolName,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return { handle, ttlSec, expiresAt };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get(handle: string): { ok: true; value: unknown; toolName?: string; expiresAt: number } | { ok: false } {
|
|
48
|
+
const entry = this.data.get(handle);
|
|
49
|
+
if (!entry) {
|
|
50
|
+
return { ok: false };
|
|
51
|
+
}
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
if (entry.expiresAt <= now) {
|
|
54
|
+
this.data.delete(handle);
|
|
55
|
+
return { ok: false };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, value: entry.value, toolName: entry.toolName, expiresAt: entry.expiresAt };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
prune(nowMs = Date.now()): void {
|
|
61
|
+
for (const [key, value] of this.data.entries()) {
|
|
62
|
+
if (value.expiresAt <= nowMs) {
|
|
63
|
+
this.data.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clear(): void {
|
|
69
|
+
this.data.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export type ToolEntry = {
|
|
4
|
+
name: string;
|
|
5
|
+
group: string;
|
|
6
|
+
description: string;
|
|
7
|
+
paramsHint?: string;
|
|
8
|
+
source: "builtin" | "policy" | "live";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const BUILTIN: ToolEntry[] = [
|
|
12
|
+
{ name: "exec", group: "runtime", description: "Run shell commands.", paramsHint: "{ command }", source: "builtin" },
|
|
13
|
+
{ name: "bash", group: "runtime", description: "Alias for exec in many contexts.", paramsHint: "{ command }", source: "builtin" },
|
|
14
|
+
{ name: "process", group: "runtime", description: "Manage background command sessions.", paramsHint: "{ action, sessionId? }", source: "builtin" },
|
|
15
|
+
{ name: "read", group: "fs", description: "Read files.", paramsHint: "{ path }", source: "builtin" },
|
|
16
|
+
{ name: "write", group: "fs", description: "Write files.", paramsHint: "{ path, content }", source: "builtin" },
|
|
17
|
+
{ name: "edit", group: "fs", description: "Patch file text by replacement.", paramsHint: "{ path, oldText, newText }", source: "builtin" },
|
|
18
|
+
{ name: "apply_patch", group: "fs", description: "Apply unified-style file patch.", paramsHint: "{ input }", source: "builtin" },
|
|
19
|
+
{ name: "sessions_list", group: "sessions", description: "List sessions.", paramsHint: "{ ... }", source: "builtin" },
|
|
20
|
+
{ name: "sessions_history", group: "sessions", description: "Read session history.", paramsHint: "{ sessionKey? }", source: "builtin" },
|
|
21
|
+
{ name: "sessions_send", group: "sessions", description: "Send to another session.", paramsHint: "{ ... }", source: "builtin" },
|
|
22
|
+
{ name: "sessions_spawn", group: "sessions", description: "Spawn subagent session.", paramsHint: "{ prompt, ... }", source: "builtin" },
|
|
23
|
+
{ name: "session_status", group: "sessions", description: "Show current session status.", paramsHint: "{}", source: "builtin" },
|
|
24
|
+
{ name: "memory_search", group: "memory", description: "Search memory snippets.", paramsHint: "{ query }", source: "builtin" },
|
|
25
|
+
{ name: "memory_get", group: "memory", description: "Read memory entries.", paramsHint: "{ key }", source: "builtin" },
|
|
26
|
+
{ name: "web_search", group: "web", description: "Search the web.", paramsHint: "{ query }", source: "builtin" },
|
|
27
|
+
{ name: "web_fetch", group: "web", description: "Fetch and extract web page content.", paramsHint: "{ url }", source: "builtin" },
|
|
28
|
+
{ name: "browser", group: "ui", description: "Browser automation.", paramsHint: "{ action, ... }", source: "builtin" },
|
|
29
|
+
{ name: "canvas", group: "ui", description: "Canvas/artifact rendering actions.", paramsHint: "{ action, ... }", source: "builtin" },
|
|
30
|
+
{ name: "cron", group: "automation", description: "Manage scheduled jobs.", paramsHint: "{ action, ... }", source: "builtin" },
|
|
31
|
+
{ name: "gateway", group: "automation", description: "Gateway control-plane actions.", paramsHint: "{ action, ... }", source: "builtin" },
|
|
32
|
+
{ name: "message", group: "messaging", description: "Message actions across channels.", paramsHint: "{ action, ... }", source: "builtin" },
|
|
33
|
+
{ name: "nodes", group: "nodes", description: "Invoke node-side capabilities.", paramsHint: "{ action, ... }", source: "builtin" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const GROUP_NAMES = new Set([
|
|
37
|
+
"group:runtime",
|
|
38
|
+
"group:fs",
|
|
39
|
+
"group:sessions",
|
|
40
|
+
"group:memory",
|
|
41
|
+
"group:web",
|
|
42
|
+
"group:ui",
|
|
43
|
+
"group:automation",
|
|
44
|
+
"group:messaging",
|
|
45
|
+
"group:nodes",
|
|
46
|
+
"group:openclaw",
|
|
47
|
+
"group:plugins",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function asObj(value: unknown): Record<string, unknown> {
|
|
51
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
52
|
+
? (value as Record<string, unknown>)
|
|
53
|
+
: {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asArray(value: unknown): unknown[] {
|
|
57
|
+
return Array.isArray(value) ? value : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveGatewayPort(cfg: OpenClawConfig): number {
|
|
61
|
+
const root = cfg as Record<string, unknown>;
|
|
62
|
+
const gateway = asObj(root.gateway);
|
|
63
|
+
const raw = gateway.port;
|
|
64
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
65
|
+
return Math.trunc(raw);
|
|
66
|
+
}
|
|
67
|
+
const envPort = process.env.OPENCLAW_GATEWAY_PORT;
|
|
68
|
+
if (envPort && Number.isFinite(Number(envPort))) {
|
|
69
|
+
return Number(envPort);
|
|
70
|
+
}
|
|
71
|
+
return 18789;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveBearer(cfg: OpenClawConfig): string | undefined {
|
|
75
|
+
const root = cfg as Record<string, unknown>;
|
|
76
|
+
const gateway = asObj(root.gateway);
|
|
77
|
+
const auth = asObj(gateway.auth);
|
|
78
|
+
const mode = typeof auth.mode === "string" ? auth.mode : "token";
|
|
79
|
+
if (mode === "none") {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
|
83
|
+
if (token) {
|
|
84
|
+
return token;
|
|
85
|
+
}
|
|
86
|
+
const password = typeof auth.password === "string" ? auth.password.trim() : "";
|
|
87
|
+
if (password) {
|
|
88
|
+
return password;
|
|
89
|
+
}
|
|
90
|
+
const envToken = (process.env.OPENCLAW_GATEWAY_TOKEN ?? "").trim();
|
|
91
|
+
if (envToken) {
|
|
92
|
+
return envToken;
|
|
93
|
+
}
|
|
94
|
+
const envPassword = (process.env.OPENCLAW_GATEWAY_PASSWORD ?? "").trim();
|
|
95
|
+
if (envPassword) {
|
|
96
|
+
return envPassword;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractToolsFromPayload(payload: unknown): unknown[] {
|
|
102
|
+
if (Array.isArray(payload)) {
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
const obj = asObj(payload);
|
|
106
|
+
if (Array.isArray(obj.tools)) {
|
|
107
|
+
return obj.tools;
|
|
108
|
+
}
|
|
109
|
+
const resultObj = asObj(obj.result);
|
|
110
|
+
if (Array.isArray(resultObj.tools)) {
|
|
111
|
+
return resultObj.tools;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(obj.result)) {
|
|
114
|
+
return obj.result as unknown[];
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeLiveToolEntry(value: unknown): ToolEntry | null {
|
|
120
|
+
const obj = asObj(value);
|
|
121
|
+
const name = typeof obj.name === "string" ? obj.name.trim() : "";
|
|
122
|
+
if (!name) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const group = typeof obj.group === "string" && obj.group.trim() ? obj.group.trim() : "live";
|
|
126
|
+
const description =
|
|
127
|
+
typeof obj.description === "string" && obj.description.trim()
|
|
128
|
+
? obj.description.trim()
|
|
129
|
+
: "Tool discovered from live gateway registry.";
|
|
130
|
+
const hasParameters = obj.parameters !== undefined || obj.schema !== undefined || obj.input_schema !== undefined;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
group,
|
|
135
|
+
description,
|
|
136
|
+
paramsHint: hasParameters ? "Runtime schema available." : undefined,
|
|
137
|
+
source: "live",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchLiveToolsFromGateway(
|
|
142
|
+
cfg: OpenClawConfig,
|
|
143
|
+
timeoutMs: number,
|
|
144
|
+
): Promise<ToolEntry[]> {
|
|
145
|
+
const port = resolveGatewayPort(cfg);
|
|
146
|
+
const base = `http://127.0.0.1:${port}`;
|
|
147
|
+
const bearer = resolveBearer(cfg);
|
|
148
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
149
|
+
if (bearer) {
|
|
150
|
+
headers.Authorization = `Bearer ${bearer}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const attempts: Array<{ method: "GET" | "POST"; path: string; body?: unknown }> = [
|
|
154
|
+
{ method: "GET", path: "/tools" },
|
|
155
|
+
{ method: "GET", path: "/tools/list" },
|
|
156
|
+
{ method: "GET", path: "/gateway/tools" },
|
|
157
|
+
{ method: "POST", path: "/tools/list", body: {} },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const attempt of attempts) {
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timer = setTimeout(() => controller.abort(), Math.max(250, timeoutMs));
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(`${base}${attempt.path}`, {
|
|
165
|
+
method: attempt.method,
|
|
166
|
+
headers,
|
|
167
|
+
body: attempt.method === "POST" ? JSON.stringify(attempt.body ?? {}) : undefined,
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
});
|
|
170
|
+
const text = await response.text();
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let parsed: unknown = undefined;
|
|
175
|
+
try {
|
|
176
|
+
parsed = text ? JSON.parse(text) : undefined;
|
|
177
|
+
} catch {
|
|
178
|
+
parsed = undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rawTools = extractToolsFromPayload(parsed);
|
|
182
|
+
if (!rawTools.length) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const normalized = rawTools
|
|
186
|
+
.map((tool) => normalizeLiveToolEntry(tool))
|
|
187
|
+
.filter((tool): tool is ToolEntry => Boolean(tool));
|
|
188
|
+
if (normalized.length) {
|
|
189
|
+
return normalized;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// Fallback to next endpoint.
|
|
193
|
+
} finally {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function gatherPolicyTools(cfg: OpenClawConfig): string[] {
|
|
202
|
+
const out = new Set<string>();
|
|
203
|
+
|
|
204
|
+
const root = cfg as Record<string, unknown>;
|
|
205
|
+
const tools = asObj(root.tools);
|
|
206
|
+
const agents = asObj(root.agents);
|
|
207
|
+
const defaults = asObj(agents.defaults);
|
|
208
|
+
const list = asArray(agents.list);
|
|
209
|
+
|
|
210
|
+
const collect = (arr: unknown) => {
|
|
211
|
+
for (const item of asArray(arr)) {
|
|
212
|
+
if (typeof item !== "string") {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const name = item.trim();
|
|
216
|
+
if (!name || name.includes("*") || GROUP_NAMES.has(name.toLowerCase())) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
out.add(name);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
collect(tools.allow);
|
|
224
|
+
collect(defaults?.tools && asObj(defaults.tools).allow);
|
|
225
|
+
for (const agent of list) {
|
|
226
|
+
const a = asObj(agent);
|
|
227
|
+
const at = asObj(a.tools);
|
|
228
|
+
collect(at.allow);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return [...out];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function buildCatalog(cfg: OpenClawConfig): ToolEntry[] {
|
|
235
|
+
const map = new Map<string, ToolEntry>();
|
|
236
|
+
for (const tool of BUILTIN) {
|
|
237
|
+
map.set(tool.name, tool);
|
|
238
|
+
}
|
|
239
|
+
for (const toolName of gatherPolicyTools(cfg)) {
|
|
240
|
+
if (!map.has(toolName)) {
|
|
241
|
+
map.set(toolName, {
|
|
242
|
+
name: toolName,
|
|
243
|
+
group: "plugin",
|
|
244
|
+
description: "Tool discovered from allowlist/policy configuration.",
|
|
245
|
+
paramsHint: "See tool schema at runtime.",
|
|
246
|
+
source: "policy",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [...map.values()];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function buildCatalogWithLive(params: {
|
|
254
|
+
cfg: OpenClawConfig;
|
|
255
|
+
useLiveRegistry: boolean;
|
|
256
|
+
liveTimeoutMs: number;
|
|
257
|
+
}): Promise<{
|
|
258
|
+
catalog: ToolEntry[];
|
|
259
|
+
source: "live" | "fallback" | "mixed";
|
|
260
|
+
liveCount: number;
|
|
261
|
+
}> {
|
|
262
|
+
const { cfg, useLiveRegistry, liveTimeoutMs } = params;
|
|
263
|
+
const fallbackCatalog = buildCatalog(cfg);
|
|
264
|
+
if (!useLiveRegistry) {
|
|
265
|
+
return {
|
|
266
|
+
catalog: fallbackCatalog,
|
|
267
|
+
source: "fallback",
|
|
268
|
+
liveCount: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const liveTools = await fetchLiveToolsFromGateway(cfg, liveTimeoutMs);
|
|
273
|
+
if (!liveTools.length) {
|
|
274
|
+
return {
|
|
275
|
+
catalog: fallbackCatalog,
|
|
276
|
+
source: "fallback",
|
|
277
|
+
liveCount: 0,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const merged = new Map<string, ToolEntry>();
|
|
282
|
+
for (const tool of fallbackCatalog) {
|
|
283
|
+
merged.set(tool.name, tool);
|
|
284
|
+
}
|
|
285
|
+
for (const tool of liveTools) {
|
|
286
|
+
merged.set(tool.name, tool);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
catalog: [...merged.values()],
|
|
291
|
+
source: merged.size === liveTools.length ? "live" : "mixed",
|
|
292
|
+
liveCount: liveTools.length,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function score(query: string, entry: ToolEntry): number {
|
|
297
|
+
const q = query.toLowerCase();
|
|
298
|
+
const n = entry.name.toLowerCase();
|
|
299
|
+
const d = entry.description.toLowerCase();
|
|
300
|
+
const g = entry.group.toLowerCase();
|
|
301
|
+
|
|
302
|
+
let s = 0;
|
|
303
|
+
if (n === q) s += 100;
|
|
304
|
+
if (n.startsWith(q)) s += 50;
|
|
305
|
+
if (n.includes(q)) s += 30;
|
|
306
|
+
if (g.includes(q)) s += 15;
|
|
307
|
+
if (d.includes(q)) s += 10;
|
|
308
|
+
|
|
309
|
+
for (const token of q.split(/\s+/).filter(Boolean)) {
|
|
310
|
+
if (n.includes(token)) s += 12;
|
|
311
|
+
if (d.includes(token)) s += 4;
|
|
312
|
+
}
|
|
313
|
+
return s;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function searchCatalog(params: {
|
|
317
|
+
catalog: ToolEntry[];
|
|
318
|
+
query: string;
|
|
319
|
+
limit: number;
|
|
320
|
+
group?: string;
|
|
321
|
+
}): ToolEntry[] {
|
|
322
|
+
const { catalog, query, limit, group } = params;
|
|
323
|
+
const normalizedGroup = group?.trim().toLowerCase();
|
|
324
|
+
const filtered = normalizedGroup
|
|
325
|
+
? catalog.filter((entry) => entry.group.toLowerCase() === normalizedGroup)
|
|
326
|
+
: catalog;
|
|
327
|
+
|
|
328
|
+
const q = query.trim();
|
|
329
|
+
if (!q) {
|
|
330
|
+
return filtered.slice(0, limit);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return filtered
|
|
334
|
+
.map((entry) => ({ entry, score: score(q, entry) }))
|
|
335
|
+
.filter((item) => item.score > 0)
|
|
336
|
+
.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name))
|
|
337
|
+
.slice(0, limit)
|
|
338
|
+
.map((item) => item.entry);
|
|
339
|
+
}
|