@syengup/friday-channel-next 0.1.36 → 0.1.38
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/dist/index.js +1 -1
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
- package/dist/src/agent/node-pairing-bridge.js +6 -2
- package/dist/src/agent/operator-scope.d.ts +19 -0
- package/dist/src/agent/operator-scope.js +54 -0
- package/dist/src/agent/subagent-registry.js +0 -3
- package/dist/src/channel-actions.js +3 -1
- package/dist/src/channel.js +0 -2
- package/dist/src/collect-message-media-paths.js +10 -1
- package/dist/src/friday-session.js +34 -10
- package/dist/src/history/normalize-message.js +22 -8
- package/dist/src/http/handlers/agent-config.js +10 -4
- package/dist/src/http/handlers/cancel.js +4 -2
- package/dist/src/http/handlers/device-approve.js +3 -1
- package/dist/src/http/handlers/files-download.js +6 -8
- package/dist/src/http/handlers/files.js +1 -1
- package/dist/src/http/handlers/health.js +18 -4
- package/dist/src/http/handlers/history-messages.js +1 -1
- package/dist/src/http/handlers/history-sessions.js +5 -3
- package/dist/src/http/handlers/messages.js +34 -11
- package/dist/src/http/handlers/models-list.js +1 -1
- package/dist/src/http/handlers/nodes-approve.js +1 -6
- package/dist/src/http/handlers/plugin-info.js +1 -1
- package/dist/src/http/server.js +4 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/plugin-install-info.js +4 -1
- package/dist/src/session/session-manager.js +9 -3
- package/dist/src/session-usage-store.js +3 -1
- package/dist/src/skills-discovery.d.ts +5 -4
- package/dist/src/skills-discovery.js +27 -22
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/tool-catalog.js +2 -3
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +3 -1
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +2 -3
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.ts +9 -2
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/operator-scope.test.ts +66 -0
- package/src/agent/operator-scope.ts +63 -0
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +10 -4
- package/src/channel-actions.ts +3 -1
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +121 -123
- package/src/collect-message-media-paths.ts +15 -6
- package/src/config.ts +1 -4
- package/src/e2e/agents-list.e2e.test.ts +9 -2
- package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
- package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
- package/src/e2e/auto-approve.integration.test.ts +13 -7
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
- package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
- package/src/e2e/offline-replay.e2e.test.ts +17 -3
- package/src/e2e/send-text.e2e.test.ts +11 -2
- package/src/e2e/slash-commands.e2e.test.ts +5 -1
- package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
- package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
- package/src/e2e/subagent.e2e.test.ts +136 -53
- package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
- package/src/friday-session.forward-agent.test.ts +44 -12
- package/src/friday-session.ts +44 -20
- package/src/history/normalize-message.test.ts +35 -8
- package/src/history/normalize-message.ts +24 -12
- package/src/history/read-transcript.ts +1 -4
- package/src/http/handlers/agent-config.test.ts +10 -3
- package/src/http/handlers/agent-config.ts +22 -8
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/cancel.test.ts +12 -3
- package/src/http/handlers/cancel.ts +4 -2
- package/src/http/handlers/device-approve.test.ts +12 -3
- package/src/http/handlers/device-approve.ts +33 -21
- package/src/http/handlers/files-download.ts +17 -13
- package/src/http/handlers/files.test.ts +8 -2
- package/src/http/handlers/files.ts +21 -7
- package/src/http/handlers/health.test.ts +43 -11
- package/src/http/handlers/health.ts +22 -6
- package/src/http/handlers/history-messages.test.ts +51 -9
- package/src/http/handlers/history-messages.ts +4 -1
- package/src/http/handlers/history-sessions.test.ts +46 -9
- package/src/http/handlers/history-sessions.ts +5 -3
- package/src/http/handlers/history-set-title.test.ts +14 -5
- package/src/http/handlers/link-preview.test.ts +57 -16
- package/src/http/handlers/link-preview.ts +4 -1
- package/src/http/handlers/messages.test.ts +12 -8
- package/src/http/handlers/messages.ts +67 -19
- package/src/http/handlers/models-list.ts +14 -8
- package/src/http/handlers/nodes-approve.test.ts +15 -4
- package/src/http/handlers/nodes-approve.ts +38 -40
- package/src/http/handlers/plugin-info.ts +5 -6
- package/src/http/handlers/plugin-upgrade.ts +4 -1
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +9 -6
- package/src/link-preview/og-parse.test.ts +6 -2
- package/src/link-preview/og-parse.ts +10 -3
- package/src/link-preview/preview-service.ts +4 -1
- package/src/link-preview/ssrf-guard.test.ts +72 -15
- package/src/link-preview/ssrf-guard.ts +2 -1
- package/src/media-fetch.test.ts +7 -2
- package/src/media-fetch.ts +1 -2
- package/src/openclaw.d.ts +26 -9
- package/src/plugin-install-info.ts +20 -9
- package/src/run-metadata.ts +2 -1
- package/src/session/session-manager.ts +19 -11
- package/src/session-usage-snapshot.ts +3 -1
- package/src/session-usage-store.ts +3 -1
- package/src/skills-discovery.test.ts +14 -10
- package/src/skills-discovery.ts +43 -27
- package/src/sse/emitter.test.ts +1 -1
- package/src/sse/emitter.ts +9 -3
- package/src/sse/offline-queue.ts +17 -8
- package/src/test-support/app-simulator.ts +17 -3
- package/src/test-support/mock-dispatch.ts +17 -4
- package/src/thinking-levels.ts +3 -1
- package/src/tool-catalog.ts +16 -7
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +5 -1
- package/tsconfig.json +1 -1
package/src/http/handlers/sse.ts
CHANGED
|
@@ -67,7 +67,9 @@ export async function handleSseStream(req: IncomingMessage, res: ServerResponse)
|
|
|
67
67
|
const lastEventId = parseLastEventId(req, url);
|
|
68
68
|
if (lastEventId > 0) sseEmitter.replayBacklog(deviceId, lastEventId);
|
|
69
69
|
|
|
70
|
-
const config = resolveFridayNextConfig(
|
|
70
|
+
const config = resolveFridayNextConfig(
|
|
71
|
+
getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
|
|
72
|
+
);
|
|
71
73
|
const keepalive = setInterval(() => {
|
|
72
74
|
if (conn.isClosed) {
|
|
73
75
|
clearInterval(keepalive);
|
package/src/http/server.ts
CHANGED
|
@@ -34,10 +34,7 @@ import { getFridayNextRuntime } from "../runtime.js";
|
|
|
34
34
|
import { sseEmitter } from "../sse/emitter.js";
|
|
35
35
|
|
|
36
36
|
/** Route matcher - returns the matched handler or null. */
|
|
37
|
-
async function handleFridayNextRoute(
|
|
38
|
-
req: IncomingMessage,
|
|
39
|
-
res: ServerResponse,
|
|
40
|
-
): Promise<boolean> {
|
|
37
|
+
async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
41
38
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
42
39
|
const pathname = url.pathname;
|
|
43
40
|
applyCorsHeaders(res);
|
|
@@ -79,7 +76,10 @@ async function handleFridayNextRoute(
|
|
|
79
76
|
return await handleNodesApprove(req, res);
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
if (
|
|
79
|
+
if (
|
|
80
|
+
(req.method === "PUT" || req.method === "GET") &&
|
|
81
|
+
pathname === "/friday-next/sessions/settings"
|
|
82
|
+
) {
|
|
83
83
|
return await handleSessionsSettings(req, res);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -127,7 +127,10 @@ async function handleFridayNextRoute(
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
|
|
130
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
(req.method === "PUT" || req.method === "POST") &&
|
|
132
|
+
pathname === "/friday-next/sessions/title"
|
|
133
|
+
) {
|
|
131
134
|
return await handleHistorySetTitle(req, res);
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -5,7 +5,9 @@ const BASE = "https://example.com/article/42";
|
|
|
5
5
|
|
|
6
6
|
describe("decodeHtmlEntities", () => {
|
|
7
7
|
it("decodes named, decimal, and hex entities", () => {
|
|
8
|
-
expect(decodeHtmlEntities("Tom & Jerry — "fun"")).toBe(
|
|
8
|
+
expect(decodeHtmlEntities("Tom & Jerry — "fun"")).toBe(
|
|
9
|
+
'Tom & Jerry — "fun"',
|
|
10
|
+
);
|
|
9
11
|
expect(decodeHtmlEntities("中文")).toBe("中文");
|
|
10
12
|
expect(decodeHtmlEntities("'quoted'")).toBe("'quoted'");
|
|
11
13
|
});
|
|
@@ -143,7 +145,9 @@ describe("parseOpenGraph", () => {
|
|
|
143
145
|
it("extracts a cover image from inline JSON (extensionless, escaped slashes)", () => {
|
|
144
146
|
const html = `<title>搜索资讯页</title>
|
|
145
147
|
<script>window.__INFO__={"imgUrl":"http:\\/\\/qqpublic.qpic.cn\\/qq_public_cover\\/0\\/0-2342_op"}</script>`;
|
|
146
|
-
expect(parseOpenGraph(html, BASE).imageUrl).toBe(
|
|
148
|
+
expect(parseOpenGraph(html, BASE).imageUrl).toBe(
|
|
149
|
+
"http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op",
|
|
150
|
+
);
|
|
147
151
|
});
|
|
148
152
|
|
|
149
153
|
it("standard og tags still win over body/json fallbacks", () => {
|
|
@@ -91,7 +91,9 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
|
|
|
91
91
|
let metaDescription: string | null = null;
|
|
92
92
|
for (const match of slice.matchAll(META_TAG_RE)) {
|
|
93
93
|
const tag = match[0];
|
|
94
|
-
const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
|
|
94
|
+
const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
|
|
95
|
+
?.trim()
|
|
96
|
+
.toLowerCase();
|
|
95
97
|
if (!key) continue;
|
|
96
98
|
const content = attributeValue(tag, "content");
|
|
97
99
|
if (content == null || !content.trim()) continue;
|
|
@@ -140,10 +142,15 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
|
|
|
140
142
|
};
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
const JSON_LD_RE =
|
|
145
|
+
const JSON_LD_RE =
|
|
146
|
+
/<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
144
147
|
|
|
145
148
|
/** Extract title/description/image from JSON-LD blocks (schema.org Article/NewsArticle/etc.). */
|
|
146
|
-
function parseJsonLd(html: string): {
|
|
149
|
+
function parseJsonLd(html: string): {
|
|
150
|
+
title: string | null;
|
|
151
|
+
description: string | null;
|
|
152
|
+
image: string | null;
|
|
153
|
+
} {
|
|
147
154
|
for (const match of html.matchAll(JSON_LD_RE)) {
|
|
148
155
|
let data: unknown;
|
|
149
156
|
try {
|
|
@@ -155,7 +155,10 @@ async function buildPreview(pageUrl: string): Promise<LinkPreviewResult> {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/** Re-host a favicon: try the parsed `<link rel icon>`, then `<origin>/favicon.ico`. */
|
|
158
|
-
async function resolveFavicon(
|
|
158
|
+
async function resolveFavicon(
|
|
159
|
+
parsedIconUrl: string | null,
|
|
160
|
+
finalUrl: string,
|
|
161
|
+
): Promise<string | null> {
|
|
159
162
|
const candidates: string[] = [];
|
|
160
163
|
if (parsedIconUrl) candidates.push(parsedIconUrl);
|
|
161
164
|
try {
|
|
@@ -46,7 +46,9 @@ describe("parseHttpUrl", () => {
|
|
|
46
46
|
describe("assertPublicHttpUrl", () => {
|
|
47
47
|
it("rejects non-default ports", () => {
|
|
48
48
|
expect(() => assertPublicHttpUrl(new URL("http://example.com:8080/"))).toThrow(BlockedUrlError);
|
|
49
|
-
expect(() => assertPublicHttpUrl(new URL("https://example.com:8443/"))).toThrow(
|
|
49
|
+
expect(() => assertPublicHttpUrl(new URL("https://example.com:8443/"))).toThrow(
|
|
50
|
+
BlockedUrlError,
|
|
51
|
+
);
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
it("allows default and explicit 80/443 ports", () => {
|
|
@@ -95,7 +97,14 @@ describe("isPrivateAddress", () => {
|
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
it("passes public IPv4", () => {
|
|
98
|
-
for (const ip of [
|
|
100
|
+
for (const ip of [
|
|
101
|
+
"8.8.8.8",
|
|
102
|
+
"93.184.216.34",
|
|
103
|
+
"100.63.0.1",
|
|
104
|
+
"100.128.0.1",
|
|
105
|
+
"172.32.0.1",
|
|
106
|
+
"198.20.0.1",
|
|
107
|
+
]) {
|
|
99
108
|
expect(isPrivateAddress(ip), ip).toBe(false);
|
|
100
109
|
}
|
|
101
110
|
});
|
|
@@ -106,7 +115,15 @@ describe("isPrivateAddress", () => {
|
|
|
106
115
|
});
|
|
107
116
|
|
|
108
117
|
it("flags private/reserved IPv6 and mapped IPv4", () => {
|
|
109
|
-
for (const ip of [
|
|
118
|
+
for (const ip of [
|
|
119
|
+
"::1",
|
|
120
|
+
"::",
|
|
121
|
+
"fd00::1",
|
|
122
|
+
"fd12:3456::1",
|
|
123
|
+
"fe80::1",
|
|
124
|
+
"::ffff:10.0.0.1",
|
|
125
|
+
"::ffff:127.0.0.1",
|
|
126
|
+
]) {
|
|
110
127
|
expect(isPrivateAddress(ip), ip).toBe(true);
|
|
111
128
|
}
|
|
112
129
|
});
|
|
@@ -140,11 +157,15 @@ describe("assertResolvesPublic", () => {
|
|
|
140
157
|
{ address: "93.184.216.34", family: 4 },
|
|
141
158
|
{ address: "10.0.0.5", family: 4 },
|
|
142
159
|
] as never);
|
|
143
|
-
await expect(assertResolvesPublic(new URL("https://rebind.example.com/"))).rejects.toThrow(
|
|
160
|
+
await expect(assertResolvesPublic(new URL("https://rebind.example.com/"))).rejects.toThrow(
|
|
161
|
+
BlockedUrlError,
|
|
162
|
+
);
|
|
144
163
|
});
|
|
145
164
|
|
|
146
165
|
it("validates IP-literal hosts without a DNS lookup", async () => {
|
|
147
|
-
await expect(assertResolvesPublic(new URL("http://127.0.0.1/"))).rejects.toThrow(
|
|
166
|
+
await expect(assertResolvesPublic(new URL("http://127.0.0.1/"))).rejects.toThrow(
|
|
167
|
+
BlockedUrlError,
|
|
168
|
+
);
|
|
148
169
|
await expect(assertResolvesPublic(new URL("http://93.184.216.34/"))).resolves.toBeUndefined();
|
|
149
170
|
expect(lookupMock).not.toHaveBeenCalled();
|
|
150
171
|
});
|
|
@@ -164,7 +185,13 @@ describe("fetchPublicUrl", () => {
|
|
|
164
185
|
mockPublicDns();
|
|
165
186
|
vi.stubGlobal(
|
|
166
187
|
"fetch",
|
|
167
|
-
vi.fn(
|
|
188
|
+
vi.fn(
|
|
189
|
+
async () =>
|
|
190
|
+
new Response("<html>hi</html>", {
|
|
191
|
+
status: 200,
|
|
192
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
193
|
+
}),
|
|
194
|
+
),
|
|
168
195
|
);
|
|
169
196
|
const result = await fetchPublicUrl("https://example.com/page", opts);
|
|
170
197
|
expect(result?.finalUrl).toBe("https://example.com/page");
|
|
@@ -176,8 +203,15 @@ describe("fetchPublicUrl", () => {
|
|
|
176
203
|
mockPublicDns();
|
|
177
204
|
const fetchMock = vi
|
|
178
205
|
.fn()
|
|
179
|
-
.mockResolvedValueOnce(
|
|
180
|
-
|
|
206
|
+
.mockResolvedValueOnce(
|
|
207
|
+
new Response(null, {
|
|
208
|
+
status: 302,
|
|
209
|
+
headers: { location: "https://other.example.com/final" },
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
.mockResolvedValueOnce(
|
|
213
|
+
new Response("ok", { status: 200, headers: { "content-type": "text/html" } }),
|
|
214
|
+
);
|
|
181
215
|
vi.stubGlobal("fetch", fetchMock);
|
|
182
216
|
const result = await fetchPublicUrl("https://example.com/start", opts);
|
|
183
217
|
expect(result?.finalUrl).toBe("https://other.example.com/final");
|
|
@@ -189,9 +223,14 @@ describe("fetchPublicUrl", () => {
|
|
|
189
223
|
mockPublicDns();
|
|
190
224
|
vi.stubGlobal(
|
|
191
225
|
"fetch",
|
|
192
|
-
vi.fn(
|
|
226
|
+
vi.fn(
|
|
227
|
+
async () =>
|
|
228
|
+
new Response(null, { status: 302, headers: { location: "http://127.0.0.1/admin" } }),
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(
|
|
232
|
+
BlockedUrlError,
|
|
193
233
|
);
|
|
194
|
-
await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(BlockedUrlError);
|
|
195
234
|
});
|
|
196
235
|
|
|
197
236
|
it("throws BlockedUrlError for a directly-blocked URL", async () => {
|
|
@@ -203,7 +242,13 @@ describe("fetchPublicUrl", () => {
|
|
|
203
242
|
const big = "x".repeat(2048);
|
|
204
243
|
vi.stubGlobal(
|
|
205
244
|
"fetch",
|
|
206
|
-
vi.fn(
|
|
245
|
+
vi.fn(
|
|
246
|
+
async () =>
|
|
247
|
+
new Response(big, {
|
|
248
|
+
status: 200,
|
|
249
|
+
headers: { "content-type": "text/html", "content-length": "10" },
|
|
250
|
+
}),
|
|
251
|
+
),
|
|
207
252
|
);
|
|
208
253
|
expect(await fetchPublicUrl("https://example.com/big", opts)).toBeNull();
|
|
209
254
|
});
|
|
@@ -212,16 +257,25 @@ describe("fetchPublicUrl", () => {
|
|
|
212
257
|
mockPublicDns();
|
|
213
258
|
vi.stubGlobal(
|
|
214
259
|
"fetch",
|
|
215
|
-
vi.fn(
|
|
260
|
+
vi.fn(
|
|
261
|
+
async () =>
|
|
262
|
+
new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
|
|
263
|
+
),
|
|
216
264
|
);
|
|
217
265
|
expect(
|
|
218
|
-
await fetchPublicUrl("https://example.com/api", {
|
|
266
|
+
await fetchPublicUrl("https://example.com/api", {
|
|
267
|
+
...opts,
|
|
268
|
+
requireContentTypePrefixes: ["text/html", "application/xhtml+xml"],
|
|
269
|
+
}),
|
|
219
270
|
).toBeNull();
|
|
220
271
|
});
|
|
221
272
|
|
|
222
273
|
it("returns null on non-2xx and on DNS failure", async () => {
|
|
223
274
|
mockPublicDns();
|
|
224
|
-
vi.stubGlobal(
|
|
275
|
+
vi.stubGlobal(
|
|
276
|
+
"fetch",
|
|
277
|
+
vi.fn(async () => new Response("nope", { status: 404 })),
|
|
278
|
+
);
|
|
225
279
|
expect(await fetchPublicUrl("https://example.com/missing", opts)).toBeNull();
|
|
226
280
|
|
|
227
281
|
lookupMock.mockRejectedValue(new Error("ENOTFOUND"));
|
|
@@ -232,7 +286,10 @@ describe("fetchPublicUrl", () => {
|
|
|
232
286
|
mockPublicDns();
|
|
233
287
|
vi.stubGlobal(
|
|
234
288
|
"fetch",
|
|
235
|
-
vi.fn(
|
|
289
|
+
vi.fn(
|
|
290
|
+
async () =>
|
|
291
|
+
new Response(null, { status: 302, headers: { location: "https://example.com/loop" } }),
|
|
292
|
+
),
|
|
236
293
|
);
|
|
237
294
|
expect(await fetchPublicUrl("https://example.com/loop", opts)).toBeNull();
|
|
238
295
|
});
|
|
@@ -66,7 +66,8 @@ export function isPrivateAddress(ip: string): boolean {
|
|
|
66
66
|
|
|
67
67
|
function isPrivateIPv4(ip: string): boolean {
|
|
68
68
|
const parts = ip.split(".").map(Number);
|
|
69
|
-
if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
69
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
70
|
+
return true;
|
|
70
71
|
const [a, b] = parts;
|
|
71
72
|
if (a === 0) return true; // 0.0.0.0/8
|
|
72
73
|
if (a === 10) return true; // 10/8
|
package/src/media-fetch.test.ts
CHANGED
|
@@ -21,7 +21,9 @@ describe("downloadRemoteMedia", () => {
|
|
|
21
21
|
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
22
22
|
vi.stubGlobal(
|
|
23
23
|
"fetch",
|
|
24
|
-
vi.fn(
|
|
24
|
+
vi.fn(
|
|
25
|
+
async () => new Response(bytes, { status: 200, headers: { "content-type": "image/png" } }),
|
|
26
|
+
),
|
|
25
27
|
);
|
|
26
28
|
|
|
27
29
|
const result = await downloadRemoteMedia("https://picsum.photos/600/400");
|
|
@@ -48,7 +50,10 @@ describe("downloadRemoteMedia", () => {
|
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
it("returns null on a non-2xx response", async () => {
|
|
51
|
-
vi.stubGlobal(
|
|
53
|
+
vi.stubGlobal(
|
|
54
|
+
"fetch",
|
|
55
|
+
vi.fn(async () => new Response("nope", { status: 404 })),
|
|
56
|
+
);
|
|
52
57
|
expect(await downloadRemoteMedia("https://example.com/missing.png")).toBeNull();
|
|
53
58
|
});
|
|
54
59
|
|
package/src/media-fetch.ts
CHANGED
|
@@ -53,8 +53,7 @@ export function decodeBase64Media(
|
|
|
53
53
|
}
|
|
54
54
|
if (!buffer.length) return null;
|
|
55
55
|
|
|
56
|
-
const mimeType =
|
|
57
|
-
mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
|
|
56
|
+
const mimeType = mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
|
|
58
57
|
return { buffer, mimeType };
|
|
59
58
|
}
|
|
60
59
|
|
package/src/openclaw.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ declare module "openclaw/plugin-sdk/agent-harness" {
|
|
|
16
16
|
|
|
17
17
|
declare module "openclaw/plugin-sdk/device-bootstrap" {
|
|
18
18
|
export const listDevicePairing: (baseDir?: string) => Promise<DevicePairingList>;
|
|
19
|
-
export const approveDevicePairing: (
|
|
19
|
+
export const approveDevicePairing: (
|
|
20
|
+
requestId: string,
|
|
21
|
+
options?: { callerScopes?: readonly string[] },
|
|
22
|
+
baseDir?: string,
|
|
23
|
+
) => Promise<ApproveDevicePairingResult>;
|
|
20
24
|
|
|
21
25
|
interface DevicePairingPendingRequest {
|
|
22
26
|
requestId: string;
|
|
@@ -34,14 +38,17 @@ declare module "openclaw/plugin-sdk/device-bootstrap" {
|
|
|
34
38
|
pending: DevicePairingPendingRequest[];
|
|
35
39
|
paired: PairedDevice[];
|
|
36
40
|
}
|
|
37
|
-
type ApproveDevicePairingResult =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
type ApproveDevicePairingResult =
|
|
42
|
+
| {
|
|
43
|
+
status: "approved";
|
|
44
|
+
requestId: string;
|
|
45
|
+
device: PairedDevice;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
status: "forbidden";
|
|
49
|
+
reason: string;
|
|
50
|
+
}
|
|
51
|
+
| null;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
declare module "openclaw/plugin-sdk/core" {
|
|
@@ -86,6 +93,16 @@ declare module "openclaw/plugin-sdk/reply-dispatch-runtime" {
|
|
|
86
93
|
export const dispatchReplyWithDispatcher: (...args: any[]) => any;
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
declare module "openclaw/plugin-sdk/plugin-runtime" {
|
|
97
|
+
/**
|
|
98
|
+
* Returns the request-local plugin gateway-request-scope (operator client/scopes,
|
|
99
|
+
* context) when called from within a plugin HTTP-route handler's async context.
|
|
100
|
+
*/
|
|
101
|
+
export const getPluginRuntimeGatewayRequestScope: () =>
|
|
102
|
+
| { client?: { connect?: { scopes?: string[] } } }
|
|
103
|
+
| undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
declare module "openclaw/plugin-sdk/status-helpers" {
|
|
90
107
|
export type ChannelAccountSnapshot = any;
|
|
91
108
|
}
|
|
@@ -7,7 +7,14 @@ import { getUpgradeRuntime } from "./upgrade-runtime.js";
|
|
|
7
7
|
import { PLUGIN_ID, PLUGIN_PACKAGE_NAME } from "./version.js";
|
|
8
8
|
|
|
9
9
|
/** Install source from the OpenClaw config `plugins.installs[<id>].source`. */
|
|
10
|
-
export type InstallSource =
|
|
10
|
+
export type InstallSource =
|
|
11
|
+
| "npm"
|
|
12
|
+
| "path"
|
|
13
|
+
| "archive"
|
|
14
|
+
| "clawhub"
|
|
15
|
+
| "git"
|
|
16
|
+
| "marketplace"
|
|
17
|
+
| "unknown";
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Infer the install source from the loaded plugin's filesystem path (`api.source`).
|
|
@@ -20,7 +27,9 @@ export type InstallSource = "npm" | "path" | "archive" | "clawhub" | "git" | "ma
|
|
|
20
27
|
* dev-no-duplicate-plugin-install), so anything under the managed projects dir
|
|
21
28
|
* is treated as "npm".
|
|
22
29
|
*/
|
|
23
|
-
export function classifyInstallSourceFromLoadedPath(
|
|
30
|
+
export function classifyInstallSourceFromLoadedPath(
|
|
31
|
+
loadedPath: string | null | undefined,
|
|
32
|
+
): InstallSource {
|
|
24
33
|
if (!loadedPath) return "unknown";
|
|
25
34
|
return loadedPath.includes("/.openclaw/npm/projects/") ? "npm" : "path";
|
|
26
35
|
}
|
|
@@ -42,9 +51,11 @@ export function getInstallSource(): InstallSource {
|
|
|
42
51
|
const rt = getUpgradeRuntime();
|
|
43
52
|
if (!rt) return "unknown";
|
|
44
53
|
try {
|
|
45
|
-
const cfg = rt.currentConfig() as
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
const cfg = rt.currentConfig() as
|
|
55
|
+
| {
|
|
56
|
+
plugins?: { installs?: Record<string, { source?: string } | undefined> };
|
|
57
|
+
}
|
|
58
|
+
| undefined;
|
|
48
59
|
const source = cfg?.plugins?.installs?.[PLUGIN_ID]?.source;
|
|
49
60
|
if (
|
|
50
61
|
source === "npm" ||
|
|
@@ -94,10 +105,10 @@ export async function fetchLatestVersion(nowMs: number): Promise<string | null>
|
|
|
94
105
|
const controller = new AbortController();
|
|
95
106
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
96
107
|
try {
|
|
97
|
-
const res = await fetch(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
108
|
+
const res = await fetch(`https://registry.npmjs.org/${PLUGIN_PACKAGE_NAME}/latest`, {
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
headers: { Accept: "application/json" },
|
|
111
|
+
});
|
|
101
112
|
if (res.ok) {
|
|
102
113
|
const body = (await res.json()) as { version?: string };
|
|
103
114
|
if (typeof body.version === "string" && body.version) version = body.version;
|
package/src/run-metadata.ts
CHANGED
|
@@ -173,7 +173,8 @@ export function ingestAgentEventMetadata(runId: string, data: Record<string, unk
|
|
|
173
173
|
const cacheRead = pickCacheRead(usageForTokens);
|
|
174
174
|
if (typeof cacheRead === "number" && cacheRead >= 0) next.cacheReadTokens = Math.floor(cacheRead);
|
|
175
175
|
const cacheWrite = pickCacheWrite(usageForTokens);
|
|
176
|
-
if (typeof cacheWrite === "number" && cacheWrite >= 0)
|
|
176
|
+
if (typeof cacheWrite === "number" && cacheWrite >= 0)
|
|
177
|
+
next.cacheWriteTokens = Math.floor(cacheWrite);
|
|
177
178
|
|
|
178
179
|
const usageForContext = usage ?? data;
|
|
179
180
|
const ctxUsed = contextTokensFromUsageRecord(usageForContext);
|
|
@@ -162,7 +162,11 @@ export function setSessionSettings(
|
|
|
162
162
|
upsertSessionEntry(data, fileKey, sessionKey);
|
|
163
163
|
|
|
164
164
|
const fieldKeys: (keyof FridaySessionSettingsUpdate)[] = [
|
|
165
|
-
"reasoningLevel",
|
|
165
|
+
"reasoningLevel",
|
|
166
|
+
"thinkingLevel",
|
|
167
|
+
"modelRef",
|
|
168
|
+
"providerOverride",
|
|
169
|
+
"modelOverride",
|
|
166
170
|
];
|
|
167
171
|
let updated = false;
|
|
168
172
|
for (const key of fieldKeys) {
|
|
@@ -193,22 +197,21 @@ export function setSessionSettings(
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
function readSettingsFromEntry(entry: Record<string, unknown>): FridaySessionSettings {
|
|
196
|
-
const provider =
|
|
200
|
+
const provider =
|
|
201
|
+
typeof entry["providerOverride"] === "string" ? entry["providerOverride"] : undefined;
|
|
197
202
|
const model = typeof entry["modelOverride"] === "string" ? entry["modelOverride"] : undefined;
|
|
198
203
|
const storedModelRef = typeof entry["modelRef"] === "string" ? entry["modelRef"] : undefined;
|
|
199
204
|
const modelRef = storedModelRef ?? (provider && model ? `${provider}/${model}` : undefined);
|
|
200
205
|
|
|
201
206
|
return {
|
|
202
|
-
reasoningLevel:
|
|
207
|
+
reasoningLevel:
|
|
208
|
+
typeof entry["reasoningLevel"] === "string" ? entry["reasoningLevel"] : undefined,
|
|
203
209
|
thinkingLevel: typeof entry["thinkingLevel"] === "string" ? entry["thinkingLevel"] : undefined,
|
|
204
210
|
modelRef,
|
|
205
211
|
};
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
export function getSessionSettings(
|
|
209
|
-
sessionKey: string,
|
|
210
|
-
historyDir?: string,
|
|
211
|
-
): FridaySessionSettings {
|
|
214
|
+
export function getSessionSettings(sessionKey: string, historyDir?: string): FridaySessionSettings {
|
|
212
215
|
try {
|
|
213
216
|
const fileKey = toSessionStoreKey(sessionKey);
|
|
214
217
|
const sessionsFile = resolveSessionsFilePath(historyDir, agentIdFromSessionKey(fileKey));
|
|
@@ -243,7 +246,10 @@ export function resolveAgentDefaults(sessionKey: string): { model?: string; thin
|
|
|
243
246
|
const targetAgentId = agentIdFromSessionKey(sessionKey);
|
|
244
247
|
|
|
245
248
|
const agentEntry = (agents?.list as Array<Record<string, unknown>> | undefined)?.find(
|
|
246
|
-
(a) =>
|
|
249
|
+
(a) =>
|
|
250
|
+
agentIdFromSessionKey(
|
|
251
|
+
`agent:${typeof a?.id === "string" ? a.id : typeof a?.id === "number" ? String(a.id) : ""}:x`,
|
|
252
|
+
) === targetAgentId,
|
|
247
253
|
);
|
|
248
254
|
const agentModel = agentEntry?.model;
|
|
249
255
|
const perAgentModel =
|
|
@@ -253,13 +259,15 @@ export function resolveAgentDefaults(sessionKey: string): { model?: string; thin
|
|
|
253
259
|
? ((agentModel as Record<string, unknown>).primary as string)
|
|
254
260
|
: undefined;
|
|
255
261
|
const perAgentThinking =
|
|
256
|
-
typeof agentEntry?.thinkingDefault === "string" ?
|
|
262
|
+
typeof agentEntry?.thinkingDefault === "string" ? agentEntry.thinkingDefault : undefined;
|
|
257
263
|
|
|
258
264
|
const agentDefaults = agents?.defaults as Record<string, unknown> | undefined;
|
|
259
265
|
const model = agentDefaults?.model as Record<string, unknown> | undefined;
|
|
260
|
-
const globalModel = typeof model?.primary === "string" ?
|
|
266
|
+
const globalModel = typeof model?.primary === "string" ? model.primary : undefined;
|
|
261
267
|
const globalThinking =
|
|
262
|
-
typeof agentDefaults?.thinkingDefault === "string"
|
|
268
|
+
typeof agentDefaults?.thinkingDefault === "string"
|
|
269
|
+
? agentDefaults.thinkingDefault
|
|
270
|
+
: undefined;
|
|
263
271
|
|
|
264
272
|
return { model: perAgentModel ?? globalModel, thinking: perAgentThinking ?? globalThinking };
|
|
265
273
|
} catch {
|
|
@@ -33,7 +33,9 @@ function finiteCost(n: unknown): number | undefined {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Build a compact snapshot from a loaded session store entry (unknown shape). */
|
|
36
|
-
export function buildSessionUsageSnapshot(
|
|
36
|
+
export function buildSessionUsageSnapshot(
|
|
37
|
+
entry: Record<string, unknown>,
|
|
38
|
+
): FridaySessionUsagePayload | undefined {
|
|
37
39
|
const payload: FridaySessionUsagePayload = {};
|
|
38
40
|
|
|
39
41
|
const modelId = typeof entry.model === "string" ? entry.model.trim() : "";
|
|
@@ -28,7 +28,9 @@ export function readSessionUsageSnapshotFromStore(
|
|
|
28
28
|
const cfg = access.getConfig() as { session?: { store?: string } } | null | undefined;
|
|
29
29
|
const storeConfig = cfg?.session?.store;
|
|
30
30
|
const canonical = toSessionStoreKey(sessionKeyForStore);
|
|
31
|
-
const storePath = access.resolveStorePath(storeConfig, {
|
|
31
|
+
const storePath = access.resolveStorePath(storeConfig, {
|
|
32
|
+
agentId: agentIdFromSessionKey(canonical),
|
|
33
|
+
});
|
|
32
34
|
const store = access.loadSessionStore(storePath, { skipCache: true }) as Record<
|
|
33
35
|
string,
|
|
34
36
|
Record<string, unknown>
|
|
@@ -45,17 +45,21 @@ describe("discoverAvailableSkills", () => {
|
|
|
45
45
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it("
|
|
48
|
+
it("scans only the target agent's own workspace (not the default agent's), plus managed + extra dirs, deduped and sorted", () => {
|
|
49
49
|
root = fs.mkdtempSync(path.join(os.tmpdir(), "friday-disc-"));
|
|
50
50
|
const configRoot = path.join(root, "configdir");
|
|
51
51
|
const extraDir = path.join(root, "extra");
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
makeSkills(path.join(configRoot, "workspace", "skills"), ["alpha"
|
|
55
|
-
// operator's own workspace —
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
// Default agent "main" workspace — its skills must NOT leak into other agents' catalogs.
|
|
54
|
+
makeSkills(path.join(configRoot, "workspace", "skills"), ["alpha"]);
|
|
55
|
+
// operator's own workspace — opencli also lives in the managed dir below, to prove
|
|
56
|
+
// workspace > installed precedence in dedup.
|
|
57
|
+
makeSkills(path.join(configRoot, "workspace", "agents", "operator", "skills"), [
|
|
58
|
+
"beta",
|
|
59
|
+
"opencli",
|
|
60
|
+
]);
|
|
61
|
+
// managed dir: <configDir>/skills (sibling of the default workspace)
|
|
62
|
+
makeSkills(path.join(configRoot, "skills"), ["managed-one", "opencli"]);
|
|
59
63
|
// config extraDirs
|
|
60
64
|
makeSkills(extraDir, ["gamma"]);
|
|
61
65
|
|
|
@@ -66,12 +70,12 @@ describe("discoverAvailableSkills", () => {
|
|
|
66
70
|
wire(configRoot, cfg);
|
|
67
71
|
|
|
68
72
|
const result = discoverAvailableSkills(cfg, "operator");
|
|
69
|
-
|
|
73
|
+
// "alpha" is main-only → absent for operator (regression guard for the main-leak bug).
|
|
74
|
+
expect(result.map((s) => s.id)).toEqual(["beta", "gamma", "managed-one", "opencli"]);
|
|
70
75
|
const bySource = Object.fromEntries(result.map((s) => [s.id, s.source]));
|
|
71
76
|
expect(bySource).toEqual({
|
|
72
|
-
alpha: "workspace",
|
|
73
77
|
beta: "workspace",
|
|
74
|
-
opencli: "workspace",
|
|
78
|
+
opencli: "workspace", // workspace wins over the managed-dir duplicate
|
|
75
79
|
"managed-one": "installed",
|
|
76
80
|
gamma: "extra",
|
|
77
81
|
});
|