@openbrt/weclawbotctl 0.1.9 → 0.1.14
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 +35 -1
- package/bin/weclawbot-openclaw-bridge.mjs +34 -0
- package/bin/weclawbotctl.mjs +58 -1
- package/index.mjs +53 -0
- package/lib/direct-control.mjs +29 -4
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/weclawbot-curator/SKILL.md +33 -3
- package/systemd/weclawbot-openclaw-curator.service +1 -0
- package/test/direct-control.test.mjs +19 -0
- package/workspace/AGENTS.md +6 -0
package/README.md
CHANGED
|
@@ -52,6 +52,12 @@ weclawbotctl thinking --id "$task_id" --ttl 45
|
|
|
52
52
|
weclawbotctl idle --id "$task_id"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
`idle` must use the same id as the active `thinking` message. Newer firmware
|
|
56
|
+
rejects stale or unrelated `idle` messages and keeps the visible thinking state.
|
|
57
|
+
The bundled OpenClaw bridge publishes `thinking` before every curator job and
|
|
58
|
+
`idle` after it finishes, so WeChat-origin official-mode work also gets a visible
|
|
59
|
+
processing state without relying on the model to remember it.
|
|
60
|
+
|
|
55
61
|
To put text, status, diagrams, or images on the screen, render them into a
|
|
56
62
|
pre-rendered `weclawbot.screen_document.v1` first. The firmware receives pixels;
|
|
57
63
|
it does not lay out text, choose fonts, or split pages for agents.
|
|
@@ -65,9 +71,19 @@ only after the firmware reports `applied`; a firmware `rejected` status or a
|
|
|
65
71
|
timeout is a failed delivery. Use `--no-wait` only for diagnostics where MQTT
|
|
66
72
|
publish acknowledgement is enough.
|
|
67
73
|
|
|
74
|
+
To clear the current note, use the firmware clear command:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
weclawbotctl clear
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Do not emulate clear by publishing a blank, white, or black screen document.
|
|
81
|
+
That creates a new note and can leave the physical screen looking black.
|
|
82
|
+
|
|
68
83
|
The package also includes an OpenClaw integration: the `weclawbot-curator`
|
|
69
84
|
skill, `weclawbot_status`, `weclawbot_validate_screen_document`,
|
|
70
|
-
`
|
|
85
|
+
`weclawbot_clear_screen`, `weclawbot_publish_screen_document`,
|
|
86
|
+
`weclawbot_validate_activity`,
|
|
71
87
|
`weclawbot_publish_activity`, and a small outbound bridge service. The bridge
|
|
72
88
|
polls `weclawbot.link`; no public HTTP endpoint, port forwarding, or WeChat
|
|
73
89
|
credential is required on the OpenClaw host.
|
|
@@ -248,6 +264,24 @@ three `mono1` pages, and a future UTC expiry. Agents may use PIL, Canvas, SVG
|
|
|
248
264
|
rasterization, screenshots, or any local renderer, but the MQTT payload must be
|
|
249
265
|
pixels:
|
|
250
266
|
|
|
267
|
+
There is no canonical WeClawBot renderer that agents must use. The stable
|
|
268
|
+
contract is the bounded pixel document plus device feedback. Keep layout,
|
|
269
|
+
typography, and page-composition decisions in the agent/tool layer so skills and
|
|
270
|
+
models can improve the result without requiring users to flash firmware.
|
|
271
|
+
Preserve any layout preferences, visual language, page rhythm, font choices, or
|
|
272
|
+
review habits that the user and agent have already developed; package upgrades
|
|
273
|
+
should add capabilities without resetting that accumulated practice.
|
|
274
|
+
|
|
275
|
+
The hardware facts are stable: the content viewport is 368 x 206 mono1 pixels,
|
|
276
|
+
content documents may contain one to three pages, and the firmware will not split
|
|
277
|
+
a single pixel page after receiving it. If `pages.length === 1`, the physical
|
|
278
|
+
screen has exactly one page.
|
|
279
|
+
|
|
280
|
+
Before publishing, agents should inspect or otherwise self-evaluate the rendered
|
|
281
|
+
pages against the user's preferences and their own learned standards when their
|
|
282
|
+
runtime supports it. Regenerate the document if the bitmap does not satisfy those
|
|
283
|
+
standards.
|
|
284
|
+
|
|
251
285
|
```bash
|
|
252
286
|
weclawbotctl screen /path/to/screen-document.json
|
|
253
287
|
```
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
|
|
6
7
|
const ACTIONS = new Set([
|
|
7
8
|
"ignore",
|
|
@@ -53,6 +54,10 @@ function stop() {
|
|
|
53
54
|
|
|
54
55
|
async function handleJob(job) {
|
|
55
56
|
const started = Date.now();
|
|
57
|
+
const activityId = `openclaw-${String(job.id || cryptoRandom()).replace(/[^a-zA-Z0-9_.-]/gu, "_").slice(0, 64)}`;
|
|
58
|
+
await publishBridgeActivity("thinking", activityId, {
|
|
59
|
+
ttlSeconds: Math.min(120, Math.max(5, config.agentTimeoutSeconds + 15)),
|
|
60
|
+
});
|
|
56
61
|
try {
|
|
57
62
|
const decision = await curateWithOpenClaw(job);
|
|
58
63
|
await gatewayJson("POST", `${config.jobsPath}/${encodeURIComponent(job.id)}/result`, {
|
|
@@ -71,6 +76,24 @@ async function handleJob(job) {
|
|
|
71
76
|
} catch (resultError) {
|
|
72
77
|
log("failure_report_failed", { job: job.id, error: errorMessage(resultError) });
|
|
73
78
|
}
|
|
79
|
+
} finally {
|
|
80
|
+
await publishBridgeActivity("idle", activityId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function publishBridgeActivity(state, id, options = {}) {
|
|
85
|
+
if (!config.screenActivity) return;
|
|
86
|
+
const args = [state, "--id", id];
|
|
87
|
+
if (state === "thinking") args.push("--ttl", String(options.ttlSeconds || 45));
|
|
88
|
+
try {
|
|
89
|
+
const result = await run(config.weclawbotctlBin, args, 8000);
|
|
90
|
+
if (result.code !== 0) {
|
|
91
|
+
log("activity_failed", { state, id, error: shortText(result.stderr || result.stdout || `exit ${result.code}`) });
|
|
92
|
+
} else {
|
|
93
|
+
log("activity_sent", { state, id });
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
log("activity_failed", { state, id, error: errorMessage(error) });
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
|
|
@@ -316,8 +339,10 @@ function loadConfig(env) {
|
|
|
316
339
|
jobsPath: String(env.WEC_GATEWAY_JOBS_PATH || "/api/agent/curator/jobs"),
|
|
317
340
|
agentId: String(env.WEC_OPENCLAW_AGENT || "weclawbot"),
|
|
318
341
|
openclawBin: String(env.WEC_OPENCLAW_BIN || "openclaw"),
|
|
342
|
+
weclawbotctlBin: String(env.WEC_WECLAWBOTCTL_BIN || siblingBin("weclawbotctl")),
|
|
319
343
|
transport: env.WEC_OPENCLAW_TRANSPORT === "local" ? "local" : "gateway",
|
|
320
344
|
thinking: String(env.WEC_OPENCLAW_THINKING || "low"),
|
|
345
|
+
screenActivity: env.WEC_SCREEN_ACTIVITY !== "0" && env.WEC_SCREEN_ACTIVITY !== "false",
|
|
321
346
|
pollWaitMs: positiveInteger(env.WEC_POLL_WAIT_MS, 20000),
|
|
322
347
|
retryMs: positiveInteger(env.WEC_RETRY_MS, 3000),
|
|
323
348
|
agentTimeoutSeconds: positiveInteger(env.WEC_AGENT_TIMEOUT_SECONDS, 20),
|
|
@@ -326,6 +351,15 @@ function loadConfig(env) {
|
|
|
326
351
|
};
|
|
327
352
|
}
|
|
328
353
|
|
|
354
|
+
function siblingBin(name) {
|
|
355
|
+
const script = process.argv[1] || "";
|
|
356
|
+
return script ? path.join(path.dirname(script), name) : name;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function cryptoRandom() {
|
|
360
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
329
363
|
function positiveInteger(value, fallback) {
|
|
330
364
|
const parsed = Number.parseInt(String(value || ""), 10);
|
|
331
365
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
package/bin/weclawbotctl.mjs
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_OPENCLAW_PLUGIN_SPEC = "@openbrt/weclawbotctl";
|
|
|
17
17
|
const MIN_OPENCLAW_VERSION = "2026.6.9";
|
|
18
18
|
|
|
19
19
|
const [command, ...args] = process.argv.slice(2);
|
|
20
|
-
const commands = new Set(["bind", "status", "doctor", "export", "unbind", "thinking", "idle", "screen", "openclaw"]);
|
|
20
|
+
const commands = new Set(["bind", "status", "doctor", "export", "unbind", "thinking", "idle", "screen", "clear", "openclaw"]);
|
|
21
21
|
if (!commands.has(command)) {
|
|
22
22
|
usage();
|
|
23
23
|
process.exit(64);
|
|
@@ -30,6 +30,7 @@ try {
|
|
|
30
30
|
else if (command === "export") await commandExport(args);
|
|
31
31
|
else if (command === "unbind") await commandUnbind(args);
|
|
32
32
|
else if (command === "screen") await commandScreen(args);
|
|
33
|
+
else if (command === "clear") await commandClear(args);
|
|
33
34
|
else if (command === "openclaw") await commandOpenClaw(args);
|
|
34
35
|
else await commandActivity(command, args);
|
|
35
36
|
} catch (error) {
|
|
@@ -245,6 +246,8 @@ async function commandScreen(values) {
|
|
|
245
246
|
id: document.id,
|
|
246
247
|
pages: document.pages.length,
|
|
247
248
|
force_replace: document.force_replace === true,
|
|
249
|
+
warnings: validation.warnings,
|
|
250
|
+
layout_guidance: validation.layout_guidance,
|
|
248
251
|
status: delivery.status,
|
|
249
252
|
}));
|
|
250
253
|
return;
|
|
@@ -257,9 +260,50 @@ async function commandScreen(values) {
|
|
|
257
260
|
id: document.id,
|
|
258
261
|
pages: document.pages.length,
|
|
259
262
|
force_replace: document.force_replace === true,
|
|
263
|
+
warnings: validation.warnings,
|
|
264
|
+
layout_guidance: validation.layout_guidance,
|
|
260
265
|
}));
|
|
261
266
|
}
|
|
262
267
|
|
|
268
|
+
async function commandClear(values) {
|
|
269
|
+
const options = parseOptions(values, {
|
|
270
|
+
credentials: credentialsPath(),
|
|
271
|
+
target: "note",
|
|
272
|
+
wait: true,
|
|
273
|
+
timeout: 12,
|
|
274
|
+
});
|
|
275
|
+
if (options._.length > 1) {
|
|
276
|
+
throw new Error("Usage: weclawbotctl clear [--target note|idle_photo] [--no-wait] [--timeout seconds]");
|
|
277
|
+
}
|
|
278
|
+
const target = normalizeClearTarget(options._[0] || options.target);
|
|
279
|
+
const control = {
|
|
280
|
+
schema: "weclawbot.control.v1",
|
|
281
|
+
id: `clear_${crypto.randomUUID()}`,
|
|
282
|
+
kind: "screen_clear",
|
|
283
|
+
target,
|
|
284
|
+
};
|
|
285
|
+
const credentials = await requireCredentials(expandPath(options.credentials));
|
|
286
|
+
if (options.wait) {
|
|
287
|
+
const delivery = await publishControlAndWaitStatus(credentials, control, {
|
|
288
|
+
expectedDetail: clearStatusDetail(target),
|
|
289
|
+
timeoutMs: Math.max(1, Number(options.timeout) || 12) * 1000,
|
|
290
|
+
});
|
|
291
|
+
if (delivery.status.kind !== "applied") {
|
|
292
|
+
throw new Error(`Device rejected screen clear: ${delivery.status.detail || "unknown"}`);
|
|
293
|
+
}
|
|
294
|
+
console.log(JSON.stringify({
|
|
295
|
+
ok: true,
|
|
296
|
+
published: true,
|
|
297
|
+
applied: true,
|
|
298
|
+
target,
|
|
299
|
+
status: delivery.status,
|
|
300
|
+
}));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await publishControl(credentials, control);
|
|
304
|
+
console.log(JSON.stringify({ ok: true, published: true, applied: null, target }));
|
|
305
|
+
}
|
|
306
|
+
|
|
263
307
|
async function commandActivity(state, values) {
|
|
264
308
|
const options = parseOptions(values, {
|
|
265
309
|
credentials: credentialsPath(),
|
|
@@ -643,6 +687,18 @@ function compactText(value) {
|
|
|
643
687
|
return String(value || "").replace(/\s+/gu, " ").trim().slice(0, 500);
|
|
644
688
|
}
|
|
645
689
|
|
|
690
|
+
function normalizeClearTarget(value) {
|
|
691
|
+
const target = String(value || "note").trim();
|
|
692
|
+
if (target === "note" || target === "idle_photo" || target === "photo") {
|
|
693
|
+
return target === "photo" ? "idle_photo" : target;
|
|
694
|
+
}
|
|
695
|
+
throw new Error("clear target must be note or idle_photo");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function clearStatusDetail(target) {
|
|
699
|
+
return target === "idle_photo" ? "clear_idle_photo" : "clear_note";
|
|
700
|
+
}
|
|
701
|
+
|
|
646
702
|
function shellValue(value) {
|
|
647
703
|
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
648
704
|
}
|
|
@@ -661,6 +717,7 @@ function usage() {
|
|
|
661
717
|
weclawbotctl thinking [--ttl seconds] [--id correlation-id]
|
|
662
718
|
weclawbotctl idle [--id correlation-id]
|
|
663
719
|
weclawbotctl screen <document.json> [--force] [--no-wait] [--timeout seconds]
|
|
720
|
+
weclawbotctl clear [--target note|idle_photo] [--no-wait] [--timeout seconds]
|
|
664
721
|
weclawbotctl openclaw install [--spec @openbrt/weclawbotctl] [--bin /path/to/openclaw] [--force=false]
|
|
665
722
|
weclawbotctl openclaw doctor [--gateway=false] [--bin /path/to/openclaw] [--json]`);
|
|
666
723
|
}
|
package/index.mjs
CHANGED
|
@@ -59,6 +59,43 @@ export default defineToolPlugin({
|
|
|
59
59
|
}, { additionalProperties: false }),
|
|
60
60
|
execute: ({ document, device_context }) => validateScreenDocument(document, device_context),
|
|
61
61
|
}),
|
|
62
|
+
tool({
|
|
63
|
+
name: "weclawbot_clear_screen",
|
|
64
|
+
label: "Clear WeClawBot screen",
|
|
65
|
+
description: "Clear the paired WeClawBot note or idle-photo state with the firmware screen_clear control. Do not emulate clearing by publishing a blank or black bitmap.",
|
|
66
|
+
parameters: Type.Object({
|
|
67
|
+
target: Type.Optional(Type.String()),
|
|
68
|
+
credentials_path: Type.Optional(Type.String()),
|
|
69
|
+
wait_status: Type.Optional(Type.Boolean()),
|
|
70
|
+
timeout_seconds: Type.Optional(Type.Number()),
|
|
71
|
+
}, { additionalProperties: false }),
|
|
72
|
+
execute: async ({ target, credentials_path, wait_status, timeout_seconds }) => {
|
|
73
|
+
const clearTarget = normalizeClearTarget(target);
|
|
74
|
+
const control = {
|
|
75
|
+
schema: "weclawbot.control.v1",
|
|
76
|
+
id: `clear_${crypto.randomUUID()}`,
|
|
77
|
+
kind: "screen_clear",
|
|
78
|
+
target: clearTarget,
|
|
79
|
+
};
|
|
80
|
+
const credentials = await requireCredentials(credentials_path);
|
|
81
|
+
if (wait_status !== false) {
|
|
82
|
+
const delivery = await publishControlAndWaitStatus(credentials, control, {
|
|
83
|
+
expectedDetail: clearStatusDetail(clearTarget),
|
|
84
|
+
timeoutMs: Math.max(1, Number(timeout_seconds) || 12) * 1000,
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
ok: delivery.status.kind === "applied",
|
|
88
|
+
published: true,
|
|
89
|
+
applied: delivery.status.kind === "applied",
|
|
90
|
+
rejected: delivery.status.kind === "rejected",
|
|
91
|
+
target: clearTarget,
|
|
92
|
+
status: delivery.status,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
await publishControl(credentials, control);
|
|
96
|
+
return { ok: true, published: true, applied: null, target: clearTarget };
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
62
99
|
tool({
|
|
63
100
|
name: "weclawbot_publish_screen_document",
|
|
64
101
|
label: "Publish WeClawBot screen document",
|
|
@@ -103,6 +140,8 @@ export default defineToolPlugin({
|
|
|
103
140
|
id: outbound.id,
|
|
104
141
|
pages: outbound.pages.length,
|
|
105
142
|
force_replace: outbound.force_replace === true,
|
|
143
|
+
warnings: validation.warnings,
|
|
144
|
+
layout_guidance: validation.layout_guidance,
|
|
106
145
|
status: delivery.status,
|
|
107
146
|
};
|
|
108
147
|
}
|
|
@@ -114,6 +153,8 @@ export default defineToolPlugin({
|
|
|
114
153
|
id: outbound.id,
|
|
115
154
|
pages: outbound.pages.length,
|
|
116
155
|
force_replace: outbound.force_replace === true,
|
|
156
|
+
warnings: validation.warnings,
|
|
157
|
+
layout_guidance: validation.layout_guidance,
|
|
117
158
|
};
|
|
118
159
|
},
|
|
119
160
|
}),
|
|
@@ -186,6 +227,18 @@ function cloneObject(value) {
|
|
|
186
227
|
return JSON.parse(JSON.stringify(value));
|
|
187
228
|
}
|
|
188
229
|
|
|
230
|
+
function normalizeClearTarget(value) {
|
|
231
|
+
const target = String(value || "note").trim();
|
|
232
|
+
if (target === "note" || target === "idle_photo" || target === "photo") {
|
|
233
|
+
return target === "photo" ? "idle_photo" : target;
|
|
234
|
+
}
|
|
235
|
+
throw new Error("clear target must be note or idle_photo");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function clearStatusDetail(target) {
|
|
239
|
+
return target === "idle_photo" ? "clear_idle_photo" : "clear_note";
|
|
240
|
+
}
|
|
241
|
+
|
|
189
242
|
function maskedMqtt(config, topics) {
|
|
190
243
|
return {
|
|
191
244
|
url: config.url,
|
package/lib/direct-control.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export function resolveDeviceContext(value) {
|
|
|
29
29
|
export function validateScreenDocument(value, suppliedContext) {
|
|
30
30
|
const context = resolveDeviceContext(suppliedContext);
|
|
31
31
|
const errors = [];
|
|
32
|
+
const pageStats = [];
|
|
32
33
|
const document = value && typeof value === "object" ? value : null;
|
|
33
34
|
const viewport = context.content_viewport;
|
|
34
35
|
if (!document) return result(context, errors.concat("document must be an object"));
|
|
@@ -48,12 +49,15 @@ export function validateScreenDocument(value, suppliedContext) {
|
|
|
48
49
|
if (!Array.isArray(document.pages) || document.pages.length < 1 || document.pages.length > viewport.max_pages) {
|
|
49
50
|
errors.push(`pages must contain 1..${viewport.max_pages} items`);
|
|
50
51
|
} else {
|
|
51
|
-
document.pages.forEach((page, index) => validatePage(page, index, viewport, errors));
|
|
52
|
+
document.pages.forEach((page, index) => validatePage(page, index, viewport, errors, pageStats));
|
|
53
|
+
if (pageStats.length > 0 && pageStats.every((stat) => stat.uniform)) {
|
|
54
|
+
errors.push("uniform_screen_document: content pages cannot all be a single color; use screen_clear for clearing");
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
|
-
return result(context, errors);
|
|
57
|
+
return result(context, errors, document);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
function validatePage(page, index, viewport, errors) {
|
|
60
|
+
function validatePage(page, index, viewport, errors, pageStats) {
|
|
57
61
|
if (!page || typeof page !== "object") {
|
|
58
62
|
errors.push(`pages[${index}] must be an object`);
|
|
59
63
|
return;
|
|
@@ -77,14 +81,35 @@ function validatePage(page, index, viewport, errors) {
|
|
|
77
81
|
errors.push(`pages[${index}].data_b64 is not valid base64`);
|
|
78
82
|
} else if (Number.isInteger(stride) && Number.isInteger(height) && bytes.length !== stride * height) {
|
|
79
83
|
errors.push(`pages[${index}].data_b64 byte length does not match stride * height`);
|
|
84
|
+
} else if (bytes.length > 0) {
|
|
85
|
+
pageStats.push({ index, uniform: bytes.every((byte) => byte === bytes[0]) });
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
function result(context, errors) {
|
|
89
|
+
function result(context, errors, document = null) {
|
|
90
|
+
const pageCount = Array.isArray(document?.pages) ? document.pages.length : 0;
|
|
91
|
+
const warnings = [];
|
|
92
|
+
if (pageCount === 1) {
|
|
93
|
+
warnings.push("single_page_document: firmware will not split pixels; verify the page remains readable before publishing");
|
|
94
|
+
} else if (pageCount > 1) {
|
|
95
|
+
warnings.push("multi_page_document: firmware will auto-flip pages and manual left/right buttons can change pages");
|
|
96
|
+
}
|
|
84
97
|
return {
|
|
85
98
|
ok: errors.length === 0,
|
|
86
99
|
errors,
|
|
100
|
+
warnings,
|
|
87
101
|
viewport: context.content_viewport,
|
|
102
|
+
layout_guidance: {
|
|
103
|
+
hardware_contract: "agent supplies pre-rendered mono1 pixels; firmware validates geometry and does not lay out text or split pages",
|
|
104
|
+
page_contract: "pages.length is the physical page count; content documents support 1-3 pages",
|
|
105
|
+
preference_policy: "preserve user-agent learned layout preferences unless they violate hardware bounds or the user asks to change them",
|
|
106
|
+
review_policy: "agent should inspect rendered bitmap pages against user preferences and learned standards before publishing when possible",
|
|
107
|
+
max_pages: context.content_viewport?.max_pages,
|
|
108
|
+
content_viewport_px: {
|
|
109
|
+
width: context.content_viewport?.width,
|
|
110
|
+
height: context.content_viewport?.height,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
88
113
|
agent_transport: context.agent_transport || null,
|
|
89
114
|
direct_delivery_ready: context.agent_transport?.screen_document_available === true
|
|
90
115
|
|| context.agent_transport?.available === true,
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -59,6 +59,34 @@ OpenClaw Canvas for requests that mention WeClawBot, the physical screen, or
|
|
|
59
59
|
not send raw text to firmware. The agent owns text layout, font choice, image
|
|
60
60
|
rasterization, and page splitting; the device consumes pixels.
|
|
61
61
|
|
|
62
|
+
For clear requests such as “清屏”, “清理屏幕”, “clear screen”, or removing the
|
|
63
|
+
current note, call `weclawbot_clear_screen` or run `weclawbotctl clear`. Never
|
|
64
|
+
simulate clearing by publishing a blank, white, or black `screen_document`; that
|
|
65
|
+
creates a new note instead of clearing firmware state.
|
|
66
|
+
|
|
67
|
+
## Layout, preferences, and page splitting
|
|
68
|
+
|
|
69
|
+
Treat this skill as a hardware contract and starting point, not as a fixed house
|
|
70
|
+
style. If the user and agent have already developed layout preferences, visual
|
|
71
|
+
language, page rhythm, font choices, or review habits, preserve those choices
|
|
72
|
+
unless the user asks to change them or they violate the device bounds below.
|
|
73
|
+
Skill upgrades must be additive and compatible with accumulated user-agent
|
|
74
|
+
practice; do not reset or overwrite local style memory just because this package
|
|
75
|
+
changed.
|
|
76
|
+
|
|
77
|
+
Hardware facts: the content viewport is 368 x 206 mono1 pixels, and a document
|
|
78
|
+
may contain one to three content pages. The firmware will not split a single
|
|
79
|
+
pixel page after receiving it; if the document has `pages.length === 1`, the
|
|
80
|
+
physical screen has exactly one page. Multi-page documents can be auto-flipped by
|
|
81
|
+
firmware and changed with the physical left/right buttons.
|
|
82
|
+
|
|
83
|
+
Before publishing, review the actual rendered bitmap pages when your runtime can
|
|
84
|
+
inspect images. Judge the preview against the user's preferences and the agent's
|
|
85
|
+
own learned standards: legibility, margins, crowding, page count, and continuity
|
|
86
|
+
across pages. If the preview does not satisfy those standards, regenerate the
|
|
87
|
+
pages before publishing. This review loop belongs in the agent/tool layer; do not
|
|
88
|
+
expect firmware to fix typography or split pages after the pixels arrive.
|
|
89
|
+
|
|
62
90
|
`weclawbotctl screen` and `weclawbot_publish_screen_document` wait for the
|
|
63
91
|
device status topic by default. Treat only `applied` as success. If the device
|
|
64
92
|
returns `rejected`, tell the user the real rejection reason. Use
|
|
@@ -92,9 +120,11 @@ For a paired device, publish `weclawbot.activity.v1` with `state: "thinking"`
|
|
|
92
120
|
immediately before an LLM call, long retrieval, or multi-step operation, then
|
|
93
121
|
always publish `state: "idle"` in a `finally` path after success or failure.
|
|
94
122
|
The thinking message requires a 5-120 second `ttl_seconds` and a stable
|
|
95
|
-
`correlation_id`;
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
`correlation_id`; the matching idle message must reuse the same id. Newer
|
|
124
|
+
firmware rejects stale or unrelated idle messages and keeps the active thinking
|
|
125
|
+
state. Use `weclawbot_validate_activity` first. It is a temporary overlay that
|
|
126
|
+
restores the exact prior page, not a screen document and not a status to leave on
|
|
127
|
+
indefinitely. Do not publish it when
|
|
98
128
|
`agent_transport.available` is false.
|
|
99
129
|
|
|
100
130
|
Return this shape:
|
|
@@ -7,6 +7,7 @@ Wants=network-online.target
|
|
|
7
7
|
Type=simple
|
|
8
8
|
EnvironmentFile=%h/.config/weclawbot/openclaw-curator.env
|
|
9
9
|
Environment=NODE_EXTRA_CA_CERTS=%h/.openclaw/gateway/tls/gateway-cert.pem
|
|
10
|
+
Environment=WEC_WECLAWBOTCTL_BIN=%h/.npm-global/bin/weclawbotctl
|
|
10
11
|
ExecStart=%h/.npm-global/bin/weclawbot-openclaw-bridge
|
|
11
12
|
Restart=always
|
|
12
13
|
RestartSec=3
|
|
@@ -5,6 +5,7 @@ import { resolveDeviceContext, validateScreenDocument } from "../lib/direct-cont
|
|
|
5
5
|
const context = resolveDeviceContext();
|
|
6
6
|
const viewport = context.content_viewport;
|
|
7
7
|
const bytes = Buffer.alloc(Math.ceil(viewport.width / 8) * viewport.height, 0xff);
|
|
8
|
+
bytes[0] = 0x00;
|
|
8
9
|
const valid = validateScreenDocument({
|
|
9
10
|
schema: "weclawbot.screen_document.v1",
|
|
10
11
|
id: "test-card",
|
|
@@ -33,4 +34,22 @@ const invalid = validateScreenDocument({
|
|
|
33
34
|
assert.equal(invalid.ok, false);
|
|
34
35
|
assert.ok(invalid.errors.some((error) => error.includes("width")));
|
|
35
36
|
|
|
37
|
+
const uniform = validateScreenDocument({
|
|
38
|
+
schema: "weclawbot.screen_document.v1",
|
|
39
|
+
id: "bad-clear",
|
|
40
|
+
base_revision: "",
|
|
41
|
+
expires_at: new Date(Date.now() + 60_000).toISOString(),
|
|
42
|
+
target: "content",
|
|
43
|
+
kind: "replace",
|
|
44
|
+
pages: [{
|
|
45
|
+
format: "mono1",
|
|
46
|
+
width: viewport.width,
|
|
47
|
+
height: viewport.height,
|
|
48
|
+
stride: Math.ceil(viewport.width / 8),
|
|
49
|
+
data_b64: Buffer.alloc(Math.ceil(viewport.width / 8) * viewport.height, 0x00).toString("base64"),
|
|
50
|
+
}],
|
|
51
|
+
}, context);
|
|
52
|
+
assert.equal(uniform.ok, false);
|
|
53
|
+
assert.ok(uniform.errors.some((error) => error.includes("uniform_screen_document")));
|
|
54
|
+
|
|
36
55
|
console.log("direct-control validator: ok");
|
package/workspace/AGENTS.md
CHANGED
|
@@ -45,3 +45,9 @@ screen revision in `base_revision`.
|
|
|
45
45
|
The firmware receives pixels. Do not send raw text to firmware, and do not
|
|
46
46
|
answer that direct delivery is unavailable before checking the local
|
|
47
47
|
`weclawbotctl` profile.
|
|
48
|
+
|
|
49
|
+
The content viewport is 368 x 206 mono1 pixels, with one to three content pages.
|
|
50
|
+
The firmware will not split a single pixel page after receiving it; `pages.length`
|
|
51
|
+
is the page count on the physical screen. Preserve user-agent layout preferences,
|
|
52
|
+
visual language, and review habits across plugin upgrades unless they violate
|
|
53
|
+
the hardware limits or the user asks to change them.
|