@jhytabest/plashboard 0.1.3 → 0.1.5
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/README.md +89 -0
- package/package.json +1 -1
- package/skills/plashboard-admin/SKILL.md +6 -0
- package/src/plugin.ts +450 -1
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @jhytabest/plashboard
|
|
2
|
+
|
|
3
|
+
Template-driven dashboard runtime plugin for OpenClaw.
|
|
4
|
+
|
|
5
|
+
This plugin manages dashboard templates, scheduled fill runs, validation, and atomic publish to a live `dashboard.json`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @jhytabest/plashboard
|
|
11
|
+
openclaw plugins enable plashboard
|
|
12
|
+
openclaw plugins doctor
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Update
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
openclaw plugins update plashboard
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Minimal Config
|
|
22
|
+
|
|
23
|
+
Add to `openclaw.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"plugins": {
|
|
28
|
+
"entries": {
|
|
29
|
+
"plashboard": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"config": {
|
|
32
|
+
"data_dir": "/var/lib/openclaw/plash-data",
|
|
33
|
+
"scheduler_tick_seconds": 30,
|
|
34
|
+
"max_parallel_runs": 1,
|
|
35
|
+
"default_retry_count": 1,
|
|
36
|
+
"retry_backoff_seconds": 20,
|
|
37
|
+
"session_timeout_seconds": 90,
|
|
38
|
+
"fill_provider": "mock",
|
|
39
|
+
"display_profile": {
|
|
40
|
+
"width_px": 1920,
|
|
41
|
+
"height_px": 1080,
|
|
42
|
+
"safe_top_px": 96,
|
|
43
|
+
"safe_bottom_px": 106,
|
|
44
|
+
"safe_side_px": 28,
|
|
45
|
+
"layout_safety_margin_px": 24
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For real model runs, switch `fill_provider` to `command` and provide `fill_command`.
|
|
55
|
+
|
|
56
|
+
## Runtime Command
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
/plashboard setup [mock|command <fill_command>]
|
|
60
|
+
/plashboard expose-guide [local_url] [https_port]
|
|
61
|
+
/plashboard expose-check [local_url] [https_port]
|
|
62
|
+
/plashboard init
|
|
63
|
+
/plashboard status
|
|
64
|
+
/plashboard list
|
|
65
|
+
/plashboard activate <template-id>
|
|
66
|
+
/plashboard copy <source-template-id> <new-template-id> [new-name] [activate]
|
|
67
|
+
/plashboard delete <template-id>
|
|
68
|
+
/plashboard run <template-id>
|
|
69
|
+
/plashboard set-display <width> <height> <safe_top> <safe_bottom>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Recommended first run:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
/plashboard setup mock
|
|
76
|
+
/plashboard init
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Tailscale helper flow:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
/plashboard expose-guide
|
|
83
|
+
/plashboard expose-check
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Notes
|
|
87
|
+
|
|
88
|
+
- The plugin includes an admin skill (`plashboard-admin`) for tool-guided management.
|
|
89
|
+
- Trusted publishing (OIDC) is enabled in CI/CD for npm releases.
|
package/package.json
CHANGED
|
@@ -17,6 +17,9 @@ Use this skill for plashboard runtime administration.
|
|
|
17
17
|
|
|
18
18
|
## Required Tooling
|
|
19
19
|
Always use plugin tools:
|
|
20
|
+
- `plashboard_setup`
|
|
21
|
+
- `plashboard_exposure_guide`
|
|
22
|
+
- `plashboard_exposure_check`
|
|
20
23
|
- `plashboard_init`
|
|
21
24
|
- `plashboard_template_create`
|
|
22
25
|
- `plashboard_template_update`
|
|
@@ -36,6 +39,9 @@ Always use plugin tools:
|
|
|
36
39
|
- Never ask the model to generate full dashboard structure when filling values.
|
|
37
40
|
|
|
38
41
|
## Command Shortcuts
|
|
42
|
+
- `/plashboard setup [mock|command <fill_command>]`
|
|
43
|
+
- `/plashboard expose-guide [local_url] [https_port]`
|
|
44
|
+
- `/plashboard expose-check [local_url] [https_port]`
|
|
39
45
|
- `/plashboard init`
|
|
40
46
|
- `/plashboard status`
|
|
41
47
|
- `/plashboard list`
|
package/src/plugin.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { access, stat } from 'node:fs/promises';
|
|
1
4
|
import type { DisplayProfile, ToolResponse } from './types.js';
|
|
2
5
|
import { resolveConfig } from './config.js';
|
|
3
6
|
import { PlashboardRuntime } from './runtime.js';
|
|
@@ -11,6 +14,12 @@ type UnknownApi = {
|
|
|
11
14
|
warn?: (...args: unknown[]) => void;
|
|
12
15
|
error?: (...args: unknown[]) => void;
|
|
13
16
|
};
|
|
17
|
+
runtime?: {
|
|
18
|
+
config?: {
|
|
19
|
+
loadConfig?: () => unknown;
|
|
20
|
+
writeConfigFile?: (nextConfig: unknown) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
14
23
|
config?: unknown;
|
|
15
24
|
pluginConfig?: unknown;
|
|
16
25
|
};
|
|
@@ -42,6 +51,361 @@ function asString(value: unknown): string {
|
|
|
42
51
|
return typeof value === 'string' ? value : '';
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
function asNumber(value: unknown): number | undefined {
|
|
55
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
56
|
+
if (typeof value === 'string' && value.trim()) {
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function asErrorMessage(error: unknown): string {
|
|
64
|
+
if (error instanceof Error && error.message) return error.message;
|
|
65
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
66
|
+
return 'unknown error';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type SetupParams = {
|
|
70
|
+
fill_provider?: 'mock' | 'command';
|
|
71
|
+
fill_command?: string;
|
|
72
|
+
data_dir?: string;
|
|
73
|
+
scheduler_tick_seconds?: number;
|
|
74
|
+
session_timeout_seconds?: number;
|
|
75
|
+
width_px?: number;
|
|
76
|
+
height_px?: number;
|
|
77
|
+
safe_top_px?: number;
|
|
78
|
+
safe_bottom_px?: number;
|
|
79
|
+
safe_side_px?: number;
|
|
80
|
+
layout_safety_margin_px?: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type ExposureParams = {
|
|
84
|
+
local_url?: string;
|
|
85
|
+
tailscale_https_port?: number;
|
|
86
|
+
dashboard_output_path?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type CommandExecResult = {
|
|
90
|
+
ok: boolean;
|
|
91
|
+
stdout: string;
|
|
92
|
+
stderr: string;
|
|
93
|
+
code: number | null;
|
|
94
|
+
error?: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function normalizeLocalUrl(raw: string | undefined): string {
|
|
98
|
+
const fallback = 'http://127.0.0.1:18888';
|
|
99
|
+
if (!raw || !raw.trim()) return fallback;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = new URL(raw.trim());
|
|
102
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return fallback;
|
|
103
|
+
return parsed.toString();
|
|
104
|
+
} catch {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizePort(raw: number | undefined, fallback: number): number {
|
|
110
|
+
const value = typeof raw === 'number' && Number.isFinite(raw) ? Math.floor(raw) : fallback;
|
|
111
|
+
return Math.max(1, Math.min(65535, value));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function runCommand(binary: string, args: string[], timeoutMs: number): Promise<CommandExecResult> {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const child = spawn(binary, args, {
|
|
117
|
+
env: process.env
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
let stdout = '';
|
|
121
|
+
let stderr = '';
|
|
122
|
+
let settled = false;
|
|
123
|
+
|
|
124
|
+
const finish = (result: CommandExecResult) => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
resolve(result);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
child.kill('SIGKILL');
|
|
132
|
+
finish({
|
|
133
|
+
ok: false,
|
|
134
|
+
stdout,
|
|
135
|
+
stderr,
|
|
136
|
+
code: null,
|
|
137
|
+
error: `timed out after ${Math.floor(timeoutMs / 1000)}s`
|
|
138
|
+
});
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
|
|
141
|
+
child.stdout.on('data', (chunk) => {
|
|
142
|
+
stdout += String(chunk);
|
|
143
|
+
});
|
|
144
|
+
child.stderr.on('data', (chunk) => {
|
|
145
|
+
stderr += String(chunk);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
child.on('error', (error) => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
finish({
|
|
151
|
+
ok: false,
|
|
152
|
+
stdout,
|
|
153
|
+
stderr,
|
|
154
|
+
code: null,
|
|
155
|
+
error: asString((error as { message?: unknown }).message) || 'spawn failed'
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
child.on('close', (code) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
finish({
|
|
162
|
+
ok: code === 0,
|
|
163
|
+
stdout: stdout.trim(),
|
|
164
|
+
stderr: stderr.trim(),
|
|
165
|
+
code
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function buildExposureGuide(resolvedConfig: ReturnType<typeof resolveConfig>, params: ExposureParams = {}) {
|
|
172
|
+
const localUrl = normalizeLocalUrl(params.local_url);
|
|
173
|
+
const httpsPort = normalizePort(asNumber(params.tailscale_https_port), 8444);
|
|
174
|
+
const dashboardPath = (params.dashboard_output_path || resolvedConfig.dashboard_output_path).trim();
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
errors: [],
|
|
179
|
+
data: {
|
|
180
|
+
local_url: localUrl,
|
|
181
|
+
tailscale_https_port: httpsPort,
|
|
182
|
+
dashboard_output_path: dashboardPath,
|
|
183
|
+
commands: [
|
|
184
|
+
`tailscale serve status`,
|
|
185
|
+
`tailscale serve --https=${httpsPort} ${localUrl}`,
|
|
186
|
+
`tailscale serve status`,
|
|
187
|
+
`tailscale serve --https=${httpsPort} off`
|
|
188
|
+
],
|
|
189
|
+
checks: [
|
|
190
|
+
`test -f ${dashboardPath}`,
|
|
191
|
+
`curl -I ${localUrl}`
|
|
192
|
+
],
|
|
193
|
+
notes: [
|
|
194
|
+
'plashboard only writes dashboard JSON; your local UI/server must serve it.',
|
|
195
|
+
'the tailscale mapping reuses your existing tailnet identity.',
|
|
196
|
+
'choose a port not already used by another tailscale serve mapping.'
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runExposureCheck(resolvedConfig: ReturnType<typeof resolveConfig>, params: ExposureParams = {}) {
|
|
203
|
+
const localUrl = normalizeLocalUrl(params.local_url);
|
|
204
|
+
const httpsPort = normalizePort(asNumber(params.tailscale_https_port), 8444);
|
|
205
|
+
const dashboardPath = (params.dashboard_output_path || resolvedConfig.dashboard_output_path).trim();
|
|
206
|
+
const errors: string[] = [];
|
|
207
|
+
|
|
208
|
+
let dashboardExists = false;
|
|
209
|
+
let dashboardSizeBytes: number | undefined;
|
|
210
|
+
let dashboardMtimeIso: string | undefined;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await access(dashboardPath, fsConstants.R_OK);
|
|
214
|
+
const info = await stat(dashboardPath);
|
|
215
|
+
dashboardExists = true;
|
|
216
|
+
dashboardSizeBytes = info.size;
|
|
217
|
+
dashboardMtimeIso = info.mtime.toISOString();
|
|
218
|
+
} catch {
|
|
219
|
+
errors.push(`dashboard file is not readable: ${dashboardPath}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let localUrlOk = false;
|
|
223
|
+
let localStatusCode: number | undefined;
|
|
224
|
+
let localError: string | undefined;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
229
|
+
const response = await fetch(localUrl, {
|
|
230
|
+
method: 'GET',
|
|
231
|
+
signal: controller.signal
|
|
232
|
+
});
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
localStatusCode = response.status;
|
|
235
|
+
localUrlOk = response.status >= 200 && response.status < 500;
|
|
236
|
+
if (!localUrlOk) {
|
|
237
|
+
errors.push(`local dashboard URL returned status ${response.status}: ${localUrl}`);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
localError = asErrorMessage(error);
|
|
241
|
+
errors.push(`local dashboard URL is not reachable: ${localUrl} (${localError})`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const tailscale = await runCommand('tailscale', ['serve', 'status'], 8000);
|
|
245
|
+
const tailscaleOutput = `${tailscale.stdout}\n${tailscale.stderr}`.trim();
|
|
246
|
+
let tailscalePortConfigured = false;
|
|
247
|
+
|
|
248
|
+
if (!tailscale.ok) {
|
|
249
|
+
errors.push(`tailscale serve status failed: ${tailscale.error || tailscale.stderr || `exit ${tailscale.code}`}`);
|
|
250
|
+
} else {
|
|
251
|
+
tailscalePortConfigured = tailscaleOutput.includes(`:${httpsPort}`);
|
|
252
|
+
if (!tailscalePortConfigured) {
|
|
253
|
+
errors.push(`tailscale serve has no mapping for https port ${httpsPort}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
ok: errors.length === 0,
|
|
259
|
+
errors,
|
|
260
|
+
data: {
|
|
261
|
+
dashboard_output_path: dashboardPath,
|
|
262
|
+
dashboard_exists: dashboardExists,
|
|
263
|
+
dashboard_size_bytes: dashboardSizeBytes,
|
|
264
|
+
dashboard_mtime_utc: dashboardMtimeIso,
|
|
265
|
+
local_url: localUrl,
|
|
266
|
+
local_url_ok: localUrlOk,
|
|
267
|
+
local_status_code: localStatusCode,
|
|
268
|
+
local_error: localError,
|
|
269
|
+
tailscale_https_port: httpsPort,
|
|
270
|
+
tailscale_status_ok: tailscale.ok,
|
|
271
|
+
tailscale_port_configured: tailscalePortConfigured,
|
|
272
|
+
tailscale_status_excerpt: tailscaleOutput.slice(0, 1200)
|
|
273
|
+
}
|
|
274
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resolveConfig>, params: SetupParams = {}) {
|
|
278
|
+
const loadConfig = api.runtime?.config?.loadConfig;
|
|
279
|
+
const writeConfigFile = api.runtime?.config?.writeConfigFile;
|
|
280
|
+
|
|
281
|
+
if (!loadConfig || !writeConfigFile) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
errors: ['setup is unavailable: runtime config API is not exposed by this OpenClaw build']
|
|
285
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const rootConfig = asObject(loadConfig());
|
|
289
|
+
const plugins = asObject(rootConfig.plugins);
|
|
290
|
+
const entries = asObject(plugins.entries);
|
|
291
|
+
const currentEntry = asObject(entries.plashboard);
|
|
292
|
+
const currentPluginConfig = asObject(currentEntry.config);
|
|
293
|
+
|
|
294
|
+
const existingDisplay = asObject(currentPluginConfig.display_profile);
|
|
295
|
+
const displayProfile = {
|
|
296
|
+
width_px: Math.max(
|
|
297
|
+
320,
|
|
298
|
+
Math.floor(asNumber(params.width_px) ?? asNumber(existingDisplay.width_px) ?? resolvedConfig.display_profile.width_px)
|
|
299
|
+
),
|
|
300
|
+
height_px: Math.max(
|
|
301
|
+
240,
|
|
302
|
+
Math.floor(asNumber(params.height_px) ?? asNumber(existingDisplay.height_px) ?? resolvedConfig.display_profile.height_px)
|
|
303
|
+
),
|
|
304
|
+
safe_top_px: Math.max(
|
|
305
|
+
0,
|
|
306
|
+
Math.floor(asNumber(params.safe_top_px) ?? asNumber(existingDisplay.safe_top_px) ?? resolvedConfig.display_profile.safe_top_px)
|
|
307
|
+
),
|
|
308
|
+
safe_bottom_px: Math.max(
|
|
309
|
+
0,
|
|
310
|
+
Math.floor(
|
|
311
|
+
asNumber(params.safe_bottom_px) ?? asNumber(existingDisplay.safe_bottom_px) ?? resolvedConfig.display_profile.safe_bottom_px
|
|
312
|
+
)
|
|
313
|
+
),
|
|
314
|
+
safe_side_px: Math.max(
|
|
315
|
+
0,
|
|
316
|
+
Math.floor(asNumber(params.safe_side_px) ?? asNumber(existingDisplay.safe_side_px) ?? resolvedConfig.display_profile.safe_side_px)
|
|
317
|
+
),
|
|
318
|
+
layout_safety_margin_px: Math.max(
|
|
319
|
+
0,
|
|
320
|
+
Math.floor(
|
|
321
|
+
asNumber(params.layout_safety_margin_px)
|
|
322
|
+
?? asNumber(existingDisplay.layout_safety_margin_px)
|
|
323
|
+
?? resolvedConfig.display_profile.layout_safety_margin_px
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const selectedProvider =
|
|
329
|
+
params.fill_provider
|
|
330
|
+
|| (currentPluginConfig.fill_provider === 'command' ? 'command' : currentPluginConfig.fill_provider === 'mock' ? 'mock' : resolvedConfig.fill_provider);
|
|
331
|
+
const selectedCommand = (
|
|
332
|
+
params.fill_command
|
|
333
|
+
|| asString(currentPluginConfig.fill_command)
|
|
334
|
+
|| asString(resolvedConfig.fill_command)
|
|
335
|
+
).trim();
|
|
336
|
+
|
|
337
|
+
if (selectedProvider === 'command' && !selectedCommand) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
errors: ['fill_provider=command requires fill_command']
|
|
341
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const nextPluginConfig: Record<string, unknown> = {
|
|
345
|
+
...currentPluginConfig,
|
|
346
|
+
data_dir: params.data_dir || asString(currentPluginConfig.data_dir) || resolvedConfig.data_dir,
|
|
347
|
+
scheduler_tick_seconds: Math.max(
|
|
348
|
+
5,
|
|
349
|
+
Math.floor(
|
|
350
|
+
asNumber(params.scheduler_tick_seconds)
|
|
351
|
+
?? asNumber(currentPluginConfig.scheduler_tick_seconds)
|
|
352
|
+
?? resolvedConfig.scheduler_tick_seconds
|
|
353
|
+
)
|
|
354
|
+
),
|
|
355
|
+
session_timeout_seconds: Math.max(
|
|
356
|
+
10,
|
|
357
|
+
Math.floor(
|
|
358
|
+
asNumber(params.session_timeout_seconds)
|
|
359
|
+
?? asNumber(currentPluginConfig.session_timeout_seconds)
|
|
360
|
+
?? resolvedConfig.session_timeout_seconds
|
|
361
|
+
)
|
|
362
|
+
),
|
|
363
|
+
fill_provider: selectedProvider,
|
|
364
|
+
display_profile: displayProfile
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (selectedCommand) {
|
|
368
|
+
nextPluginConfig.fill_command = selectedCommand;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const nextRootConfig = {
|
|
372
|
+
...rootConfig,
|
|
373
|
+
plugins: {
|
|
374
|
+
...plugins,
|
|
375
|
+
entries: {
|
|
376
|
+
...entries,
|
|
377
|
+
plashboard: {
|
|
378
|
+
...currentEntry,
|
|
379
|
+
enabled: true,
|
|
380
|
+
config: nextPluginConfig
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
await writeConfigFile(nextRootConfig);
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
errors: [],
|
|
391
|
+
data: {
|
|
392
|
+
configured: true,
|
|
393
|
+
restart_required: true,
|
|
394
|
+
plugin_id: 'plashboard',
|
|
395
|
+
fill_provider: selectedProvider,
|
|
396
|
+
fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
|
|
397
|
+
data_dir: nextPluginConfig.data_dir,
|
|
398
|
+
scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
|
|
399
|
+
session_timeout_seconds: nextPluginConfig.session_timeout_seconds,
|
|
400
|
+
display_profile: displayProfile,
|
|
401
|
+
next_steps: [
|
|
402
|
+
'restart OpenClaw gateway',
|
|
403
|
+
'run /plashboard init'
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
407
|
+
}
|
|
408
|
+
|
|
45
409
|
export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
46
410
|
const config = resolveConfig(api);
|
|
47
411
|
const runtime = new PlashboardRuntime(config, {
|
|
@@ -60,6 +424,65 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
60
424
|
}
|
|
61
425
|
});
|
|
62
426
|
|
|
427
|
+
api.registerTool?.({
|
|
428
|
+
name: 'plashboard_exposure_guide',
|
|
429
|
+
description: 'Return copy-paste commands to expose dashboard UI over existing Tailscale.',
|
|
430
|
+
optional: true,
|
|
431
|
+
parameters: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
local_url: { type: 'string' },
|
|
435
|
+
tailscale_https_port: { type: 'number' },
|
|
436
|
+
dashboard_output_path: { type: 'string' }
|
|
437
|
+
},
|
|
438
|
+
additionalProperties: false
|
|
439
|
+
},
|
|
440
|
+
execute: async (_toolCallId: unknown, params: ExposureParams = {}) =>
|
|
441
|
+
toToolResult(await buildExposureGuide(config, params))
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
api.registerTool?.({
|
|
445
|
+
name: 'plashboard_exposure_check',
|
|
446
|
+
description: 'Check dashboard file, local URL, and tailscale serve mapping health.',
|
|
447
|
+
optional: true,
|
|
448
|
+
parameters: {
|
|
449
|
+
type: 'object',
|
|
450
|
+
properties: {
|
|
451
|
+
local_url: { type: 'string' },
|
|
452
|
+
tailscale_https_port: { type: 'number' },
|
|
453
|
+
dashboard_output_path: { type: 'string' }
|
|
454
|
+
},
|
|
455
|
+
additionalProperties: false
|
|
456
|
+
},
|
|
457
|
+
execute: async (_toolCallId: unknown, params: ExposureParams = {}) =>
|
|
458
|
+
toToolResult(await runExposureCheck(config, params))
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
api.registerTool?.({
|
|
462
|
+
name: 'plashboard_setup',
|
|
463
|
+
description: 'Bootstrap or update plashboard plugin configuration in openclaw.json.',
|
|
464
|
+
optional: true,
|
|
465
|
+
parameters: {
|
|
466
|
+
type: 'object',
|
|
467
|
+
properties: {
|
|
468
|
+
fill_provider: { type: 'string', enum: ['mock', 'command'] },
|
|
469
|
+
fill_command: { type: 'string' },
|
|
470
|
+
data_dir: { type: 'string' },
|
|
471
|
+
scheduler_tick_seconds: { type: 'number' },
|
|
472
|
+
session_timeout_seconds: { type: 'number' },
|
|
473
|
+
width_px: { type: 'number' },
|
|
474
|
+
height_px: { type: 'number' },
|
|
475
|
+
safe_top_px: { type: 'number' },
|
|
476
|
+
safe_bottom_px: { type: 'number' },
|
|
477
|
+
safe_side_px: { type: 'number' },
|
|
478
|
+
layout_safety_margin_px: { type: 'number' }
|
|
479
|
+
},
|
|
480
|
+
additionalProperties: false
|
|
481
|
+
},
|
|
482
|
+
execute: async (_toolCallId: unknown, params: SetupParams = {}) =>
|
|
483
|
+
toToolResult(await runSetup(api, config, params))
|
|
484
|
+
});
|
|
485
|
+
|
|
63
486
|
api.registerTool?.({
|
|
64
487
|
name: 'plashboard_init',
|
|
65
488
|
description: 'Initialize plashboard state directories and optional default template.',
|
|
@@ -240,6 +663,32 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
240
663
|
const args = asString(ctx.args).split(/\s+/).filter(Boolean);
|
|
241
664
|
const [cmd, ...rest] = args;
|
|
242
665
|
|
|
666
|
+
if (cmd === 'expose-guide') {
|
|
667
|
+
const localUrl = rest.find((token) => token.startsWith('http://') || token.startsWith('https://'));
|
|
668
|
+
const portToken = rest.find((token) => /^[0-9]+$/.test(token));
|
|
669
|
+
return toCommandResult(
|
|
670
|
+
await buildExposureGuide(config, {
|
|
671
|
+
local_url: localUrl,
|
|
672
|
+
tailscale_https_port: portToken ? Number(portToken) : undefined
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (cmd === 'expose-check') {
|
|
677
|
+
const localUrl = rest.find((token) => token.startsWith('http://') || token.startsWith('https://'));
|
|
678
|
+
const portToken = rest.find((token) => /^[0-9]+$/.test(token));
|
|
679
|
+
return toCommandResult(
|
|
680
|
+
await runExposureCheck(config, {
|
|
681
|
+
local_url: localUrl,
|
|
682
|
+
tailscale_https_port: portToken ? Number(portToken) : undefined
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (cmd === 'setup') {
|
|
687
|
+
const mode = asString(rest[0]).toLowerCase();
|
|
688
|
+
const fillProvider = mode === 'command' || mode === 'mock' ? mode : undefined;
|
|
689
|
+
const fillCommand = fillProvider === 'command' ? rest.slice(1).join(' ').trim() || undefined : undefined;
|
|
690
|
+
return toCommandResult(await runSetup(api, config, { fill_provider: fillProvider, fill_command: fillCommand }));
|
|
691
|
+
}
|
|
243
692
|
if (cmd === 'init') return toCommandResult(await runtime.init());
|
|
244
693
|
if (cmd === 'status') return toCommandResult(await runtime.status());
|
|
245
694
|
if (cmd === 'list') return toCommandResult(await runtime.templateList());
|
|
@@ -264,7 +713,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
264
713
|
return toCommandResult({
|
|
265
714
|
ok: false,
|
|
266
715
|
errors: [
|
|
267
|
-
'unknown command. supported: init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
|
|
716
|
+
'unknown command. supported: setup [mock|command <fill_command>], expose-guide [local_url] [https_port], expose-check [local_url] [https_port], init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
|
|
268
717
|
]
|
|
269
718
|
});
|
|
270
719
|
}
|