@syengup/friday-channel-next 0.1.30 → 0.1.37
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/README.md +8 -4
- package/dist/index.js +1 -1
- package/dist/src/agent/abort-run.d.ts +12 -1
- package/dist/src/agent/abort-run.js +24 -9
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/media-bridge.d.ts +8 -1
- package/dist/src/agent/media-bridge.js +23 -2
- 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/subagent-registry.js +0 -3
- package/dist/src/agent-forward-runtime.d.ts +15 -0
- package/dist/src/agent-forward-runtime.js +2 -0
- package/dist/src/agent-id.d.ts +8 -0
- package/dist/src/agent-id.js +21 -0
- package/dist/src/channel-actions.js +48 -15
- package/dist/src/channel.js +22 -3
- 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.d.ts +27 -0
- package/dist/src/http/handlers/agent-config.js +188 -0
- package/dist/src/http/handlers/agent-files.d.ts +21 -0
- package/dist/src/http/handlers/agent-files.js +137 -0
- package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
- package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
- package/dist/src/http/handlers/agents-list.js +1 -19
- package/dist/src/http/handlers/cancel.js +14 -6
- 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.d.ts +16 -0
- package/dist/src/http/handlers/files.js +81 -13
- 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 +33 -14
- package/dist/src/http/handlers/models-list.d.ts +5 -0
- package/dist/src/http/handlers/models-list.js +9 -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/handlers/sessions-settings.js +15 -10
- package/dist/src/http/server.js +27 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/link-preview/ssrf-guard.js +6 -2
- package/dist/src/media-fetch.js +4 -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 +59 -0
- package/dist/src/skills-discovery.js +252 -0
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/thinking-levels.d.ts +21 -0
- package/dist/src/thinking-levels.js +48 -0
- package/dist/src/tool-catalog.d.ts +53 -0
- package/dist/src/tool-catalog.js +191 -0
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +4 -2
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +23 -8
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.test.ts +71 -0
- package/src/agent/media-bridge.ts +30 -1
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-forward-runtime.ts +11 -0
- package/src/agent-id.ts +24 -0
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +57 -4
- package/src/channel-actions.ts +41 -15
- package/src/channel.lifecycle.test.ts +41 -0
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +140 -120
- 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 +212 -0
- package/src/http/handlers/agent-config.ts +232 -0
- package/src/http/handlers/agent-files.test.ts +136 -0
- package/src/http/handlers/agent-files.ts +149 -0
- package/src/http/handlers/agent-tools-catalog.ts +42 -0
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/agents-list.ts +1 -22
- package/src/http/handlers/cancel.test.ts +23 -4
- package/src/http/handlers/cancel.ts +14 -6
- 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 +120 -0
- package/src/http/handlers/files.ts +115 -17
- 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 +64 -21
- package/src/http/handlers/models-list.test.ts +114 -0
- package/src/http/handlers/models-list.ts +26 -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/sessions-settings.ts +16 -11
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +33 -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 +78 -16
- package/src/link-preview/ssrf-guard.ts +7 -2
- package/src/media-fetch.test.ts +8 -3
- package/src/media-fetch.ts +5 -3
- package/src/openclaw.d.ts +41 -10
- 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 +152 -0
- package/src/skills-discovery.ts +264 -0
- 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.test.ts +143 -0
- package/src/thinking-levels.ts +70 -0
- package/src/tool-catalog.ts +261 -0
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +6 -2
- package/tsconfig.json +1 -1
package/src/http/server.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { handleNodesApprove } from "./handlers/nodes-approve.js";
|
|
|
16
16
|
import { handleSessionsSettings } from "./handlers/sessions-settings.js";
|
|
17
17
|
import { handleModelsList } from "./handlers/models-list.js";
|
|
18
18
|
import { handleAgentsList } from "./handlers/agents-list.js";
|
|
19
|
+
import { handleAgentConfig } from "./handlers/agent-config.js";
|
|
20
|
+
import { handleAgentFiles } from "./handlers/agent-files.js";
|
|
21
|
+
import { handleAgentToolsCatalog } from "./handlers/agent-tools-catalog.js";
|
|
19
22
|
import { handleHistorySessions } from "./handlers/history-sessions.js";
|
|
20
23
|
import { handleHistoryMessages } from "./handlers/history-messages.js";
|
|
21
24
|
import { handleHistorySetTitle } from "./handlers/history-set-title.js";
|
|
@@ -31,10 +34,7 @@ import { getFridayNextRuntime } from "../runtime.js";
|
|
|
31
34
|
import { sseEmitter } from "../sse/emitter.js";
|
|
32
35
|
|
|
33
36
|
/** Route matcher - returns the matched handler or null. */
|
|
34
|
-
async function handleFridayNextRoute(
|
|
35
|
-
req: IncomingMessage,
|
|
36
|
-
res: ServerResponse,
|
|
37
|
-
): Promise<boolean> {
|
|
37
|
+
async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
38
38
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
39
39
|
const pathname = url.pathname;
|
|
40
40
|
applyCorsHeaders(res);
|
|
@@ -76,7 +76,10 @@ async function handleFridayNextRoute(
|
|
|
76
76
|
return await handleNodesApprove(req, res);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
if (
|
|
79
|
+
if (
|
|
80
|
+
(req.method === "PUT" || req.method === "GET") &&
|
|
81
|
+
pathname === "/friday-next/sessions/settings"
|
|
82
|
+
) {
|
|
80
83
|
return await handleSessionsSettings(req, res);
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -88,6 +91,27 @@ async function handleFridayNextRoute(
|
|
|
88
91
|
return await handleAgentsList(req, res);
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// Routes: GET/PUT /friday-next/agents/{id}/config
|
|
95
|
+
// GET /friday-next/agents/{id}/files
|
|
96
|
+
// GET/PUT /friday-next/agents/{id}/files/{name}
|
|
97
|
+
if (pathname.startsWith("/friday-next/agents/")) {
|
|
98
|
+
const segs = pathname
|
|
99
|
+
.slice("/friday-next/agents/".length)
|
|
100
|
+
.split("/")
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.map((s) => decodeURIComponent(s));
|
|
103
|
+
const [id, sub, name] = segs;
|
|
104
|
+
if (id && sub === "config" && segs.length === 2) {
|
|
105
|
+
return await handleAgentConfig(req, res, id);
|
|
106
|
+
}
|
|
107
|
+
if (id && sub === "files" && (segs.length === 2 || segs.length === 3)) {
|
|
108
|
+
return await handleAgentFiles(req, res, id, name);
|
|
109
|
+
}
|
|
110
|
+
if (id && sub === "tools" && name === "catalog" && segs.length === 3) {
|
|
111
|
+
return await handleAgentToolsCatalog(req, res, id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
if (req.method === "GET" && pathname === "/friday-next/status") {
|
|
92
116
|
return await handleStatus(req, res);
|
|
93
117
|
}
|
|
@@ -103,7 +127,10 @@ async function handleFridayNextRoute(
|
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
|
|
106
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
(req.method === "PUT" || req.method === "POST") &&
|
|
132
|
+
pathname === "/friday-next/sessions/title"
|
|
133
|
+
) {
|
|
107
134
|
return await handleHistorySetTitle(req, res);
|
|
108
135
|
}
|
|
109
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", () => {
|
|
@@ -66,7 +68,7 @@ describe("assertPublicHttpUrl", () => {
|
|
|
66
68
|
expect(() => assertPublicHttpUrl(new URL("http://127.0.0.1/"))).toThrow(BlockedUrlError);
|
|
67
69
|
expect(() => assertPublicHttpUrl(new URL("http://10.0.0.5/"))).toThrow(BlockedUrlError);
|
|
68
70
|
expect(() => assertPublicHttpUrl(new URL("http://[::1]/"))).toThrow(BlockedUrlError);
|
|
69
|
-
expect(() => assertPublicHttpUrl(new URL("http://[
|
|
71
|
+
expect(() => assertPublicHttpUrl(new URL("http://[fd00::1]/"))).toThrow(BlockedUrlError);
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
it("allows public IP literals", () => {
|
|
@@ -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,11 +115,24 @@ 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
|
});
|
|
113
130
|
|
|
131
|
+
it("passes fc00::/8 (fake-IP DNS v6 range used by clash/mihomo tun mode)", () => {
|
|
132
|
+
expect(isPrivateAddress("fc00::1e7")).toBe(false);
|
|
133
|
+
expect(isPrivateAddress("fc00::ffff:ffff")).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
114
136
|
it("passes public IPv6 and mapped public IPv4", () => {
|
|
115
137
|
expect(isPrivateAddress("2606:2800:220:1:248:1893:25c8:1946")).toBe(false);
|
|
116
138
|
expect(isPrivateAddress("::ffff:93.184.216.34")).toBe(false);
|
|
@@ -135,11 +157,15 @@ describe("assertResolvesPublic", () => {
|
|
|
135
157
|
{ address: "93.184.216.34", family: 4 },
|
|
136
158
|
{ address: "10.0.0.5", family: 4 },
|
|
137
159
|
] as never);
|
|
138
|
-
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
|
+
);
|
|
139
163
|
});
|
|
140
164
|
|
|
141
165
|
it("validates IP-literal hosts without a DNS lookup", async () => {
|
|
142
|
-
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
|
+
);
|
|
143
169
|
await expect(assertResolvesPublic(new URL("http://93.184.216.34/"))).resolves.toBeUndefined();
|
|
144
170
|
expect(lookupMock).not.toHaveBeenCalled();
|
|
145
171
|
});
|
|
@@ -159,7 +185,13 @@ describe("fetchPublicUrl", () => {
|
|
|
159
185
|
mockPublicDns();
|
|
160
186
|
vi.stubGlobal(
|
|
161
187
|
"fetch",
|
|
162
|
-
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
|
+
),
|
|
163
195
|
);
|
|
164
196
|
const result = await fetchPublicUrl("https://example.com/page", opts);
|
|
165
197
|
expect(result?.finalUrl).toBe("https://example.com/page");
|
|
@@ -171,8 +203,15 @@ describe("fetchPublicUrl", () => {
|
|
|
171
203
|
mockPublicDns();
|
|
172
204
|
const fetchMock = vi
|
|
173
205
|
.fn()
|
|
174
|
-
.mockResolvedValueOnce(
|
|
175
|
-
|
|
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
|
+
);
|
|
176
215
|
vi.stubGlobal("fetch", fetchMock);
|
|
177
216
|
const result = await fetchPublicUrl("https://example.com/start", opts);
|
|
178
217
|
expect(result?.finalUrl).toBe("https://other.example.com/final");
|
|
@@ -184,9 +223,14 @@ describe("fetchPublicUrl", () => {
|
|
|
184
223
|
mockPublicDns();
|
|
185
224
|
vi.stubGlobal(
|
|
186
225
|
"fetch",
|
|
187
|
-
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,
|
|
188
233
|
);
|
|
189
|
-
await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(BlockedUrlError);
|
|
190
234
|
});
|
|
191
235
|
|
|
192
236
|
it("throws BlockedUrlError for a directly-blocked URL", async () => {
|
|
@@ -198,7 +242,13 @@ describe("fetchPublicUrl", () => {
|
|
|
198
242
|
const big = "x".repeat(2048);
|
|
199
243
|
vi.stubGlobal(
|
|
200
244
|
"fetch",
|
|
201
|
-
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
|
+
),
|
|
202
252
|
);
|
|
203
253
|
expect(await fetchPublicUrl("https://example.com/big", opts)).toBeNull();
|
|
204
254
|
});
|
|
@@ -207,16 +257,25 @@ describe("fetchPublicUrl", () => {
|
|
|
207
257
|
mockPublicDns();
|
|
208
258
|
vi.stubGlobal(
|
|
209
259
|
"fetch",
|
|
210
|
-
vi.fn(
|
|
260
|
+
vi.fn(
|
|
261
|
+
async () =>
|
|
262
|
+
new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
|
|
263
|
+
),
|
|
211
264
|
);
|
|
212
265
|
expect(
|
|
213
|
-
await fetchPublicUrl("https://example.com/api", {
|
|
266
|
+
await fetchPublicUrl("https://example.com/api", {
|
|
267
|
+
...opts,
|
|
268
|
+
requireContentTypePrefixes: ["text/html", "application/xhtml+xml"],
|
|
269
|
+
}),
|
|
214
270
|
).toBeNull();
|
|
215
271
|
});
|
|
216
272
|
|
|
217
273
|
it("returns null on non-2xx and on DNS failure", async () => {
|
|
218
274
|
mockPublicDns();
|
|
219
|
-
vi.stubGlobal(
|
|
275
|
+
vi.stubGlobal(
|
|
276
|
+
"fetch",
|
|
277
|
+
vi.fn(async () => new Response("nope", { status: 404 })),
|
|
278
|
+
);
|
|
220
279
|
expect(await fetchPublicUrl("https://example.com/missing", opts)).toBeNull();
|
|
221
280
|
|
|
222
281
|
lookupMock.mockRejectedValue(new Error("ENOTFOUND"));
|
|
@@ -227,7 +286,10 @@ describe("fetchPublicUrl", () => {
|
|
|
227
286
|
mockPublicDns();
|
|
228
287
|
vi.stubGlobal(
|
|
229
288
|
"fetch",
|
|
230
|
-
vi.fn(
|
|
289
|
+
vi.fn(
|
|
290
|
+
async () =>
|
|
291
|
+
new Response(null, { status: 302, headers: { location: "https://example.com/loop" } }),
|
|
292
|
+
),
|
|
231
293
|
);
|
|
232
294
|
expect(await fetchPublicUrl("https://example.com/loop", opts)).toBeNull();
|
|
233
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
|
|
@@ -90,7 +91,11 @@ function isPrivateIPv6(ip: string): boolean {
|
|
|
90
91
|
if (mapped) return isPrivateIPv4(mapped[1]);
|
|
91
92
|
if (lower === "::" || lower === "::1") return true; // unspecified / loopback
|
|
92
93
|
const head = lower.split(":")[0];
|
|
93
|
-
|
|
94
|
+
// fc00::/8 (the reserved, never-assigned half of ULA fc00::/7) intentionally NOT blocked:
|
|
95
|
+
// RFC 4193 requires locally-assigned ULA to set the L bit, so real LAN services live in
|
|
96
|
+
// fd00::/8, while fake-IP DNS setups (mihomo/clash tun mode with IPv6, common on gateway
|
|
97
|
+
// hosts) resolve EVERY domain into fc00::/18. Same rationale as 198.18/15 on the IPv4 side.
|
|
98
|
+
if (head.startsWith("fd")) return true; // fd00::/8 locally-assigned ULA
|
|
94
99
|
if (/^fe[89ab]/.test(head)) return true; // fe80::/10 link-local
|
|
95
100
|
return false;
|
|
96
101
|
}
|
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
|
|
|
@@ -59,7 +64,7 @@ describe("downloadRemoteMedia", () => {
|
|
|
59
64
|
async () =>
|
|
60
65
|
new Response(new Uint8Array([1, 2, 3]), {
|
|
61
66
|
status: 200,
|
|
62
|
-
headers: { "content-type": "image/png", "content-length": String(
|
|
67
|
+
headers: { "content-type": "image/png", "content-length": String(128 * 1024 * 1024) },
|
|
63
68
|
}),
|
|
64
69
|
),
|
|
65
70
|
);
|
package/src/media-fetch.ts
CHANGED
|
@@ -10,7 +10,10 @@ import { guessMimeType } from "./http/handlers/files.js";
|
|
|
10
10
|
* download story uniform (always talks to the trusted gateway host with a bearer token).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Aligns with openclaw's own remote-media ceiling (DEFAULT_FETCH_MEDIA_MAX_BYTES =
|
|
14
|
+
// MAX_DOCUMENT_BYTES = 100MB). The real per-kind limit is enforced downstream by
|
|
15
|
+
// saveMediaBuffer (via resolveMediaMaxBytes); this is just the download ceiling.
|
|
16
|
+
const MAX_REMOTE_MEDIA_BYTES = 100 * 1024 * 1024; // 100MB
|
|
14
17
|
const REMOTE_MEDIA_TIMEOUT_MS = 20_000;
|
|
15
18
|
|
|
16
19
|
export function isHttpUrl(value: string): boolean {
|
|
@@ -50,8 +53,7 @@ export function decodeBase64Media(
|
|
|
50
53
|
}
|
|
51
54
|
if (!buffer.length) return null;
|
|
52
55
|
|
|
53
|
-
const mimeType =
|
|
54
|
-
mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
|
|
56
|
+
const mimeType = mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
|
|
55
57
|
return { buffer, mimeType };
|
|
56
58
|
}
|
|
57
59
|
|
package/src/openclaw.d.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
declare module "openclaw/plugin-sdk/agent-harness" {
|
|
2
|
-
|
|
2
|
+
/** Abort the active embedded run keyed by its internal `sessionId` (NOT the channel runId). */
|
|
3
|
+
export const abortAgentHarnessRun: (sessionId: string) => boolean;
|
|
4
|
+
/** Abort the active embedded run and wait for it to actually settle. */
|
|
5
|
+
export const abortAndDrainAgentHarnessRun: (params: {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
sessionKey?: string;
|
|
8
|
+
settleMs?: number;
|
|
9
|
+
forceClear?: boolean;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}) => Promise<{ aborted: boolean; drained: boolean; forceCleared: boolean }>;
|
|
12
|
+
/** Map a channel sessionKey → the active embedded run's internal sessionId. */
|
|
13
|
+
export const resolveActiveEmbeddedRunSessionId: (sessionKey: string) => string | undefined;
|
|
3
14
|
export const runAgentHarness: (...args: any[]) => any;
|
|
4
15
|
}
|
|
5
16
|
|
|
6
17
|
declare module "openclaw/plugin-sdk/device-bootstrap" {
|
|
7
18
|
export const listDevicePairing: (baseDir?: string) => Promise<DevicePairingList>;
|
|
8
|
-
export const approveDevicePairing: (
|
|
19
|
+
export const approveDevicePairing: (
|
|
20
|
+
requestId: string,
|
|
21
|
+
options?: { callerScopes?: readonly string[] },
|
|
22
|
+
baseDir?: string,
|
|
23
|
+
) => Promise<ApproveDevicePairingResult>;
|
|
9
24
|
|
|
10
25
|
interface DevicePairingPendingRequest {
|
|
11
26
|
requestId: string;
|
|
@@ -23,14 +38,17 @@ declare module "openclaw/plugin-sdk/device-bootstrap" {
|
|
|
23
38
|
pending: DevicePairingPendingRequest[];
|
|
24
39
|
paired: PairedDevice[];
|
|
25
40
|
}
|
|
26
|
-
type ApproveDevicePairingResult =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
type ApproveDevicePairingResult =
|
|
42
|
+
| {
|
|
43
|
+
status: "approved";
|
|
44
|
+
requestId: string;
|
|
45
|
+
device: PairedDevice;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
status: "forbidden";
|
|
49
|
+
reason: string;
|
|
50
|
+
}
|
|
51
|
+
| null;
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
declare module "openclaw/plugin-sdk/core" {
|
|
@@ -43,6 +61,19 @@ declare module "openclaw/plugin-sdk/media-store" {
|
|
|
43
61
|
export const saveMediaBuffer: (...args: any[]) => any;
|
|
44
62
|
}
|
|
45
63
|
|
|
64
|
+
declare module "openclaw/plugin-sdk/channel-lifecycle" {
|
|
65
|
+
export const waitUntilAbort: (
|
|
66
|
+
signal?: AbortSignal,
|
|
67
|
+
onAbort?: () => void | Promise<void>,
|
|
68
|
+
) => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare module "openclaw/plugin-sdk/media-runtime" {
|
|
72
|
+
export type MediaKind = "image" | "audio" | "video" | "document";
|
|
73
|
+
export const mediaKindFromMime: (mime?: string | null) => MediaKind | undefined;
|
|
74
|
+
export const maxBytesForKind: (kind: MediaKind) => number;
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
declare module "openclaw/plugin-sdk/plugin-entry" {
|
|
47
78
|
export type OpenClawPluginApi = any;
|
|
48
79
|
}
|
|
@@ -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>
|