@syengup/friday-channel-next 0.1.15 → 0.1.17
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
327
|
+
const verified = await verifyGateway(verifyUrl, gatewayToken);
|
|
316
328
|
|
|
317
329
|
// --------------- show connection info ---------------
|
|
318
330
|
|
|
@@ -372,4 +384,45 @@ if (ipType === "tailscale") {
|
|
|
372
384
|
} else {
|
|
373
385
|
log("This URL appears to be publicly accessible (" + ip + ").");
|
|
374
386
|
}
|
|
387
|
+
|
|
388
|
+
// The advertised IP is the local/NAT address, which a phone over the internet
|
|
389
|
+
// can't reach. Best-effort: detect the network's public-facing IP. NOTE: an echo
|
|
390
|
+
// service only reports the egress address — on a cloud VPS that's a routable 1:1
|
|
391
|
+
// NAT, but on a home/carrier-grade NAT (CGNAT) it's a shared egress IP that is
|
|
392
|
+
// NOT reachable inbound. So we present it as a hint, not a guarantee.
|
|
393
|
+
if (ipType === "private" || ipType === "loopback") {
|
|
394
|
+
const publicIp = await detectPublicIp();
|
|
395
|
+
if (publicIp && publicIp !== ip) {
|
|
396
|
+
log("");
|
|
397
|
+
log(BOLD_YELLOW("Network public-facing IP detected — for remote access try:"));
|
|
398
|
+
log(BOLD_YELLOW("检测到网络出口公网 IP —— 远程连接可尝试:"));
|
|
399
|
+
log("Public URL: " + BOLD_YELLOW(`http://${publicIp}:${gatewayPort}`));
|
|
400
|
+
log("First open inbound TCP port " + gatewayPort + " in your firewall / cloud security group.");
|
|
401
|
+
log("请先在防火墙 / 云安全组放行入站 TCP 端口 " + gatewayPort + "。");
|
|
402
|
+
log("If this is a home/carrier network (CGNAT), this IP may NOT be reachable from outside —");
|
|
403
|
+
log("use a tunnel like Tailscale instead.");
|
|
404
|
+
log("若为家庭宽带 / 运营商大内网(CGNAT),此地址可能无法从外部入站访问,");
|
|
405
|
+
log("请改用 Tailscale 等内网穿透方案。");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
375
408
|
log("--------------------------------------------------");
|
|
409
|
+
|
|
410
|
+
async function detectPublicIp() {
|
|
411
|
+
const endpoints = ["http://api.ipify.org", "http://ifconfig.me/ip", "http://icanhazip.com"];
|
|
412
|
+
const http = await import("node:http");
|
|
413
|
+
for (const url of endpoints) {
|
|
414
|
+
try {
|
|
415
|
+
const ipStr = await new Promise((resolve, reject) => {
|
|
416
|
+
const req = http.get(url, { timeout: 3000 }, (res) => {
|
|
417
|
+
let body = "";
|
|
418
|
+
res.on("data", (c) => body += c);
|
|
419
|
+
res.on("end", () => resolve(body.trim()));
|
|
420
|
+
});
|
|
421
|
+
req.on("error", reject);
|
|
422
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
423
|
+
});
|
|
424
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ipStr)) return ipStr;
|
|
425
|
+
} catch { /* try next */ }
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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()) {
|