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,202 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export type InvokeParams = {
|
|
4
|
+
tool: string;
|
|
5
|
+
args?: Record<string, unknown>;
|
|
6
|
+
action?: string;
|
|
7
|
+
sessionKey?: string;
|
|
8
|
+
channel?: string;
|
|
9
|
+
accountId?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type InvokeResult = {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
status: number;
|
|
17
|
+
latencyMs: number;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: {
|
|
20
|
+
type?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
details?: unknown;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function asObj(value: unknown): Record<string, unknown> {
|
|
27
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
28
|
+
? (value as Record<string, unknown>)
|
|
29
|
+
: {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveGatewayPort(cfg: OpenClawConfig): number {
|
|
33
|
+
const root = cfg as Record<string, unknown>;
|
|
34
|
+
const gateway = asObj(root.gateway);
|
|
35
|
+
const raw = gateway.port;
|
|
36
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
37
|
+
return Math.trunc(raw);
|
|
38
|
+
}
|
|
39
|
+
const envPort = process.env.OPENCLAW_GATEWAY_PORT;
|
|
40
|
+
if (envPort && Number.isFinite(Number(envPort))) {
|
|
41
|
+
return Number(envPort);
|
|
42
|
+
}
|
|
43
|
+
return 18789;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveBearer(cfg: OpenClawConfig): string | undefined {
|
|
47
|
+
const root = cfg as Record<string, unknown>;
|
|
48
|
+
const gateway = asObj(root.gateway);
|
|
49
|
+
const auth = asObj(gateway.auth);
|
|
50
|
+
const mode = typeof auth.mode === "string" ? auth.mode : "token";
|
|
51
|
+
|
|
52
|
+
if (mode === "none") {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
|
57
|
+
if (token) {
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const password = typeof auth.password === "string" ? auth.password.trim() : "";
|
|
62
|
+
if (password) {
|
|
63
|
+
return password;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const envToken = (process.env.OPENCLAW_GATEWAY_TOKEN ?? "").trim();
|
|
67
|
+
if (envToken) {
|
|
68
|
+
return envToken;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const envPassword = (process.env.OPENCLAW_GATEWAY_PASSWORD ?? "").trim();
|
|
72
|
+
if (envPassword) {
|
|
73
|
+
return envPassword;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function invokeToolViaGateway(cfg: OpenClawConfig, params: InvokeParams): Promise<InvokeResult> {
|
|
80
|
+
const startedAtMs = Date.now();
|
|
81
|
+
const port = resolveGatewayPort(cfg);
|
|
82
|
+
const url = `http://127.0.0.1:${port}/tools/invoke`;
|
|
83
|
+
const bearer = resolveBearer(cfg);
|
|
84
|
+
const timeoutMs =
|
|
85
|
+
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
|
86
|
+
? Math.max(0, Math.min(1800000, Math.trunc(params.timeoutMs)))
|
|
87
|
+
: 0;
|
|
88
|
+
|
|
89
|
+
const headers: Record<string, string> = {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (bearer) {
|
|
94
|
+
headers.Authorization = `Bearer ${bearer}`;
|
|
95
|
+
}
|
|
96
|
+
if (params.channel) {
|
|
97
|
+
headers["x-openclaw-message-channel"] = params.channel;
|
|
98
|
+
}
|
|
99
|
+
if (params.accountId) {
|
|
100
|
+
headers["x-openclaw-account-id"] = params.accountId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const body = {
|
|
104
|
+
tool: params.tool,
|
|
105
|
+
action: params.action,
|
|
106
|
+
args: params.args ?? {},
|
|
107
|
+
sessionKey: params.sessionKey,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let response: Response;
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const abortFromParent = () => controller.abort();
|
|
113
|
+
if (params.signal) {
|
|
114
|
+
if (params.signal.aborted) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
status: 0,
|
|
118
|
+
latencyMs: Date.now() - startedAtMs,
|
|
119
|
+
error: {
|
|
120
|
+
type: "aborted",
|
|
121
|
+
message: "tool invoke aborted",
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
params.signal.addEventListener("abort", abortFromParent, { once: true });
|
|
126
|
+
}
|
|
127
|
+
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
128
|
+
try {
|
|
129
|
+
response = await fetch(url, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers,
|
|
132
|
+
body: JSON.stringify(body),
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (timer) {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
}
|
|
139
|
+
if (params.signal) {
|
|
140
|
+
params.signal.removeEventListener("abort", abortFromParent);
|
|
141
|
+
}
|
|
142
|
+
if (controller.signal.aborted) {
|
|
143
|
+
const reason = params.signal?.aborted ? "aborted" : "timeout";
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
status: 0,
|
|
147
|
+
latencyMs: Date.now() - startedAtMs,
|
|
148
|
+
error: {
|
|
149
|
+
type: reason,
|
|
150
|
+
message: reason === "timeout" ? "tool invoke request timed out" : "tool invoke aborted",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
status: 0,
|
|
157
|
+
latencyMs: Date.now() - startedAtMs,
|
|
158
|
+
error: {
|
|
159
|
+
message: error instanceof Error ? error.message : String(error),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (timer) {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
if (params.signal) {
|
|
167
|
+
params.signal.removeEventListener("abort", abortFromParent);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const text = await response.text();
|
|
171
|
+
let parsed: unknown = undefined;
|
|
172
|
+
try {
|
|
173
|
+
parsed = text ? JSON.parse(text) : undefined;
|
|
174
|
+
} catch {
|
|
175
|
+
parsed = { raw: text };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const obj = asObj(parsed);
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const errObj = asObj(obj.error);
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
status: response.status,
|
|
185
|
+
latencyMs: Date.now() - startedAtMs,
|
|
186
|
+
error: {
|
|
187
|
+
type: typeof errObj.type === "string" ? errObj.type : undefined,
|
|
188
|
+
message:
|
|
189
|
+
(typeof errObj.message === "string" && errObj.message) ||
|
|
190
|
+
`Tool invoke failed with status ${response.status}`,
|
|
191
|
+
details: obj,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
status: response.status,
|
|
199
|
+
latencyMs: Date.now() - startedAtMs,
|
|
200
|
+
result: obj.result,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export type InvocationOutcome = "success" | "failure" | "timeout";
|
|
2
|
+
|
|
3
|
+
export type InvocationMetric = {
|
|
4
|
+
tool: string;
|
|
5
|
+
outcome: InvocationOutcome;
|
|
6
|
+
latencyMs: number;
|
|
7
|
+
originalChars?: number;
|
|
8
|
+
returnedChars?: number;
|
|
9
|
+
shaped?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ToolCounters = {
|
|
13
|
+
calls: number;
|
|
14
|
+
success: number;
|
|
15
|
+
failure: number;
|
|
16
|
+
timeout: number;
|
|
17
|
+
shaped: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MetricsSnapshot = {
|
|
21
|
+
startedAt: string;
|
|
22
|
+
totalCalls: number;
|
|
23
|
+
success: number;
|
|
24
|
+
failure: number;
|
|
25
|
+
timeout: number;
|
|
26
|
+
successRate: number;
|
|
27
|
+
timeoutRate: number;
|
|
28
|
+
avgLatencyMs: number;
|
|
29
|
+
p95LatencyApproxMs: number;
|
|
30
|
+
shapedResults: number;
|
|
31
|
+
shapedRateOnSuccess: number;
|
|
32
|
+
originalChars: number;
|
|
33
|
+
returnedChars: number;
|
|
34
|
+
charsSaved: number;
|
|
35
|
+
charReductionRate: number;
|
|
36
|
+
estimatedTokensSaved: number;
|
|
37
|
+
topTools: Array<{
|
|
38
|
+
tool: string;
|
|
39
|
+
calls: number;
|
|
40
|
+
success: number;
|
|
41
|
+
failure: number;
|
|
42
|
+
timeout: number;
|
|
43
|
+
shaped: number;
|
|
44
|
+
}>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function clampRate(value: number): number {
|
|
48
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
if (value > 1) {
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class MetricsStore {
|
|
58
|
+
private startedAtMs = Date.now();
|
|
59
|
+
private totalCalls = 0;
|
|
60
|
+
private success = 0;
|
|
61
|
+
private failure = 0;
|
|
62
|
+
private timeout = 0;
|
|
63
|
+
private shapedResults = 0;
|
|
64
|
+
private originalChars = 0;
|
|
65
|
+
private returnedChars = 0;
|
|
66
|
+
private latencyTotalMs = 0;
|
|
67
|
+
private readonly latencyRing: number[] = [];
|
|
68
|
+
private readonly latencyRingSize = 512;
|
|
69
|
+
private readonly perTool = new Map<string, ToolCounters>();
|
|
70
|
+
|
|
71
|
+
record(metric: InvocationMetric): void {
|
|
72
|
+
this.totalCalls += 1;
|
|
73
|
+
this.latencyTotalMs += Math.max(0, Math.trunc(metric.latencyMs));
|
|
74
|
+
|
|
75
|
+
this.latencyRing.push(Math.max(0, Math.trunc(metric.latencyMs)));
|
|
76
|
+
if (this.latencyRing.length > this.latencyRingSize) {
|
|
77
|
+
this.latencyRing.shift();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (metric.outcome === "success") {
|
|
81
|
+
this.success += 1;
|
|
82
|
+
} else if (metric.outcome === "timeout") {
|
|
83
|
+
this.timeout += 1;
|
|
84
|
+
} else {
|
|
85
|
+
this.failure += 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (metric.shaped) {
|
|
89
|
+
this.shapedResults += 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof metric.originalChars === "number" && Number.isFinite(metric.originalChars)) {
|
|
93
|
+
this.originalChars += Math.max(0, Math.trunc(metric.originalChars));
|
|
94
|
+
}
|
|
95
|
+
if (typeof metric.returnedChars === "number" && Number.isFinite(metric.returnedChars)) {
|
|
96
|
+
this.returnedChars += Math.max(0, Math.trunc(metric.returnedChars));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const tool = metric.tool.trim() || "unknown";
|
|
100
|
+
const bucket = this.perTool.get(tool) ?? {
|
|
101
|
+
calls: 0,
|
|
102
|
+
success: 0,
|
|
103
|
+
failure: 0,
|
|
104
|
+
timeout: 0,
|
|
105
|
+
shaped: 0,
|
|
106
|
+
};
|
|
107
|
+
bucket.calls += 1;
|
|
108
|
+
if (metric.outcome === "success") {
|
|
109
|
+
bucket.success += 1;
|
|
110
|
+
} else if (metric.outcome === "timeout") {
|
|
111
|
+
bucket.timeout += 1;
|
|
112
|
+
} else {
|
|
113
|
+
bucket.failure += 1;
|
|
114
|
+
}
|
|
115
|
+
if (metric.shaped) {
|
|
116
|
+
bucket.shaped += 1;
|
|
117
|
+
}
|
|
118
|
+
this.perTool.set(tool, bucket);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
reset(): void {
|
|
122
|
+
this.startedAtMs = Date.now();
|
|
123
|
+
this.totalCalls = 0;
|
|
124
|
+
this.success = 0;
|
|
125
|
+
this.failure = 0;
|
|
126
|
+
this.timeout = 0;
|
|
127
|
+
this.shapedResults = 0;
|
|
128
|
+
this.originalChars = 0;
|
|
129
|
+
this.returnedChars = 0;
|
|
130
|
+
this.latencyTotalMs = 0;
|
|
131
|
+
this.latencyRing.length = 0;
|
|
132
|
+
this.perTool.clear();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
snapshot(topN = 5): MetricsSnapshot {
|
|
136
|
+
const total = this.totalCalls;
|
|
137
|
+
const successRate = total > 0 ? this.success / total : 0;
|
|
138
|
+
const timeoutRate = total > 0 ? this.timeout / total : 0;
|
|
139
|
+
const avgLatencyMs = total > 0 ? this.latencyTotalMs / total : 0;
|
|
140
|
+
|
|
141
|
+
const sortedLatency = [...this.latencyRing].sort((a, b) => a - b);
|
|
142
|
+
const p95Index = sortedLatency.length > 0
|
|
143
|
+
? Math.min(sortedLatency.length - 1, Math.floor(sortedLatency.length * 0.95))
|
|
144
|
+
: 0;
|
|
145
|
+
const p95LatencyApproxMs = sortedLatency.length > 0 ? sortedLatency[p95Index] : 0;
|
|
146
|
+
|
|
147
|
+
const shapedRateOnSuccess = this.success > 0 ? this.shapedResults / this.success : 0;
|
|
148
|
+
const charsSaved = Math.max(0, this.originalChars - this.returnedChars);
|
|
149
|
+
const charReductionRate = this.originalChars > 0 ? charsSaved / this.originalChars : 0;
|
|
150
|
+
const estimatedTokensSaved = Math.round(charsSaved / 4);
|
|
151
|
+
|
|
152
|
+
const topTools = [...this.perTool.entries()]
|
|
153
|
+
.map(([tool, c]) => ({ tool, ...c }))
|
|
154
|
+
.sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool))
|
|
155
|
+
.slice(0, Math.max(1, topN));
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
startedAt: new Date(this.startedAtMs).toISOString(),
|
|
159
|
+
totalCalls: total,
|
|
160
|
+
success: this.success,
|
|
161
|
+
failure: this.failure,
|
|
162
|
+
timeout: this.timeout,
|
|
163
|
+
successRate: clampRate(successRate),
|
|
164
|
+
timeoutRate: clampRate(timeoutRate),
|
|
165
|
+
avgLatencyMs,
|
|
166
|
+
p95LatencyApproxMs,
|
|
167
|
+
shapedResults: this.shapedResults,
|
|
168
|
+
shapedRateOnSuccess: clampRate(shapedRateOnSuccess),
|
|
169
|
+
originalChars: this.originalChars,
|
|
170
|
+
returnedChars: this.returnedChars,
|
|
171
|
+
charsSaved,
|
|
172
|
+
charReductionRate: clampRate(charReductionRate),
|
|
173
|
+
estimatedTokensSaved,
|
|
174
|
+
topTools,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export type AdvToolsMode = "safe" | "standard";
|
|
4
|
+
|
|
5
|
+
export type AdvToolsSettings = {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
mode: AdvToolsMode;
|
|
8
|
+
maxSteps: number;
|
|
9
|
+
maxForEach: number;
|
|
10
|
+
maxResultChars: number;
|
|
11
|
+
invokeTimeoutMs: number;
|
|
12
|
+
storeLargeResults: boolean;
|
|
13
|
+
resultStoreTtlSec: number;
|
|
14
|
+
resultSampleItems: number;
|
|
15
|
+
requireSandbox: boolean;
|
|
16
|
+
denyControlPlane: boolean;
|
|
17
|
+
toolSearch: {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
defaultLimit: number;
|
|
20
|
+
useLiveRegistry: boolean;
|
|
21
|
+
liveTimeoutMs: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_SETTINGS: AdvToolsSettings = {
|
|
26
|
+
enabled: true,
|
|
27
|
+
mode: "standard",
|
|
28
|
+
maxSteps: 25,
|
|
29
|
+
maxForEach: 20,
|
|
30
|
+
maxResultChars: 40000,
|
|
31
|
+
invokeTimeoutMs: 0,
|
|
32
|
+
storeLargeResults: true,
|
|
33
|
+
resultStoreTtlSec: 1800,
|
|
34
|
+
resultSampleItems: 8,
|
|
35
|
+
requireSandbox: false,
|
|
36
|
+
denyControlPlane: true,
|
|
37
|
+
toolSearch: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
defaultLimit: 8,
|
|
40
|
+
useLiveRegistry: true,
|
|
41
|
+
liveTimeoutMs: 1500,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function asObj(value: unknown): Record<string, unknown> {
|
|
46
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
47
|
+
? (value as Record<string, unknown>)
|
|
48
|
+
: {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function asBool(value: unknown, fallback: boolean): boolean {
|
|
52
|
+
return typeof value === "boolean" ? value : fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function asInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
56
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function asMode(value: unknown, fallback: AdvToolsMode): AdvToolsMode {
|
|
63
|
+
return value === "safe" || value === "standard" ? value : fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveSettings(api: OpenClawPluginApi, cfg: OpenClawConfig = api.config): AdvToolsSettings {
|
|
67
|
+
const plugins = asObj((cfg as Record<string, unknown>).plugins);
|
|
68
|
+
const entries = asObj(plugins.entries);
|
|
69
|
+
const entry = asObj(entries[api.id]);
|
|
70
|
+
const pluginCfg = asObj(entry.config ?? api.pluginConfig);
|
|
71
|
+
const ts = asObj(pluginCfg.toolSearch);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
enabled: asBool(pluginCfg.enabled, DEFAULT_SETTINGS.enabled),
|
|
75
|
+
mode: asMode(pluginCfg.mode, DEFAULT_SETTINGS.mode),
|
|
76
|
+
maxSteps: asInt(pluginCfg.maxSteps, DEFAULT_SETTINGS.maxSteps, 1, 200),
|
|
77
|
+
maxForEach: asInt(pluginCfg.maxForEach, DEFAULT_SETTINGS.maxForEach, 1, 200),
|
|
78
|
+
maxResultChars: asInt(pluginCfg.maxResultChars, DEFAULT_SETTINGS.maxResultChars, 500, 500000),
|
|
79
|
+
invokeTimeoutMs: asInt(pluginCfg.invokeTimeoutMs, DEFAULT_SETTINGS.invokeTimeoutMs, 0, 1800000),
|
|
80
|
+
storeLargeResults: asBool(pluginCfg.storeLargeResults, DEFAULT_SETTINGS.storeLargeResults),
|
|
81
|
+
resultStoreTtlSec: asInt(
|
|
82
|
+
pluginCfg.resultStoreTtlSec,
|
|
83
|
+
DEFAULT_SETTINGS.resultStoreTtlSec,
|
|
84
|
+
60,
|
|
85
|
+
86400,
|
|
86
|
+
),
|
|
87
|
+
resultSampleItems: asInt(
|
|
88
|
+
pluginCfg.resultSampleItems,
|
|
89
|
+
DEFAULT_SETTINGS.resultSampleItems,
|
|
90
|
+
1,
|
|
91
|
+
50,
|
|
92
|
+
),
|
|
93
|
+
requireSandbox: asBool(pluginCfg.requireSandbox, DEFAULT_SETTINGS.requireSandbox),
|
|
94
|
+
denyControlPlane: asBool(pluginCfg.denyControlPlane, DEFAULT_SETTINGS.denyControlPlane),
|
|
95
|
+
toolSearch: {
|
|
96
|
+
enabled: asBool(ts.enabled, DEFAULT_SETTINGS.toolSearch.enabled),
|
|
97
|
+
defaultLimit: asInt(ts.defaultLimit, DEFAULT_SETTINGS.toolSearch.defaultLimit, 1, 50),
|
|
98
|
+
useLiveRegistry: asBool(ts.useLiveRegistry, DEFAULT_SETTINGS.toolSearch.useLiveRegistry),
|
|
99
|
+
liveTimeoutMs: asInt(ts.liveTimeoutMs, DEFAULT_SETTINGS.toolSearch.liveTimeoutMs, 250, 10000),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deepCloneConfig<T>(value: T): T {
|
|
105
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function writeConfig(api: OpenClawPluginApi, cfg: OpenClawConfig): Promise<void> {
|
|
109
|
+
await api.runtime.config.writeConfigFile(cfg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function ensurePluginEntry(cfg: OpenClawConfig, pluginId: string): {
|
|
113
|
+
pluginsObj: Record<string, unknown>;
|
|
114
|
+
entriesObj: Record<string, unknown>;
|
|
115
|
+
entryObj: Record<string, unknown>;
|
|
116
|
+
} {
|
|
117
|
+
const root = cfg as Record<string, unknown>;
|
|
118
|
+
const pluginsObj = asObj(root.plugins);
|
|
119
|
+
root.plugins = pluginsObj;
|
|
120
|
+
|
|
121
|
+
const entriesObj = asObj(pluginsObj.entries);
|
|
122
|
+
pluginsObj.entries = entriesObj;
|
|
123
|
+
|
|
124
|
+
const entryObj = asObj(entriesObj[pluginId]);
|
|
125
|
+
entriesObj[pluginId] = entryObj;
|
|
126
|
+
|
|
127
|
+
return { pluginsObj, entriesObj, entryObj };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function mergeUniqueStrings(items: unknown[], additions: string[]): string[] {
|
|
131
|
+
const merged = new Set<string>();
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
if (typeof item === "string" && item.trim()) {
|
|
134
|
+
merged.add(item.trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const item of additions) {
|
|
138
|
+
if (item.trim()) {
|
|
139
|
+
merged.add(item.trim());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...merged];
|
|
143
|
+
}
|
package/src/lib/refs.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function normalizePath(pathExpr: string): string[] {
|
|
2
|
+
const trimmed = pathExpr.trim().replace(/^\$/, "");
|
|
3
|
+
const bracketToDot = trimmed
|
|
4
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
5
|
+
.replace(/\[['\"]([^'\"]+)['\"]\]/g, ".$1");
|
|
6
|
+
return bracketToDot.split(".").filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readPath(root: unknown, pathExpr: string): unknown {
|
|
10
|
+
const parts = normalizePath(pathExpr);
|
|
11
|
+
let cursor: unknown = root;
|
|
12
|
+
for (const part of parts) {
|
|
13
|
+
if (cursor === null || cursor === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (typeof cursor !== "object") {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
20
|
+
}
|
|
21
|
+
return cursor;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stringifyTemplateValue(value: unknown): string {
|
|
25
|
+
if (value === null || value === undefined) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveValueTemplates(value: unknown, context: Record<string, unknown>): unknown {
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
|
|
41
|
+
if (trimmed.startsWith("$") && !trimmed.includes(" ") && !trimmed.includes("{{")) {
|
|
42
|
+
return readPath(context, trimmed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!value.includes("{{")) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return value.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, group) => {
|
|
50
|
+
const resolved = readPath(context, String(group));
|
|
51
|
+
return stringifyTemplateValue(resolved);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value.map((entry) => resolveValueTemplates(entry, context));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!value || typeof value !== "object") {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const out: Record<string, unknown> = {};
|
|
64
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
65
|
+
out[k] = resolveValueTemplates(v, context);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|