@syengup/friday-channel-next 0.1.25 → 0.1.26
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.
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `runtime.subagent.getSessionMessages`), which already resolves the active
|
|
10
10
|
* branch and compaction, then normalizes each raw message into a stable DTO.
|
|
11
11
|
*/
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { getFridayNextRuntime } from "../../runtime.js";
|
|
13
14
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
14
15
|
import { normalizeHistoryMessages } from "../../history/normalize-message.js";
|
|
@@ -17,6 +18,28 @@ import { resolveMediaAttachment } from "./files.js";
|
|
|
17
18
|
import { readSessionUsageSnapshotFromStore } from "../../session-usage-store.js";
|
|
18
19
|
const DEFAULT_LIMIT = 200;
|
|
19
20
|
const MAX_LIMIT = 1000;
|
|
21
|
+
/**
|
|
22
|
+
* For an `images[].url` produced from a `[media attached: …]` marker: returns the
|
|
23
|
+
* server-local filesystem path to resolve (`file://…` or a bare absolute path), or
|
|
24
|
+
* null to leave the image untouched. Already-served `/friday-next/files/…` URLs and
|
|
25
|
+
* remote `http(s)://` / `data:` URLs are NOT local paths — never feed them to
|
|
26
|
+
* `resolveMediaAttachment` (it would mis-treat them as paths and break valid URLs).
|
|
27
|
+
*/
|
|
28
|
+
function serverLocalPathForImageUrl(url) {
|
|
29
|
+
if (url.startsWith("file://")) {
|
|
30
|
+
try {
|
|
31
|
+
return fileURLToPath(url);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return url.slice("file://".length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (url.startsWith("/friday-next/files/"))
|
|
38
|
+
return null;
|
|
39
|
+
if (url.startsWith("/"))
|
|
40
|
+
return url;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
20
43
|
function resolveSubagentApi() {
|
|
21
44
|
try {
|
|
22
45
|
const runtime = getFridayNextRuntime();
|
|
@@ -75,16 +98,34 @@ export async function handleHistoryMessages(req, res) {
|
|
|
75
98
|
// (copies the file into the plugin's attachments/ dir — the same mechanism the
|
|
76
99
|
// live deliver path uses), then drop the raw paths from the wire.
|
|
77
100
|
for (const message of messages) {
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
if (message.mediaPaths?.length) {
|
|
102
|
+
const resolved = message.mediaPaths
|
|
103
|
+
.map((p) => resolveMediaAttachment(p))
|
|
104
|
+
.filter((r) => Boolean(r))
|
|
105
|
+
.map((r) => ({ url: r.url, filename: r.fileName }));
|
|
106
|
+
if (resolved.length) {
|
|
107
|
+
message.images = [...(message.images ?? []), ...resolved];
|
|
108
|
+
}
|
|
109
|
+
delete message.mediaPaths;
|
|
110
|
+
}
|
|
111
|
+
// User attachments arrive as `[media attached: file://<server-path>]` markers,
|
|
112
|
+
// which normalize-message extracts into images[].url as a RAW server-local path.
|
|
113
|
+
// Unlike MEDIA: paths (resolved above), these were never copied into the file
|
|
114
|
+
// store, so the app would try to load a path that only exists on the gateway host
|
|
115
|
+
// and the attachment bubble is lost on history sync. Resolve them the same way.
|
|
116
|
+
if (message.images?.length) {
|
|
117
|
+
message.images = message.images.map((img) => {
|
|
118
|
+
if (!img.url || img.data)
|
|
119
|
+
return img;
|
|
120
|
+
const local = serverLocalPathForImageUrl(img.url);
|
|
121
|
+
if (!local)
|
|
122
|
+
return img;
|
|
123
|
+
const resolved = resolveMediaAttachment(local);
|
|
124
|
+
if (!resolved)
|
|
125
|
+
return img;
|
|
126
|
+
return { ...img, url: resolved.url, filename: img.filename ?? resolved.fileName };
|
|
127
|
+
});
|
|
86
128
|
}
|
|
87
|
-
delete message.mediaPaths;
|
|
88
129
|
}
|
|
89
130
|
const sessionId = resolveSessionId(sessionKey);
|
|
90
131
|
// Cumulative session-usage snapshot (model + context window/used) read from the
|
package/package.json
CHANGED
|
@@ -170,6 +170,39 @@ describe("handleHistoryMessages", () => {
|
|
|
170
170
|
expect(body.messages[0].text).toBe("from app");
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
it("resolves user [media attached: file://] markers into downloadable /friday-next/files URLs", async () => {
|
|
174
|
+
// The server-local source the marker points at (only exists on the gateway host).
|
|
175
|
+
const srcDir = fs.mkdtempSync(path.join(os.tmpdir(), "friday-inbound-"));
|
|
176
|
+
const srcFile = path.join(srcDir, "ce1ff405-28ad-48b9-b4a7-4f2228d77649.jpg");
|
|
177
|
+
fs.writeFileSync(srcFile, Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]));
|
|
178
|
+
const file = writeTranscript("media.jsonl", [
|
|
179
|
+
{
|
|
180
|
+
type: "message",
|
|
181
|
+
id: "u1",
|
|
182
|
+
message: {
|
|
183
|
+
role: "user",
|
|
184
|
+
content: `你见过这个可乐吗?\n\n[media attached: file://${srcFile}]`,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
|
|
189
|
+
|
|
190
|
+
const res = new MockRes();
|
|
191
|
+
await handleHistoryMessages(
|
|
192
|
+
makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
|
|
193
|
+
res as any,
|
|
194
|
+
);
|
|
195
|
+
expect(res.statusCode).toBe(200);
|
|
196
|
+
const body = JSON.parse(res.body);
|
|
197
|
+
const userMsg = body.messages.find((m: any) => m.role === "user");
|
|
198
|
+
expect(userMsg.images?.length).toBe(1);
|
|
199
|
+
// The raw server-local file:// path must be resolved to a gateway-served URL the
|
|
200
|
+
// app can actually download — otherwise the attachment bubble is lost on history sync.
|
|
201
|
+
expect(userMsg.images[0].url.startsWith("file://")).toBe(false);
|
|
202
|
+
expect(userMsg.images[0].url.startsWith("/friday-next/files/")).toBe(true);
|
|
203
|
+
fs.rmSync(srcDir, { recursive: true, force: true });
|
|
204
|
+
});
|
|
205
|
+
|
|
173
206
|
it("falls back to getSessionMessages when the transcript is not on disk", async () => {
|
|
174
207
|
setForward({}); // no entry → disk read yields nothing
|
|
175
208
|
setRuntime(async () => ({
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { getFridayNextRuntime } from "../../runtime.js";
|
|
15
16
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
16
17
|
import { normalizeHistoryMessages } from "../../history/normalize-message.js";
|
|
@@ -28,6 +29,26 @@ type SubagentSessionApi = {
|
|
|
28
29
|
}) => Promise<{ messages?: unknown[] }>;
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* For an `images[].url` produced from a `[media attached: …]` marker: returns the
|
|
34
|
+
* server-local filesystem path to resolve (`file://…` or a bare absolute path), or
|
|
35
|
+
* null to leave the image untouched. Already-served `/friday-next/files/…` URLs and
|
|
36
|
+
* remote `http(s)://` / `data:` URLs are NOT local paths — never feed them to
|
|
37
|
+
* `resolveMediaAttachment` (it would mis-treat them as paths and break valid URLs).
|
|
38
|
+
*/
|
|
39
|
+
function serverLocalPathForImageUrl(url: string): string | null {
|
|
40
|
+
if (url.startsWith("file://")) {
|
|
41
|
+
try {
|
|
42
|
+
return fileURLToPath(url);
|
|
43
|
+
} catch {
|
|
44
|
+
return url.slice("file://".length);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (url.startsWith("/friday-next/files/")) return null;
|
|
48
|
+
if (url.startsWith("/")) return url;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
function resolveSubagentApi(): SubagentSessionApi | undefined {
|
|
32
53
|
try {
|
|
33
54
|
const runtime = getFridayNextRuntime();
|
|
@@ -95,15 +116,32 @@ export async function handleHistoryMessages(
|
|
|
95
116
|
// (copies the file into the plugin's attachments/ dir — the same mechanism the
|
|
96
117
|
// live deliver path uses), then drop the raw paths from the wire.
|
|
97
118
|
for (const message of messages) {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
if (message.mediaPaths?.length) {
|
|
120
|
+
const resolved = message.mediaPaths
|
|
121
|
+
.map((p) => resolveMediaAttachment(p))
|
|
122
|
+
.filter((r): r is NonNullable<typeof r> => Boolean(r))
|
|
123
|
+
.map((r) => ({ url: r.url, filename: r.fileName }));
|
|
124
|
+
if (resolved.length) {
|
|
125
|
+
message.images = [...(message.images ?? []), ...resolved];
|
|
126
|
+
}
|
|
127
|
+
delete message.mediaPaths;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// User attachments arrive as `[media attached: file://<server-path>]` markers,
|
|
131
|
+
// which normalize-message extracts into images[].url as a RAW server-local path.
|
|
132
|
+
// Unlike MEDIA: paths (resolved above), these were never copied into the file
|
|
133
|
+
// store, so the app would try to load a path that only exists on the gateway host
|
|
134
|
+
// and the attachment bubble is lost on history sync. Resolve them the same way.
|
|
135
|
+
if (message.images?.length) {
|
|
136
|
+
message.images = message.images.map((img) => {
|
|
137
|
+
if (!img.url || img.data) return img;
|
|
138
|
+
const local = serverLocalPathForImageUrl(img.url);
|
|
139
|
+
if (!local) return img;
|
|
140
|
+
const resolved = resolveMediaAttachment(local);
|
|
141
|
+
if (!resolved) return img;
|
|
142
|
+
return { ...img, url: resolved.url, filename: img.filename ?? resolved.fileName };
|
|
143
|
+
});
|
|
105
144
|
}
|
|
106
|
-
delete message.mediaPaths;
|
|
107
145
|
}
|
|
108
146
|
|
|
109
147
|
const sessionId = resolveSessionId(sessionKey);
|