@syengup/friday-channel-next 0.1.36 → 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/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/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 +25 -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/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 +57 -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 +16 -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
|
@@ -77,7 +77,10 @@ const log = (
|
|
|
77
77
|
logger[level](`[${action}] deviceId=${deviceId}${runPart}${detailPart}`);
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
function collectReplyPayloadMediaUrls(pl: {
|
|
80
|
+
function collectReplyPayloadMediaUrls(pl: {
|
|
81
|
+
mediaUrls?: string[];
|
|
82
|
+
mediaUrl?: string | null;
|
|
83
|
+
}): string[] {
|
|
81
84
|
const fromArr = Array.isArray(pl.mediaUrls)
|
|
82
85
|
? pl.mediaUrls.filter((u): u is string => typeof u === "string" && u.trim().length > 0)
|
|
83
86
|
: [];
|
|
@@ -159,7 +162,12 @@ export function isCanvasSnapshotMediaPath(url: unknown): boolean {
|
|
|
159
162
|
export function translateDeliverPayload(
|
|
160
163
|
pl: FridayReplyPayload,
|
|
161
164
|
kind: string,
|
|
162
|
-
meta?: {
|
|
165
|
+
meta?: {
|
|
166
|
+
modelName?: string;
|
|
167
|
+
totalTokens?: number;
|
|
168
|
+
contextTokensUsed?: number;
|
|
169
|
+
contextWindowMax?: number;
|
|
170
|
+
},
|
|
163
171
|
): Record<string, unknown> {
|
|
164
172
|
// Strip canvas-snapshot tool-result images before any media resolution (paths here are still the
|
|
165
173
|
// original `/tmp/openclaw/openclaw-canvas-snapshot-*.jpg` temp paths, not yet copied to friday files).
|
|
@@ -210,7 +218,11 @@ export function translateDeliverPayload(
|
|
|
210
218
|
if (typeof meta?.modelName === "string" && meta.modelName.trim()) {
|
|
211
219
|
nextFridayNext.modelName = meta.modelName.trim();
|
|
212
220
|
}
|
|
213
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
typeof meta?.totalTokens === "number" &&
|
|
223
|
+
Number.isFinite(meta.totalTokens) &&
|
|
224
|
+
meta.totalTokens > 0
|
|
225
|
+
) {
|
|
214
226
|
nextFridayNext.totalTokens = Math.floor(meta.totalTokens);
|
|
215
227
|
}
|
|
216
228
|
if (
|
|
@@ -260,8 +272,10 @@ function scheduleLateFinalMetaPatch(runId: string, attempts = 6): void {
|
|
|
260
272
|
sessionKey: route.sessionKey,
|
|
261
273
|
modelName: meta.modelName ?? null,
|
|
262
274
|
totalTokens: typeof meta.totalTokens === "number" ? meta.totalTokens : null,
|
|
263
|
-
contextTokensUsed:
|
|
264
|
-
|
|
275
|
+
contextTokensUsed:
|
|
276
|
+
typeof meta.contextTokensUsed === "number" ? meta.contextTokensUsed : null,
|
|
277
|
+
contextWindowMax:
|
|
278
|
+
typeof meta.contextWindowMax === "number" ? meta.contextWindowMax : null,
|
|
265
279
|
ts: Date.now(),
|
|
266
280
|
},
|
|
267
281
|
},
|
|
@@ -298,11 +312,17 @@ function pickMetadataFromMessageLike(message: unknown): {
|
|
|
298
312
|
? usage.totalTokens
|
|
299
313
|
: undefined) ??
|
|
300
314
|
(typeof usage?.total === "number" && Number.isFinite(usage.total) ? usage.total : undefined) ??
|
|
301
|
-
(typeof usage?.total_tokens === "number" && Number.isFinite(usage.total_tokens)
|
|
315
|
+
(typeof usage?.total_tokens === "number" && Number.isFinite(usage.total_tokens)
|
|
316
|
+
? usage.total_tokens
|
|
317
|
+
: undefined);
|
|
302
318
|
const totalFromMessage =
|
|
303
|
-
(typeof m.totalTokens === "number" && Number.isFinite(m.totalTokens)
|
|
304
|
-
|
|
305
|
-
|
|
319
|
+
(typeof m.totalTokens === "number" && Number.isFinite(m.totalTokens)
|
|
320
|
+
? m.totalTokens
|
|
321
|
+
: undefined) ??
|
|
322
|
+
(typeof m.total_tokens === "number" && Number.isFinite(m.total_tokens)
|
|
323
|
+
? m.total_tokens
|
|
324
|
+
: undefined);
|
|
325
|
+
const totalTokens = Math.floor(totalFromUsage ?? totalFromMessage ?? 0);
|
|
306
326
|
|
|
307
327
|
let contextTokensUsed: number | undefined;
|
|
308
328
|
if (usage) {
|
|
@@ -313,8 +333,12 @@ function pickMetadataFromMessageLike(message: unknown): {
|
|
|
313
333
|
}
|
|
314
334
|
|
|
315
335
|
const ctxMaxRaw =
|
|
316
|
-
(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow)
|
|
317
|
-
|
|
336
|
+
(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow)
|
|
337
|
+
? m.contextWindow
|
|
338
|
+
: undefined) ??
|
|
339
|
+
(typeof m.maxContextTokens === "number" && Number.isFinite(m.maxContextTokens)
|
|
340
|
+
? m.maxContextTokens
|
|
341
|
+
: undefined);
|
|
318
342
|
const contextWindowMax =
|
|
319
343
|
typeof ctxMaxRaw === "number" && ctxMaxRaw > 0 ? Math.floor(ctxMaxRaw) : undefined;
|
|
320
344
|
|
|
@@ -336,9 +360,16 @@ async function resolveRunMetadataFromRuntimeSession(
|
|
|
336
360
|
contextTokensUsed?: number;
|
|
337
361
|
contextWindowMax?: number;
|
|
338
362
|
} | null> {
|
|
339
|
-
const sessionApi = (
|
|
340
|
-
|
|
341
|
-
|
|
363
|
+
const sessionApi = (
|
|
364
|
+
runtime as unknown as {
|
|
365
|
+
subagent?: {
|
|
366
|
+
getSessionMessages?: (params: {
|
|
367
|
+
sessionKey: string;
|
|
368
|
+
limit?: number;
|
|
369
|
+
}) => Promise<{ messages?: unknown[] }>;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
).subagent;
|
|
342
373
|
if (!sessionApi?.getSessionMessages) return null;
|
|
343
374
|
try {
|
|
344
375
|
const response = await sessionApi.getSessionMessages({ sessionKey, limit: 80 });
|
|
@@ -373,7 +404,10 @@ export function composeBodyWithMediaRefs(text: string, mediaRefs: string[]): str
|
|
|
373
404
|
return trimmed ? `${trimmed}\n\n${mediaRefs.join("\n")}` : mediaRefs.join("\n");
|
|
374
405
|
}
|
|
375
406
|
|
|
376
|
-
async function buildBodyForAgentWithAttachments(
|
|
407
|
+
async function buildBodyForAgentWithAttachments(
|
|
408
|
+
text: string,
|
|
409
|
+
attachmentIds: string[],
|
|
410
|
+
): Promise<string> {
|
|
377
411
|
if (attachmentIds.length === 0) return text.trim();
|
|
378
412
|
|
|
379
413
|
const mediaRefs: string[] = [];
|
|
@@ -539,7 +573,10 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
539
573
|
dispatcherOptions: {
|
|
540
574
|
deliver: async (pl: any, info: any) => {
|
|
541
575
|
let meta = getRunMetadata(runId);
|
|
542
|
-
if (
|
|
576
|
+
if (
|
|
577
|
+
info.kind.toLowerCase() === "final" &&
|
|
578
|
+
!(meta?.modelName || typeof meta?.totalTokens === "number")
|
|
579
|
+
) {
|
|
543
580
|
const resolved = await resolveRunMetadataFromRuntimeSession(runtime, baseSessionKey);
|
|
544
581
|
if (resolved) {
|
|
545
582
|
setRunMetadata(runId, resolved);
|
|
@@ -602,10 +639,11 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
602
639
|
// OpenClaw `pi-embedded-subscribe` gates `streamReasoning` on `typeof onReasoningStream === "function"`.
|
|
603
640
|
// Without this, `emitReasoningStream` never runs and Friday SSE never sees `stream: "thinking"`.
|
|
604
641
|
onReasoningStream: async (pl: unknown) => {
|
|
605
|
-
const
|
|
642
|
+
const rawText =
|
|
606
643
|
typeof pl === "object" && pl !== null && "text" in pl
|
|
607
|
-
?
|
|
608
|
-
:
|
|
644
|
+
? (pl as { text?: unknown }).text
|
|
645
|
+
: undefined;
|
|
646
|
+
const text = typeof rawText === "string" ? rawText : "";
|
|
609
647
|
log("REASONING_STREAM", normalizedDeviceId, runId, `textLen=${text.length}`);
|
|
610
648
|
},
|
|
611
649
|
onReasoningEnd: async () => {
|
|
@@ -44,7 +44,7 @@ function resolveConfiguredModels(): ResolvedModels {
|
|
|
44
44
|
seen.add(modelKey);
|
|
45
45
|
entries.push({
|
|
46
46
|
id: modelKey,
|
|
47
|
-
name: typeof info?.alias === "string" ? info.alias : meta?.name ?? split.modelId,
|
|
47
|
+
name: typeof info?.alias === "string" ? info.alias : (meta?.name ?? split.modelId),
|
|
48
48
|
provider: split.provider,
|
|
49
49
|
reasoning: meta?.reasoning,
|
|
50
50
|
contextWindow: meta?.contextWindow,
|
|
@@ -104,13 +104,19 @@ function resolveConfiguredModels(): ResolvedModels {
|
|
|
104
104
|
return { models: entries, defaultModel };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function buildProviderModelMeta(cfg: Record<string, unknown>): Map<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
function buildProviderModelMeta(cfg: Record<string, unknown>): Map<
|
|
108
|
+
string,
|
|
109
|
+
{
|
|
110
|
+
name?: string;
|
|
111
|
+
reasoning?: boolean;
|
|
112
|
+
contextWindow?: number;
|
|
113
|
+
maxTokens?: number;
|
|
114
|
+
}
|
|
115
|
+
> {
|
|
116
|
+
const meta = new Map<
|
|
117
|
+
string,
|
|
118
|
+
{ name?: string; reasoning?: boolean; contextWindow?: number; maxTokens?: number }
|
|
119
|
+
>();
|
|
114
120
|
const models = cfg?.models as Record<string, unknown> | undefined;
|
|
115
121
|
const providers = models?.providers as Record<string, unknown> | undefined;
|
|
116
122
|
if (providers) {
|
|
@@ -24,8 +24,14 @@ class MockRes extends EventEmitter {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function mockReq(
|
|
28
|
-
|
|
27
|
+
function mockReq(
|
|
28
|
+
method: string,
|
|
29
|
+
headers: Record<string, string> = {},
|
|
30
|
+
): PassThrough & { method: string; headers: Record<string, string> } {
|
|
31
|
+
const stream = new PassThrough() as unknown as PassThrough & {
|
|
32
|
+
method: string;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
};
|
|
29
35
|
stream.method = method;
|
|
30
36
|
stream.headers = headers;
|
|
31
37
|
return stream;
|
|
@@ -92,7 +98,10 @@ describe("handleNodesApprove", () => {
|
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
it("returns 404 when listNodePairing returns data without matching node", async () => {
|
|
95
|
-
mockList.mockResolvedValueOnce({
|
|
101
|
+
mockList.mockResolvedValueOnce({
|
|
102
|
+
pending: [{ requestId: "x", nodeId: "UNMATCHED" }],
|
|
103
|
+
paired: [],
|
|
104
|
+
});
|
|
96
105
|
|
|
97
106
|
const req = mockReq("POST", { authorization: "Bearer test-token" });
|
|
98
107
|
const res = new MockRes() as unknown as ServerResponse;
|
|
@@ -135,7 +144,9 @@ describe("handleNodesApprove", () => {
|
|
|
135
144
|
it("returns 200 with alreadyApproved when node in paired with caps", async () => {
|
|
136
145
|
mockList.mockResolvedValueOnce({
|
|
137
146
|
pending: [],
|
|
138
|
-
paired: [
|
|
147
|
+
paired: [
|
|
148
|
+
{ nodeId: NODE_ID, approvedAtMs: 100, caps: ["canvas"], commands: ["canvas.present"] },
|
|
149
|
+
],
|
|
139
150
|
});
|
|
140
151
|
|
|
141
152
|
const req = mockReq("POST", { authorization: "Bearer test-token" });
|
|
@@ -14,15 +14,6 @@ interface PairedNodeEntry {
|
|
|
14
14
|
caps?: string[];
|
|
15
15
|
commands?: string[];
|
|
16
16
|
}
|
|
17
|
-
interface NodePairingList {
|
|
18
|
-
pending: PendingNodeEntry[];
|
|
19
|
-
paired: PairedNodeEntry[];
|
|
20
|
-
}
|
|
21
|
-
type ApproveNodePairingResult =
|
|
22
|
-
| { requestId: string; node: PairedNodeEntry }
|
|
23
|
-
| { status: "forbidden"; missingScope: string }
|
|
24
|
-
| null;
|
|
25
|
-
|
|
26
17
|
export async function handleNodesApprove(
|
|
27
18
|
req: IncomingMessage,
|
|
28
19
|
res: ServerResponse,
|
|
@@ -62,7 +53,7 @@ export async function handleNodesApprove(
|
|
|
62
53
|
|
|
63
54
|
const normalizedNodeId = rawNodeId.trim().toUpperCase();
|
|
64
55
|
|
|
65
|
-
let listData, listNodePairing, approveNodePairing;
|
|
56
|
+
let listData, listNodePairing, approveNodePairing;
|
|
66
57
|
try {
|
|
67
58
|
({ listNodePairing, approveNodePairing } = await loadNodePairingModule());
|
|
68
59
|
} catch (err) {
|
|
@@ -91,12 +82,7 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
91
82
|
const requestId = pendingMatch.requestId;
|
|
92
83
|
log.info(`approving nodeId=${normalizedNodeId} requestId=${requestId}`);
|
|
93
84
|
|
|
94
|
-
const callerScopes = [
|
|
95
|
-
"operator.admin",
|
|
96
|
-
"operator.pairing",
|
|
97
|
-
"operator.read",
|
|
98
|
-
"operator.write",
|
|
99
|
-
];
|
|
85
|
+
const callerScopes = ["operator.admin", "operator.pairing", "operator.read", "operator.write"];
|
|
100
86
|
|
|
101
87
|
let approved;
|
|
102
88
|
try {
|
|
@@ -105,10 +91,12 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
105
91
|
log.error(`approveNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
106
92
|
res.statusCode = 502;
|
|
107
93
|
res.setHeader("Content-Type", "application/json");
|
|
108
|
-
res.end(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
res.end(
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
error: "Node approval failed",
|
|
97
|
+
detail: err instanceof Error ? err.message : "Unknown error",
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
112
100
|
return true;
|
|
113
101
|
}
|
|
114
102
|
|
|
@@ -122,18 +110,22 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
122
110
|
if ("status" in approved && approved.status === "forbidden") {
|
|
123
111
|
res.statusCode = 403;
|
|
124
112
|
res.setHeader("Content-Type", "application/json");
|
|
125
|
-
res.end(
|
|
113
|
+
res.end(
|
|
114
|
+
JSON.stringify({ error: `Node approval forbidden: ${approved.missingScope ?? "unknown"}` }),
|
|
115
|
+
);
|
|
126
116
|
return true;
|
|
127
117
|
}
|
|
128
118
|
|
|
129
119
|
res.statusCode = 200;
|
|
130
120
|
res.setHeader("Content-Type", "application/json");
|
|
131
|
-
res.end(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
res.end(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
ok: true,
|
|
124
|
+
nodeId: normalizedNodeId,
|
|
125
|
+
requestId: approved.requestId,
|
|
126
|
+
approvedAtMs: approved.node?.approvedAtMs,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
137
129
|
return true;
|
|
138
130
|
}
|
|
139
131
|
|
|
@@ -147,26 +139,32 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
147
139
|
const caps = pairedMatch.caps ?? [];
|
|
148
140
|
const commands = pairedMatch.commands ?? [];
|
|
149
141
|
if (caps.length > 0 || commands.length > 0) {
|
|
150
|
-
log.info(
|
|
142
|
+
log.info(
|
|
143
|
+
`nodeId=${normalizedNodeId} already paired with caps=${caps.length} commands=${commands.length}`,
|
|
144
|
+
);
|
|
151
145
|
res.statusCode = 200;
|
|
152
146
|
res.setHeader("Content-Type", "application/json");
|
|
153
|
-
res.end(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
res.end(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
ok: true,
|
|
150
|
+
nodeId: normalizedNodeId,
|
|
151
|
+
alreadyApproved: true,
|
|
152
|
+
approvedAtMs: pairedMatch.approvedAtMs,
|
|
153
|
+
caps,
|
|
154
|
+
commands,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
161
157
|
return true;
|
|
162
158
|
}
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
res.statusCode = 404;
|
|
166
162
|
res.setHeader("Content-Type", "application/json");
|
|
167
|
-
res.end(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
res.end(
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
error: "No pending node found for this nodeId",
|
|
166
|
+
nodeId: normalizedNodeId,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
171
169
|
return true;
|
|
172
170
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
3
3
|
import { PLUGIN_VERSION } from "../../version.js";
|
|
4
|
-
import {
|
|
5
|
-
fetchLatestVersion,
|
|
6
|
-
getInstallSource,
|
|
7
|
-
semverGreater,
|
|
8
|
-
} from "../../plugin-install-info.js";
|
|
4
|
+
import { fetchLatestVersion, getInstallSource, semverGreater } from "../../plugin-install-info.js";
|
|
9
5
|
|
|
10
6
|
export interface PluginInfoResult {
|
|
11
7
|
currentVersion: string;
|
|
@@ -17,7 +13,10 @@ export interface PluginInfoResult {
|
|
|
17
13
|
upgradable: boolean;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
export async function handlePluginInfo(
|
|
16
|
+
export async function handlePluginInfo(
|
|
17
|
+
req: IncomingMessage,
|
|
18
|
+
res: ServerResponse,
|
|
19
|
+
): Promise<boolean> {
|
|
21
20
|
if (req.method !== "GET") {
|
|
22
21
|
res.statusCode = 405;
|
|
23
22
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -18,7 +18,10 @@ const RESTART_DELAY_MS = 500;
|
|
|
18
18
|
* eligible — dev (load.paths / source==="path") installs return 409 to protect
|
|
19
19
|
* the dev environment from duplicate npm installs.
|
|
20
20
|
*/
|
|
21
|
-
export async function handlePluginUpgrade(
|
|
21
|
+
export async function handlePluginUpgrade(
|
|
22
|
+
req: IncomingMessage,
|
|
23
|
+
res: ServerResponse,
|
|
24
|
+
): Promise<boolean> {
|
|
22
25
|
if (req.method !== "POST") {
|
|
23
26
|
res.statusCode = 405;
|
|
24
27
|
res.setHeader("Content-Type", "application/json");
|
package/src/http/handlers/sse.ts
CHANGED
|
@@ -67,7 +67,9 @@ export async function handleSseStream(req: IncomingMessage, res: ServerResponse)
|
|
|
67
67
|
const lastEventId = parseLastEventId(req, url);
|
|
68
68
|
if (lastEventId > 0) sseEmitter.replayBacklog(deviceId, lastEventId);
|
|
69
69
|
|
|
70
|
-
const config = resolveFridayNextConfig(
|
|
70
|
+
const config = resolveFridayNextConfig(
|
|
71
|
+
getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
|
|
72
|
+
);
|
|
71
73
|
const keepalive = setInterval(() => {
|
|
72
74
|
if (conn.isClosed) {
|
|
73
75
|
clearInterval(keepalive);
|
package/src/http/server.ts
CHANGED
|
@@ -34,10 +34,7 @@ import { getFridayNextRuntime } from "../runtime.js";
|
|
|
34
34
|
import { sseEmitter } from "../sse/emitter.js";
|
|
35
35
|
|
|
36
36
|
/** Route matcher - returns the matched handler or null. */
|
|
37
|
-
async function handleFridayNextRoute(
|
|
38
|
-
req: IncomingMessage,
|
|
39
|
-
res: ServerResponse,
|
|
40
|
-
): Promise<boolean> {
|
|
37
|
+
async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
41
38
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
42
39
|
const pathname = url.pathname;
|
|
43
40
|
applyCorsHeaders(res);
|
|
@@ -79,7 +76,10 @@ async function handleFridayNextRoute(
|
|
|
79
76
|
return await handleNodesApprove(req, res);
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
if (
|
|
79
|
+
if (
|
|
80
|
+
(req.method === "PUT" || req.method === "GET") &&
|
|
81
|
+
pathname === "/friday-next/sessions/settings"
|
|
82
|
+
) {
|
|
83
83
|
return await handleSessionsSettings(req, res);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -127,7 +127,10 @@ async function handleFridayNextRoute(
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
|
|
130
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
(req.method === "PUT" || req.method === "POST") &&
|
|
132
|
+
pathname === "/friday-next/sessions/title"
|
|
133
|
+
) {
|
|
131
134
|
return await handleHistorySetTitle(req, res);
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -5,7 +5,9 @@ const BASE = "https://example.com/article/42";
|
|
|
5
5
|
|
|
6
6
|
describe("decodeHtmlEntities", () => {
|
|
7
7
|
it("decodes named, decimal, and hex entities", () => {
|
|
8
|
-
expect(decodeHtmlEntities("Tom & Jerry — "fun"")).toBe(
|
|
8
|
+
expect(decodeHtmlEntities("Tom & Jerry — "fun"")).toBe(
|
|
9
|
+
'Tom & Jerry — "fun"',
|
|
10
|
+
);
|
|
9
11
|
expect(decodeHtmlEntities("中文")).toBe("中文");
|
|
10
12
|
expect(decodeHtmlEntities("'quoted'")).toBe("'quoted'");
|
|
11
13
|
});
|
|
@@ -143,7 +145,9 @@ describe("parseOpenGraph", () => {
|
|
|
143
145
|
it("extracts a cover image from inline JSON (extensionless, escaped slashes)", () => {
|
|
144
146
|
const html = `<title>搜索资讯页</title>
|
|
145
147
|
<script>window.__INFO__={"imgUrl":"http:\\/\\/qqpublic.qpic.cn\\/qq_public_cover\\/0\\/0-2342_op"}</script>`;
|
|
146
|
-
expect(parseOpenGraph(html, BASE).imageUrl).toBe(
|
|
148
|
+
expect(parseOpenGraph(html, BASE).imageUrl).toBe(
|
|
149
|
+
"http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op",
|
|
150
|
+
);
|
|
147
151
|
});
|
|
148
152
|
|
|
149
153
|
it("standard og tags still win over body/json fallbacks", () => {
|
|
@@ -91,7 +91,9 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
|
|
|
91
91
|
let metaDescription: string | null = null;
|
|
92
92
|
for (const match of slice.matchAll(META_TAG_RE)) {
|
|
93
93
|
const tag = match[0];
|
|
94
|
-
const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
|
|
94
|
+
const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
|
|
95
|
+
?.trim()
|
|
96
|
+
.toLowerCase();
|
|
95
97
|
if (!key) continue;
|
|
96
98
|
const content = attributeValue(tag, "content");
|
|
97
99
|
if (content == null || !content.trim()) continue;
|
|
@@ -140,10 +142,15 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
|
|
|
140
142
|
};
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
const JSON_LD_RE =
|
|
145
|
+
const JSON_LD_RE =
|
|
146
|
+
/<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
144
147
|
|
|
145
148
|
/** Extract title/description/image from JSON-LD blocks (schema.org Article/NewsArticle/etc.). */
|
|
146
|
-
function parseJsonLd(html: string): {
|
|
149
|
+
function parseJsonLd(html: string): {
|
|
150
|
+
title: string | null;
|
|
151
|
+
description: string | null;
|
|
152
|
+
image: string | null;
|
|
153
|
+
} {
|
|
147
154
|
for (const match of html.matchAll(JSON_LD_RE)) {
|
|
148
155
|
let data: unknown;
|
|
149
156
|
try {
|
|
@@ -155,7 +155,10 @@ async function buildPreview(pageUrl: string): Promise<LinkPreviewResult> {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/** Re-host a favicon: try the parsed `<link rel icon>`, then `<origin>/favicon.ico`. */
|
|
158
|
-
async function resolveFavicon(
|
|
158
|
+
async function resolveFavicon(
|
|
159
|
+
parsedIconUrl: string | null,
|
|
160
|
+
finalUrl: string,
|
|
161
|
+
): Promise<string | null> {
|
|
159
162
|
const candidates: string[] = [];
|
|
160
163
|
if (parsedIconUrl) candidates.push(parsedIconUrl);
|
|
161
164
|
try {
|