@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/channel.ts
CHANGED
|
@@ -9,6 +9,7 @@ import fs from "node:fs";
|
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
12
|
+
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
12
13
|
import { createFridayNextLogger } from "./logging.js";
|
|
13
14
|
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
|
14
15
|
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
|
|
@@ -16,6 +17,7 @@ import { sseEmitter } from "./sse/emitter.js";
|
|
|
16
17
|
import { describeMessageActions, handleMessageAction } from "./channel-actions.js";
|
|
17
18
|
import { guessMimeType, resolveMediaAttachment } from "./http/handlers/files.js";
|
|
18
19
|
import { downloadRemoteMedia, isHttpUrl } from "./media-fetch.js";
|
|
20
|
+
import { resolveMediaMaxBytes } from "./agent/media-bridge.js";
|
|
19
21
|
import {
|
|
20
22
|
resolveFridayDeviceIdForOutbound,
|
|
21
23
|
resolveHistorySessionKeyForFridayDevice,
|
|
@@ -100,6 +102,22 @@ const fridayLifecycle = {
|
|
|
100
102
|
},
|
|
101
103
|
};
|
|
102
104
|
|
|
105
|
+
/**
|
|
106
|
+
* friday-next is a passive HTTP+SSE channel: its routes live on the shared gateway server and
|
|
107
|
+
* SSE clients connect on demand, so there is no per-account socket or polling loop to maintain.
|
|
108
|
+
* But the core health-monitor reads the account's lifecycle `running` flag — which the framework
|
|
109
|
+
* flips to `false` the moment `startAccount` resolves/rejects. Without a long-lived startAccount
|
|
110
|
+
* the account is permanently seen as "stopped" and restarted every health poll (~5 min). A stopped
|
|
111
|
+
* account drops out of the deliverable-channel registry, so an agent `message` send landing in that
|
|
112
|
+
* window fails with `Unknown channel: friday-next`. Hold the account lifecycle open until abort
|
|
113
|
+
* (reload/shutdown) so the channel stays `running:true` and continuously deliverable.
|
|
114
|
+
*/
|
|
115
|
+
const fridayGateway = {
|
|
116
|
+
startAccount: async (ctx: { abortSignal: AbortSignal }): Promise<void> => {
|
|
117
|
+
await waitUntilAbort(ctx.abortSignal);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
103
121
|
const fridayStatus = {
|
|
104
122
|
buildAccountSnapshot: async (params: {
|
|
105
123
|
account: { accountId?: string; name?: string; enabled?: boolean };
|
|
@@ -139,6 +157,7 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
|
|
|
139
157
|
},
|
|
140
158
|
config: fridayConfigAdapter,
|
|
141
159
|
lifecycle: fridayLifecycle,
|
|
160
|
+
gateway: fridayGateway,
|
|
142
161
|
status: fridayStatus,
|
|
143
162
|
bindings: {
|
|
144
163
|
compileConfiguredBinding: () => null,
|
|
@@ -175,150 +194,151 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
|
|
|
175
194
|
outbound: {
|
|
176
195
|
deliveryMode: "direct" as const,
|
|
177
196
|
sendText: async (ctx: any) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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);
|
|
189
208
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
);
|
|
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
|
+
);
|
|
195
213
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
},
|
|
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,
|
|
212
229
|
},
|
|
213
230
|
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 ?? "";
|
|
218
257
|
|
|
258
|
+
if (!mediaUrl) {
|
|
219
259
|
return {
|
|
220
260
|
channel: CHANNEL_ID,
|
|
221
261
|
messageId: crypto.randomUUID(),
|
|
222
262
|
timestamp: Date.now(),
|
|
223
263
|
};
|
|
224
|
-
}
|
|
225
|
-
sendMedia: async (ctx: any) => {
|
|
226
|
-
const rawCtx = ctx as unknown as Record<string, unknown>;
|
|
227
|
-
const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
|
|
228
|
-
const mediaUrl = ctx.mediaUrl;
|
|
229
|
-
const runIdFromCtx = pickFirstString(rawCtx, [
|
|
230
|
-
"parentRunId",
|
|
231
|
-
"requesterRunId",
|
|
232
|
-
"originRunId",
|
|
233
|
-
"runId",
|
|
234
|
-
]);
|
|
235
|
-
const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
|
|
236
|
-
const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
|
|
237
|
-
const audioAsVoice = ctx.audioAsVoice === true;
|
|
238
|
-
const caption = ctx.text ?? "";
|
|
239
|
-
|
|
240
|
-
if (!mediaUrl) {
|
|
241
|
-
return {
|
|
242
|
-
channel: CHANNEL_ID,
|
|
243
|
-
messageId: crypto.randomUUID(),
|
|
244
|
-
timestamp: Date.now(),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
264
|
+
}
|
|
247
265
|
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
let buffer: Buffer | null = null;
|
|
267
|
+
let downloadedMimeType: string | null = null;
|
|
250
268
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
269
|
+
if (ctx.mediaReadFile) {
|
|
270
|
+
try {
|
|
271
|
+
buffer = await ctx.mediaReadFile(mediaUrl);
|
|
272
|
+
} catch {
|
|
273
|
+
// fall through to remote download / fs
|
|
257
274
|
}
|
|
275
|
+
}
|
|
258
276
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
277
|
+
if (!buffer && isHttpUrl(mediaUrl)) {
|
|
278
|
+
const remote = await downloadRemoteMedia(mediaUrl);
|
|
279
|
+
if (remote) {
|
|
280
|
+
buffer = remote.buffer;
|
|
281
|
+
downloadedMimeType = remote.mimeType;
|
|
265
282
|
}
|
|
283
|
+
}
|
|
266
284
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
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
|
|
274
291
|
}
|
|
292
|
+
}
|
|
275
293
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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;
|
|
283
304
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
);
|
|
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
|
+
);
|
|
289
309
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
},
|
|
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,
|
|
308
327
|
},
|
|
309
328
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
329
|
+
},
|
|
330
|
+
deviceId,
|
|
331
|
+
true,
|
|
332
|
+
);
|
|
314
333
|
}
|
|
315
334
|
}
|
|
335
|
+
}
|
|
316
336
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
337
|
+
return {
|
|
338
|
+
channel: CHANNEL_ID,
|
|
339
|
+
messageId: crypto.randomUUID(),
|
|
340
|
+
timestamp: Date.now(),
|
|
341
|
+
};
|
|
342
|
+
},
|
|
323
343
|
},
|
|
324
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 = "";
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } 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 { registerFridayNextHttpRoutes } from "../http/server.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e status cors auth", () => {
|
|
@@ -22,7 +26,12 @@ describe("e2e status cors auth", () => {
|
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
it("CORS 预检", async () => {
|
|
25
|
-
setMockRuntime({
|
|
29
|
+
setMockRuntime({
|
|
30
|
+
historyDir,
|
|
31
|
+
authToken: "test-token",
|
|
32
|
+
corsEnabled: true,
|
|
33
|
+
allowOrigin: "https://app.example",
|
|
34
|
+
});
|
|
26
35
|
const app = createAppSimulator({ token: "test-token" });
|
|
27
36
|
const res = await app.options("/friday-next/events", "https://app.example");
|
|
28
37
|
expect(res.status).toBe(204);
|