@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
|
@@ -75,7 +75,10 @@ export async function handleHealth(req: IncomingMessage, res: ServerResponse): P
|
|
|
75
75
|
result.nodePairing = await checkNodePairing(nodeDeviceId, selfHeal, result, log);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
result.ok =
|
|
78
|
+
result.ok =
|
|
79
|
+
!result.nodePairing ||
|
|
80
|
+
result.nodePairing.status === "ok" ||
|
|
81
|
+
result.nodePairing.status === "pending";
|
|
79
82
|
|
|
80
83
|
res.statusCode = 200;
|
|
81
84
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -113,7 +116,8 @@ async function checkNodePairing(
|
|
|
113
116
|
};
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
const pairedNodes: Array<{ nodeId: string; caps?: string[]; commands?: string[] }> =
|
|
119
|
+
const pairedNodes: Array<{ nodeId: string; caps?: string[]; commands?: string[] }> =
|
|
120
|
+
listData?.paired ?? [];
|
|
117
121
|
const pairedMatch = pairedNodes.find(
|
|
118
122
|
(entry) => entry.nodeId?.trim().toUpperCase() === normalizedNodeId,
|
|
119
123
|
);
|
|
@@ -156,16 +160,24 @@ async function checkNodePairing(
|
|
|
156
160
|
|
|
157
161
|
if (pendingMatch && selfHeal) {
|
|
158
162
|
try {
|
|
159
|
-
const callerScopes = [
|
|
163
|
+
const callerScopes = [
|
|
164
|
+
"operator.admin",
|
|
165
|
+
"operator.pairing",
|
|
166
|
+
"operator.read",
|
|
167
|
+
"operator.write",
|
|
168
|
+
];
|
|
160
169
|
const approved = await approveNodePairing(pendingMatch.requestId, { callerScopes });
|
|
161
|
-
const succeeded =
|
|
170
|
+
const succeeded =
|
|
171
|
+
approved != null &&
|
|
172
|
+
!("status" in approved && approved.status === "forbidden") &&
|
|
173
|
+
"requestId" in approved;
|
|
162
174
|
(result.repairActions ??= []).push({
|
|
163
175
|
component: "nodePairing",
|
|
164
176
|
action: "approveNodePairing",
|
|
165
177
|
result: succeeded ? "ok" : "failed",
|
|
166
178
|
detail: succeeded
|
|
167
179
|
? `Auto-approved node ${normalizedNodeId}`
|
|
168
|
-
: `approveNodePairing returned status=${
|
|
180
|
+
: `approveNodePairing returned status=${approved?.status ?? "null"}`,
|
|
169
181
|
});
|
|
170
182
|
if (succeeded) {
|
|
171
183
|
log.info(`Auto-approved node ${normalizedNodeId}`);
|
|
@@ -191,5 +203,9 @@ async function checkNodePairing(
|
|
|
191
203
|
return { status: "pending", detail: "Node is pending approval", nodePaired: false };
|
|
192
204
|
}
|
|
193
205
|
|
|
194
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
status: "not_found",
|
|
208
|
+
detail: `Node ${normalizedNodeId} not registered`,
|
|
209
|
+
nodePaired: false,
|
|
210
|
+
};
|
|
195
211
|
}
|
|
@@ -35,7 +35,12 @@ const CFG = {
|
|
|
35
35
|
let tmpDir = "";
|
|
36
36
|
|
|
37
37
|
/** Auth config + optional subagent fallback. */
|
|
38
|
-
function setRuntime(
|
|
38
|
+
function setRuntime(
|
|
39
|
+
getSessionMessages?: (params: {
|
|
40
|
+
sessionKey: string;
|
|
41
|
+
limit?: number;
|
|
42
|
+
}) => Promise<{ messages?: unknown[] }>,
|
|
43
|
+
): void {
|
|
39
44
|
setFridayNextRuntime({
|
|
40
45
|
config: { loadConfig: () => CFG },
|
|
41
46
|
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
|
|
@@ -100,13 +105,30 @@ describe("handleHistoryMessages", () => {
|
|
|
100
105
|
it("reads the transcript file from disk including user + assistant messages", async () => {
|
|
101
106
|
const file = writeTranscript("sess.jsonl", [
|
|
102
107
|
{ type: "session", version: 1, sessionId: "s" },
|
|
103
|
-
{
|
|
104
|
-
|
|
108
|
+
{
|
|
109
|
+
type: "message",
|
|
110
|
+
id: "u1",
|
|
111
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
112
|
+
message: { role: "user", content: "hi there" },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "message",
|
|
116
|
+
id: "a1",
|
|
117
|
+
timestamp: "2026-01-01T00:00:01.000Z",
|
|
118
|
+
message: {
|
|
119
|
+
role: "assistant",
|
|
120
|
+
content: [{ type: "text", text: "hello" }],
|
|
121
|
+
model: "openai/gpt-4",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
105
124
|
]);
|
|
106
125
|
setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
|
|
107
126
|
|
|
108
127
|
const res = new MockRes();
|
|
109
|
-
await handleHistoryMessages(
|
|
128
|
+
await handleHistoryMessages(
|
|
129
|
+
makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
|
|
130
|
+
res as any,
|
|
131
|
+
);
|
|
110
132
|
expect(res.statusCode).toBe(200);
|
|
111
133
|
const body = JSON.parse(res.body);
|
|
112
134
|
expect(body.messages.map((m: any) => m.role)).toEqual(["user", "assistant"]);
|
|
@@ -117,7 +139,15 @@ describe("handleHistoryMessages", () => {
|
|
|
117
139
|
it("returns the cumulative sessionUsage snapshot from the store", async () => {
|
|
118
140
|
const file = writeTranscript("usage.jsonl", [
|
|
119
141
|
{ type: "message", id: "u1", message: { role: "user", content: "hi" } },
|
|
120
|
-
{
|
|
142
|
+
{
|
|
143
|
+
type: "message",
|
|
144
|
+
id: "a1",
|
|
145
|
+
message: {
|
|
146
|
+
role: "assistant",
|
|
147
|
+
content: [{ type: "text", text: "yo" }],
|
|
148
|
+
model: "openai/gpt-4",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
121
151
|
]);
|
|
122
152
|
setForward({
|
|
123
153
|
"agent:main:main": {
|
|
@@ -132,7 +162,10 @@ describe("handleHistoryMessages", () => {
|
|
|
132
162
|
});
|
|
133
163
|
|
|
134
164
|
const res = new MockRes();
|
|
135
|
-
await handleHistoryMessages(
|
|
165
|
+
await handleHistoryMessages(
|
|
166
|
+
makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
|
|
167
|
+
res as any,
|
|
168
|
+
);
|
|
136
169
|
expect(res.statusCode).toBe(200);
|
|
137
170
|
const body = JSON.parse(res.body);
|
|
138
171
|
expect(body.sessionUsage).toBeDefined();
|
|
@@ -148,7 +181,10 @@ describe("handleHistoryMessages", () => {
|
|
|
148
181
|
setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
|
|
149
182
|
|
|
150
183
|
const res = new MockRes();
|
|
151
|
-
await handleHistoryMessages(
|
|
184
|
+
await handleHistoryMessages(
|
|
185
|
+
makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
|
|
186
|
+
res as any,
|
|
187
|
+
);
|
|
152
188
|
const body = JSON.parse(res.body);
|
|
153
189
|
expect(body.sessionUsage).toBeUndefined();
|
|
154
190
|
});
|
|
@@ -162,7 +198,10 @@ describe("handleHistoryMessages", () => {
|
|
|
162
198
|
|
|
163
199
|
const res = new MockRes();
|
|
164
200
|
await handleHistoryMessages(
|
|
165
|
-
makeReq(
|
|
201
|
+
makeReq(
|
|
202
|
+
"/friday-next/history/messages?sessionKey=agent:main:friday:direct:ABCD-1234:9",
|
|
203
|
+
AUTH,
|
|
204
|
+
),
|
|
166
205
|
res as any,
|
|
167
206
|
);
|
|
168
207
|
const body = JSON.parse(res.body);
|
|
@@ -209,7 +248,10 @@ describe("handleHistoryMessages", () => {
|
|
|
209
248
|
messages: [{ role: "assistant", content: "fallback", __openclaw: { id: "a1", seq: 1 } }],
|
|
210
249
|
}));
|
|
211
250
|
const res = new MockRes();
|
|
212
|
-
await handleHistoryMessages(
|
|
251
|
+
await handleHistoryMessages(
|
|
252
|
+
makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
|
|
253
|
+
res as any,
|
|
254
|
+
);
|
|
213
255
|
const body = JSON.parse(res.body);
|
|
214
256
|
expect(body.messages.map((m: any) => m.id)).toEqual(["a1"]);
|
|
215
257
|
});
|
|
@@ -15,7 +15,10 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { getFridayNextRuntime } from "../../runtime.js";
|
|
16
16
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
17
17
|
import { normalizeHistoryMessages } from "../../history/normalize-message.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
readSessionTranscriptRawMessages,
|
|
20
|
+
resolveSessionId,
|
|
21
|
+
} from "../../history/read-transcript.js";
|
|
19
22
|
import { resolveMediaAttachment } from "./files.js";
|
|
20
23
|
import { readSessionUsageSnapshotFromStore } from "../../session-usage-store.js";
|
|
21
24
|
|
|
@@ -33,7 +33,11 @@ let tmpDir = "";
|
|
|
33
33
|
/** Write a non-empty transcript file and return its absolute path. */
|
|
34
34
|
function transcript(name: string): string {
|
|
35
35
|
const file = path.join(tmpDir, name);
|
|
36
|
-
fs.writeFileSync(
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
file,
|
|
38
|
+
`${JSON.stringify({ type: "message", id: "m", message: { role: "user", content: "hi" } })}\n`,
|
|
39
|
+
"utf-8",
|
|
40
|
+
);
|
|
37
41
|
return file;
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -86,7 +90,11 @@ describe("handleHistorySessions", () => {
|
|
|
86
90
|
{ agents: { list: [{ id: "main" }] } },
|
|
87
91
|
{
|
|
88
92
|
main: {
|
|
89
|
-
"agent:main:main": {
|
|
93
|
+
"agent:main:main": {
|
|
94
|
+
sessionId: "s-main",
|
|
95
|
+
updatedAt: 100,
|
|
96
|
+
sessionFile: transcript("main.jsonl"),
|
|
97
|
+
},
|
|
90
98
|
"agent:main:friday:direct:dev:1": {
|
|
91
99
|
sessionId: "s-fd",
|
|
92
100
|
updatedAt: 300,
|
|
@@ -112,8 +120,16 @@ describe("handleHistorySessions", () => {
|
|
|
112
120
|
{ agents: { list: [{ id: "main" }] } },
|
|
113
121
|
{
|
|
114
122
|
main: {
|
|
115
|
-
"agent:main:live": {
|
|
116
|
-
|
|
123
|
+
"agent:main:live": {
|
|
124
|
+
sessionId: "a",
|
|
125
|
+
updatedAt: 1,
|
|
126
|
+
sessionFile: transcript("live.jsonl"),
|
|
127
|
+
},
|
|
128
|
+
"agent:main:archived": {
|
|
129
|
+
sessionId: "b",
|
|
130
|
+
updatedAt: 2,
|
|
131
|
+
sessionFile: path.join(tmpDir, "gone.jsonl"),
|
|
132
|
+
},
|
|
117
133
|
},
|
|
118
134
|
},
|
|
119
135
|
);
|
|
@@ -129,11 +145,32 @@ describe("handleHistorySessions", () => {
|
|
|
129
145
|
{
|
|
130
146
|
main: {
|
|
131
147
|
"agent:main:main": { sessionId: "ok", updatedAt: 5, sessionFile: transcript("ok.jsonl") },
|
|
132
|
-
"agent:main:main:heartbeat": {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
"agent:main:main:heartbeat": {
|
|
149
|
+
sessionId: "hb",
|
|
150
|
+
updatedAt: 4,
|
|
151
|
+
sessionFile: transcript("hb.jsonl"),
|
|
152
|
+
},
|
|
153
|
+
"agent:main:cron:abc": {
|
|
154
|
+
sessionId: "c",
|
|
155
|
+
updatedAt: 3,
|
|
156
|
+
sessionFile: transcript("c.jsonl"),
|
|
157
|
+
},
|
|
158
|
+
"agent:main:subagent:xyz": {
|
|
159
|
+
sessionId: "sa",
|
|
160
|
+
updatedAt: 2,
|
|
161
|
+
sessionFile: transcript("sa.jsonl"),
|
|
162
|
+
},
|
|
163
|
+
"agent:main:dreaming-narrative-rem-1": {
|
|
164
|
+
sessionId: "d",
|
|
165
|
+
updatedAt: 1,
|
|
166
|
+
sessionFile: transcript("d.jsonl"),
|
|
167
|
+
},
|
|
168
|
+
"agent:main:child": {
|
|
169
|
+
sessionId: "ch",
|
|
170
|
+
updatedAt: 6,
|
|
171
|
+
spawnedBy: "agent:main:main",
|
|
172
|
+
sessionFile: transcript("ch.jsonl"),
|
|
173
|
+
},
|
|
137
174
|
global: { sessionId: "g", updatedAt: 7, sessionFile: transcript("g.jsonl") },
|
|
138
175
|
},
|
|
139
176
|
},
|
|
@@ -139,12 +139,14 @@ function readAgentSessions(agentId: string): FridayHistorySessionSummary[] {
|
|
|
139
139
|
sessionKey: canonicalKey,
|
|
140
140
|
agentId,
|
|
141
141
|
...(readString(entry.sessionId) ? { sessionId: readString(entry.sessionId) } : {}),
|
|
142
|
-
...(readNumber(entry.updatedAt) !== undefined
|
|
143
|
-
|
|
142
|
+
...(readNumber(entry.updatedAt) !== undefined
|
|
143
|
+
? { updatedAt: readNumber(entry.updatedAt) }
|
|
144
|
+
: {}),
|
|
145
|
+
...((readString(entry.model) ?? readString(entry.modelOverride))
|
|
144
146
|
? { model: readString(entry.model) ?? readString(entry.modelOverride) }
|
|
145
147
|
: {}),
|
|
146
148
|
// Server-side session display name (matches OpenClaw's resolution order).
|
|
147
|
-
...(readString(entry.displayName) ?? readString(entry.label)
|
|
149
|
+
...((readString(entry.displayName) ?? readString(entry.label))
|
|
148
150
|
? { title: readString(entry.displayName) ?? readString(entry.label) }
|
|
149
151
|
: {}),
|
|
150
152
|
});
|
|
@@ -42,7 +42,8 @@ function setForward(store: Record<string, unknown>, withWriter = true): void {
|
|
|
42
42
|
runtime: {
|
|
43
43
|
agent: {
|
|
44
44
|
session: {
|
|
45
|
-
resolveStorePath: (_s?: string, opts?: { agentId?: string }) =>
|
|
45
|
+
resolveStorePath: (_s?: string, opts?: { agentId?: string }) =>
|
|
46
|
+
`/store/${opts?.agentId ?? "main"}.json`,
|
|
46
47
|
loadSessionStore: () => store,
|
|
47
48
|
...(withWriter
|
|
48
49
|
? {
|
|
@@ -61,7 +62,9 @@ function setForward(store: Record<string, unknown>, withWriter = true): void {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
describe("handleHistorySetTitle", () => {
|
|
64
|
-
beforeEach(() =>
|
|
65
|
+
beforeEach(() =>
|
|
66
|
+
setFridayNextRuntime({ config: { loadConfig: () => CFG }, logger: {} } as never),
|
|
67
|
+
);
|
|
65
68
|
afterEach(() => resetFridayAgentForwardRuntimeForTest());
|
|
66
69
|
|
|
67
70
|
it("rejects GET with 405", async () => {
|
|
@@ -102,14 +105,20 @@ describe("handleHistorySetTitle", () => {
|
|
|
102
105
|
it("404s when the session is unknown", async () => {
|
|
103
106
|
setForward({});
|
|
104
107
|
const res = new MockRes();
|
|
105
|
-
await handleHistorySetTitle(
|
|
108
|
+
await handleHistorySetTitle(
|
|
109
|
+
makeReq({ sessionKey: "agent:main:nope", title: "x" }, AUTH),
|
|
110
|
+
res as any,
|
|
111
|
+
);
|
|
106
112
|
expect(res.statusCode).toBe(404);
|
|
107
113
|
});
|
|
108
114
|
|
|
109
115
|
it("503s when the store writer is unavailable", async () => {
|
|
110
116
|
setForward({ "agent:main:main": { sessionId: "s" } }, false);
|
|
111
117
|
const res = new MockRes();
|
|
112
|
-
await handleHistorySetTitle(
|
|
118
|
+
await handleHistorySetTitle(
|
|
119
|
+
makeReq({ sessionKey: "agent:main:main", title: "x" }, AUTH),
|
|
120
|
+
res as any,
|
|
121
|
+
);
|
|
113
122
|
expect(res.statusCode).toBe(503);
|
|
114
123
|
});
|
|
115
|
-
})
|
|
124
|
+
});
|
|
@@ -11,7 +11,10 @@ vi.mock("node:dns/promises", () => ({
|
|
|
11
11
|
|
|
12
12
|
import dns from "node:dns/promises";
|
|
13
13
|
import { handleLinkPreview } from "./link-preview.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
resetLinkPreviewCacheForTest,
|
|
16
|
+
type LinkPreviewPayload,
|
|
17
|
+
} from "../../link-preview/preview-service.js";
|
|
15
18
|
import { setAttachmentsDirForTest } from "./files.js";
|
|
16
19
|
import { clearFridayNextRuntime, setFridayNextRuntime } from "../../runtime.js";
|
|
17
20
|
|
|
@@ -33,7 +36,10 @@ class MockRes extends EventEmitter {
|
|
|
33
36
|
function makeReq(query: string | null, token: string | null = "tok"): IncomingMessage {
|
|
34
37
|
return {
|
|
35
38
|
method: "GET",
|
|
36
|
-
url:
|
|
39
|
+
url:
|
|
40
|
+
query == null
|
|
41
|
+
? "/friday-next/link-preview"
|
|
42
|
+
: `/friday-next/link-preview?url=${encodeURIComponent(query)}`,
|
|
37
43
|
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
38
44
|
} as unknown as IncomingMessage;
|
|
39
45
|
}
|
|
@@ -76,7 +82,10 @@ afterEach(() => {
|
|
|
76
82
|
describe("handleLinkPreview", () => {
|
|
77
83
|
it("405 on non-GET", async () => {
|
|
78
84
|
const res = new MockRes();
|
|
79
|
-
await handleLinkPreview(
|
|
85
|
+
await handleLinkPreview(
|
|
86
|
+
{ method: "POST", url: "/friday-next/link-preview", headers: {} } as never,
|
|
87
|
+
res as never,
|
|
88
|
+
);
|
|
80
89
|
expect(res.statusCode).toBe(405);
|
|
81
90
|
});
|
|
82
91
|
|
|
@@ -105,7 +114,10 @@ describe("handleLinkPreview", () => {
|
|
|
105
114
|
if (url.includes("cover.png")) {
|
|
106
115
|
return new Response(PNG_BYTES, { status: 200, headers: { "content-type": "image/png" } });
|
|
107
116
|
}
|
|
108
|
-
return new Response(PAGE_HTML, {
|
|
117
|
+
return new Response(PAGE_HTML, {
|
|
118
|
+
status: 200,
|
|
119
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
120
|
+
});
|
|
109
121
|
}),
|
|
110
122
|
);
|
|
111
123
|
const res = await invoke(makeReq("https://example.com/article"));
|
|
@@ -142,7 +154,9 @@ describe("handleLinkPreview", () => {
|
|
|
142
154
|
const html = `<meta property="og:title" content="T">`;
|
|
143
155
|
vi.stubGlobal(
|
|
144
156
|
"fetch",
|
|
145
|
-
vi.fn(
|
|
157
|
+
vi.fn(
|
|
158
|
+
async () => new Response(html, { status: 200, headers: { "content-type": "text/html" } }),
|
|
159
|
+
),
|
|
146
160
|
);
|
|
147
161
|
const res = await invoke(makeReq("https://example.com/x"));
|
|
148
162
|
expect(JSON.parse(res.body).preview.siteName).toBe("example.com");
|
|
@@ -152,7 +166,13 @@ describe("handleLinkPreview", () => {
|
|
|
152
166
|
// 可达但无 OG/title → 退到 hostname 卡片(不再折叠)。
|
|
153
167
|
vi.stubGlobal(
|
|
154
168
|
"fetch",
|
|
155
|
-
vi.fn(
|
|
169
|
+
vi.fn(
|
|
170
|
+
async () =>
|
|
171
|
+
new Response("<html><body>plain</body></html>", {
|
|
172
|
+
status: 200,
|
|
173
|
+
headers: { "content-type": "text/html" },
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
156
176
|
);
|
|
157
177
|
const res = await invoke(makeReq("https://example.com/bare"));
|
|
158
178
|
expect(res.statusCode).toBe(200);
|
|
@@ -166,9 +186,15 @@ describe("handleLinkPreview", () => {
|
|
|
166
186
|
vi.fn(async (input: URL | string) => {
|
|
167
187
|
const url = String(input);
|
|
168
188
|
if (url.includes("favicon")) {
|
|
169
|
-
return new Response(ICO_BYTES, {
|
|
189
|
+
return new Response(ICO_BYTES, {
|
|
190
|
+
status: 200,
|
|
191
|
+
headers: { "content-type": "image/x-icon" },
|
|
192
|
+
});
|
|
170
193
|
}
|
|
171
|
-
return new Response(`<meta property="og:title" content="Titled">`, {
|
|
194
|
+
return new Response(`<meta property="og:title" content="Titled">`, {
|
|
195
|
+
status: 200,
|
|
196
|
+
headers: { "content-type": "text/html" },
|
|
197
|
+
});
|
|
172
198
|
}),
|
|
173
199
|
);
|
|
174
200
|
const res = await invoke(makeReq("https://example.com/p"));
|
|
@@ -185,7 +211,10 @@ describe("handleLinkPreview", () => {
|
|
|
185
211
|
vi.fn(async (input: URL | string) => {
|
|
186
212
|
const url = String(input);
|
|
187
213
|
if (url.endsWith("/favicon.ico")) {
|
|
188
|
-
return new Response(ICO_BYTES, {
|
|
214
|
+
return new Response(ICO_BYTES, {
|
|
215
|
+
status: 200,
|
|
216
|
+
headers: { "content-type": "image/vnd.microsoft.icon" },
|
|
217
|
+
});
|
|
189
218
|
}
|
|
190
219
|
return new Response("blocked", { status: 403, headers: { "content-type": "text/html" } }); // page blocks bots
|
|
191
220
|
}),
|
|
@@ -199,29 +228,41 @@ describe("handleLinkPreview", () => {
|
|
|
199
228
|
});
|
|
200
229
|
|
|
201
230
|
it("502 fetch_failed for a dead domain (page and favicon both fail)", async () => {
|
|
202
|
-
vi.stubGlobal(
|
|
231
|
+
vi.stubGlobal(
|
|
232
|
+
"fetch",
|
|
233
|
+
vi.fn(async () => new Response("nope", { status: 404 })),
|
|
234
|
+
);
|
|
203
235
|
const res = await invoke(makeReq("https://dead.example.com/x"));
|
|
204
236
|
expect(res.statusCode).toBe(502);
|
|
205
237
|
expect(JSON.parse(res.body).error).toBe("fetch_failed");
|
|
206
238
|
});
|
|
207
239
|
|
|
208
240
|
it("502 fetch_failed on non-2xx and non-HTML responses", async () => {
|
|
209
|
-
vi.stubGlobal(
|
|
241
|
+
vi.stubGlobal(
|
|
242
|
+
"fetch",
|
|
243
|
+
vi.fn(async () => new Response("nope", { status: 500 })),
|
|
244
|
+
);
|
|
210
245
|
expect((await invoke(makeReq("https://example.com/down"))).statusCode).toBe(502);
|
|
211
246
|
|
|
212
247
|
resetLinkPreviewCacheForTest();
|
|
213
248
|
vi.stubGlobal(
|
|
214
249
|
"fetch",
|
|
215
|
-
vi.fn(
|
|
250
|
+
vi.fn(
|
|
251
|
+
async () =>
|
|
252
|
+
new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
|
|
253
|
+
),
|
|
216
254
|
);
|
|
217
255
|
expect((await invoke(makeReq("https://example.com/api"))).statusCode).toBe(502);
|
|
218
256
|
});
|
|
219
257
|
|
|
220
258
|
it("serves the second request from cache without refetching", async () => {
|
|
221
|
-
const fetchMock = vi.fn(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
259
|
+
const fetchMock = vi.fn(
|
|
260
|
+
async () =>
|
|
261
|
+
new Response(`<meta property="og:title" content="Cached">`, {
|
|
262
|
+
status: 200,
|
|
263
|
+
headers: { "content-type": "text/html" },
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
225
266
|
vi.stubGlobal("fetch", fetchMock);
|
|
226
267
|
await invoke(makeReq("https://example.com/cached"));
|
|
227
268
|
const afterFirst = fetchMock.mock.calls.length;
|
|
@@ -17,7 +17,10 @@ const ERROR_STATUS: Record<LinkPreviewError, number> = {
|
|
|
17
17
|
fetch_failed: 502,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export async function handleLinkPreview(
|
|
20
|
+
export async function handleLinkPreview(
|
|
21
|
+
req: IncomingMessage,
|
|
22
|
+
res: ServerResponse,
|
|
23
|
+
): Promise<boolean> {
|
|
21
24
|
if (req.method !== "GET") {
|
|
22
25
|
res.statusCode = 405;
|
|
23
26
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -35,9 +35,9 @@ describe("composeBodyWithMediaRefs", () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it("omits the leading blank line when text is empty (attachment-only)", () => {
|
|
38
|
-
expect(
|
|
39
|
-
"[media attached: file:///a]
|
|
40
|
-
);
|
|
38
|
+
expect(
|
|
39
|
+
composeBodyWithMediaRefs("", ["[media attached: file:///a]", "[media attached: file:///b]"]),
|
|
40
|
+
).toBe("[media attached: file:///a]\n[media attached: file:///b]");
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -152,7 +152,9 @@ describe("handleMessages dispatch context (owner fields)", () => {
|
|
|
152
152
|
const dispatchCalled = new Promise<void>((resolve) => {
|
|
153
153
|
__setMockFridayDispatchForTests(async (args: unknown) => {
|
|
154
154
|
const a = args as {
|
|
155
|
-
dispatcherOptions?: {
|
|
155
|
+
dispatcherOptions?: {
|
|
156
|
+
deliver?: (payload: unknown, info: { kind: string }) => Promise<void>;
|
|
157
|
+
};
|
|
156
158
|
};
|
|
157
159
|
if (a.dispatcherOptions?.deliver) {
|
|
158
160
|
await a.dispatcherOptions.deliver(
|
|
@@ -169,10 +171,12 @@ describe("handleMessages dispatch context (owner fields)", () => {
|
|
|
169
171
|
req.headers = { authorization: "Bearer tok" };
|
|
170
172
|
const res = new MockRes() as unknown as ServerResponse;
|
|
171
173
|
let observedPayload: Record<string, unknown> | null = null;
|
|
172
|
-
const broadcastSpy = vi
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
const broadcastSpy = vi
|
|
175
|
+
.spyOn(sseEmitter, "broadcastToRun")
|
|
176
|
+
.mockImplementation((_: string, evt: unknown) => {
|
|
177
|
+
const data = (evt as { data?: { payload?: Record<string, unknown> } })?.data;
|
|
178
|
+
if (data?.payload) observedPayload = data.payload;
|
|
179
|
+
});
|
|
176
180
|
|
|
177
181
|
const p = handleMessages(req, res);
|
|
178
182
|
req.end(
|