@rethinkingstudio/clawpilot 1.1.15-beta.0 → 1.1.15-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,280 @@
1
+ import { createHmac } from "crypto";
2
+ import { randomUUID } from "crypto";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Types
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export interface StsCredentials {
9
+ bucket: string;
10
+ region: string;
11
+ endpoint: string;
12
+ baseUrl: string;
13
+ dirPrefix: string;
14
+ expiresAt: number;
15
+ credentials: {
16
+ accessKeyId: string;
17
+ accessKeySecret: string;
18
+ securityToken: string;
19
+ };
20
+ }
21
+
22
+ export interface UploadResult {
23
+ url: string; // public CDN URL
24
+ thumbnailUrl?: string; // OSS snapshot URL (video only)
25
+ mimeType: string;
26
+ size: number;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // OSS HMAC-SHA1 request signing (with STS security token)
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function ossSign(
34
+ method: string,
35
+ contentMd5: string,
36
+ contentType: string,
37
+ date: string,
38
+ canonicalizedHeaders: string,
39
+ canonicalizedResource: string,
40
+ secretKey: string
41
+ ): string {
42
+ const stringToSign = [
43
+ method,
44
+ contentMd5,
45
+ contentType,
46
+ date,
47
+ canonicalizedHeaders + canonicalizedResource,
48
+ ].join("\n");
49
+ return createHmac("sha1", secretKey).update(stringToSign).digest("base64");
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Simple PutObject upload (images / small files)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export async function putObject(
57
+ sts: StsCredentials,
58
+ data: Buffer,
59
+ ext: string,
60
+ mimeType: string
61
+ ): Promise<UploadResult> {
62
+ const { bucket, endpoint, baseUrl, dirPrefix, credentials } = sts;
63
+ const objectKey = `${dirPrefix}/${randomUUID()}${ext}`;
64
+ const date = new Date().toUTCString();
65
+ const canonicalizedResource = `/${bucket}/${objectKey}`;
66
+ const canonicalizedHeaders = `x-oss-security-token:${credentials.securityToken}\n`;
67
+
68
+ const sig = ossSign(
69
+ "PUT",
70
+ "",
71
+ mimeType,
72
+ date,
73
+ canonicalizedHeaders,
74
+ canonicalizedResource,
75
+ credentials.accessKeySecret
76
+ );
77
+
78
+ const url = `${endpoint.replace(/\/$/, "")}/${objectKey}`;
79
+ const res = await fetch(url, {
80
+ method: "PUT",
81
+ headers: {
82
+ "Content-Type": mimeType,
83
+ "Date": date,
84
+ "x-oss-security-token": credentials.securityToken,
85
+ "Authorization": `OSS ${credentials.accessKeyId}:${sig}`,
86
+ },
87
+ body: data as unknown as BodyInit,
88
+ });
89
+
90
+ if (!res.ok) {
91
+ const text = await res.text().catch(() => "");
92
+ throw new Error(`OSS PutObject failed: ${res.status} ${text}`);
93
+ }
94
+
95
+ const publicUrl = `${baseUrl.replace(/\/$/, "")}/${objectKey}`;
96
+ return { url: publicUrl, mimeType, size: data.length };
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Multipart upload (videos > 10 MB)
101
+ // ---------------------------------------------------------------------------
102
+
103
+ const PART_SIZE = 5 * 1024 * 1024; // 5 MB per part
104
+
105
+ export async function multipartUpload(
106
+ sts: StsCredentials,
107
+ data: Buffer,
108
+ ext: string,
109
+ mimeType: string
110
+ ): Promise<UploadResult> {
111
+ const { bucket, endpoint, baseUrl, dirPrefix, credentials } = sts;
112
+ const objectKey = `${dirPrefix}/${randomUUID()}${ext}`;
113
+ const baseOssUrl = `${endpoint.replace(/\/$/, "")}/${objectKey}`;
114
+
115
+ function makeHeaders(
116
+ method: string,
117
+ contentType: string,
118
+ extraHeaders: Record<string, string>,
119
+ resource: string
120
+ ): Headers {
121
+ const date = new Date().toUTCString();
122
+ const canonicalizedHeaders = [
123
+ "x-oss-security-token:" + credentials.securityToken,
124
+ ...Object.entries(extraHeaders)
125
+ .filter(([k]) => k.startsWith("x-oss-"))
126
+ .sort()
127
+ .map(([k, v]) => `${k}:${v}`),
128
+ ]
129
+ .sort()
130
+ .join("\n") + "\n";
131
+
132
+ const sig = ossSign(
133
+ method,
134
+ "",
135
+ contentType,
136
+ date,
137
+ canonicalizedHeaders,
138
+ `/${bucket}/${objectKey}`,
139
+ credentials.accessKeySecret
140
+ );
141
+
142
+ const headers = new Headers({
143
+ "Date": date,
144
+ "x-oss-security-token": credentials.securityToken,
145
+ "Authorization": `OSS ${credentials.accessKeyId}:${sig}`,
146
+ ...extraHeaders,
147
+ });
148
+ if (contentType) headers.set("Content-Type", contentType);
149
+ return headers;
150
+ }
151
+
152
+ // 1. Initiate
153
+ const initRes = await fetch(`${baseOssUrl}?uploads`, {
154
+ method: "POST",
155
+ headers: makeHeaders("POST", mimeType, {}, `/${bucket}/${objectKey}`),
156
+ });
157
+ if (!initRes.ok) throw new Error(`OSS InitiateMultipartUpload failed: ${initRes.status}`);
158
+ const initText = await initRes.text();
159
+ const uploadIdMatch = initText.match(/<UploadId>([^<]+)<\/UploadId>/);
160
+ if (!uploadIdMatch) throw new Error("OSS: could not parse UploadId");
161
+ const uploadId = uploadIdMatch[1];
162
+
163
+ // 2. Upload parts
164
+ const parts: Array<{ partNumber: number; etag: string }> = [];
165
+ let partNumber = 1;
166
+ let offset = 0;
167
+
168
+ try {
169
+ while (offset < data.length) {
170
+ const chunk = data.slice(offset, offset + PART_SIZE);
171
+ const partDate = new Date().toUTCString();
172
+ const canonicalizedHeaders = `x-oss-security-token:${credentials.securityToken}\n`;
173
+ const sig = ossSign(
174
+ "PUT", "", "application/octet-stream", partDate,
175
+ canonicalizedHeaders, `/${bucket}/${objectKey}`,
176
+ credentials.accessKeySecret
177
+ );
178
+ const partRes = await fetch(
179
+ `${baseOssUrl}?partNumber=${partNumber}&uploadId=${uploadId}`,
180
+ {
181
+ method: "PUT",
182
+ headers: {
183
+ "Content-Type": "application/octet-stream",
184
+ "Date": partDate,
185
+ "x-oss-security-token": credentials.securityToken,
186
+ "Authorization": `OSS ${credentials.accessKeyId}:${sig}`,
187
+ },
188
+ body: chunk as unknown as BodyInit,
189
+ }
190
+ );
191
+ if (!partRes.ok) throw new Error(`OSS UploadPart ${partNumber} failed: ${partRes.status}`);
192
+ const etag = partRes.headers.get("etag") ?? "";
193
+ parts.push({ partNumber, etag });
194
+ partNumber++;
195
+ offset += PART_SIZE;
196
+ }
197
+
198
+ // 3. Complete
199
+ const completeBody = [
200
+ "<CompleteMultipartUpload>",
201
+ ...parts.map(
202
+ (p) => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>${p.etag}</ETag></Part>`
203
+ ),
204
+ "</CompleteMultipartUpload>",
205
+ ].join("");
206
+
207
+ const completeDate = new Date().toUTCString();
208
+ const cHeaders = `x-oss-security-token:${credentials.securityToken}\n`;
209
+ const cSig = ossSign(
210
+ "POST", "", "application/xml", completeDate,
211
+ cHeaders, `/${bucket}/${objectKey}`, credentials.accessKeySecret
212
+ );
213
+ const completeRes = await fetch(`${baseOssUrl}?uploadId=${uploadId}`, {
214
+ method: "POST",
215
+ headers: {
216
+ "Content-Type": "application/xml",
217
+ "Date": completeDate,
218
+ "x-oss-security-token": credentials.securityToken,
219
+ "Authorization": `OSS ${credentials.accessKeyId}:${cSig}`,
220
+ },
221
+ body: completeBody,
222
+ });
223
+ if (!completeRes.ok) throw new Error(`OSS CompleteMultipartUpload failed: ${completeRes.status}`);
224
+
225
+ } catch (err) {
226
+ // Best-effort abort to avoid billing for incomplete upload
227
+ try {
228
+ const abortDate = new Date().toUTCString();
229
+ const aHeaders = `x-oss-security-token:${credentials.securityToken}\n`;
230
+ const aSig = ossSign(
231
+ "DELETE", "", "", abortDate,
232
+ aHeaders, `/${bucket}/${objectKey}`, credentials.accessKeySecret
233
+ );
234
+ await fetch(`${baseOssUrl}?uploadId=${uploadId}`, {
235
+ method: "DELETE",
236
+ headers: {
237
+ "Date": abortDate,
238
+ "x-oss-security-token": credentials.securityToken,
239
+ "Authorization": `OSS ${credentials.accessKeyId}:${aSig}`,
240
+ },
241
+ });
242
+ } catch { /* ignore abort errors */ }
243
+ throw err;
244
+ }
245
+
246
+ const publicUrl = `${baseUrl.replace(/\/$/, "")}/${objectKey}`;
247
+ // OSS video snapshot thumbnail URL
248
+ const thumbnailUrl = `${publicUrl}?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast`;
249
+ return { url: publicUrl, thumbnailUrl, mimeType, size: data.length };
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Convenience: auto-select PutObject vs multipart based on size
254
+ // ---------------------------------------------------------------------------
255
+
256
+ const MULTIPART_THRESHOLD = 10 * 1024 * 1024; // 10 MB
257
+
258
+ export async function uploadMedia(
259
+ sts: StsCredentials,
260
+ data: Buffer,
261
+ mimeType: string
262
+ ): Promise<UploadResult> {
263
+ const ext = mimeTypeToExt(mimeType);
264
+ if (data.length >= MULTIPART_THRESHOLD) {
265
+ return multipartUpload(sts, data, ext, mimeType);
266
+ }
267
+ return putObject(sts, data, ext, mimeType);
268
+ }
269
+
270
+ function mimeTypeToExt(mimeType: string): string {
271
+ const map: Record<string, string> = {
272
+ "image/jpeg": ".jpg",
273
+ "image/png": ".png",
274
+ "image/gif": ".gif",
275
+ "image/webp": ".webp",
276
+ "video/mp4": ".mp4",
277
+ "video/quicktime": ".mov",
278
+ };
279
+ return map[mimeType] ?? ".bin";
280
+ }
@@ -225,19 +225,23 @@ function installLinuxServiceNohup(): boolean {
225
225
  }
226
226
 
227
227
  function installLinuxService(): boolean {
228
+ try {
229
+ if (installLinuxServiceNohup()) {
230
+ return true;
231
+ }
232
+ } catch {
233
+ // Fall back to systemd below.
234
+ }
235
+
228
236
  if (canUseSystemdUser()) {
229
237
  try {
230
238
  return installLinuxServiceSystemd();
231
239
  } catch {
232
- // Fall back to nohup below.
240
+ // Give up below.
233
241
  }
234
242
  }
235
243
 
236
- try {
237
- return installLinuxServiceNohup();
238
- } catch {
239
- return false;
240
- }
244
+ return false;
241
245
  }
242
246
 
243
247
  function installWindowsService(): boolean {
@@ -2,6 +2,8 @@ import { WebSocket } from "ws";
2
2
  import { OpenClawGatewayClient } from "./gateway-client.js";
3
3
  import { handleLocalCommand } from "../commands/local-handlers.js";
4
4
  import { handleProviderCommand } from "../commands/provider-handlers.js";
5
+ import { getServicePlatform } from "../platform/service-manager.js";
6
+ import { uploadAssistantAttachments } from "../media/assistant-attachments.js";
5
7
  import { homedir } from "os";
6
8
  import { join } from "path";
7
9
  import { mkdir, writeFile } from "fs/promises";
@@ -19,6 +21,7 @@ const OUTBOUND_DIR = join(homedir(), ".openclaw", "media", "outbound");
19
21
 
20
22
  /** Messages the relay client sends to the relay server. */
21
23
  type ToServer =
24
+ | { type: "relay_hello"; platform: "macos" | "linux" | "windows" | "unsupported" }
22
25
  | { type: "gateway_connected" }
23
26
  | { type: "gateway_disconnected"; reason: string }
24
27
  | { type: "event"; event: string; payload: unknown }
@@ -82,6 +85,7 @@ export async function runRelayManager(opts: RelayManagerOptions): Promise<boolea
82
85
 
83
86
  relayWs.on("open", () => {
84
87
  console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
88
+ send({ type: "relay_hello", platform: getServicePlatform() });
85
89
  opts.onConnected?.();
86
90
 
87
91
  // Start the persistent gateway connection as soon as we're connected
@@ -102,15 +106,15 @@ export async function runRelayManager(opts: RelayManagerOptions): Promise<boolea
102
106
  },
103
107
 
104
108
  onEvent: (event, payload) => {
105
- // On chat final, fetch history to get actual content (OpenClaw 2026.3.2+
106
- // no longer includes message content in the chat final event payload).
107
- // This mirrors what the macOS 2026.3.2 client does.
109
+ // On chat final, fetch history to get actual content + extract media attachments.
110
+ // OpenClaw 2026.3.2+ no longer includes message content in chat final payload.
108
111
  if (event === "chat") {
109
112
  const p = payload as { state?: string; sessionKey?: string; runId?: string; message?: unknown };
110
113
  if (p?.state === "final" && p?.sessionKey) {
111
114
  const sessionKey = p.sessionKey;
112
115
  const runId = p.runId;
113
- type HistoryResponse = { messages?: Array<{ role: string; content?: Array<{ type: string; text?: string }> }> };
116
+ type ContentBlock = { type: string; text?: string; source?: { type?: string; media_type?: string; data?: string; path?: string } };
117
+ type HistoryResponse = { messages?: Array<{ role: string; content?: ContentBlock[] }> };
114
118
  const fetchHistory = () =>
115
119
  gatewayClient!.request<HistoryResponse>("chat.history", { sessionKey, limit: 10 });
116
120
  const extractText = (h: HistoryResponse | undefined) => {
@@ -126,10 +130,28 @@ export async function runRelayManager(opts: RelayManagerOptions): Promise<boolea
126
130
  await new Promise((resolve) => setTimeout(resolve, 600));
127
131
  const retryHistory = await fetchHistory();
128
132
  text = extractText(retryHistory);
133
+ history = retryHistory;
129
134
  }
130
135
  if (text) {
131
136
  (p as Record<string, unknown>).message = { content: [{ type: "text", text }] };
132
137
  }
138
+
139
+ // Upload any media blocks found in the history
140
+ try {
141
+ const attachments = await uploadAssistantAttachments(
142
+ history ?? {},
143
+ opts.relayServerUrl,
144
+ opts.gatewayId,
145
+ opts.relaySecret
146
+ );
147
+ if (attachments.length > 0) {
148
+ (p as Record<string, unknown>).attachments = attachments;
149
+ console.log(`[relay] chat final: injected ${attachments.length} attachment(s) runId=${runId}`);
150
+ }
151
+ } catch (mediaErr) {
152
+ console.error(`[relay] media upload error (non-fatal): ${mediaErr}`);
153
+ }
154
+
133
155
  console.log(`[relay] chat final (history fetched): runId=${runId} textLength=${text?.length ?? 0}`);
134
156
  send({ type: "event", event, payload });
135
157
  })
@@ -137,7 +159,7 @@ export async function runRelayManager(opts: RelayManagerOptions): Promise<boolea
137
159
  console.error(`[relay] chat.history fetch failed: ${err}`);
138
160
  send({ type: "event", event, payload });
139
161
  });
140
- return; // will send after history fetch
162
+ return; // will send after history fetch + media upload
141
163
  }
142
164
  }
143
165
  send({ type: "event", event, payload });
@@ -1,18 +0,0 @@
1
- import { WebSocket } from "ws";
2
- /**
3
- * Proxies WebSocket frames between the relay server (via relayWs) and the
4
- * local OpenClaw Gateway (via a new direct WebSocket connection).
5
- *
6
- * All frames are forwarded as raw bytes — no parsing of OpenClaw protocol.
7
- */
8
- export declare class SessionProxy {
9
- private readonly sessionId;
10
- private readonly relayWs;
11
- private readonly gatewayUrl;
12
- private gwWs;
13
- private closed;
14
- constructor(sessionId: string, relayWs: WebSocket, gatewayUrl: string);
15
- start(): Promise<void>;
16
- forwardToGateway(base64Data: string): void;
17
- close(): void;
18
- }
@@ -1,75 +0,0 @@
1
- import { WebSocket } from "ws";
2
- /**
3
- * Proxies WebSocket frames between the relay server (via relayWs) and the
4
- * local OpenClaw Gateway (via a new direct WebSocket connection).
5
- *
6
- * All frames are forwarded as raw bytes — no parsing of OpenClaw protocol.
7
- */
8
- export class SessionProxy {
9
- sessionId;
10
- relayWs;
11
- gatewayUrl;
12
- gwWs = null;
13
- closed = false;
14
- constructor(sessionId, relayWs, gatewayUrl) {
15
- this.sessionId = sessionId;
16
- this.relayWs = relayWs;
17
- this.gatewayUrl = gatewayUrl;
18
- }
19
- async start() {
20
- return new Promise((resolve, reject) => {
21
- const gw = new WebSocket(this.gatewayUrl);
22
- this.gwWs = gw;
23
- const timeout = setTimeout(() => {
24
- gw.terminate();
25
- reject(new Error(`Timeout connecting to gateway at ${this.gatewayUrl}`));
26
- }, 10_000);
27
- gw.on("open", () => {
28
- clearTimeout(timeout);
29
- resolve();
30
- });
31
- gw.on("error", (err) => {
32
- clearTimeout(timeout);
33
- if (!this.closed)
34
- reject(err);
35
- });
36
- // Gateway → relay server
37
- gw.on("message", (raw) => {
38
- if (this.relayWs.readyState !== WebSocket.OPEN)
39
- return;
40
- const data = raw instanceof Buffer ? raw : Buffer.from(raw);
41
- const msg = {
42
- ctrl: "DATA",
43
- sessionId: this.sessionId,
44
- data: data.toString("base64"),
45
- };
46
- this.relayWs.send(JSON.stringify(msg));
47
- });
48
- gw.on("close", () => {
49
- if (!this.closed) {
50
- this.closed = true;
51
- // Notify relay server that session is done
52
- if (this.relayWs.readyState === WebSocket.OPEN) {
53
- const msg = {
54
- ctrl: "SESSION_CLOSE",
55
- sessionId: this.sessionId,
56
- };
57
- this.relayWs.send(JSON.stringify(msg));
58
- }
59
- }
60
- });
61
- });
62
- }
63
- // Called when DATA arrives from the relay server for this session
64
- forwardToGateway(base64Data) {
65
- if (!this.gwWs || this.gwWs.readyState !== WebSocket.OPEN)
66
- return;
67
- const buf = Buffer.from(base64Data, "base64");
68
- this.gwWs.send(buf);
69
- }
70
- close() {
71
- this.closed = true;
72
- this.gwWs?.close();
73
- }
74
- }
75
- //# sourceMappingURL=session-proxy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-proxy.js","sourceRoot":"","sources":["../../src/relay/session-proxy.ts"],"names":[],"mappings":"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"}