@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
|
@@ -6,7 +6,11 @@ import { handleMessageAction } from "./channel-actions.js";
|
|
|
6
6
|
import { sseEmitter } from "./sse/emitter.js";
|
|
7
7
|
import { setOfflineQueueBaseDirForTest } from "./sse/offline-queue.js";
|
|
8
8
|
import { registerRunRoute } from "./run-metadata.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createTempHistoryDir,
|
|
11
|
+
removeTempHistoryDir,
|
|
12
|
+
setMockRuntime,
|
|
13
|
+
} from "./test-support/mock-runtime.js";
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* The `message` tool's `action=send` is handled here (NOT via outbound.sendText/sendMedia).
|
|
@@ -107,8 +111,8 @@ describe("channel-actions handleSend sessionKey routing", () => {
|
|
|
107
111
|
// 8-byte PNG magic header so saveMediaBuffer's magic-byte detection recognizes an image.
|
|
108
112
|
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01]);
|
|
109
113
|
const directLink = "https://picsum.photos/600/400";
|
|
110
|
-
const fetchMock = vi.fn(
|
|
111
|
-
new Response(pngBytes, { status: 200, headers: { "content-type": "image/png" } }),
|
|
114
|
+
const fetchMock = vi.fn(
|
|
115
|
+
async () => new Response(pngBytes, { status: 200, headers: { "content-type": "image/png" } }),
|
|
112
116
|
);
|
|
113
117
|
vi.stubGlobal("fetch", fetchMock);
|
|
114
118
|
|
|
@@ -220,7 +224,9 @@ describe("channel-actions handleSend sessionKey routing", () => {
|
|
|
220
224
|
sessionKey: "agent:operator:friday:direct:fallback-session",
|
|
221
225
|
});
|
|
222
226
|
|
|
223
|
-
const text = parseOutboundFrames(res).find(
|
|
227
|
+
const text = parseOutboundFrames(res).find(
|
|
228
|
+
(f) => f.type === "outbound" && f.data.op === "text",
|
|
229
|
+
);
|
|
224
230
|
expect(text?.data.sessionKey).toBe("agent:operator:friday:direct:fallback-session");
|
|
225
231
|
});
|
|
226
232
|
});
|
package/src/channel-actions.ts
CHANGED
|
@@ -6,7 +6,11 @@ import { fridayNextChannelPlugin } from "./channel.js";
|
|
|
6
6
|
import { sseEmitter } from "./sse/emitter.js";
|
|
7
7
|
import { setOfflineQueueBaseDirForTest } from "./sse/offline-queue.js";
|
|
8
8
|
import { registerRunRoute } from "./run-metadata.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createTempHistoryDir,
|
|
11
|
+
removeTempHistoryDir,
|
|
12
|
+
setMockRuntime,
|
|
13
|
+
} from "./test-support/mock-runtime.js";
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Outbound (message-tool send) must route to the session that started the run.
|
|
@@ -95,11 +99,19 @@ describe("friday-next channel outbound sessionKey routing", () => {
|
|
|
95
99
|
it("run-route wins over ctx sessionKey (ctx carries the agent's base/main session, not the active app session)", async () => {
|
|
96
100
|
const deviceId = "DEV-TEXT-2";
|
|
97
101
|
const runId = "run-text-2";
|
|
98
|
-
registerRunRoute({
|
|
102
|
+
registerRunRoute({
|
|
103
|
+
runId,
|
|
104
|
+
deviceId,
|
|
105
|
+
sessionKey: "agent:operator:friday-next:direct:route-session",
|
|
106
|
+
});
|
|
99
107
|
sseEmitter.trackDeviceForRun(deviceId, runId);
|
|
100
108
|
const res = connect(deviceId);
|
|
101
109
|
|
|
102
|
-
await outbound.sendText({
|
|
110
|
+
await outbound.sendText({
|
|
111
|
+
to: deviceId,
|
|
112
|
+
text: "hi",
|
|
113
|
+
requesterSessionKey: "agent:operator:main",
|
|
114
|
+
});
|
|
103
115
|
|
|
104
116
|
const evt = parseOutboundFrames(res).find((f) => f.type === "outbound" && f.data.op === "text");
|
|
105
117
|
expect(evt?.data.sessionKey).toBe("agent:operator:friday-next:direct:route-session");
|
|
@@ -129,7 +141,9 @@ describe("friday-next channel outbound sessionKey routing", () => {
|
|
|
129
141
|
|
|
130
142
|
await outbound.sendMedia({ to: deviceId, text: "caption", mediaUrl: mediaFile });
|
|
131
143
|
|
|
132
|
-
const evt = parseOutboundFrames(res).find(
|
|
144
|
+
const evt = parseOutboundFrames(res).find(
|
|
145
|
+
(f) => f.type === "outbound" && f.data.op === "media",
|
|
146
|
+
);
|
|
133
147
|
expect(evt).toBeDefined();
|
|
134
148
|
expect(evt?.data.sessionKey).toBe(sessionKey);
|
|
135
149
|
expect(evt?.data.deviceId).toBe(deviceId);
|
package/src/channel.ts
CHANGED
|
@@ -194,153 +194,151 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
|
|
|
194
194
|
outbound: {
|
|
195
195
|
deliveryMode: "direct" as const,
|
|
196
196
|
sendText: async (ctx: any) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
const text = ctx.text ?? "";
|
|
198
|
+
const rawCtx = ctx as unknown as Record<string, unknown>;
|
|
199
|
+
const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
|
|
200
|
+
const runIdFromCtx = pickFirstString(rawCtx, [
|
|
201
|
+
"parentRunId",
|
|
202
|
+
"requesterRunId",
|
|
203
|
+
"originRunId",
|
|
204
|
+
"runId",
|
|
205
|
+
]);
|
|
206
|
+
const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
|
|
207
|
+
const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
);
|
|
209
|
+
const conn = sseEmitter.getConnection(deviceId);
|
|
210
|
+
logger.info(
|
|
211
|
+
`[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
|
|
212
|
+
);
|
|
214
213
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
},
|
|
214
|
+
if (conn) {
|
|
215
|
+
sseEmitter.broadcast(
|
|
216
|
+
{
|
|
217
|
+
type: "outbound",
|
|
218
|
+
data: {
|
|
219
|
+
op: "text",
|
|
220
|
+
ts: Date.now(),
|
|
221
|
+
runId,
|
|
222
|
+
deviceId,
|
|
223
|
+
sessionKey,
|
|
224
|
+
ctx: {
|
|
225
|
+
text,
|
|
226
|
+
to: ctx.to,
|
|
227
|
+
mediaUrl: ctx.mediaUrl,
|
|
228
|
+
audioAsVoice: ctx.audioAsVoice,
|
|
231
229
|
},
|
|
232
230
|
},
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
},
|
|
232
|
+
deviceId,
|
|
233
|
+
true,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
channel: CHANNEL_ID,
|
|
239
|
+
messageId: crypto.randomUUID(),
|
|
240
|
+
timestamp: Date.now(),
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
sendMedia: async (ctx: any) => {
|
|
244
|
+
const rawCtx = ctx as unknown as Record<string, unknown>;
|
|
245
|
+
const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
|
|
246
|
+
const mediaUrl = ctx.mediaUrl;
|
|
247
|
+
const runIdFromCtx = pickFirstString(rawCtx, [
|
|
248
|
+
"parentRunId",
|
|
249
|
+
"requesterRunId",
|
|
250
|
+
"originRunId",
|
|
251
|
+
"runId",
|
|
252
|
+
]);
|
|
253
|
+
const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
|
|
254
|
+
const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
|
|
255
|
+
const audioAsVoice = ctx.audioAsVoice === true;
|
|
256
|
+
const caption = ctx.text ?? "";
|
|
237
257
|
|
|
258
|
+
if (!mediaUrl) {
|
|
238
259
|
return {
|
|
239
260
|
channel: CHANNEL_ID,
|
|
240
261
|
messageId: crypto.randomUUID(),
|
|
241
262
|
timestamp: Date.now(),
|
|
242
263
|
};
|
|
243
|
-
}
|
|
244
|
-
sendMedia: async (ctx: any) => {
|
|
245
|
-
const rawCtx = ctx as unknown as Record<string, unknown>;
|
|
246
|
-
const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
|
|
247
|
-
const mediaUrl = ctx.mediaUrl;
|
|
248
|
-
const runIdFromCtx = pickFirstString(rawCtx, [
|
|
249
|
-
"parentRunId",
|
|
250
|
-
"requesterRunId",
|
|
251
|
-
"originRunId",
|
|
252
|
-
"runId",
|
|
253
|
-
]);
|
|
254
|
-
const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
|
|
255
|
-
const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
|
|
256
|
-
const audioAsVoice = ctx.audioAsVoice === true;
|
|
257
|
-
const caption = ctx.text ?? "";
|
|
258
|
-
|
|
259
|
-
if (!mediaUrl) {
|
|
260
|
-
return {
|
|
261
|
-
channel: CHANNEL_ID,
|
|
262
|
-
messageId: crypto.randomUUID(),
|
|
263
|
-
timestamp: Date.now(),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
264
|
+
}
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
let buffer: Buffer | null = null;
|
|
267
|
+
let downloadedMimeType: string | null = null;
|
|
269
268
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
269
|
+
if (ctx.mediaReadFile) {
|
|
270
|
+
try {
|
|
271
|
+
buffer = await ctx.mediaReadFile(mediaUrl);
|
|
272
|
+
} catch {
|
|
273
|
+
// fall through to remote download / fs
|
|
276
274
|
}
|
|
275
|
+
}
|
|
277
276
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
277
|
+
if (!buffer && isHttpUrl(mediaUrl)) {
|
|
278
|
+
const remote = await downloadRemoteMedia(mediaUrl);
|
|
279
|
+
if (remote) {
|
|
280
|
+
buffer = remote.buffer;
|
|
281
|
+
downloadedMimeType = remote.mimeType;
|
|
284
282
|
}
|
|
283
|
+
}
|
|
285
284
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
285
|
+
if (!buffer) {
|
|
286
|
+
try {
|
|
287
|
+
const resolvedPath = resolveLocalMediaPath(mediaUrl, ctx.mediaLocalRoots);
|
|
288
|
+
buffer = fs.readFileSync(resolvedPath);
|
|
289
|
+
} catch {
|
|
290
|
+
// file not found — skip media
|
|
293
291
|
}
|
|
292
|
+
}
|
|
294
293
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
294
|
+
if (buffer) {
|
|
295
|
+
const mimeType = downloadedMimeType ?? guessMimeType(mediaUrl);
|
|
296
|
+
// Match what openclaw itself supports for this media kind rather than
|
|
297
|
+
// saveMediaBuffer's 5MB default.
|
|
298
|
+
const maxBytes = await resolveMediaMaxBytes(mimeType);
|
|
299
|
+
const saved = await saveMediaBuffer(buffer, mimeType, "inbound", maxBytes);
|
|
300
|
+
if (saved.id) {
|
|
301
|
+
const fileUrl = `/friday-next/files/${encodeURIComponent(saved.id)}`;
|
|
302
|
+
const resolved = resolveMediaAttachment(fileUrl);
|
|
303
|
+
const publicUrl = resolved ? resolved.url : fileUrl;
|
|
305
304
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
);
|
|
305
|
+
const conn = sseEmitter.getConnection(deviceId);
|
|
306
|
+
logger.info(
|
|
307
|
+
`[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
|
|
308
|
+
);
|
|
311
309
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
310
|
+
if (conn) {
|
|
311
|
+
sseEmitter.broadcast(
|
|
312
|
+
{
|
|
313
|
+
type: "outbound",
|
|
314
|
+
data: {
|
|
315
|
+
op: "media",
|
|
316
|
+
ts: Date.now(),
|
|
317
|
+
runId,
|
|
318
|
+
deviceId,
|
|
319
|
+
sessionKey,
|
|
320
|
+
audioAsVoice,
|
|
321
|
+
caption,
|
|
322
|
+
mediaUrl: publicUrl,
|
|
323
|
+
ctx: {
|
|
324
|
+
to: ctx.to,
|
|
325
|
+
text: caption,
|
|
326
|
+
originalMediaUrl: mediaUrl,
|
|
330
327
|
},
|
|
331
328
|
},
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
329
|
+
},
|
|
330
|
+
deviceId,
|
|
331
|
+
true,
|
|
332
|
+
);
|
|
336
333
|
}
|
|
337
334
|
}
|
|
335
|
+
}
|
|
338
336
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
337
|
+
return {
|
|
338
|
+
channel: CHANNEL_ID,
|
|
339
|
+
messageId: crypto.randomUUID(),
|
|
340
|
+
timestamp: Date.now(),
|
|
341
|
+
};
|
|
342
|
+
},
|
|
345
343
|
},
|
|
346
344
|
});
|
|
@@ -50,7 +50,16 @@ export function collectMediaPathsFromToolResult(raw: unknown, acc?: Set<string>)
|
|
|
50
50
|
else if (media && typeof media === "object" && !Array.isArray(media)) visit(media);
|
|
51
51
|
const filePath = o.filePath;
|
|
52
52
|
if (typeof filePath === "string") add(filePath);
|
|
53
|
-
for (const k of [
|
|
53
|
+
for (const k of [
|
|
54
|
+
"details",
|
|
55
|
+
"result",
|
|
56
|
+
"content",
|
|
57
|
+
"text",
|
|
58
|
+
"body",
|
|
59
|
+
"message",
|
|
60
|
+
"arguments",
|
|
61
|
+
"args",
|
|
62
|
+
]) {
|
|
54
63
|
if (o[k] !== undefined) visit(o[k]);
|
|
55
64
|
}
|
|
56
65
|
for (const val of Object.values(o)) {
|
|
@@ -112,20 +121,20 @@ export function extractLocalPathsFromToolTextBlob(s: string): Set<string> {
|
|
|
112
121
|
|
|
113
122
|
// Verbatim /Users/.../file.ext (stop before quote or backslash — avoids eating JSON commas)
|
|
114
123
|
for (const m of s.matchAll(/(\/Users\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
|
|
115
|
-
add(m[1]
|
|
124
|
+
add(m[1]);
|
|
116
125
|
}
|
|
117
126
|
for (const m of s.matchAll(/(\/private\/var\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
|
|
118
|
-
add(m[1]
|
|
127
|
+
add(m[1]);
|
|
119
128
|
}
|
|
120
129
|
for (const m of s.matchAll(/(\/tmp\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
|
|
121
|
-
add(m[1]
|
|
130
|
+
add(m[1]);
|
|
122
131
|
}
|
|
123
132
|
for (const m of s.matchAll(/(\/home\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
|
|
124
|
-
add(m[1]
|
|
133
|
+
add(m[1]);
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
for (const m of s.matchAll(/([A-Za-z]:\\[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
|
|
128
|
-
add(m[1]
|
|
137
|
+
add(m[1]);
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
return out;
|
package/src/config.ts
CHANGED
|
@@ -52,10 +52,7 @@ export function resolveFridayNextConfig(cfg: unknown): FridayNextConfig {
|
|
|
52
52
|
pathPrefix: asString(section.pathPrefix, "/friday-next"),
|
|
53
53
|
transport: asString(section.transport, "http+sse"),
|
|
54
54
|
historyLimit: asNumber(section.historyLimit, 25, 1, 200),
|
|
55
|
-
historyDir: asString(
|
|
56
|
-
section.historyDir,
|
|
57
|
-
`${homedir()}/.openclaw/friday-next/history`,
|
|
58
|
-
),
|
|
55
|
+
historyDir: asString(section.historyDir, `${homedir()}/.openclaw/friday-next/history`),
|
|
59
56
|
logLevel: asString(section.logLevel, "info") as FridayNextLogLevel,
|
|
60
57
|
authToken,
|
|
61
58
|
corsEnabled: asBool(cors.enabled, false),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createTempHistoryDir,
|
|
5
|
+
removeTempHistoryDir,
|
|
6
|
+
setMockRuntime,
|
|
7
|
+
} from "../test-support/mock-runtime.js";
|
|
4
8
|
import {
|
|
5
9
|
setFridayAgentForwardRuntime,
|
|
6
10
|
resetFridayAgentForwardRuntimeForTest,
|
|
@@ -20,7 +24,10 @@ function setForwardConfig(config: unknown): void {
|
|
|
20
24
|
} as never);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
async function getAgents(
|
|
27
|
+
async function getAgents(
|
|
28
|
+
app: ReturnType<typeof createAppSimulator>,
|
|
29
|
+
headers?: Record<string, string>,
|
|
30
|
+
) {
|
|
24
31
|
const res = await app.rawRequest({ method: "GET", path: "/friday-next/agents", headers });
|
|
25
32
|
return { status: res.status, body: res.body ? JSON.parse(res.body) : {}, headers: res.headers };
|
|
26
33
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
3
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createTempHistoryDir,
|
|
6
|
+
removeTempHistoryDir,
|
|
7
|
+
setMockRuntime,
|
|
8
|
+
} from "../test-support/mock-runtime.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e attachments inbound", () => {
|
|
7
11
|
let historyDir = "";
|
|
@@ -3,7 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
5
5
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createTempHistoryDir,
|
|
8
|
+
removeTempHistoryDir,
|
|
9
|
+
setMockRuntime,
|
|
10
|
+
} from "../test-support/mock-runtime.js";
|
|
7
11
|
|
|
8
12
|
describe("e2e attachments outbound", () => {
|
|
9
13
|
let historyDir = "";
|
|
@@ -36,7 +40,8 @@ describe("e2e attachments outbound", () => {
|
|
|
36
40
|
|
|
37
41
|
const delivers = frames.filter((x) => x.event === "deliver");
|
|
38
42
|
expect(delivers.length).toBeGreaterThanOrEqual(1);
|
|
39
|
-
const urls =
|
|
43
|
+
const urls =
|
|
44
|
+
(delivers[delivers.length - 1]?.data?.payload as { mediaUrls?: string[] })?.mediaUrls ?? [];
|
|
40
45
|
expect(urls.some((u) => u.includes("/friday-next/files/"))).toBe(true);
|
|
41
46
|
app.disconnectSSE();
|
|
42
47
|
});
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
10
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createTempHistoryDir,
|
|
13
|
+
removeTempHistoryDir,
|
|
14
|
+
setMockRuntime,
|
|
15
|
+
} from "../test-support/mock-runtime.js";
|
|
12
16
|
import { __setMockNodePairingForTests } from "../agent/node-pairing-bridge.js";
|
|
13
17
|
|
|
14
18
|
const FAKE_DEVICE_ID = "a80b8c4b305fb02c5772c409c6dfcbacde691b61557f7779511ad1a5be8fdf06";
|
|
@@ -146,12 +150,14 @@ describe("e2e two-step auto-approval", () => {
|
|
|
146
150
|
it("Step 2: node 已在 paired 中且有 caps 时返回 alreadyApproved", async () => {
|
|
147
151
|
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
148
152
|
pending: [],
|
|
149
|
-
paired: [
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
paired: [
|
|
154
|
+
{
|
|
155
|
+
nodeId: FAKE_DEVICE_ID,
|
|
156
|
+
approvedAtMs: 1700000000000,
|
|
157
|
+
caps: ["location", "canvas"],
|
|
158
|
+
commands: ["canvas.present"],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
155
161
|
});
|
|
156
162
|
const mockApproveNodePairing = vi.fn();
|
|
157
163
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
3
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createTempHistoryDir,
|
|
6
|
+
removeTempHistoryDir,
|
|
7
|
+
setMockRuntime,
|
|
8
|
+
} from "../test-support/mock-runtime.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e cancel reconnect errors", () => {
|
|
7
11
|
let historyDir = "";
|
|
@@ -15,7 +19,12 @@ describe("e2e cancel reconnect errors", () => {
|
|
|
15
19
|
});
|
|
16
20
|
|
|
17
21
|
it("cancel 缺 runId 与 200 cancel", async () => {
|
|
18
|
-
mockDispatchScript()
|
|
22
|
+
mockDispatchScript()
|
|
23
|
+
.lifecycle("start")
|
|
24
|
+
.partial("a")
|
|
25
|
+
.deliverFinal({ text: "a" })
|
|
26
|
+
.lifecycle("end")
|
|
27
|
+
.install();
|
|
19
28
|
const app = createAppSimulator({ token: "test-token" });
|
|
20
29
|
await app.connectSSE();
|
|
21
30
|
const sent = await app.sendMessage({ text: "go", sessionKey: "c1" });
|
|
@@ -35,7 +44,13 @@ describe("e2e cancel reconnect errors", () => {
|
|
|
35
44
|
});
|
|
36
45
|
|
|
37
46
|
it("Last-Event-ID replay 与多 device 隔离", async () => {
|
|
38
|
-
mockDispatchScript()
|
|
47
|
+
mockDispatchScript()
|
|
48
|
+
.lifecycle("start")
|
|
49
|
+
.partial("h")
|
|
50
|
+
.partial("hi")
|
|
51
|
+
.deliverFinal({ text: "hi" })
|
|
52
|
+
.lifecycle("end")
|
|
53
|
+
.install();
|
|
39
54
|
const appA = createAppSimulator({ token: "test-token", deviceId: "A" });
|
|
40
55
|
await appA.connectSSE();
|
|
41
56
|
await appA.sendMessage({ text: "one", sessionKey: "r1" });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createTempHistoryDir,
|
|
5
|
+
removeTempHistoryDir,
|
|
6
|
+
setMockRuntime,
|
|
7
|
+
} from "../test-support/mock-runtime.js";
|
|
4
8
|
|
|
5
9
|
describe("e2e connect and connected", () => {
|
|
6
10
|
let historyDir = "";
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
3
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createTempHistoryDir,
|
|
6
|
+
removeTempHistoryDir,
|
|
7
|
+
setMockRuntime,
|
|
8
|
+
} from "../test-support/mock-runtime.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e offline SSE replay", () => {
|
|
7
11
|
let historyDir = "";
|
|
@@ -17,7 +21,12 @@ describe("e2e offline SSE replay", () => {
|
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
it("断开后产生的事件在 Last-Event-ID 重连后回放", async () => {
|
|
20
|
-
mockDispatchScript()
|
|
24
|
+
mockDispatchScript()
|
|
25
|
+
.lifecycle("start")
|
|
26
|
+
.partial("x")
|
|
27
|
+
.deliverFinal({ text: "x" })
|
|
28
|
+
.lifecycle("end")
|
|
29
|
+
.install();
|
|
21
30
|
|
|
22
31
|
const app = createAppSimulator({ token: "test-token", deviceId: "replay-dev" });
|
|
23
32
|
await app.connectSSE({ deviceId: "replay-dev" });
|
|
@@ -28,7 +37,12 @@ describe("e2e offline SSE replay", () => {
|
|
|
28
37
|
app.disconnectSSE();
|
|
29
38
|
|
|
30
39
|
resetMockDispatch();
|
|
31
|
-
mockDispatchScript()
|
|
40
|
+
mockDispatchScript()
|
|
41
|
+
.lifecycle("start")
|
|
42
|
+
.partial("y")
|
|
43
|
+
.deliverFinal({ text: "y" })
|
|
44
|
+
.lifecycle("end")
|
|
45
|
+
.install();
|
|
32
46
|
await app.sendMessage({ text: "again", sessionKey: "r1", deviceId: "replay-dev" });
|
|
33
47
|
await new Promise((r) => setTimeout(r, 80));
|
|
34
48
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
3
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createTempHistoryDir,
|
|
6
|
+
removeTempHistoryDir,
|
|
7
|
+
setMockRuntime,
|
|
8
|
+
} from "../test-support/mock-runtime.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e send text", () => {
|
|
7
11
|
let historyDir = "";
|
|
@@ -41,7 +45,12 @@ describe("e2e send text", () => {
|
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
it("assistant 流式事件可达", async () => {
|
|
44
|
-
mockDispatchScript()
|
|
48
|
+
mockDispatchScript()
|
|
49
|
+
.lifecycle("start")
|
|
50
|
+
.partial("abXdef")
|
|
51
|
+
.deliverFinal({ text: "abXdef" })
|
|
52
|
+
.lifecycle("end")
|
|
53
|
+
.install();
|
|
45
54
|
const app = createAppSimulator({ token: "test-token" });
|
|
46
55
|
await app.connectSSE();
|
|
47
56
|
await app.sendMessage({ text: "hi", sessionKey: "s1" });
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
3
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createTempHistoryDir,
|
|
6
|
+
removeTempHistoryDir,
|
|
7
|
+
setMockRuntime,
|
|
8
|
+
} from "../test-support/mock-runtime.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e slash commands", () => {
|
|
7
11
|
let historyDir = "";
|