-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
360 lines (301 loc) · 9.99 KB
/
server.js
File metadata and controls
360 lines (301 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
const http = require("node:http");
const fs = require("node:fs/promises");
const fsSync = require("node:fs");
const path = require("node:path");
const BASE_PORT = Number(process.env.PORT || 3000);
const HOST = process.env.HOST || (process.env.NODE_ENV === "production" ? "0.0.0.0" : "127.0.0.1");
const DEFAULT_MODEL = "@cf/meta/llama-4-scout-17b-16e-instruct";
const INDEX_PATH = path.join(__dirname, "index.html");
const ENV_PATH = path.join(__dirname, ".env");
const SYSTEM_PROMPT = `You are LENS, an AI tutor. Your only job is to help the user learn and understand
whatever is on their screen right now.
When you see code: walk through what it does, line by line if needed. Explain the
logic like you are a patient senior developer talking to a junior.
When you see an error message: explain exactly why it happened, what caused it,
and what the user should do to fix it step by step.
When you see a document, article, or webpage: pull out the key ideas and explain
them clearly. Connect concepts. Ask a question at the end to check understanding.
When you see a design or UI: give honest, constructive feedback. Explain what
works and what could be clearer from a user's perspective.
Always explain your reasoning, not just the answer. Your goal is that after
talking to you, the user actually understands, not just has a solution.
Be warm, direct, and clear. Talk like a knowledgeable friend, not a textbook.
Never use markdown formatting. No hashtags, asterisks, dashes, or code fences.
Write in flowing plain prose the way a teacher speaks out loud.
Keep responses focused and appropriately concise. Do not ramble.
When describing screenshots, only use visible evidence. If anything is unclear,
say so explicitly instead of guessing.`;
loadDotEnv();
function json(response, statusCode, payload) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
});
response.end(JSON.stringify(payload));
}
function text(response, statusCode, payload, contentType = "text/plain; charset=utf-8") {
response.writeHead(statusCode, {
"Content-Type": contentType,
"Cache-Control": "no-store",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
});
response.end(payload);
}
function loadDotEnv() {
if (!fsSync.existsSync(ENV_PATH)) {
return;
}
const raw = fsSync.readFileSync(ENV_PATH, "utf8");
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const separatorIndex = trimmed.indexOf("=");
if (separatorIndex === -1) {
continue;
}
const key = trimmed.slice(0, separatorIndex).trim();
let value = trimmed.slice(separatorIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (key && process.env[key] === undefined) {
process.env[key] = value;
}
}
}
async function readJson(request) {
let raw = "";
for await (const chunk of request) {
raw += chunk;
if (raw.length > 20 * 1024 * 1024) {
throw new Error("Request body is too large.");
}
}
if (!raw) {
return {};
}
try {
return JSON.parse(raw);
} catch {
throw new Error("Request body must be valid JSON.");
}
}
function cloudflareUrl(accountId) {
const model = String(process.env.CLOUDFLARE_MODEL || DEFAULT_MODEL).trim() || DEFAULT_MODEL;
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`;
}
function resolveCredentials(body) {
const accountId = String(body.accountId || process.env.CLOUDFLARE_ACCOUNT_ID || "").trim();
const apiToken = String(body.apiToken || process.env.CLOUDFLARE_API_TOKEN || "").trim();
if (!accountId || !apiToken) {
throw new Error("Missing Cloudflare Account ID or API Token.");
}
return { accountId, apiToken };
}
async function callCloudflare(credentials, payload) {
let response;
let raw = "";
let data = null;
try {
response = await fetch(cloudflareUrl(credentials.accountId), {
method: "POST",
headers: {
Authorization: `Bearer ${credentials.apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
} catch {
throw new Error("Could not reach Cloudflare Workers AI.");
}
try {
raw = await response.text();
} catch {
throw new Error(`Cloudflare returned status ${response.status} and the response body could not be read.`);
}
if (raw) {
try {
data = JSON.parse(raw);
} catch {
data = null;
}
}
const fallbackText = raw
? raw
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 220)
: "";
if (!response.ok || data?.success === false) {
const apiError =
(Array.isArray(data?.errors) && data.errors.map((entry) => entry?.message).filter(Boolean).join(" ")) ||
data?.message ||
fallbackText ||
`Cloudflare returned status ${response.status}.`;
throw new Error(apiError);
}
if (!data) {
throw new Error(
fallbackText
? `Cloudflare returned status ${response.status} with a non-JSON response: ${fallbackText}`
: `Cloudflare returned status ${response.status} with an unreadable response.`
);
}
return data;
}
function shouldRetryWithLicense(error) {
const message = error instanceof Error ? error.message : "";
return /license|acceptable use|agree/i.test(message);
}
async function acceptMetaLicense(credentials) {
try {
await callCloudflare(credentials, { prompt: "agree" });
} catch (error) {
const message = error instanceof Error ? error.message : "";
if (/already accepted|previously accepted|already agreed|thank you for agreeing/i.test(message)) {
return;
}
throw error;
}
}
function extractReply(data) {
const result = data?.result;
const reply =
(result && typeof result === "object" && (result.response || result.output_text || result.text)) ||
(typeof result === "string" ? result : "") ||
"";
return String(reply || "").trim();
}
async function proxyLensTurn(body) {
const credentials = resolveCredentials(body);
const userMessage = String(body.userMessage || "").trim();
const history = Array.isArray(body.history) ? body.history : [];
const frameDataUrl = String(body.frameDataUrl || "").trim();
if (!userMessage) {
throw new Error("Missing user message.");
}
const messages = [{ role: "system", content: SYSTEM_PROMPT }, ...history];
if (frameDataUrl) {
messages.push({
role: "user",
content: [
{
type: "image_url",
image_url: { url: frameDataUrl },
},
{
type: "text",
text: userMessage,
},
],
});
} else {
messages.push({
role: "user",
content: userMessage,
});
}
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
const data = await callCloudflare(credentials, {
messages,
max_tokens: 1500,
temperature: 0.15,
});
const reply = extractReply(data);
if (!reply) {
throw new Error("Cloudflare returned no tutor response.");
}
return reply;
} catch (error) {
if (attempt === 0 && shouldRetryWithLicense(error)) {
await acceptMetaLicense(credentials);
continue;
}
throw error;
}
}
throw new Error("Cloudflare returned no tutor response.");
}
const server = http.createServer(async (request, response) => {
const url = new URL(request.url || "/", `http://${request.headers.host || `${HOST}:${BASE_PORT}`}`);
if (request.method === "OPTIONS") {
response.writeHead(204, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Cache-Control": "no-store",
});
response.end();
return;
}
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
try {
const html = await fs.readFile(INDEX_PATH, "utf8");
text(response, 200, html, "text/html; charset=utf-8");
} catch {
text(response, 500, "Could not read index.html");
}
return;
}
if (request.method === "GET" && url.pathname === "/health") {
json(response, 200, { ok: true });
return;
}
if (request.method === "GET" && url.pathname === "/api/config") {
json(response, 200, {
hasServerCredentials: Boolean(
process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_API_TOKEN
),
});
return;
}
if (request.method === "GET" && url.pathname === "/favicon.ico") {
response.writeHead(204);
response.end();
return;
}
if (request.method === "POST" && url.pathname === "/api/lens") {
try {
const body = await readJson(request);
const reply = await proxyLensTurn(body);
json(response, 200, { reply });
} catch (error) {
const message = error instanceof Error ? error.message : "Lens proxy request failed.";
json(response, 500, { error: message });
}
return;
}
json(response, 404, { error: "Not found." });
});
function startServer(port, retriesLeft = 10) {
const onError = (error) => {
server.off("listening", onListening);
if (error && error.code === "EADDRINUSE" && retriesLeft > 0) {
const nextPort = port + 1;
console.warn(`Port ${port} is busy, trying ${nextPort}...`);
startServer(nextPort, retriesLeft - 1);
return;
}
throw error;
};
const onListening = () => {
server.off("error", onError);
console.log(`LENS running at http://${HOST}:${port}`);
};
server.once("error", onError);
server.once("listening", onListening);
server.listen(port, HOST);
}
startServer(BASE_PORT);