@clawroom/openclaw 0.2.0 → 0.2.1
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/package.json +1 -1
- package/src/channel.ts +8 -8
- package/src/{ws-client.ts → client.ts} +57 -34
- package/src/task-executor.ts +13 -13
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
|
2
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { collectSkills } from "./skill-reporter.js";
|
|
4
4
|
import { getClawroomRuntime } from "./runtime.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ClawroomClient } from "./client.js";
|
|
6
6
|
import { setupTaskExecutor } from "./task-executor.js";
|
|
7
7
|
|
|
8
8
|
// ── Config resolution ────────────────────────────────────────────────
|
|
@@ -54,9 +54,9 @@ function resolveClawroomAccount(opts: {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// ── Persistent
|
|
57
|
+
// ── Persistent client per gateway lifecycle ───────────────────────
|
|
58
58
|
|
|
59
|
-
let activeClient:
|
|
59
|
+
let activeClient: ClawroomClient | null = null;
|
|
60
60
|
|
|
61
61
|
// ── Channel plugin definition ────────────────────────────────────────
|
|
62
62
|
|
|
@@ -103,8 +103,8 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
103
103
|
deliveryMode: "direct",
|
|
104
104
|
|
|
105
105
|
sendText: async ({ to, text }) => {
|
|
106
|
-
// Outbound messages are task results sent via
|
|
107
|
-
// The task-executor sends results directly through
|
|
106
|
+
// Outbound messages are task results sent via HTTP.
|
|
107
|
+
// The task-executor sends results directly through client.send();
|
|
108
108
|
// this adapter exists for completeness if openclaw routing tries
|
|
109
109
|
// to deliver a reply through the channel outbound path.
|
|
110
110
|
if (activeClient) {
|
|
@@ -137,7 +137,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
137
137
|
const deviceId = resolveDeviceId(ctx);
|
|
138
138
|
const log = ctx.log ?? undefined;
|
|
139
139
|
|
|
140
|
-
const client = new
|
|
140
|
+
const client = new ClawroomClient({
|
|
141
141
|
endpoint: account.endpoint,
|
|
142
142
|
token: account.token,
|
|
143
143
|
deviceId,
|
|
@@ -169,7 +169,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
169
169
|
});
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
// Wire up
|
|
172
|
+
// Wire up SSE events to OpenClaw health status
|
|
173
173
|
client.onWelcome(() => publishConnected());
|
|
174
174
|
client.onTask(() => {
|
|
175
175
|
// Any server message = update lastEventAt so gateway knows we're alive
|
|
@@ -208,7 +208,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
setupTaskExecutor({
|
|
211
|
+
setupTaskExecutor({ client: client, runtime, log });
|
|
212
212
|
client.connect();
|
|
213
213
|
activeClient = client;
|
|
214
214
|
|
|
@@ -34,7 +34,7 @@ type WelcomeCallback = (agentId: string) => void;
|
|
|
34
34
|
type FatalCallback = (reason: string) => void;
|
|
35
35
|
type ModeChangeCallback = (mode: "sse" | "polling") => void;
|
|
36
36
|
|
|
37
|
-
export type
|
|
37
|
+
export type ClawroomClientOptions = {
|
|
38
38
|
endpoint: string;
|
|
39
39
|
token: string;
|
|
40
40
|
deviceId: string;
|
|
@@ -55,8 +55,7 @@ export type ClawroomWsClientOptions = {
|
|
|
55
55
|
* Agent→Server actions always use HTTP POST:
|
|
56
56
|
* /api/agents/heartbeat, /complete, /fail, /progress, /claim
|
|
57
57
|
*/
|
|
58
|
-
export class
|
|
59
|
-
private eventSource: EventSource | null = null;
|
|
58
|
+
export class ClawroomClient {
|
|
60
59
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
60
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
62
61
|
private reconnectAttempt = 0;
|
|
@@ -77,9 +76,9 @@ export class ClawroomWsClient {
|
|
|
77
76
|
private fatalCallbacks: FatalCallback[] = [];
|
|
78
77
|
private modeChangeCallbacks: ModeChangeCallback[] = [];
|
|
79
78
|
|
|
80
|
-
constructor(private readonly options:
|
|
79
|
+
constructor(private readonly options: ClawroomClientOptions) {
|
|
81
80
|
// Derive HTTP base from endpoint
|
|
82
|
-
// wss://
|
|
81
|
+
// Legacy: wss://host/ws/agent → https://clawroom.site9.ai/api/agents
|
|
83
82
|
// https://clawroom.site9.ai/api/agents/stream → https://clawroom.site9.ai/api/agents
|
|
84
83
|
const ep = options.endpoint;
|
|
85
84
|
if (ep.includes("/api/agents")) {
|
|
@@ -116,7 +115,7 @@ export class ClawroomWsClient {
|
|
|
116
115
|
get isAlive(): boolean { return !this.stopped; }
|
|
117
116
|
get isConnected(): boolean {
|
|
118
117
|
if (this.mode === "polling") return true;
|
|
119
|
-
return this.
|
|
118
|
+
return this.sseAbort !== null && !this.sseAbort.signal.aborted;
|
|
120
119
|
}
|
|
121
120
|
get isFatal(): boolean { return false; }
|
|
122
121
|
get currentMode(): "sse" | "polling" { return this.mode; }
|
|
@@ -145,53 +144,77 @@ export class ClawroomWsClient {
|
|
|
145
144
|
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
// ── SSE Connection
|
|
147
|
+
// ── SSE Connection (fetch-based, works in Node.js) ───────────────
|
|
148
|
+
|
|
149
|
+
private sseAbort: AbortController | null = null;
|
|
149
150
|
|
|
150
151
|
private doConnectSSE(): void {
|
|
151
152
|
this.destroySSE();
|
|
152
153
|
const { token, deviceId, skills } = this.options;
|
|
153
|
-
const params = new URLSearchParams({
|
|
154
|
-
token,
|
|
155
|
-
deviceId,
|
|
156
|
-
skills: skills.join(","),
|
|
157
|
-
});
|
|
154
|
+
const params = new URLSearchParams({ token, deviceId, skills: skills.join(",") });
|
|
158
155
|
const url = `${this.httpBase}/stream?${params}`;
|
|
159
156
|
|
|
160
157
|
this.options.log?.info?.(`[clawroom] SSE connecting to ${this.httpBase}/stream`);
|
|
161
158
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
this.sseAbort = new AbortController();
|
|
160
|
+
const signal = this.sseAbort.signal;
|
|
161
|
+
|
|
162
|
+
fetch(url, {
|
|
163
|
+
headers: { "Accept": "text/event-stream", "Authorization": `Bearer ${token}` },
|
|
164
|
+
signal,
|
|
165
|
+
}).then(async (res) => {
|
|
166
|
+
if (!res.ok || !res.body) {
|
|
167
|
+
if (res.status === 401) {
|
|
168
|
+
this.stopped = true;
|
|
169
|
+
this.stopHeartbeat();
|
|
170
|
+
this.stopPolling();
|
|
171
|
+
for (const cb of this.fatalCallbacks) cb("Unauthorized");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`HTTP ${res.status}`);
|
|
175
|
+
}
|
|
168
176
|
|
|
169
|
-
this.eventSource.onopen = () => {
|
|
170
177
|
this.options.log?.info?.("[clawroom] SSE connected");
|
|
171
178
|
this.reconnectAttempt = 0;
|
|
172
179
|
this.consecutiveSseFails = 0;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
if (this.mode === "polling") this.switchToSSE();
|
|
181
|
+
|
|
182
|
+
const reader = res.body.getReader();
|
|
183
|
+
const decoder = new TextDecoder();
|
|
184
|
+
let buffer = "";
|
|
185
|
+
|
|
186
|
+
while (true) {
|
|
187
|
+
const { done, value } = await reader.read();
|
|
188
|
+
if (done) break;
|
|
189
|
+
|
|
190
|
+
buffer += decoder.decode(value, { stream: true });
|
|
191
|
+
const lines = buffer.split("\n");
|
|
192
|
+
buffer = lines.pop() ?? "";
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
if (line.startsWith("data: ")) {
|
|
196
|
+
this.handleMessage(line.slice(6));
|
|
197
|
+
}
|
|
198
|
+
// Ignore comments (: keepalive) and empty lines
|
|
199
|
+
}
|
|
176
200
|
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
this.eventSource.onmessage = (event) => {
|
|
180
|
-
this.handleMessage(event.data);
|
|
181
|
-
};
|
|
182
201
|
|
|
183
|
-
|
|
184
|
-
this.options.log?.info?.("[clawroom] SSE
|
|
185
|
-
this.
|
|
202
|
+
// Stream ended
|
|
203
|
+
this.options.log?.info?.("[clawroom] SSE stream ended");
|
|
204
|
+
for (const cb of this.disconnectCallbacks) cb();
|
|
205
|
+
this.triggerReconnect("stream ended");
|
|
206
|
+
}).catch((err) => {
|
|
207
|
+
if (signal.aborted) return;
|
|
208
|
+
this.options.log?.warn?.(`[clawroom] SSE error: ${err}`);
|
|
186
209
|
for (const cb of this.disconnectCallbacks) cb();
|
|
187
210
|
this.triggerReconnect("SSE error");
|
|
188
|
-
};
|
|
211
|
+
});
|
|
189
212
|
}
|
|
190
213
|
|
|
191
214
|
private destroySSE(): void {
|
|
192
|
-
if (this.
|
|
193
|
-
try { this.
|
|
194
|
-
this.
|
|
215
|
+
if (this.sseAbort) {
|
|
216
|
+
try { this.sseAbort.abort(); } catch {}
|
|
217
|
+
this.sseAbort = null;
|
|
195
218
|
}
|
|
196
219
|
}
|
|
197
220
|
|
package/src/task-executor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ServerClaimAck, ServerTask, AgentResultFile } from "@clawroom/sdk";
|
|
2
2
|
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ClawroomClient } from "./client.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
|
18
18
|
* Claiming is triggered externally (by the owner via Dashboard).
|
|
19
19
|
*/
|
|
20
20
|
export function setupTaskExecutor(opts: {
|
|
21
|
-
|
|
21
|
+
client: ClawroomClient;
|
|
22
22
|
runtime: PluginRuntime;
|
|
23
23
|
log?: {
|
|
24
24
|
info?: (...args: unknown[]) => void;
|
|
@@ -26,19 +26,19 @@ export function setupTaskExecutor(opts: {
|
|
|
26
26
|
error?: (...args: unknown[]) => void;
|
|
27
27
|
};
|
|
28
28
|
}): void {
|
|
29
|
-
const {
|
|
29
|
+
const { client, runtime, log } = opts;
|
|
30
30
|
|
|
31
31
|
// Track received tasks (from broadcast or server push after claim)
|
|
32
32
|
const knownTasks = new Map<string, ServerTask>();
|
|
33
33
|
|
|
34
34
|
// New task received — store it for potential execution
|
|
35
|
-
|
|
35
|
+
client.onTask((task: ServerTask) => {
|
|
36
36
|
log?.info?.(`[clawroom:executor] received task ${task.taskId}: ${task.title}`);
|
|
37
37
|
knownTasks.set(task.taskId, task);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Task list on connect — store all
|
|
41
|
-
|
|
41
|
+
client.onTaskList((tasks: ServerTask[]) => {
|
|
42
42
|
log?.info?.(`[clawroom:executor] ${tasks.length} open task(s) available`);
|
|
43
43
|
for (const t of tasks) {
|
|
44
44
|
log?.info?.(`[clawroom:executor] - ${t.taskId}: ${t.title}`);
|
|
@@ -47,7 +47,7 @@ export function setupTaskExecutor(opts: {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
// Claim ack — either from plugin-initiated claim or Dashboard-initiated claim
|
|
50
|
-
|
|
50
|
+
client.onClaimAck((ack: ServerClaimAck) => {
|
|
51
51
|
if (!ack.ok) {
|
|
52
52
|
log?.warn?.(
|
|
53
53
|
`[clawroom:executor] claim rejected for ${ack.taskId}: ${ack.reason ?? "unknown"}`,
|
|
@@ -67,7 +67,7 @@ export function setupTaskExecutor(opts: {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
log?.info?.(`[clawroom:executor] executing task ${task.taskId}`);
|
|
70
|
-
void executeTask({
|
|
70
|
+
void executeTask({ client, runtime, task, log });
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -76,7 +76,7 @@ export function setupTaskExecutor(opts: {
|
|
|
76
76
|
* result back to the ClawRoom server.
|
|
77
77
|
*/
|
|
78
78
|
async function executeTask(opts: {
|
|
79
|
-
|
|
79
|
+
client: ClawroomClient;
|
|
80
80
|
runtime: PluginRuntime;
|
|
81
81
|
task: ServerTask;
|
|
82
82
|
log?: {
|
|
@@ -84,7 +84,7 @@ async function executeTask(opts: {
|
|
|
84
84
|
error?: (...args: unknown[]) => void;
|
|
85
85
|
};
|
|
86
86
|
}): Promise<void> {
|
|
87
|
-
const {
|
|
87
|
+
const { client, runtime, task, log } = opts;
|
|
88
88
|
const sessionKey = `clawroom:task:${task.taskId}`;
|
|
89
89
|
|
|
90
90
|
const agentMessage = buildAgentMessage(task);
|
|
@@ -116,7 +116,7 @@ async function executeTask(opts: {
|
|
|
116
116
|
log?.error?.(
|
|
117
117
|
`[clawroom:executor] subagent error for task ${task.taskId}: ${waitResult.error}`,
|
|
118
118
|
);
|
|
119
|
-
|
|
119
|
+
client.send({
|
|
120
120
|
type: "agent.fail",
|
|
121
121
|
taskId: task.taskId,
|
|
122
122
|
reason: waitResult.error ?? "Agent execution failed",
|
|
@@ -126,7 +126,7 @@ async function executeTask(opts: {
|
|
|
126
126
|
|
|
127
127
|
if (waitResult.status === "timeout") {
|
|
128
128
|
log?.error?.(`[clawroom:executor] subagent timeout for task ${task.taskId}`);
|
|
129
|
-
|
|
129
|
+
client.send({
|
|
130
130
|
type: "agent.fail",
|
|
131
131
|
taskId: task.taskId,
|
|
132
132
|
reason: "Agent execution timed out",
|
|
@@ -155,7 +155,7 @@ async function executeTask(opts: {
|
|
|
155
155
|
.trim();
|
|
156
156
|
|
|
157
157
|
log?.info?.(`[clawroom:executor] task ${task.taskId} completed${files.length > 0 ? ` (with ${files.length} file(s))` : ""}`);
|
|
158
|
-
|
|
158
|
+
client.send({
|
|
159
159
|
type: "agent.complete",
|
|
160
160
|
taskId: task.taskId,
|
|
161
161
|
output,
|
|
@@ -169,7 +169,7 @@ async function executeTask(opts: {
|
|
|
169
169
|
} catch (err) {
|
|
170
170
|
const reason = err instanceof Error ? err.message : String(err);
|
|
171
171
|
log?.error?.(`[clawroom:executor] unexpected error for task ${task.taskId}: ${reason}`);
|
|
172
|
-
|
|
172
|
+
client.send({
|
|
173
173
|
type: "agent.release",
|
|
174
174
|
taskId: task.taskId,
|
|
175
175
|
reason,
|