@jhytabest/plashboard 0.1.5 → 0.1.6
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 +6 -4
- package/openclaw.plugin.json +3 -2
- package/package.json +1 -1
- package/skills/plashboard-admin/SKILL.md +1 -1
- package/src/config.ts +7 -2
- package/src/fill-runner.test.ts +120 -0
- package/src/fill-runner.ts +224 -33
- package/src/plugin.ts +84 -6
- package/src/runtime.ts +6 -4
- package/src/types.ts +2 -1
package/README.md
CHANGED
|
@@ -35,7 +35,8 @@ Add to `openclaw.json`:
|
|
|
35
35
|
"default_retry_count": 1,
|
|
36
36
|
"retry_backoff_seconds": 20,
|
|
37
37
|
"session_timeout_seconds": 90,
|
|
38
|
-
"fill_provider": "
|
|
38
|
+
"fill_provider": "openclaw",
|
|
39
|
+
"openclaw_fill_agent_id": "main",
|
|
39
40
|
"display_profile": {
|
|
40
41
|
"width_px": 1920,
|
|
41
42
|
"height_px": 1080,
|
|
@@ -51,12 +52,13 @@ Add to `openclaw.json`:
|
|
|
51
52
|
}
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
`fill_provider: "openclaw"` is the default real mode and calls `openclaw agent` directly.
|
|
56
|
+
Use `fill_provider: "command"` only if you need a custom external runner.
|
|
55
57
|
|
|
56
58
|
## Runtime Command
|
|
57
59
|
|
|
58
60
|
```text
|
|
59
|
-
/plashboard setup [mock|command <fill_command>]
|
|
61
|
+
/plashboard setup [openclaw [agent_id]|mock|command <fill_command>]
|
|
60
62
|
/plashboard expose-guide [local_url] [https_port]
|
|
61
63
|
/plashboard expose-check [local_url] [https_port]
|
|
62
64
|
/plashboard init
|
|
@@ -72,7 +74,7 @@ For real model runs, switch `fill_provider` to `command` and provide `fill_comma
|
|
|
72
74
|
Recommended first run:
|
|
73
75
|
|
|
74
76
|
```text
|
|
75
|
-
/plashboard setup
|
|
77
|
+
/plashboard setup openclaw
|
|
76
78
|
/plashboard init
|
|
77
79
|
```
|
|
78
80
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "plashboard",
|
|
3
3
|
"name": "Plashboard",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
5
5
|
"description": "Template-driven dashboard runtime with scheduled OpenClaw fills and safe publish.",
|
|
6
6
|
"entry": "./src/index.ts",
|
|
7
7
|
"skills": ["./skills/plashboard-admin"],
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"default_retry_count": { "type": "integer", "minimum": 0, "maximum": 5, "default": 1 },
|
|
16
16
|
"retry_backoff_seconds": { "type": "integer", "minimum": 1, "maximum": 300, "default": 20 },
|
|
17
17
|
"session_timeout_seconds": { "type": "integer", "minimum": 10, "maximum": 600, "default": 90 },
|
|
18
|
-
"fill_provider": { "type": "string", "enum": ["command", "mock"], "default": "
|
|
18
|
+
"fill_provider": { "type": "string", "enum": ["command", "mock", "openclaw"], "default": "openclaw" },
|
|
19
19
|
"fill_command": { "type": "string" },
|
|
20
|
+
"openclaw_fill_agent_id": { "type": "string", "default": "main" },
|
|
20
21
|
"python_bin": { "type": "string", "default": "python3" },
|
|
21
22
|
"writer_script_path": { "type": "string" },
|
|
22
23
|
"dashboard_output_path": { "type": "string" },
|
package/package.json
CHANGED
|
@@ -39,7 +39,7 @@ Always use plugin tools:
|
|
|
39
39
|
- Never ask the model to generate full dashboard structure when filling values.
|
|
40
40
|
|
|
41
41
|
## Command Shortcuts
|
|
42
|
-
- `/plashboard setup [mock|command <fill_command>]`
|
|
42
|
+
- `/plashboard setup [openclaw [agent_id]|mock|command <fill_command>]`
|
|
43
43
|
- `/plashboard expose-guide [local_url] [https_port]`
|
|
44
44
|
- `/plashboard expose-check [local_url] [https_port]`
|
|
45
45
|
- `/plashboard init`
|
package/src/config.ts
CHANGED
|
@@ -50,8 +50,12 @@ export function resolveConfig(api: unknown): PlashboardConfig {
|
|
|
50
50
|
|
|
51
51
|
const dataDir = asString(raw.data_dir, '/var/lib/openclaw/plash-data');
|
|
52
52
|
const outputPath = asString(raw.dashboard_output_path, join(dataDir, 'dashboard.json'));
|
|
53
|
-
const fillProviderRaw = asString(raw.fill_provider, '
|
|
54
|
-
const fillProvider = fillProviderRaw === 'command'
|
|
53
|
+
const fillProviderRaw = asString(raw.fill_provider, 'openclaw');
|
|
54
|
+
const fillProvider = fillProviderRaw === 'command'
|
|
55
|
+
? 'command'
|
|
56
|
+
: fillProviderRaw === 'mock'
|
|
57
|
+
? 'mock'
|
|
58
|
+
: 'openclaw';
|
|
55
59
|
|
|
56
60
|
return {
|
|
57
61
|
data_dir: dataDir,
|
|
@@ -63,6 +67,7 @@ export function resolveConfig(api: unknown): PlashboardConfig {
|
|
|
63
67
|
session_timeout_seconds: Math.max(10, Math.floor(asNumber(raw.session_timeout_seconds, 90))),
|
|
64
68
|
fill_provider: fillProvider,
|
|
65
69
|
fill_command: typeof raw.fill_command === 'string' ? raw.fill_command : undefined,
|
|
70
|
+
openclaw_fill_agent_id: asString(raw.openclaw_fill_agent_id, 'main'),
|
|
66
71
|
python_bin: asString(raw.python_bin, 'python3'),
|
|
67
72
|
writer_script_path: asString(raw.writer_script_path, DEFAULT_WRITER_PATH),
|
|
68
73
|
dashboard_output_path: outputPath,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createFillRunner } from './fill-runner.js';
|
|
3
|
+
import type { DashboardTemplate, FillRunContext, PlashboardConfig } from './types.js';
|
|
4
|
+
|
|
5
|
+
function template(): DashboardTemplate {
|
|
6
|
+
return {
|
|
7
|
+
id: 'ops',
|
|
8
|
+
name: 'Ops',
|
|
9
|
+
enabled: true,
|
|
10
|
+
schedule: {
|
|
11
|
+
mode: 'interval',
|
|
12
|
+
every_minutes: 5,
|
|
13
|
+
timezone: 'UTC'
|
|
14
|
+
},
|
|
15
|
+
base_dashboard: {
|
|
16
|
+
title: 'Ops',
|
|
17
|
+
summary: 'old summary'
|
|
18
|
+
},
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
id: 'summary',
|
|
22
|
+
pointer: '/summary',
|
|
23
|
+
type: 'string',
|
|
24
|
+
prompt: 'Summarize current status',
|
|
25
|
+
required: true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function config(overrides: Partial<PlashboardConfig>): PlashboardConfig {
|
|
32
|
+
return {
|
|
33
|
+
data_dir: '/tmp/plash-test',
|
|
34
|
+
timezone: 'UTC',
|
|
35
|
+
scheduler_tick_seconds: 30,
|
|
36
|
+
max_parallel_runs: 1,
|
|
37
|
+
default_retry_count: 0,
|
|
38
|
+
retry_backoff_seconds: 1,
|
|
39
|
+
session_timeout_seconds: 30,
|
|
40
|
+
fill_provider: 'mock',
|
|
41
|
+
fill_command: undefined,
|
|
42
|
+
openclaw_fill_agent_id: 'main',
|
|
43
|
+
python_bin: 'python3',
|
|
44
|
+
writer_script_path: '/tmp/writer.py',
|
|
45
|
+
dashboard_output_path: '/tmp/dashboard.json',
|
|
46
|
+
layout_overflow_tolerance_px: 40,
|
|
47
|
+
display_profile: {
|
|
48
|
+
width_px: 1920,
|
|
49
|
+
height_px: 1080,
|
|
50
|
+
safe_top_px: 96,
|
|
51
|
+
safe_bottom_px: 106,
|
|
52
|
+
safe_side_px: 28,
|
|
53
|
+
layout_safety_margin_px: 24
|
|
54
|
+
},
|
|
55
|
+
model_defaults: {},
|
|
56
|
+
...overrides
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function context(): FillRunContext {
|
|
61
|
+
return {
|
|
62
|
+
template: template(),
|
|
63
|
+
currentValues: { summary: 'old summary' },
|
|
64
|
+
attempt: 1
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('createFillRunner', () => {
|
|
69
|
+
it('parses openclaw json envelope output', async () => {
|
|
70
|
+
const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
|
|
71
|
+
stdout: JSON.stringify({
|
|
72
|
+
result: {
|
|
73
|
+
payloads: [
|
|
74
|
+
{
|
|
75
|
+
text: '{"values":{"summary":"new summary"}}'
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
stderr: '',
|
|
81
|
+
code: 0
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const runner = createFillRunner(
|
|
85
|
+
config({ fill_provider: 'openclaw', openclaw_fill_agent_id: 'ops' }),
|
|
86
|
+
{ commandRunner }
|
|
87
|
+
);
|
|
88
|
+
const response = await runner.run(context());
|
|
89
|
+
|
|
90
|
+
expect(response.values.summary).toBe('new summary');
|
|
91
|
+
expect(commandRunner).toHaveBeenCalledTimes(1);
|
|
92
|
+
const firstCall = commandRunner.mock.calls[0];
|
|
93
|
+
const argv = firstCall[0];
|
|
94
|
+
expect(argv.slice(0, 2)).toEqual(['openclaw', 'agent']);
|
|
95
|
+
expect(argv).toContain('--agent');
|
|
96
|
+
expect(argv).toContain('ops');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('parses command runner fenced json output', async () => {
|
|
100
|
+
const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
|
|
101
|
+
stdout: '```json\n{"values":{"summary":"from command"}}\n```',
|
|
102
|
+
stderr: '',
|
|
103
|
+
code: 0
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const runner = createFillRunner(
|
|
107
|
+
config({ fill_provider: 'command', fill_command: 'echo "$PLASHBOARD_PROMPT_JSON"' }),
|
|
108
|
+
{ commandRunner }
|
|
109
|
+
);
|
|
110
|
+
const response = await runner.run(context());
|
|
111
|
+
|
|
112
|
+
expect(response.values.summary).toBe('from command');
|
|
113
|
+
expect(commandRunner).toHaveBeenCalledTimes(1);
|
|
114
|
+
const firstCall = commandRunner.mock.calls[0];
|
|
115
|
+
const argv = firstCall[0];
|
|
116
|
+
expect(argv.slice(0, 2)).toEqual(['sh', '-lc']);
|
|
117
|
+
const options = firstCall[1] as { env?: Record<string, string> };
|
|
118
|
+
expect(options.env?.PLASHBOARD_PROMPT_JSON).toContain('"template"');
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/fill-runner.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import type { FillResponse, FillRunContext, FillRunner, PlashboardConfig } from './types.js';
|
|
3
3
|
|
|
4
|
+
type CommandOptions = {
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
input?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CommandRunResult = {
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
code: number | null;
|
|
14
|
+
signal?: NodeJS.Signals | null;
|
|
15
|
+
killed?: boolean;
|
|
16
|
+
termination?: 'exit' | 'timeout' | 'signal' | string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CommandRunner = (
|
|
20
|
+
argv: string[],
|
|
21
|
+
optionsOrTimeout: number | CommandOptions
|
|
22
|
+
) => Promise<CommandRunResult>;
|
|
23
|
+
|
|
24
|
+
export interface FillRunnerDeps {
|
|
25
|
+
commandRunner?: CommandRunner;
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
|
|
5
29
|
return {
|
|
6
30
|
instructions: {
|
|
@@ -32,6 +56,16 @@ function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
|
|
|
32
56
|
};
|
|
33
57
|
}
|
|
34
58
|
|
|
59
|
+
function buildOpenClawMessage(context: FillRunContext): string {
|
|
60
|
+
const payload = buildPromptPayload(context);
|
|
61
|
+
return [
|
|
62
|
+
'Fill dashboard fields from the provided template context.',
|
|
63
|
+
'Return exactly one JSON object with this shape: {"values": {...}}.',
|
|
64
|
+
'Do not include markdown, comments, explanations, or extra keys.',
|
|
65
|
+
JSON.stringify(payload)
|
|
66
|
+
].join('\n\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
function mockValue(type: string, currentValue: unknown, fieldId: string): unknown {
|
|
36
70
|
if (type === 'number') return typeof currentValue === 'number' ? currentValue : 0;
|
|
37
71
|
if (type === 'boolean') return typeof currentValue === 'boolean' ? currentValue : false;
|
|
@@ -40,30 +74,35 @@ function mockValue(type: string, currentValue: unknown, fieldId: string): unknow
|
|
|
40
74
|
return `updated ${fieldId} at ${now}`;
|
|
41
75
|
}
|
|
42
76
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (const field of context.template.fields) {
|
|
47
|
-
values[field.id] = mockValue(field.type, context.currentValues[field.id], field.id);
|
|
48
|
-
}
|
|
49
|
-
return { values };
|
|
77
|
+
function normalizeCommandOptions(optionsOrTimeout: number | CommandOptions): CommandOptions {
|
|
78
|
+
if (typeof optionsOrTimeout === 'number') {
|
|
79
|
+
return { timeoutMs: optionsOrTimeout };
|
|
50
80
|
}
|
|
81
|
+
return { timeoutMs: optionsOrTimeout.timeoutMs, env: optionsOrTimeout.env, input: optionsOrTimeout.input };
|
|
51
82
|
}
|
|
52
83
|
|
|
53
|
-
function
|
|
84
|
+
function defaultCommandRunner(argv: string[], optionsOrTimeout: number | CommandOptions): Promise<CommandRunResult> {
|
|
85
|
+
const options = normalizeCommandOptions(optionsOrTimeout);
|
|
54
86
|
return new Promise((resolve, reject) => {
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
if (!Array.isArray(argv) || argv.length === 0 || !argv[0]) {
|
|
88
|
+
reject(new Error('command argv must include a binary name'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
57
93
|
env: {
|
|
58
94
|
...process.env,
|
|
59
|
-
|
|
60
|
-
}
|
|
95
|
+
...(options.env || {})
|
|
96
|
+
},
|
|
97
|
+
stdio: 'pipe'
|
|
61
98
|
});
|
|
62
99
|
|
|
100
|
+
const timeoutMs = Math.max(1000, Math.floor(options.timeoutMs));
|
|
101
|
+
let terminatedByTimeout = false;
|
|
63
102
|
const timer = setTimeout(() => {
|
|
103
|
+
terminatedByTimeout = true;
|
|
64
104
|
child.kill('SIGKILL');
|
|
65
|
-
|
|
66
|
-
}, timeoutSeconds * 1000);
|
|
105
|
+
}, timeoutMs);
|
|
67
106
|
|
|
68
107
|
let stdout = '';
|
|
69
108
|
let stderr = '';
|
|
@@ -71,7 +110,6 @@ function runCommand(command: string, promptPayload: Record<string, unknown>, tim
|
|
|
71
110
|
child.stdout.on('data', (chunk) => {
|
|
72
111
|
stdout += String(chunk);
|
|
73
112
|
});
|
|
74
|
-
|
|
75
113
|
child.stderr.on('data', (chunk) => {
|
|
76
114
|
stderr += String(chunk);
|
|
77
115
|
});
|
|
@@ -81,42 +119,195 @@ function runCommand(command: string, promptPayload: Record<string, unknown>, tim
|
|
|
81
119
|
reject(error);
|
|
82
120
|
});
|
|
83
121
|
|
|
84
|
-
child.on('close', (code) => {
|
|
122
|
+
child.on('close', (code, signal) => {
|
|
85
123
|
clearTimeout(timer);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
124
|
+
resolve({
|
|
125
|
+
stdout: stdout.trim(),
|
|
126
|
+
stderr: stderr.trim(),
|
|
127
|
+
code,
|
|
128
|
+
signal,
|
|
129
|
+
killed: terminatedByTimeout || code === null,
|
|
130
|
+
termination: terminatedByTimeout ? 'timeout' : signal ? 'signal' : 'exit'
|
|
131
|
+
});
|
|
91
132
|
});
|
|
133
|
+
|
|
134
|
+
if (typeof options.input === 'string') {
|
|
135
|
+
child.stdin.write(options.input);
|
|
136
|
+
}
|
|
137
|
+
child.stdin.end();
|
|
92
138
|
});
|
|
93
139
|
}
|
|
94
140
|
|
|
141
|
+
function tryParseJson(input: string): unknown | undefined {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(input);
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stripCodeFence(input: string): string {
|
|
150
|
+
const trimmed = input.trim();
|
|
151
|
+
if (!trimmed.startsWith('```') || !trimmed.endsWith('```')) return trimmed;
|
|
152
|
+
const lines = trimmed.split('\n');
|
|
153
|
+
if (lines.length < 3) return trimmed;
|
|
154
|
+
return lines.slice(1, -1).join('\n').trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseJsonCandidate(input: string): unknown | undefined {
|
|
158
|
+
const trimmed = input.trim();
|
|
159
|
+
if (!trimmed) return undefined;
|
|
160
|
+
|
|
161
|
+
const direct = tryParseJson(trimmed);
|
|
162
|
+
if (direct !== undefined) return direct;
|
|
163
|
+
|
|
164
|
+
const unfenced = stripCodeFence(trimmed);
|
|
165
|
+
if (unfenced !== trimmed) {
|
|
166
|
+
const parsed = tryParseJson(unfenced);
|
|
167
|
+
if (parsed !== undefined) return parsed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const start = trimmed.indexOf('{');
|
|
171
|
+
const end = trimmed.lastIndexOf('}');
|
|
172
|
+
if (start >= 0 && end > start) {
|
|
173
|
+
const maybeObject = trimmed.slice(start, end + 1);
|
|
174
|
+
const parsed = tryParseJson(maybeObject);
|
|
175
|
+
if (parsed !== undefined) return parsed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function asObject(value: unknown): Record<string, unknown> | null {
|
|
182
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
183
|
+
? (value as Record<string, unknown>)
|
|
184
|
+
: null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function extractFillResponse(value: unknown, depth = 0): FillResponse | null {
|
|
188
|
+
if (depth > 10) return null;
|
|
189
|
+
|
|
190
|
+
if (typeof value === 'string') {
|
|
191
|
+
const parsed = parseJsonCandidate(value);
|
|
192
|
+
if (parsed !== undefined) {
|
|
193
|
+
return extractFillResponse(parsed, depth + 1);
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
for (const item of value) {
|
|
200
|
+
const found = extractFillResponse(item, depth + 1);
|
|
201
|
+
if (found) return found;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const objectValue = asObject(value);
|
|
207
|
+
if (!objectValue) return null;
|
|
208
|
+
|
|
209
|
+
const valuesRecord = asObject(objectValue.values);
|
|
210
|
+
if (valuesRecord) {
|
|
211
|
+
return { values: valuesRecord };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const nested of Object.values(objectValue)) {
|
|
215
|
+
const found = extractFillResponse(nested, depth + 1);
|
|
216
|
+
if (found) return found;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseFillResponse(output: string, source: string): FillResponse {
|
|
222
|
+
const extracted = extractFillResponse(output);
|
|
223
|
+
if (!extracted) {
|
|
224
|
+
throw new Error(`${source} output did not include a valid {"values": ...} JSON object`);
|
|
225
|
+
}
|
|
226
|
+
return extracted;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function runAndReadStdout(
|
|
230
|
+
commandRunner: CommandRunner,
|
|
231
|
+
argv: string[],
|
|
232
|
+
optionsOrTimeout: number | CommandOptions,
|
|
233
|
+
label: string
|
|
234
|
+
): Promise<string> {
|
|
235
|
+
const result = await commandRunner(argv, optionsOrTimeout);
|
|
236
|
+
if (result.code !== 0) {
|
|
237
|
+
const reason = result.stderr || result.stdout || result.termination || `exit=${String(result.code)}`;
|
|
238
|
+
throw new Error(`${label} failed: ${reason}`);
|
|
239
|
+
}
|
|
240
|
+
return result.stdout.trim();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
class MockFillRunner implements FillRunner {
|
|
244
|
+
async run(context: FillRunContext): Promise<FillResponse> {
|
|
245
|
+
const values: Record<string, unknown> = {};
|
|
246
|
+
for (const field of context.template.fields) {
|
|
247
|
+
values[field.id] = mockValue(field.type, context.currentValues[field.id], field.id);
|
|
248
|
+
}
|
|
249
|
+
return { values };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
95
253
|
class CommandFillRunner implements FillRunner {
|
|
96
|
-
constructor(
|
|
254
|
+
constructor(
|
|
255
|
+
private readonly config: PlashboardConfig,
|
|
256
|
+
private readonly commandRunner: CommandRunner
|
|
257
|
+
) {}
|
|
97
258
|
|
|
98
259
|
async run(context: FillRunContext): Promise<FillResponse> {
|
|
99
260
|
if (!this.config.fill_command) {
|
|
100
261
|
throw new Error('fill_provider=command but fill_command is not configured');
|
|
101
262
|
}
|
|
102
263
|
|
|
103
|
-
const
|
|
104
|
-
|
|
264
|
+
const output = await runAndReadStdout(
|
|
265
|
+
this.commandRunner,
|
|
266
|
+
['sh', '-lc', this.config.fill_command],
|
|
267
|
+
{
|
|
268
|
+
timeoutMs: this.config.session_timeout_seconds * 1000,
|
|
269
|
+
env: {
|
|
270
|
+
PLASHBOARD_PROMPT_JSON: JSON.stringify(buildPromptPayload(context))
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
'fill command'
|
|
274
|
+
);
|
|
105
275
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
276
|
+
return parseFillResponse(output, 'fill command');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class OpenClawFillRunner implements FillRunner {
|
|
281
|
+
constructor(
|
|
282
|
+
private readonly config: PlashboardConfig,
|
|
283
|
+
private readonly commandRunner: CommandRunner
|
|
284
|
+
) {}
|
|
112
285
|
|
|
113
|
-
|
|
286
|
+
async run(context: FillRunContext): Promise<FillResponse> {
|
|
287
|
+
const agentId = (this.config.openclaw_fill_agent_id || 'main').trim() || 'main';
|
|
288
|
+
const timeoutSeconds = Math.max(10, Math.floor(this.config.session_timeout_seconds));
|
|
289
|
+
const message = buildOpenClawMessage(context);
|
|
290
|
+
|
|
291
|
+
const output = await runAndReadStdout(
|
|
292
|
+
this.commandRunner,
|
|
293
|
+
['openclaw', 'agent', '--agent', agentId, '--message', message, '--json', '--timeout', String(timeoutSeconds)],
|
|
294
|
+
{
|
|
295
|
+
timeoutMs: (timeoutSeconds + 30) * 1000
|
|
296
|
+
},
|
|
297
|
+
'openclaw fill'
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return parseFillResponse(output, 'openclaw fill');
|
|
114
301
|
}
|
|
115
302
|
}
|
|
116
303
|
|
|
117
|
-
export function createFillRunner(config: PlashboardConfig): FillRunner {
|
|
304
|
+
export function createFillRunner(config: PlashboardConfig, deps: FillRunnerDeps = {}): FillRunner {
|
|
305
|
+
const commandRunner = deps.commandRunner || defaultCommandRunner;
|
|
118
306
|
if (config.fill_provider === 'command') {
|
|
119
|
-
return new CommandFillRunner(config);
|
|
307
|
+
return new CommandFillRunner(config, commandRunner);
|
|
308
|
+
}
|
|
309
|
+
if (config.fill_provider === 'openclaw') {
|
|
310
|
+
return new OpenClawFillRunner(config, commandRunner);
|
|
120
311
|
}
|
|
121
312
|
return new MockFillRunner();
|
|
122
313
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -19,6 +19,26 @@ type UnknownApi = {
|
|
|
19
19
|
loadConfig?: () => unknown;
|
|
20
20
|
writeConfigFile?: (nextConfig: unknown) => Promise<void>;
|
|
21
21
|
};
|
|
22
|
+
system?: {
|
|
23
|
+
runCommandWithTimeout?: (
|
|
24
|
+
argv: string[],
|
|
25
|
+
optionsOrTimeout: number | {
|
|
26
|
+
timeoutMs: number;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
input?: string;
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
windowsVerbatimArguments?: boolean;
|
|
31
|
+
noOutputTimeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
) => Promise<{
|
|
34
|
+
stdout: string;
|
|
35
|
+
stderr: string;
|
|
36
|
+
code: number | null;
|
|
37
|
+
signal?: NodeJS.Signals | null;
|
|
38
|
+
killed?: boolean;
|
|
39
|
+
termination?: string;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
22
42
|
};
|
|
23
43
|
config?: unknown;
|
|
24
44
|
pluginConfig?: unknown;
|
|
@@ -67,8 +87,9 @@ function asErrorMessage(error: unknown): string {
|
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
type SetupParams = {
|
|
70
|
-
fill_provider?: 'mock' | 'command';
|
|
90
|
+
fill_provider?: 'mock' | 'command' | 'openclaw';
|
|
71
91
|
fill_command?: string;
|
|
92
|
+
openclaw_fill_agent_id?: string;
|
|
72
93
|
data_dir?: string;
|
|
73
94
|
scheduler_tick_seconds?: number;
|
|
74
95
|
session_timeout_seconds?: number;
|
|
@@ -325,14 +346,23 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
325
346
|
)
|
|
326
347
|
};
|
|
327
348
|
|
|
349
|
+
const currentProvider = asString(currentPluginConfig.fill_provider);
|
|
328
350
|
const selectedProvider =
|
|
329
351
|
params.fill_provider
|
|
330
|
-
|| (
|
|
352
|
+
|| (currentProvider === 'command' || currentProvider === 'mock' || currentProvider === 'openclaw'
|
|
353
|
+
? currentProvider
|
|
354
|
+
: resolvedConfig.fill_provider);
|
|
331
355
|
const selectedCommand = (
|
|
332
356
|
params.fill_command
|
|
333
357
|
|| asString(currentPluginConfig.fill_command)
|
|
334
358
|
|| asString(resolvedConfig.fill_command)
|
|
335
359
|
).trim();
|
|
360
|
+
const selectedAgentId = (
|
|
361
|
+
params.openclaw_fill_agent_id
|
|
362
|
+
|| asString(currentPluginConfig.openclaw_fill_agent_id)
|
|
363
|
+
|| asString(resolvedConfig.openclaw_fill_agent_id)
|
|
364
|
+
|| 'main'
|
|
365
|
+
).trim();
|
|
336
366
|
|
|
337
367
|
if (selectedProvider === 'command' && !selectedCommand) {
|
|
338
368
|
return {
|
|
@@ -340,6 +370,12 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
340
370
|
errors: ['fill_provider=command requires fill_command']
|
|
341
371
|
} satisfies ToolResponse<Record<string, unknown>>;
|
|
342
372
|
}
|
|
373
|
+
if (selectedProvider === 'openclaw' && !selectedAgentId) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
errors: ['fill_provider=openclaw requires openclaw_fill_agent_id']
|
|
377
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
378
|
+
}
|
|
343
379
|
|
|
344
380
|
const nextPluginConfig: Record<string, unknown> = {
|
|
345
381
|
...currentPluginConfig,
|
|
@@ -366,6 +402,13 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
366
402
|
|
|
367
403
|
if (selectedCommand) {
|
|
368
404
|
nextPluginConfig.fill_command = selectedCommand;
|
|
405
|
+
} else {
|
|
406
|
+
delete nextPluginConfig.fill_command;
|
|
407
|
+
}
|
|
408
|
+
if (selectedProvider === 'openclaw') {
|
|
409
|
+
nextPluginConfig.openclaw_fill_agent_id = selectedAgentId;
|
|
410
|
+
} else {
|
|
411
|
+
delete nextPluginConfig.openclaw_fill_agent_id;
|
|
369
412
|
}
|
|
370
413
|
|
|
371
414
|
const nextRootConfig = {
|
|
@@ -394,6 +437,7 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
394
437
|
plugin_id: 'plashboard',
|
|
395
438
|
fill_provider: selectedProvider,
|
|
396
439
|
fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
|
|
440
|
+
openclaw_fill_agent_id: selectedProvider === 'openclaw' ? selectedAgentId : undefined,
|
|
397
441
|
data_dir: nextPluginConfig.data_dir,
|
|
398
442
|
scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
|
|
399
443
|
session_timeout_seconds: nextPluginConfig.session_timeout_seconds,
|
|
@@ -408,10 +452,36 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
408
452
|
|
|
409
453
|
export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
410
454
|
const config = resolveConfig(api);
|
|
455
|
+
const runtimeCommand = api.runtime?.system?.runCommandWithTimeout;
|
|
456
|
+
const fillCommandRunner = runtimeCommand
|
|
457
|
+
? async (
|
|
458
|
+
argv: string[],
|
|
459
|
+
optionsOrTimeout: number | {
|
|
460
|
+
timeoutMs: number;
|
|
461
|
+
cwd?: string;
|
|
462
|
+
input?: string;
|
|
463
|
+
env?: NodeJS.ProcessEnv;
|
|
464
|
+
windowsVerbatimArguments?: boolean;
|
|
465
|
+
noOutputTimeoutMs?: number;
|
|
466
|
+
}
|
|
467
|
+
) => {
|
|
468
|
+
const result = await runtimeCommand(argv, optionsOrTimeout);
|
|
469
|
+
return {
|
|
470
|
+
stdout: result.stdout,
|
|
471
|
+
stderr: result.stderr,
|
|
472
|
+
code: result.code,
|
|
473
|
+
signal: result.signal,
|
|
474
|
+
killed: result.killed,
|
|
475
|
+
termination: result.termination
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
: undefined;
|
|
411
479
|
const runtime = new PlashboardRuntime(config, {
|
|
412
480
|
info: (...args) => api.logger?.info?.(...args),
|
|
413
481
|
warn: (...args) => api.logger?.warn?.(...args),
|
|
414
482
|
error: (...args) => api.logger?.error?.(...args)
|
|
483
|
+
}, {
|
|
484
|
+
commandRunner: fillCommandRunner
|
|
415
485
|
});
|
|
416
486
|
|
|
417
487
|
api.registerService?.({
|
|
@@ -465,8 +535,9 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
465
535
|
parameters: {
|
|
466
536
|
type: 'object',
|
|
467
537
|
properties: {
|
|
468
|
-
fill_provider: { type: 'string', enum: ['mock', 'command'] },
|
|
538
|
+
fill_provider: { type: 'string', enum: ['mock', 'command', 'openclaw'] },
|
|
469
539
|
fill_command: { type: 'string' },
|
|
540
|
+
openclaw_fill_agent_id: { type: 'string' },
|
|
470
541
|
data_dir: { type: 'string' },
|
|
471
542
|
scheduler_tick_seconds: { type: 'number' },
|
|
472
543
|
session_timeout_seconds: { type: 'number' },
|
|
@@ -685,9 +756,16 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
685
756
|
}
|
|
686
757
|
if (cmd === 'setup') {
|
|
687
758
|
const mode = asString(rest[0]).toLowerCase();
|
|
688
|
-
const fillProvider = mode === 'command' || mode === 'mock' ? mode : undefined;
|
|
759
|
+
const fillProvider = mode === 'command' || mode === 'mock' || mode === 'openclaw' ? mode : undefined;
|
|
689
760
|
const fillCommand = fillProvider === 'command' ? rest.slice(1).join(' ').trim() || undefined : undefined;
|
|
690
|
-
|
|
761
|
+
const fillAgentId = fillProvider === 'openclaw' ? (rest[1] || '').trim() || undefined : undefined;
|
|
762
|
+
return toCommandResult(
|
|
763
|
+
await runSetup(api, config, {
|
|
764
|
+
fill_provider: fillProvider,
|
|
765
|
+
fill_command: fillCommand,
|
|
766
|
+
openclaw_fill_agent_id: fillAgentId
|
|
767
|
+
})
|
|
768
|
+
);
|
|
691
769
|
}
|
|
692
770
|
if (cmd === 'init') return toCommandResult(await runtime.init());
|
|
693
771
|
if (cmd === 'status') return toCommandResult(await runtime.status());
|
|
@@ -713,7 +791,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
713
791
|
return toCommandResult({
|
|
714
792
|
ok: false,
|
|
715
793
|
errors: [
|
|
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>'
|
|
794
|
+
'unknown command. supported: setup [openclaw [agent_id]|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>'
|
|
717
795
|
]
|
|
718
796
|
});
|
|
719
797
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DashboardTemplate,
|
|
6
6
|
DisplayProfile,
|
|
7
7
|
FillResponse,
|
|
8
|
+
FillRunner,
|
|
8
9
|
PlashboardConfig,
|
|
9
10
|
PlashboardState,
|
|
10
11
|
RunArtifact,
|
|
@@ -16,7 +17,7 @@ import { asErrorMessage, atomicWriteJson, deepClone, ensureDir, nowIso, sleep }
|
|
|
16
17
|
import { collectCurrentValues, mergeTemplateValues, validateFieldPointers } from './merge.js';
|
|
17
18
|
import { validateFillShape, validateTemplateShape } from './schema-validation.js';
|
|
18
19
|
import { DashboardValidatorPublisher } from './publisher.js';
|
|
19
|
-
import { createFillRunner } from './fill-runner.js';
|
|
20
|
+
import { createFillRunner, type FillRunnerDeps } from './fill-runner.js';
|
|
20
21
|
|
|
21
22
|
interface Logger {
|
|
22
23
|
info(message: string, ...args: unknown[]): void;
|
|
@@ -55,7 +56,7 @@ export class PlashboardRuntime {
|
|
|
55
56
|
private readonly templateStore: TemplateStore;
|
|
56
57
|
private readonly runStore: RunStore;
|
|
57
58
|
private readonly publisher: DashboardValidatorPublisher;
|
|
58
|
-
private readonly fillRunner;
|
|
59
|
+
private readonly fillRunner: FillRunner;
|
|
59
60
|
|
|
60
61
|
private schedulerTimer: NodeJS.Timeout | null = null;
|
|
61
62
|
private tickInProgress = false;
|
|
@@ -64,14 +65,15 @@ export class PlashboardRuntime {
|
|
|
64
65
|
|
|
65
66
|
constructor(
|
|
66
67
|
private readonly config: PlashboardConfig,
|
|
67
|
-
private readonly logger: Logger = NOOP_LOGGER
|
|
68
|
+
private readonly logger: Logger = NOOP_LOGGER,
|
|
69
|
+
fillRunnerDeps: FillRunnerDeps = {}
|
|
68
70
|
) {
|
|
69
71
|
this.paths = new Paths(config);
|
|
70
72
|
this.stateStore = new StateStore(this.paths);
|
|
71
73
|
this.templateStore = new TemplateStore(this.paths);
|
|
72
74
|
this.runStore = new RunStore(this.paths);
|
|
73
75
|
this.publisher = new DashboardValidatorPublisher(config);
|
|
74
|
-
this.fillRunner = createFillRunner(config);
|
|
76
|
+
this.fillRunner = createFillRunner(config, fillRunnerDeps);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
async start(): Promise<void> {
|
package/src/types.ts
CHANGED
|
@@ -23,8 +23,9 @@ export interface PlashboardConfig {
|
|
|
23
23
|
default_retry_count: number;
|
|
24
24
|
retry_backoff_seconds: number;
|
|
25
25
|
session_timeout_seconds: number;
|
|
26
|
-
fill_provider: 'command' | 'mock';
|
|
26
|
+
fill_provider: 'command' | 'mock' | 'openclaw';
|
|
27
27
|
fill_command?: string;
|
|
28
|
+
openclaw_fill_agent_id?: string;
|
|
28
29
|
python_bin: string;
|
|
29
30
|
writer_script_path: string;
|
|
30
31
|
dashboard_output_path: string;
|