@syengup/friday-channel-next 0.1.15 → 0.1.16

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.
@@ -22,6 +22,22 @@ export type FridayReplyPayload = {
22
22
  interactive?: unknown;
23
23
  channelData?: unknown;
24
24
  };
25
+ /** Map local / gateway paths to public `/friday-next/files/...` URLs where possible. */
26
+ /**
27
+ * Canvas snapshots are captured so the *agent* can "see" the rendered canvas. OpenClaw core surfaces
28
+ * any image tool result as deliverable media on the assistant reply block, which for Friday Next would
29
+ * make the snapshot auto-appear as an attachment mid-stream — not what we want. Snapshot temp files are
30
+ * named `openclaw-canvas-snapshot-<uuid>.<ext>`, so we detect them by basename and drop them from the
31
+ * delivered payload (the assistant text is preserved). Agent-initiated media sends are unaffected —
32
+ * those flow through the `outbound` channel action, not the deliver block path.
33
+ */
34
+ export declare function isCanvasSnapshotMediaPath(url: unknown): boolean;
35
+ export declare function translateDeliverPayload(pl: FridayReplyPayload, kind: string, meta?: {
36
+ modelName?: string;
37
+ totalTokens?: number;
38
+ contextTokensUsed?: number;
39
+ contextWindowMax?: number;
40
+ }): Record<string, unknown>;
25
41
  export interface FridayMessagePayload {
26
42
  deviceId: string;
27
43
  text: string;
@@ -84,7 +84,28 @@ function inferFridayNextMediaKind(params) {
84
84
  return "file";
85
85
  }
86
86
  /** Map local / gateway paths to public `/friday-next/files/...` URLs where possible. */
87
- function translateDeliverPayload(pl, kind, meta) {
87
+ /**
88
+ * Canvas snapshots are captured so the *agent* can "see" the rendered canvas. OpenClaw core surfaces
89
+ * any image tool result as deliverable media on the assistant reply block, which for Friday Next would
90
+ * make the snapshot auto-appear as an attachment mid-stream — not what we want. Snapshot temp files are
91
+ * named `openclaw-canvas-snapshot-<uuid>.<ext>`, so we detect them by basename and drop them from the
92
+ * delivered payload (the assistant text is preserved). Agent-initiated media sends are unaffected —
93
+ * those flow through the `outbound` channel action, not the deliver block path.
94
+ */
95
+ export function isCanvasSnapshotMediaPath(url) {
96
+ if (typeof url !== "string")
97
+ return false;
98
+ const base = url.split(/[/\\]/).pop() ?? url;
99
+ return /canvas-snapshot-/i.test(base);
100
+ }
101
+ export function translateDeliverPayload(pl, kind, meta) {
102
+ // Strip canvas-snapshot tool-result images before any media resolution (paths here are still the
103
+ // original `/tmp/openclaw/openclaw-canvas-snapshot-*.jpg` temp paths, not yet copied to friday files).
104
+ const filteredSingle = typeof pl.mediaUrl === "string" && !isCanvasSnapshotMediaPath(pl.mediaUrl) ? pl.mediaUrl : null;
105
+ const filteredArr = Array.isArray(pl.mediaUrls)
106
+ ? pl.mediaUrls.filter((u) => !isCanvasSnapshotMediaPath(u))
107
+ : pl.mediaUrls;
108
+ pl = { ...pl, mediaUrl: filteredSingle, mediaUrls: filteredArr };
88
109
  const raw = { ...pl };
89
110
  const originalUrls = collectReplyPayloadMediaUrls(pl);
90
111
  if (typeof pl.mediaUrl === "string" && pl.mediaUrl.trim()) {
package/install.js CHANGED
@@ -238,14 +238,22 @@ if (configChanged) {
238
238
 
239
239
  // --------------- restart gateway ---------------
240
240
 
241
- log("Restarting OpenClaw gateway...");
241
+ log("Restarting OpenClaw gateway... (this can take 20-30s)");
242
242
  try {
243
- const out = execSync(`${openclawCmd} gateway restart`, { encoding: "utf8", stdio: "pipe", timeout: 15000 });
243
+ // A full gateway restart commonly takes 20s+ on a fresh boot; give it plenty of room
244
+ // so we don't kill it mid-restart and report a false failure.
245
+ const out = execSync(`${openclawCmd} gateway restart`, { encoding: "utf8", stdio: "pipe", timeout: 90000 });
244
246
  if (out.trim()) console.log(out.trim());
245
247
  } catch (e) {
246
248
  if (e.stdout?.trim()) console.log(e.stdout.trim());
247
249
  if (e.stderr?.trim()) console.error(e.stderr.trim());
248
- warn("Gateway restart failed. Restart manually: openclaw gateway restart");
250
+ // ETIMEDOUT/SIGTERM here usually means the restart is simply slow, not broken
251
+ // the verify step below will confirm whether the gateway actually came up.
252
+ if (e.code === "ETIMEDOUT" || e.signal === "SIGTERM") {
253
+ warn("Gateway restart is taking a while — will verify below.");
254
+ } else {
255
+ warn("Gateway restart failed. Restart manually: openclaw gateway restart");
256
+ }
249
257
  }
250
258
 
251
259
  // --------------- verify ---------------
@@ -270,7 +278,11 @@ const gatewayUrl = bindMode === "lan"
270
278
  ? `http://${getLanIp()}:${gatewayPort}`
271
279
  : `http://127.0.0.1:${gatewayPort}`;
272
280
 
273
- async function verifyGateway(url, token, retries = 6) {
281
+ // Always verify against loopback: the gateway binds 0.0.0.0 so it's reachable here,
282
+ // and this avoids false negatives from LAN/NAT routing of the advertised IP.
283
+ const verifyUrl = `http://127.0.0.1:${gatewayPort}`;
284
+
285
+ async function verifyGateway(url, token, retries = 30) {
274
286
  const http = await import("node:http");
275
287
  const { hostname, port } = new URL(url);
276
288
  for (let i = 1; i <= retries; i++) {
@@ -312,7 +324,7 @@ async function verifyGateway(url, token, retries = 6) {
312
324
  }
313
325
 
314
326
  log("Verifying gateway...");
315
- const verified = await verifyGateway(gatewayUrl, gatewayToken);
327
+ const verified = await verifyGateway(verifyUrl, gatewayToken);
316
328
 
317
329
  // --------------- show connection info ---------------
318
330
 
@@ -372,4 +384,39 @@ if (ipType === "tailscale") {
372
384
  } else {
373
385
  log("This URL appears to be publicly accessible (" + ip + ").");
374
386
  }
387
+
388
+ // On a cloud server the advertised IP is the internal/NAT address, which a phone
389
+ // over the internet can't reach. Best-effort: detect the public IP so the user
390
+ // has the address to actually connect with (and a reminder to open the port).
391
+ if (ipType === "private" || ipType === "loopback") {
392
+ const publicIp = await detectPublicIp();
393
+ if (publicIp && publicIp !== ip) {
394
+ log("");
395
+ log(BOLD_YELLOW("Looks like a cloud server. For remote access use the PUBLIC address:"));
396
+ log(BOLD_YELLOW("检测到云服务器,远程连接请改用公网地址:"));
397
+ log("Public URL: " + BOLD_YELLOW(`http://${publicIp}:${gatewayPort}`));
398
+ log("(Open inbound TCP port " + gatewayPort + " in your firewall / security group first.)");
399
+ log("(需先在防火墙/安全组放行入站 TCP 端口 " + gatewayPort + "。)");
400
+ }
401
+ }
375
402
  log("--------------------------------------------------");
403
+
404
+ async function detectPublicIp() {
405
+ const endpoints = ["http://api.ipify.org", "http://ifconfig.me/ip", "http://icanhazip.com"];
406
+ const http = await import("node:http");
407
+ for (const url of endpoints) {
408
+ try {
409
+ const ipStr = await new Promise((resolve, reject) => {
410
+ const req = http.get(url, { timeout: 3000 }, (res) => {
411
+ let body = "";
412
+ res.on("data", (c) => body += c);
413
+ res.on("end", () => resolve(body.trim()));
414
+ });
415
+ req.on("error", reject);
416
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
417
+ });
418
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ipStr)) return ipStr;
419
+ } catch { /* try next */ }
420
+ }
421
+ return null;
422
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isCanvasSnapshotMediaPath, translateDeliverPayload } from "./messages.js";
3
+
4
+ /**
5
+ * canvas.snapshot tool results are an agent-facing capture. OpenClaw core surfaces image tool
6
+ * results as deliverable media on the assistant reply block, which would otherwise auto-attach the
7
+ * snapshot to the user's stream. translateDeliverPayload must drop snapshot temp files (by basename)
8
+ * while preserving the assistant text and any non-snapshot media.
9
+ */
10
+ describe("canvas snapshot media suppression", () => {
11
+ it("detects canvas snapshot temp paths by basename", () => {
12
+ expect(
13
+ isCanvasSnapshotMediaPath("/tmp/openclaw/openclaw-canvas-snapshot-d2e6aef2-0441.jpg"),
14
+ ).toBe(true);
15
+ expect(isCanvasSnapshotMediaPath("C:\\tmp\\openclaw-canvas-snapshot-abc.png")).toBe(true);
16
+ expect(isCanvasSnapshotMediaPath("/Users/me/Pictures/screenshot_latest.jpg")).toBe(false);
17
+ expect(isCanvasSnapshotMediaPath("/friday-next/files/uuid.jpg")).toBe(false);
18
+ expect(isCanvasSnapshotMediaPath(undefined)).toBe(false);
19
+ });
20
+
21
+ it("strips a snapshot mediaUrl from a block deliver payload, keeping the text", () => {
22
+ const out = translateDeliverPayload(
23
+ {
24
+ text: "🎉 通了!",
25
+ mediaUrl: "/tmp/openclaw/openclaw-canvas-snapshot-d2e6aef2-0441.jpg",
26
+ },
27
+ "block",
28
+ );
29
+ expect(out.text).toBe("🎉 通了!");
30
+ expect(out.mediaUrl).toBeNull();
31
+ // No media survived → no image mediaKind tagged onto the payload.
32
+ const channelData = out.channelData as { fridayNext?: { mediaKind?: string } } | undefined;
33
+ expect(channelData?.fridayNext?.mediaKind).toBeUndefined();
34
+ });
35
+
36
+ it("strips snapshot entries from mediaUrls but preserves non-snapshot media", () => {
37
+ const out = translateDeliverPayload(
38
+ {
39
+ text: "page rendered",
40
+ mediaUrls: [
41
+ "/tmp/openclaw/openclaw-canvas-snapshot-aaa.jpg",
42
+ "/Users/me/Pictures/real-image.png",
43
+ ],
44
+ },
45
+ "block",
46
+ );
47
+ const urls = out.mediaUrls as string[];
48
+ expect(urls).toHaveLength(1);
49
+ expect(urls.some((u) => u.includes("canvas-snapshot-"))).toBe(false);
50
+ });
51
+ });
@@ -132,11 +132,34 @@ function inferFridayNextMediaKind(params: {
132
132
  }
133
133
 
134
134
  /** Map local / gateway paths to public `/friday-next/files/...` URLs where possible. */
135
- function translateDeliverPayload(
135
+ /**
136
+ * Canvas snapshots are captured so the *agent* can "see" the rendered canvas. OpenClaw core surfaces
137
+ * any image tool result as deliverable media on the assistant reply block, which for Friday Next would
138
+ * make the snapshot auto-appear as an attachment mid-stream — not what we want. Snapshot temp files are
139
+ * named `openclaw-canvas-snapshot-<uuid>.<ext>`, so we detect them by basename and drop them from the
140
+ * delivered payload (the assistant text is preserved). Agent-initiated media sends are unaffected —
141
+ * those flow through the `outbound` channel action, not the deliver block path.
142
+ */
143
+ export function isCanvasSnapshotMediaPath(url: unknown): boolean {
144
+ if (typeof url !== "string") return false;
145
+ const base = url.split(/[/\\]/).pop() ?? url;
146
+ return /canvas-snapshot-/i.test(base);
147
+ }
148
+
149
+ export function translateDeliverPayload(
136
150
  pl: FridayReplyPayload,
137
151
  kind: string,
138
152
  meta?: { modelName?: string; totalTokens?: number; contextTokensUsed?: number; contextWindowMax?: number },
139
153
  ): Record<string, unknown> {
154
+ // Strip canvas-snapshot tool-result images before any media resolution (paths here are still the
155
+ // original `/tmp/openclaw/openclaw-canvas-snapshot-*.jpg` temp paths, not yet copied to friday files).
156
+ const filteredSingle =
157
+ typeof pl.mediaUrl === "string" && !isCanvasSnapshotMediaPath(pl.mediaUrl) ? pl.mediaUrl : null;
158
+ const filteredArr = Array.isArray(pl.mediaUrls)
159
+ ? pl.mediaUrls.filter((u) => !isCanvasSnapshotMediaPath(u))
160
+ : pl.mediaUrls;
161
+ pl = { ...pl, mediaUrl: filteredSingle, mediaUrls: filteredArr };
162
+
140
163
  const raw = { ...pl } as Record<string, unknown>;
141
164
  const originalUrls = collectReplyPayloadMediaUrls(pl);
142
165
  if (typeof pl.mediaUrl === "string" && pl.mediaUrl.trim()) {