weifuwu 0.4.0 → 0.5.1
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 +382 -239
- package/dist/ai.d.ts +7 -0
- package/dist/graphql.d.ts +12 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +1950 -1721
- package/dist/postgres/client.d.ts +2 -0
- package/dist/postgres/index.d.ts +2 -0
- package/dist/postgres/migrate.d.ts +3 -0
- package/dist/postgres/table.d.ts +4 -0
- package/dist/postgres/types.d.ts +51 -0
- package/dist/router.d.ts +0 -22
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/route.d.ts +11 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -107,1896 +107,2122 @@ function serve(handler, options) {
|
|
|
107
107
|
|
|
108
108
|
// router.ts
|
|
109
109
|
import { WebSocketServer } from "ws";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let current = obj;
|
|
127
|
-
for (const key of path) {
|
|
128
|
-
if (current === null || current === void 0) return void 0;
|
|
129
|
-
if (typeof current === "object" && key in current) {
|
|
130
|
-
current = current[key];
|
|
131
|
-
} else {
|
|
132
|
-
return void 0;
|
|
110
|
+
var createTrieNode = () => ({
|
|
111
|
+
children: /* @__PURE__ */ new Map(),
|
|
112
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
113
|
+
middlewares: /* @__PURE__ */ new Map(),
|
|
114
|
+
pathMws: []
|
|
115
|
+
});
|
|
116
|
+
var createWsNode = () => ({
|
|
117
|
+
children: /* @__PURE__ */ new Map(),
|
|
118
|
+
middlewares: []
|
|
119
|
+
});
|
|
120
|
+
var getTrieNode = (node, segment) => {
|
|
121
|
+
if (segment.startsWith(":")) {
|
|
122
|
+
if (!node.children.has(":")) {
|
|
123
|
+
const child2 = createTrieNode();
|
|
124
|
+
child2.param = segment.slice(1);
|
|
125
|
+
node.children.set(":", child2);
|
|
133
126
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const afterNodes = path.slice(7);
|
|
140
|
-
const dotIdx = afterNodes.indexOf(".");
|
|
141
|
-
if (dotIdx === -1) {
|
|
142
|
-
return ctx.nodeOutputs.get(afterNodes);
|
|
127
|
+
const child = node.children.get(":");
|
|
128
|
+
if (child.param !== segment.slice(1)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Param name conflict: ":${child.param}" already registered at this path position, cannot register ":"${segment.slice(1)}"`
|
|
131
|
+
);
|
|
143
132
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
133
|
+
return child;
|
|
134
|
+
}
|
|
135
|
+
if (!node.children.has(segment)) {
|
|
136
|
+
node.children.set(segment, createTrieNode());
|
|
137
|
+
}
|
|
138
|
+
return node.children.get(segment);
|
|
139
|
+
};
|
|
140
|
+
var matchTrieNode = (node, segment, params) => {
|
|
141
|
+
if (node.children.has(segment)) return node.children.get(segment);
|
|
142
|
+
if (node.children.has(":")) {
|
|
143
|
+
const child = node.children.get(":");
|
|
144
|
+
if (child.param) params[child.param] = segment;
|
|
145
|
+
return child;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
var getWsNode = (node, segment) => {
|
|
150
|
+
if (segment.startsWith(":")) {
|
|
151
|
+
if (!node.children.has(":")) {
|
|
152
|
+
const child2 = createWsNode();
|
|
153
|
+
child2.param = segment.slice(1);
|
|
154
|
+
node.children.set(":", child2);
|
|
149
155
|
}
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
const child = node.children.get(":");
|
|
157
|
+
if (child.param !== segment.slice(1)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Param name conflict: ":${child.param}" already registered at this path position`
|
|
160
|
+
);
|
|
152
161
|
}
|
|
153
|
-
return
|
|
162
|
+
return child;
|
|
154
163
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
if (!node.children.has(segment)) {
|
|
165
|
+
node.children.set(segment, createWsNode());
|
|
166
|
+
}
|
|
167
|
+
return node.children.get(segment);
|
|
168
|
+
};
|
|
169
|
+
var matchWsNode = (node, segment, params) => {
|
|
170
|
+
if (node.children.has(segment)) return node.children.get(segment);
|
|
171
|
+
if (node.children.has(":")) {
|
|
172
|
+
const child = node.children.get(":");
|
|
173
|
+
if (child.param) params[child.param] = segment;
|
|
174
|
+
return child;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
var Router = class _Router {
|
|
179
|
+
root = createTrieNode();
|
|
180
|
+
wsRoot = createWsNode();
|
|
181
|
+
globalMws = [];
|
|
182
|
+
errorHandler;
|
|
183
|
+
use(arg1, arg2) {
|
|
184
|
+
if (typeof arg1 === "string") {
|
|
185
|
+
if (arg2 instanceof _Router) {
|
|
186
|
+
let node = this.root;
|
|
187
|
+
for (const segment of this.splitPath(arg1)) {
|
|
188
|
+
node = getTrieNode(node, segment);
|
|
189
|
+
}
|
|
190
|
+
node.subRouter = arg2;
|
|
191
|
+
} else if (typeof arg2 === "function") {
|
|
192
|
+
let node = this.root;
|
|
193
|
+
for (const segment of this.splitPath(arg1)) {
|
|
194
|
+
node = getTrieNode(node, segment);
|
|
195
|
+
}
|
|
196
|
+
node.pathMws.push(arg2);
|
|
197
|
+
}
|
|
198
|
+
} else if (typeof arg1 === "function") {
|
|
199
|
+
this.globalMws.push(arg1);
|
|
159
200
|
}
|
|
160
|
-
return
|
|
201
|
+
return this;
|
|
161
202
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return ctx.input[key];
|
|
203
|
+
get(path, ...args) {
|
|
204
|
+
return this.route("GET", path, ...args);
|
|
165
205
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (path === "null") return null;
|
|
169
|
-
const num = Number(path);
|
|
170
|
-
if (!isNaN(num) && path.trim() !== "") return num;
|
|
171
|
-
return path;
|
|
172
|
-
}
|
|
173
|
-
function resolveValue(v, ctx) {
|
|
174
|
-
if (typeof v === "string" && v.startsWith("$")) {
|
|
175
|
-
return resolveRef(v, ctx);
|
|
206
|
+
post(path, ...args) {
|
|
207
|
+
return this.route("POST", path, ...args);
|
|
176
208
|
}
|
|
177
|
-
|
|
178
|
-
return
|
|
209
|
+
put(path, ...args) {
|
|
210
|
+
return this.route("PUT", path, ...args);
|
|
179
211
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
for (const [k, val] of Object.entries(v)) {
|
|
183
|
-
result[k] = resolveValue(val, ctx);
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
212
|
+
delete(path, ...args) {
|
|
213
|
+
return this.route("DELETE", path, ...args);
|
|
186
214
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// workflow/nodes.ts
|
|
191
|
-
function evaluateExpression(expr, ctx) {
|
|
192
|
-
const operators = [
|
|
193
|
-
{ op: "===", fn: (a, b) => a === b },
|
|
194
|
-
{ op: "!==", fn: (a, b) => a !== b },
|
|
195
|
-
{ op: ">=", fn: (a, b) => Number(a) >= Number(b) },
|
|
196
|
-
{ op: "<=", fn: (a, b) => Number(a) <= Number(b) },
|
|
197
|
-
{ op: ">", fn: (a, b) => Number(a) > Number(b) },
|
|
198
|
-
{ op: "<", fn: (a, b) => Number(a) < Number(b) },
|
|
199
|
-
{ op: "==", fn: (a, b) => a == b },
|
|
200
|
-
{ op: "!=", fn: (a, b) => a != b },
|
|
201
|
-
{ op: "+", fn: (a, b) => Number(a) + Number(b) },
|
|
202
|
-
{ op: "-", fn: (a, b) => Number(a) - Number(b) },
|
|
203
|
-
{ op: "*", fn: (a, b) => Number(a) * Number(b) },
|
|
204
|
-
{ op: "/", fn: (a, b) => Number(a) / Number(b) },
|
|
205
|
-
{ op: "%", fn: (a, b) => Number(a) % Number(b) },
|
|
206
|
-
{ op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
|
|
207
|
-
{ op: "||", fn: (a, b) => Boolean(a) || Boolean(b) }
|
|
208
|
-
];
|
|
209
|
-
for (const { op, fn } of operators) {
|
|
210
|
-
const idx = expr.indexOf(op);
|
|
211
|
-
if (idx > 0) {
|
|
212
|
-
const leftRaw = expr.slice(0, idx).trim();
|
|
213
|
-
const rightRaw = expr.slice(idx + op.length).trim();
|
|
214
|
-
const left = resolveValue(leftRaw, ctx);
|
|
215
|
-
const right = resolveValue(rightRaw, ctx);
|
|
216
|
-
return fn(left, right);
|
|
217
|
-
}
|
|
215
|
+
patch(path, ...args) {
|
|
216
|
+
return this.route("PATCH", path, ...args);
|
|
218
217
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (trimmed === "false") return false;
|
|
222
|
-
if (trimmed === "null") return null;
|
|
223
|
-
const num = Number(trimmed);
|
|
224
|
-
if (!isNaN(num) && trimmed !== "") return num;
|
|
225
|
-
return resolveValue(expr, ctx);
|
|
226
|
-
}
|
|
227
|
-
async function executeEval(node, ctx) {
|
|
228
|
-
const expression = node.input.expression;
|
|
229
|
-
if (!expression) throw new Error('eval node requires "expression" field');
|
|
230
|
-
const result = evaluateExpression(expression, ctx);
|
|
231
|
-
return { result };
|
|
232
|
-
}
|
|
233
|
-
async function executeSet(node, ctx) {
|
|
234
|
-
const name = node.input.name;
|
|
235
|
-
const value = node.input.value;
|
|
236
|
-
if (!name) throw new Error('set node requires "name" field');
|
|
237
|
-
let resolved;
|
|
238
|
-
if (typeof value === "string") {
|
|
239
|
-
resolved = evaluateExpression(value, ctx);
|
|
240
|
-
} else {
|
|
241
|
-
resolved = resolveValue(value ?? null, ctx);
|
|
218
|
+
head(path, ...args) {
|
|
219
|
+
return this.route("HEAD", path, ...args);
|
|
242
220
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
async function executeGet(node, ctx) {
|
|
247
|
-
const name = node.input.name;
|
|
248
|
-
if (!name) throw new Error('get node requires "name" field');
|
|
249
|
-
if (!ctx.variables.has(name)) {
|
|
250
|
-
throw new Error(`Variable "${name}" is not defined`);
|
|
221
|
+
options(path, ...args) {
|
|
222
|
+
return this.route("OPTIONS", path, ...args);
|
|
251
223
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
async function executeIf(node, ctx) {
|
|
255
|
-
const conditions = node.conditions ?? [];
|
|
256
|
-
for (const condition of conditions) {
|
|
257
|
-
const test = typeof condition.test === "string" ? Boolean(resolveValue(condition.test, ctx)) : condition.test;
|
|
258
|
-
if (test && condition.body) {
|
|
259
|
-
let lastOutput = void 0;
|
|
260
|
-
for (const bodyNode of condition.body) {
|
|
261
|
-
lastOutput = await executeNode(bodyNode, ctx);
|
|
262
|
-
}
|
|
263
|
-
return lastOutput;
|
|
264
|
-
}
|
|
224
|
+
all(path, ...args) {
|
|
225
|
+
return this.route("*", path, ...args);
|
|
265
226
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
lastOutput = await executeNode(bodyNode, ctx);
|
|
227
|
+
onError(handler) {
|
|
228
|
+
this.errorHandler = handler;
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
route(method, path, ...args) {
|
|
232
|
+
const handler = args.pop();
|
|
233
|
+
const middlewares = args;
|
|
234
|
+
const segments = this.splitPath(path);
|
|
235
|
+
let node = this.root;
|
|
236
|
+
for (const segment of segments) {
|
|
237
|
+
if (segment === "*") {
|
|
238
|
+
node.wildcard = true;
|
|
239
|
+
node.handlers.set(method, handler);
|
|
240
|
+
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
node = getTrieNode(node, segment);
|
|
284
244
|
}
|
|
245
|
+
node.handlers.set(method, handler);
|
|
246
|
+
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
247
|
+
return this;
|
|
285
248
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
249
|
+
ws(path, ...args) {
|
|
250
|
+
const handler = args.pop();
|
|
251
|
+
const middlewares = args;
|
|
252
|
+
const segments = this.splitPath(path);
|
|
253
|
+
let node = this.wsRoot;
|
|
254
|
+
for (const segment of segments) {
|
|
255
|
+
node = getWsNode(node, segment);
|
|
256
|
+
}
|
|
257
|
+
node.handler = handler;
|
|
258
|
+
if (middlewares.length > 0) node.middlewares = middlewares;
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
handler() {
|
|
262
|
+
return (req, ctx) => {
|
|
263
|
+
const url = new URL(req.url);
|
|
264
|
+
return this.handle(req, ctx, this.splitPath(url.pathname), Object.fromEntries(url.searchParams));
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
websocketHandler() {
|
|
268
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
269
|
+
const wsRoot = this.wsRoot;
|
|
270
|
+
const router = this;
|
|
271
|
+
return (req, socket, head) => {
|
|
272
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
273
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
274
|
+
const query = Object.fromEntries(url.searchParams);
|
|
275
|
+
const match = router.matchWsTrie(wsRoot, segments);
|
|
276
|
+
if (!match) {
|
|
277
|
+
socket.destroy();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const webReq = new Request(url.href, {
|
|
281
|
+
method: req.method ?? "GET",
|
|
282
|
+
headers: Object.fromEntries(
|
|
283
|
+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
|
|
284
|
+
)
|
|
285
|
+
});
|
|
286
|
+
const ctx = { params: match.params, query };
|
|
287
|
+
if (match.middlewares.length === 0) {
|
|
288
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
let index = 0;
|
|
292
|
+
const dispatch = async (innerReq, ctx2) => {
|
|
293
|
+
if (index < match.middlewares.length) {
|
|
294
|
+
const mw = match.middlewares[index++];
|
|
295
|
+
return mw(innerReq, ctx2, dispatch);
|
|
296
|
+
}
|
|
297
|
+
return await new Promise((resolve3) => {
|
|
298
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
299
|
+
resolve3(new Response(null, { status: 101 }));
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
303
|
+
if (result.status !== 101) {
|
|
304
|
+
sendHttpResponseOnSocket(socket, result);
|
|
305
|
+
}
|
|
306
|
+
}).catch(() => {
|
|
307
|
+
socket.destroy();
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
splitPath(path) {
|
|
312
|
+
return path.split("/").filter(Boolean);
|
|
313
|
+
}
|
|
314
|
+
matchTrie(method, segments) {
|
|
315
|
+
let node = this.root;
|
|
316
|
+
const params = {};
|
|
317
|
+
const pathMws = [...this.root.pathMws];
|
|
318
|
+
let wildcardHandler = null;
|
|
319
|
+
let wildcardMws = [];
|
|
320
|
+
let wildcardIdx = -1;
|
|
321
|
+
for (let i = 0; i < segments.length; i++) {
|
|
322
|
+
pathMws.push(...node.pathMws);
|
|
323
|
+
if (node.wildcard) {
|
|
324
|
+
const h = node.handlers.get("*") || node.handlers.get(method);
|
|
325
|
+
if (h) {
|
|
326
|
+
wildcardHandler = h;
|
|
327
|
+
wildcardMws = node.middlewares.get(method) || node.middlewares.get("*") || [];
|
|
328
|
+
wildcardIdx = i;
|
|
301
329
|
}
|
|
302
330
|
}
|
|
303
|
-
|
|
331
|
+
const segment = segments[i];
|
|
332
|
+
if (!segment) break;
|
|
333
|
+
const next = matchTrieNode(node, segment, params);
|
|
334
|
+
if (!next) {
|
|
335
|
+
if (node.subRouter) {
|
|
336
|
+
return {
|
|
337
|
+
pathMws,
|
|
338
|
+
params,
|
|
339
|
+
middlewares: [],
|
|
340
|
+
subRouter: { router: node.subRouter, remainingIdx: i }
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (wildcardHandler) {
|
|
344
|
+
params["*"] = segments.slice(wildcardIdx).join("/");
|
|
345
|
+
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
node = next;
|
|
350
|
+
}
|
|
351
|
+
if (node.subRouter) {
|
|
352
|
+
return {
|
|
353
|
+
pathMws,
|
|
354
|
+
params,
|
|
355
|
+
middlewares: [],
|
|
356
|
+
subRouter: { router: node.subRouter, remainingIdx: segments.length }
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
pathMws.push(...node.pathMws);
|
|
360
|
+
const handler = node.handlers.get(method) || node.handlers.get("*");
|
|
361
|
+
if (handler) {
|
|
362
|
+
if (node.wildcard) params["*"] = segments.slice(segments.length).join("/");
|
|
363
|
+
return {
|
|
364
|
+
handler,
|
|
365
|
+
middlewares: node.middlewares.get(method) || node.middlewares.get("*") || [],
|
|
366
|
+
pathMws,
|
|
367
|
+
params
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (wildcardHandler) {
|
|
371
|
+
params["*"] = segments.slice(wildcardIdx).join("/");
|
|
372
|
+
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
304
375
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for (const bodyNode of fn.workflow.nodes) {
|
|
313
|
-
lastOutput = await executeNode(bodyNode, ctx);
|
|
376
|
+
matchWsTrie(root, segments) {
|
|
377
|
+
let node = root;
|
|
378
|
+
const params = {};
|
|
379
|
+
for (const segment of segments) {
|
|
380
|
+
const next = matchWsNode(node, segment, params);
|
|
381
|
+
if (!next) return null;
|
|
382
|
+
node = next;
|
|
314
383
|
}
|
|
315
|
-
|
|
316
|
-
return lastOutput;
|
|
384
|
+
return node.handler ? { handler: node.handler, middlewares: node.middlewares, params } : null;
|
|
317
385
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
headers: input.headers ?? {},
|
|
331
|
-
signal: controller.signal
|
|
332
|
-
};
|
|
333
|
-
if (input.body && fetchInit.method !== "GET") {
|
|
334
|
-
fetchInit.body = JSON.stringify(input.body);
|
|
386
|
+
async handle(req, ctx, segments, query) {
|
|
387
|
+
const match = this.matchTrie(req.method, segments);
|
|
388
|
+
if (match?.subRouter) {
|
|
389
|
+
const { router: sub, remainingIdx } = match.subRouter;
|
|
390
|
+
const remainingSegments = segments.slice(remainingIdx);
|
|
391
|
+
const delegate = (req2, ctx2) => sub.handle(req2, ctx2, remainingSegments, query);
|
|
392
|
+
const allMws = this.globalMws.length + match.pathMws.length === 0 ? [] : [...this.globalMws, ...match.pathMws];
|
|
393
|
+
try {
|
|
394
|
+
return await this.runChain(allMws, delegate, req, { ...ctx, params: { ...ctx.params, ...match.params } });
|
|
395
|
+
} catch (e) {
|
|
396
|
+
return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
|
|
397
|
+
}
|
|
335
398
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
399
|
+
if (match?.handler) {
|
|
400
|
+
const { handler, middlewares: routeMws, pathMws, params } = match;
|
|
401
|
+
const allMws = this.globalMws.length + pathMws.length + routeMws.length === 0 ? [] : [...this.globalMws, ...pathMws, ...routeMws];
|
|
402
|
+
const ctxWithMatch = { ...ctx, params: { ...ctx.params, ...params } };
|
|
403
|
+
try {
|
|
404
|
+
return await this.runChain(allMws, handler, req, ctxWithMatch);
|
|
405
|
+
} catch (e) {
|
|
406
|
+
return this.errorHandler ? this.errorHandler(e, req, ctxWithMatch) : new Response("Internal Server Error", { status: 500 });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (this.globalMws.length > 0) {
|
|
410
|
+
try {
|
|
411
|
+
const delegate = () => new Response("Not Found", { status: 404 });
|
|
412
|
+
return await this.runChain(this.globalMws, delegate, req, ctx);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return new Response("Not Found", { status: 404 });
|
|
418
|
+
}
|
|
419
|
+
async runChain(middlewares, finalHandler, req, ctx) {
|
|
420
|
+
let index = 0;
|
|
421
|
+
const dispatch = async (req2, ctx2) => {
|
|
422
|
+
if (index < middlewares.length) {
|
|
423
|
+
const mw = middlewares[index++];
|
|
424
|
+
return mw ? await mw(req2, ctx2, dispatch) : new Response("Middleware error", { status: 500 });
|
|
425
|
+
}
|
|
426
|
+
return await finalHandler(req2, ctx2);
|
|
344
427
|
};
|
|
345
|
-
|
|
346
|
-
clearTimeout(timer);
|
|
428
|
+
return dispatch(req, ctx);
|
|
347
429
|
}
|
|
348
|
-
}
|
|
349
|
-
var executors = {
|
|
350
|
-
eval: executeEval,
|
|
351
|
-
set: executeSet,
|
|
352
|
-
get: executeGet,
|
|
353
|
-
if: executeIf,
|
|
354
|
-
while: executeWhile,
|
|
355
|
-
call: executeCall,
|
|
356
|
-
http: executeHttp
|
|
357
430
|
};
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
431
|
+
function upgradeSocket(wss, req, socket, head, handler, ctx) {
|
|
432
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
433
|
+
if (handler.open) {
|
|
434
|
+
handler.open(ws, ctx);
|
|
435
|
+
}
|
|
436
|
+
ws.on("message", (data) => {
|
|
437
|
+
handler.message?.(ws, ctx, data);
|
|
438
|
+
});
|
|
439
|
+
ws.on("close", () => {
|
|
440
|
+
handler.close?.(ws, ctx);
|
|
441
|
+
});
|
|
442
|
+
ws.on("error", (err) => {
|
|
443
|
+
handler.error?.(ws, ctx, err);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
364
446
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
447
|
+
function sendHttpResponseOnSocket(socket, response) {
|
|
448
|
+
const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`;
|
|
449
|
+
const headerLines = [statusLine];
|
|
450
|
+
response.headers.forEach((value, key) => {
|
|
451
|
+
headerLines.push(`${key}: ${value}`);
|
|
452
|
+
});
|
|
453
|
+
headerLines.push("Connection: close");
|
|
454
|
+
headerLines.push("");
|
|
455
|
+
const headerStr = headerLines.join("\r\n");
|
|
456
|
+
response.arrayBuffer().then((buf) => {
|
|
457
|
+
const body = Buffer.from(buf);
|
|
458
|
+
socket.write(headerStr + "\r\n" + body.toString());
|
|
459
|
+
socket.end();
|
|
460
|
+
}).catch(() => {
|
|
461
|
+
socket.write(headerStr + "\r\n");
|
|
462
|
+
socket.end();
|
|
463
|
+
});
|
|
374
464
|
}
|
|
375
|
-
var SYSTEM_PROMPT_TEMPLATE = `You are a workflow generator. Given a user goal and available tools, output a workflow JSON.
|
|
376
|
-
|
|
377
|
-
Available tools:
|
|
378
|
-
{{TOOLS}}
|
|
379
465
|
|
|
380
|
-
|
|
381
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
"tool": "call",
|
|
392
|
-
"input": { "tool": "toolName", "args": { "param1": "$var.varName" } }
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
"id": "step3",
|
|
396
|
-
"tool": "if",
|
|
397
|
-
"input": {},
|
|
398
|
-
"conditions": [
|
|
399
|
-
{ "test": "$nodes.step2.output.someField", "body": [
|
|
400
|
-
{ "id": "step4", "tool": "call", "input": { "tool": "toolName", "args": {} } }
|
|
401
|
-
]}
|
|
402
|
-
]
|
|
403
|
-
}
|
|
404
|
-
]
|
|
466
|
+
// tsx.ts
|
|
467
|
+
import { createElement, createContext, useContext } from "react";
|
|
468
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
469
|
+
import * as esbuild from "esbuild";
|
|
470
|
+
import { readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
|
|
471
|
+
import { join, relative, resolve, sep, dirname } from "node:path";
|
|
472
|
+
import { pathToFileURL } from "node:url";
|
|
473
|
+
import { createHash } from "node:crypto";
|
|
474
|
+
var TsxContext = createContext({ params: {}, query: {} });
|
|
475
|
+
function useTsx() {
|
|
476
|
+
return useContext(TsxContext);
|
|
405
477
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
Reference syntax:
|
|
417
|
-
- $var.name - read a variable
|
|
418
|
-
- $nodes.id.output - output of a previous node
|
|
419
|
-
- $nodes.id.output.field - specific field of a node's output
|
|
420
|
-
- $input.field - workflow input parameter
|
|
421
|
-
|
|
422
|
-
Output ONLY valid JSON. No explanation, no markdown.`;
|
|
423
|
-
async function generateWorkflow(goal, tools, generateFn) {
|
|
424
|
-
const toolsDesc = buildToolsDescription(tools);
|
|
425
|
-
const system = SYSTEM_PROMPT_TEMPLATE.replace("{{TOOLS}}", toolsDesc);
|
|
426
|
-
const result = await generateFn({
|
|
427
|
-
system,
|
|
428
|
-
messages: [{ role: "user", content: goal }]
|
|
429
|
-
});
|
|
430
|
-
const text = result.text.trim();
|
|
431
|
-
const jsonStart = text.indexOf("{");
|
|
432
|
-
const jsonEnd = text.lastIndexOf("}");
|
|
433
|
-
if (jsonStart === -1 || jsonEnd === -1) {
|
|
434
|
-
throw new Error(`LLM output is not valid JSON: ${text.slice(0, 200)}`);
|
|
478
|
+
function id(s) {
|
|
479
|
+
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
480
|
+
}
|
|
481
|
+
function concatUint8(chunks) {
|
|
482
|
+
const len = chunks.reduce((a, c) => a + c.length, 0);
|
|
483
|
+
const out = new Uint8Array(len);
|
|
484
|
+
let off = 0;
|
|
485
|
+
for (const c of chunks) {
|
|
486
|
+
out.set(c, off);
|
|
487
|
+
off += c.length;
|
|
435
488
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
async function readStream(stream) {
|
|
492
|
+
const chunks = [];
|
|
493
|
+
const reader = stream.getReader();
|
|
494
|
+
while (true) {
|
|
495
|
+
const { done, value } = await reader.read();
|
|
496
|
+
if (done) break;
|
|
497
|
+
chunks.push(value);
|
|
498
|
+
}
|
|
499
|
+
return new TextDecoder().decode(concatUint8(chunks));
|
|
500
|
+
}
|
|
501
|
+
function scanPages(dir) {
|
|
502
|
+
const pages = [];
|
|
503
|
+
function walk(current) {
|
|
504
|
+
let entries;
|
|
505
|
+
try {
|
|
506
|
+
entries = readdirSync(current);
|
|
507
|
+
} catch {
|
|
508
|
+
return;
|
|
441
509
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
510
|
+
const dirs = [];
|
|
511
|
+
for (const name of entries) {
|
|
512
|
+
const full = join(current, name);
|
|
513
|
+
const st = statSync(full);
|
|
514
|
+
if (st.isDirectory()) {
|
|
515
|
+
if (!name.startsWith(".")) dirs.push(full);
|
|
516
|
+
}
|
|
446
517
|
}
|
|
447
|
-
|
|
518
|
+
const pagePath = join(current, "page.tsx");
|
|
519
|
+
const tsPagePath = join(current, "page.ts");
|
|
520
|
+
let entryPath = "";
|
|
521
|
+
if (existsSync(pagePath)) {
|
|
522
|
+
entryPath = pagePath;
|
|
523
|
+
} else if (existsSync(tsPagePath)) {
|
|
524
|
+
entryPath = tsPagePath;
|
|
525
|
+
}
|
|
526
|
+
if (entryPath) {
|
|
527
|
+
let relPath = relative(dir, entryPath).replace(sep, "/");
|
|
528
|
+
relPath = relPath.replace(/\/page\.tsx?$/, "");
|
|
529
|
+
relPath = relPath.replace(/^page\.tsx?$/, "");
|
|
530
|
+
const route = filePathToRoute(relPath);
|
|
531
|
+
const layouts = resolveLayouts(current, dir);
|
|
532
|
+
const loadPath = existsSync(join(current, "load.ts")) ? join(current, "load.ts") : void 0;
|
|
533
|
+
const rPath = existsSync(join(current, "route.ts")) ? join(current, "route.ts") : void 0;
|
|
534
|
+
pages.push({
|
|
535
|
+
route,
|
|
536
|
+
entryPath,
|
|
537
|
+
loadPath,
|
|
538
|
+
layouts,
|
|
539
|
+
routePath: rPath
|
|
540
|
+
});
|
|
541
|
+
} else {
|
|
542
|
+
const rPath = join(current, "route.ts");
|
|
543
|
+
if (existsSync(rPath)) {
|
|
544
|
+
let relPath = relative(dir, rPath).replace(sep, "/");
|
|
545
|
+
relPath = relPath.replace(/\/route\.tsx?$/, "");
|
|
546
|
+
const route = filePathToRoute(relPath);
|
|
547
|
+
pages.push({
|
|
548
|
+
route,
|
|
549
|
+
entryPath: "",
|
|
550
|
+
layouts: [],
|
|
551
|
+
routePath: rPath,
|
|
552
|
+
routeOnly: true
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const d of dirs) walk(d);
|
|
448
557
|
}
|
|
558
|
+
walk(dir);
|
|
559
|
+
return pages;
|
|
560
|
+
}
|
|
561
|
+
function filePathToRoute(relPath) {
|
|
562
|
+
let route = relPath.replace(/\\/g, "/");
|
|
563
|
+
route = route.replace(/\[\.\.\.(\w+)\]/g, "*");
|
|
564
|
+
route = route.replace(/\[(\w+)\]/g, ":$1");
|
|
565
|
+
return route.startsWith("/") ? route : "/" + route;
|
|
566
|
+
}
|
|
567
|
+
function resolveLayouts(dir, pagesDir) {
|
|
568
|
+
const layouts = [];
|
|
569
|
+
let current = dir;
|
|
570
|
+
while (current.startsWith(pagesDir)) {
|
|
571
|
+
const p = join(current, "layout.tsx");
|
|
572
|
+
if (existsSync(p)) {
|
|
573
|
+
layouts.push(p);
|
|
574
|
+
}
|
|
575
|
+
const parent = dirname(current);
|
|
576
|
+
if (parent === current) break;
|
|
577
|
+
current = parent;
|
|
578
|
+
}
|
|
579
|
+
return layouts.reverse();
|
|
580
|
+
}
|
|
581
|
+
async function compileAll(files, outDir, platform) {
|
|
582
|
+
const entryPoints = {};
|
|
583
|
+
for (const f of files) {
|
|
584
|
+
entryPoints[id(f)] = f;
|
|
585
|
+
}
|
|
586
|
+
const isBrowser = platform === "browser";
|
|
587
|
+
await esbuild.build({
|
|
588
|
+
entryPoints,
|
|
589
|
+
outdir: outDir,
|
|
590
|
+
format: "esm",
|
|
591
|
+
platform: "node",
|
|
592
|
+
jsx: "automatic",
|
|
593
|
+
jsxImportSource: "react",
|
|
594
|
+
bundle: true,
|
|
595
|
+
external: isBrowser ? void 0 : [
|
|
596
|
+
"react",
|
|
597
|
+
"react-dom",
|
|
598
|
+
"esbuild",
|
|
599
|
+
"graphql",
|
|
600
|
+
"ws",
|
|
601
|
+
"zod",
|
|
602
|
+
"@graphql-tools/schema",
|
|
603
|
+
"ai"
|
|
604
|
+
],
|
|
605
|
+
write: true,
|
|
606
|
+
allowOverwrite: true
|
|
607
|
+
});
|
|
449
608
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
609
|
+
function compiledUrl(filePath, outDir) {
|
|
610
|
+
const hash = id(join(outDir, id(filePath)));
|
|
611
|
+
const p = join(outDir, id(filePath) + ".js");
|
|
612
|
+
return pathToFileURL(p).href;
|
|
613
|
+
}
|
|
614
|
+
var clientBundleCache = /* @__PURE__ */ new Map();
|
|
615
|
+
var clientRouteLog = /* @__PURE__ */ new WeakMap();
|
|
616
|
+
async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
|
|
617
|
+
const key = id(entryPath);
|
|
618
|
+
const url = `/__wfw/client/${key}.js`;
|
|
619
|
+
if (!clientRouteLog.get(router)?.has(url)) {
|
|
620
|
+
let buf = clientBundleCache.get(key);
|
|
621
|
+
if (!buf) {
|
|
622
|
+
try {
|
|
623
|
+
const nested = layoutPaths.slice(1);
|
|
624
|
+
const layoutsImport = nested.map(
|
|
625
|
+
(p, i) => `import L${i} from${JSON.stringify(p)};`
|
|
626
|
+
).join("");
|
|
627
|
+
const layoutsWrap = nested.map((_, i) => {
|
|
628
|
+
const idx = nested.length - 1 - i;
|
|
629
|
+
return `el=createElement(L${idx},null,el);`;
|
|
630
|
+
}).join("");
|
|
631
|
+
const code = [
|
|
632
|
+
`import{hydrateRoot}from'react-dom/client';`,
|
|
633
|
+
`import{createElement}from'react';`,
|
|
634
|
+
`import P from${JSON.stringify(entryPath)};`,
|
|
635
|
+
layoutsImport,
|
|
636
|
+
`const p=window.__WEIFUWU_PROPS;`,
|
|
637
|
+
`let el=createElement(P,p);`,
|
|
638
|
+
layoutsWrap,
|
|
639
|
+
`hydrateRoot(document.getElementById('__weifuwu_root'),el);`
|
|
640
|
+
].join("");
|
|
641
|
+
const result = await esbuild.build({
|
|
642
|
+
stdin: { contents: code, loader: "tsx", resolveDir: pagesDir },
|
|
643
|
+
bundle: true,
|
|
644
|
+
format: "esm",
|
|
645
|
+
jsx: "automatic",
|
|
646
|
+
jsxImportSource: "react",
|
|
647
|
+
write: false,
|
|
648
|
+
minify: true
|
|
649
|
+
});
|
|
650
|
+
buf = result.outputFiles[0].contents;
|
|
651
|
+
clientBundleCache.set(key, buf);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
console.error("hydration bundle failed:", err);
|
|
654
|
+
return null;
|
|
477
655
|
}
|
|
478
|
-
options.sseManager?.send(ctx.workflowId ?? "", { event: "node-start", data: { nodeId: node.id, tool: node.tool, input: node.input } });
|
|
479
|
-
const output = await executeNode(node, ctx);
|
|
480
|
-
ctx.nodeOutputs.set(node.id, output);
|
|
481
|
-
lastOutput = output;
|
|
482
|
-
options.sseManager?.send(ctx.workflowId ?? "", { event: "node-end", data: { nodeId: node.id, output } });
|
|
483
656
|
}
|
|
484
|
-
|
|
657
|
+
router.get(url, () => new Response(buf, {
|
|
658
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
659
|
+
}));
|
|
660
|
+
const set = clientRouteLog.get(router) ?? /* @__PURE__ */ new Set();
|
|
661
|
+
set.add(url);
|
|
662
|
+
clientRouteLog.set(router, set);
|
|
485
663
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
state.result = result;
|
|
500
|
-
state.endTime = Date.now();
|
|
501
|
-
sse?.send(workflowId, { event: "complete", data: { result, duration: state.endTime - state.startTime } });
|
|
502
|
-
} catch (err) {
|
|
503
|
-
state.status = "error";
|
|
504
|
-
state.error = err instanceof Error ? err.message : String(err);
|
|
505
|
-
state.endTime = Date.now();
|
|
506
|
-
sse?.send(workflowId, { event: "error", data: { error: state.error } });
|
|
507
|
-
} finally {
|
|
508
|
-
sse?.close(workflowId);
|
|
664
|
+
return { url };
|
|
665
|
+
}
|
|
666
|
+
function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, pagesDir, router) {
|
|
667
|
+
return async (req, ctx) => {
|
|
668
|
+
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
669
|
+
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
670
|
+
let element = createElement(Component, allProps);
|
|
671
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
672
|
+
const isRoot = i === 0;
|
|
673
|
+
element = createElement(
|
|
674
|
+
layouts[i],
|
|
675
|
+
isRoot ? { children: element, req, ctx } : { children: element }
|
|
676
|
+
);
|
|
509
677
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
678
|
+
element = createElement(TsxContext.Provider, {
|
|
679
|
+
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
680
|
+
}, element);
|
|
681
|
+
const stream = await renderToReadableStream(element);
|
|
682
|
+
const body = await readStream(stream);
|
|
683
|
+
const scripts = [];
|
|
684
|
+
scripts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(allProps)}</script>`);
|
|
685
|
+
const bundle = await getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router);
|
|
686
|
+
if (bundle) {
|
|
687
|
+
scripts.push(`<script type="module" src="${bundle.url}"></script>`);
|
|
514
688
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
});
|
|
521
|
-
return { text: result.text };
|
|
689
|
+
const html = `<!DOCTYPE html>
|
|
690
|
+
${body}
|
|
691
|
+
${scripts.join("\n")}`;
|
|
692
|
+
return new Response(html, {
|
|
693
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
522
694
|
});
|
|
523
|
-
}
|
|
524
|
-
return {
|
|
525
|
-
execute,
|
|
526
|
-
runAsync,
|
|
527
|
-
generateWorkflow: generateWorkflow2,
|
|
528
|
-
getState(workflowId) {
|
|
529
|
-
return states.get(workflowId);
|
|
530
|
-
}
|
|
531
695
|
};
|
|
532
696
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
697
|
+
async function tsx(options) {
|
|
698
|
+
const pagesDir = resolve(options.dir);
|
|
699
|
+
const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
|
|
700
|
+
const pages = scanPages(pagesDir);
|
|
701
|
+
if (pages.length === 0) return new Router();
|
|
702
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
703
|
+
for (const p of pages) {
|
|
704
|
+
if (p.entryPath) allFiles.add(p.entryPath);
|
|
705
|
+
if (p.loadPath) allFiles.add(p.loadPath);
|
|
706
|
+
for (const lp of p.layouts) allFiles.add(lp);
|
|
707
|
+
if (p.routePath) allFiles.add(p.routePath);
|
|
708
|
+
}
|
|
709
|
+
const nfPath = join(pagesDir, "not-found.tsx");
|
|
710
|
+
const hasNotFound = existsSync(nfPath);
|
|
711
|
+
if (hasNotFound) {
|
|
712
|
+
allFiles.add(nfPath);
|
|
713
|
+
const rootLayouts = resolveLayouts(pagesDir, pagesDir);
|
|
714
|
+
for (const lp of rootLayouts) allFiles.add(lp);
|
|
715
|
+
}
|
|
716
|
+
mkdirSync(outDir, { recursive: true });
|
|
717
|
+
await compileAll([...allFiles], outDir, "node");
|
|
718
|
+
const router = new Router();
|
|
719
|
+
for (const p of pages) {
|
|
720
|
+
if (p.routeOnly && p.routePath) {
|
|
721
|
+
const rUrl = compiledUrl(p.routePath, outDir);
|
|
722
|
+
const modR = await import(rUrl);
|
|
723
|
+
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
724
|
+
for (const method of methods) {
|
|
725
|
+
if (modR[method]) {
|
|
726
|
+
router.route(method, p.route, modR[method]);
|
|
555
727
|
}
|
|
556
|
-
state.buffer = [];
|
|
557
|
-
},
|
|
558
|
-
cancel() {
|
|
559
|
-
state.closed = true;
|
|
560
|
-
streams.delete(workflowId);
|
|
561
728
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const url = compiledUrl(p.entryPath, outDir);
|
|
732
|
+
const mod = await import(url);
|
|
733
|
+
const Component = mod.default;
|
|
734
|
+
let loadFn;
|
|
735
|
+
if (p.loadPath) {
|
|
736
|
+
const loadUrl = compiledUrl(p.loadPath, outDir);
|
|
737
|
+
const modLoad = await import(loadUrl);
|
|
738
|
+
loadFn = modLoad.default;
|
|
739
|
+
}
|
|
740
|
+
const layoutComponents = [];
|
|
741
|
+
for (const lp of p.layouts) {
|
|
742
|
+
const lUrl = compiledUrl(lp, outDir);
|
|
743
|
+
const modL = await import(lUrl);
|
|
744
|
+
layoutComponents.push(modL.default);
|
|
745
|
+
}
|
|
746
|
+
const handler = makeSsrHandler(
|
|
747
|
+
Component,
|
|
748
|
+
loadFn,
|
|
749
|
+
layoutComponents,
|
|
750
|
+
p.entryPath,
|
|
751
|
+
p.layouts,
|
|
752
|
+
pagesDir,
|
|
753
|
+
router
|
|
754
|
+
);
|
|
755
|
+
router.get(p.route, handler);
|
|
756
|
+
if (p.routePath) {
|
|
757
|
+
const rUrl = compiledUrl(p.routePath, outDir);
|
|
758
|
+
const modR = await import(rUrl);
|
|
759
|
+
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
760
|
+
for (const method of methods) {
|
|
761
|
+
if (modR[method]) {
|
|
762
|
+
router.route(method, p.route, modR[method]);
|
|
763
|
+
}
|
|
578
764
|
}
|
|
579
|
-
} else {
|
|
580
|
-
state.buffer.push(data);
|
|
581
765
|
}
|
|
582
766
|
}
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
767
|
+
if (hasNotFound) {
|
|
768
|
+
const nfUrl = compiledUrl(nfPath, outDir);
|
|
769
|
+
const modNf = await import(nfUrl);
|
|
770
|
+
const NfComponent = modNf.default;
|
|
771
|
+
const nfLayouts = [];
|
|
772
|
+
const rootLayouts = resolveLayouts(pagesDir, pagesDir);
|
|
773
|
+
for (const lp of rootLayouts) {
|
|
774
|
+
const lUrl = compiledUrl(lp, outDir);
|
|
775
|
+
const modL = await import(lUrl);
|
|
776
|
+
nfLayouts.push(modL.default);
|
|
591
777
|
}
|
|
778
|
+
const handler = async (req, ctx) => {
|
|
779
|
+
let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
|
|
780
|
+
for (let i = nfLayouts.length - 1; i >= 0; i--) {
|
|
781
|
+
element = createElement(nfLayouts[i], { children: element });
|
|
782
|
+
}
|
|
783
|
+
element = createElement(TsxContext.Provider, {
|
|
784
|
+
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
785
|
+
}, element);
|
|
786
|
+
const stream = await renderToReadableStream(element);
|
|
787
|
+
const body = await readStream(stream);
|
|
788
|
+
const html = `<!DOCTYPE html>
|
|
789
|
+
${body}`;
|
|
790
|
+
return new Response(html, {
|
|
791
|
+
status: 404,
|
|
792
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
793
|
+
});
|
|
794
|
+
};
|
|
795
|
+
router.all("/*", handler);
|
|
592
796
|
}
|
|
593
|
-
return
|
|
797
|
+
return router;
|
|
594
798
|
}
|
|
595
799
|
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
});
|
|
607
|
-
var getTrieNode = (node, segment) => {
|
|
608
|
-
if (segment.startsWith(":")) {
|
|
609
|
-
if (!node.children.has(":")) {
|
|
610
|
-
const child2 = createTrieNode();
|
|
611
|
-
child2.param = segment.slice(1);
|
|
612
|
-
node.children.set(":", child2);
|
|
613
|
-
}
|
|
614
|
-
const child = node.children.get(":");
|
|
615
|
-
if (child.param !== segment.slice(1)) {
|
|
616
|
-
throw new Error(
|
|
617
|
-
`Param name conflict: ":${child.param}" already registered at this path position, cannot register ":"${segment.slice(1)}"`
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
return child;
|
|
621
|
-
}
|
|
622
|
-
if (!node.children.has(segment)) {
|
|
623
|
-
node.children.set(segment, createTrieNode());
|
|
624
|
-
}
|
|
625
|
-
return node.children.get(segment);
|
|
626
|
-
};
|
|
627
|
-
var matchTrieNode = (node, segment, params) => {
|
|
628
|
-
if (node.children.has(segment)) return node.children.get(segment);
|
|
629
|
-
if (node.children.has(":")) {
|
|
630
|
-
const child = node.children.get(":");
|
|
631
|
-
if (child.param) params[child.param] = segment;
|
|
632
|
-
return child;
|
|
633
|
-
}
|
|
634
|
-
return null;
|
|
635
|
-
};
|
|
636
|
-
var getWsNode = (node, segment) => {
|
|
637
|
-
if (segment.startsWith(":")) {
|
|
638
|
-
if (!node.children.has(":")) {
|
|
639
|
-
const child2 = createWsNode();
|
|
640
|
-
child2.param = segment.slice(1);
|
|
641
|
-
node.children.set(":", child2);
|
|
642
|
-
}
|
|
643
|
-
const child = node.children.get(":");
|
|
644
|
-
if (child.param !== segment.slice(1)) {
|
|
645
|
-
throw new Error(
|
|
646
|
-
`Param name conflict: ":${child.param}" already registered at this path position`
|
|
647
|
-
);
|
|
800
|
+
// middleware.ts
|
|
801
|
+
function logger(options) {
|
|
802
|
+
return async (req, ctx, next) => {
|
|
803
|
+
const start = Date.now();
|
|
804
|
+
const url = new URL(req.url);
|
|
805
|
+
const res = await next(req, ctx);
|
|
806
|
+
const ms = Date.now() - start;
|
|
807
|
+
if (options?.format === "combined") {
|
|
808
|
+
console.log(`${req.method} ${url.pathname}${url.search} ${res.status} ${ms}ms`);
|
|
809
|
+
} else {
|
|
810
|
+
console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`);
|
|
648
811
|
}
|
|
649
|
-
return
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
return null;
|
|
664
|
-
};
|
|
665
|
-
var Router = class _Router {
|
|
666
|
-
root = createTrieNode();
|
|
667
|
-
wsRoot = createWsNode();
|
|
668
|
-
globalMws = [];
|
|
669
|
-
errorHandler;
|
|
670
|
-
use(arg1, arg2) {
|
|
671
|
-
if (typeof arg1 === "string") {
|
|
672
|
-
if (arg2 instanceof _Router) {
|
|
673
|
-
let node = this.root;
|
|
674
|
-
for (const segment of this.splitPath(arg1)) {
|
|
675
|
-
node = getTrieNode(node, segment);
|
|
676
|
-
}
|
|
677
|
-
node.subRouter = arg2;
|
|
678
|
-
} else if (typeof arg2 === "function") {
|
|
679
|
-
let node = this.root;
|
|
680
|
-
for (const segment of this.splitPath(arg1)) {
|
|
681
|
-
node = getTrieNode(node, segment);
|
|
682
|
-
}
|
|
683
|
-
node.pathMws.push(arg2);
|
|
684
|
-
}
|
|
685
|
-
} else if (typeof arg1 === "function") {
|
|
686
|
-
this.globalMws.push(arg1);
|
|
812
|
+
return res;
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function cors(options) {
|
|
816
|
+
const opts = {
|
|
817
|
+
origin: "*",
|
|
818
|
+
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
|
|
819
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
820
|
+
...options
|
|
821
|
+
};
|
|
822
|
+
function resolveOrigin(requestOrigin) {
|
|
823
|
+
if (typeof opts.origin === "string") return opts.origin === "*" ? "*" : opts.origin;
|
|
824
|
+
if (Array.isArray(opts.origin)) {
|
|
825
|
+
return opts.origin.includes(requestOrigin) ? requestOrigin : "";
|
|
687
826
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return
|
|
692
|
-
}
|
|
693
|
-
post(path, ...args) {
|
|
694
|
-
return this.route("POST", path, ...args);
|
|
695
|
-
}
|
|
696
|
-
put(path, ...args) {
|
|
697
|
-
return this.route("PUT", path, ...args);
|
|
698
|
-
}
|
|
699
|
-
delete(path, ...args) {
|
|
700
|
-
return this.route("DELETE", path, ...args);
|
|
701
|
-
}
|
|
702
|
-
patch(path, ...args) {
|
|
703
|
-
return this.route("PATCH", path, ...args);
|
|
704
|
-
}
|
|
705
|
-
head(path, ...args) {
|
|
706
|
-
return this.route("HEAD", path, ...args);
|
|
707
|
-
}
|
|
708
|
-
options(path, ...args) {
|
|
709
|
-
return this.route("OPTIONS", path, ...args);
|
|
710
|
-
}
|
|
711
|
-
all(path, ...args) {
|
|
712
|
-
return this.route("*", path, ...args);
|
|
713
|
-
}
|
|
714
|
-
onError(handler) {
|
|
715
|
-
this.errorHandler = handler;
|
|
716
|
-
return this;
|
|
827
|
+
const result = opts.origin(requestOrigin);
|
|
828
|
+
if (typeof result === "boolean") return result ? requestOrigin : "";
|
|
829
|
+
if (typeof result === "string") return result;
|
|
830
|
+
return "";
|
|
717
831
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
node.handlers.set(method, handler);
|
|
727
|
-
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
728
|
-
return this;
|
|
729
|
-
}
|
|
730
|
-
node = getTrieNode(node, segment);
|
|
731
|
-
}
|
|
732
|
-
node.handlers.set(method, handler);
|
|
733
|
-
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
734
|
-
return this;
|
|
832
|
+
function setCORSHeaders(res, acao) {
|
|
833
|
+
if (!acao) return res;
|
|
834
|
+
const headers = new Headers(res.headers);
|
|
835
|
+
headers.set("Access-Control-Allow-Origin", acao);
|
|
836
|
+
if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
|
|
837
|
+
if (opts.exposedHeaders?.length) headers.set("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
|
|
838
|
+
headers.set("Vary", "Origin");
|
|
839
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
735
840
|
}
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
841
|
+
return (req, ctx, next) => {
|
|
842
|
+
const requestOrigin = req.headers.get("origin") ?? "";
|
|
843
|
+
const acao = resolveOrigin(requestOrigin);
|
|
844
|
+
if (req.method === "OPTIONS" && acao) {
|
|
845
|
+
const headers = new Headers();
|
|
846
|
+
headers.set("Access-Control-Allow-Origin", acao);
|
|
847
|
+
headers.set("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
848
|
+
headers.set("Access-Control-Allow-Headers", opts.allowedHeaders.join(", "));
|
|
849
|
+
if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
|
|
850
|
+
if (opts.maxAge != null) headers.set("Access-Control-Max-Age", String(opts.maxAge));
|
|
851
|
+
headers.set("Vary", "Origin");
|
|
852
|
+
return new Response(null, { status: 204, headers });
|
|
743
853
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
});
|
|
854
|
+
if (!acao) return next(req, ctx);
|
|
855
|
+
return Promise.resolve(next(req, ctx)).then((res) => setCORSHeaders(res, acao));
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function auth(options) {
|
|
859
|
+
return async (req, ctx, next) => {
|
|
860
|
+
const headerName = options.header ?? "Authorization";
|
|
861
|
+
let from = "header";
|
|
862
|
+
let header = req.headers.get(headerName);
|
|
863
|
+
let token = "";
|
|
864
|
+
if (header) {
|
|
865
|
+
token = header;
|
|
866
|
+
if (headerName.toLowerCase() === "authorization") {
|
|
867
|
+
const parts = header.split(" ");
|
|
868
|
+
if (parts[0]?.toLowerCase() === "bearer") {
|
|
869
|
+
token = parts.slice(1).join(" ");
|
|
870
|
+
}
|
|
762
871
|
}
|
|
763
|
-
|
|
764
|
-
|
|
872
|
+
} else {
|
|
873
|
+
const url = new URL(req.url);
|
|
874
|
+
const qsToken = url.searchParams.get("access_token");
|
|
875
|
+
if (qsToken) {
|
|
876
|
+
token = qsToken;
|
|
877
|
+
from = "query";
|
|
765
878
|
}
|
|
766
|
-
const paramsPromise = req.method === "GET" ? Promise.resolve(parseGraphQLParamsFromGet(url)) : parseGraphQLParamsFromPost(req);
|
|
767
|
-
return paramsPromise.then((params) => {
|
|
768
|
-
if (!params) {
|
|
769
|
-
return Response.json(
|
|
770
|
-
{ errors: [{ message: "Missing query" }] },
|
|
771
|
-
{ status: 400 }
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
return executeGraphQLQuery(schema, params, options, req, ctx);
|
|
775
|
-
});
|
|
776
|
-
};
|
|
777
|
-
return this.all(path, ...middlewares, handler);
|
|
778
|
-
}
|
|
779
|
-
ai(path, ...args) {
|
|
780
|
-
const handler = args.pop();
|
|
781
|
-
const middlewares = args;
|
|
782
|
-
const routeHandler = async (req, ctx) => {
|
|
783
|
-
const options = await handler(req, ctx);
|
|
784
|
-
const result = streamText(options);
|
|
785
|
-
return result.toTextStreamResponse();
|
|
786
|
-
};
|
|
787
|
-
return this.post(path, ...middlewares, routeHandler);
|
|
788
|
-
}
|
|
789
|
-
workflow(path, options) {
|
|
790
|
-
const sseManager = options.stream ? createSSEManager() : void 0;
|
|
791
|
-
if (options.stream && sseManager) {
|
|
792
|
-
this.get(`${path}/:workflowId/events`, async (req, ctx) => {
|
|
793
|
-
const stream = sseManager.createStream(ctx.params.workflowId);
|
|
794
|
-
return new Response(stream, {
|
|
795
|
-
headers: {
|
|
796
|
-
"Content-Type": "text/event-stream",
|
|
797
|
-
"Cache-Control": "no-cache",
|
|
798
|
-
"Connection": "keep-alive"
|
|
799
|
-
}
|
|
800
|
-
});
|
|
801
|
-
});
|
|
802
879
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
model: options.model,
|
|
808
|
-
sseManager
|
|
880
|
+
if (!token) {
|
|
881
|
+
return new Response("Unauthorized", {
|
|
882
|
+
status: 401,
|
|
883
|
+
headers: headerName.toLowerCase() === "authorization" ? { "WWW-Authenticate": "Bearer" } : void 0
|
|
809
884
|
});
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
wf = { nodes: body.nodes };
|
|
885
|
+
}
|
|
886
|
+
if (options.proxy) {
|
|
887
|
+
const proxyUrl = typeof options.proxy === "string" ? new URL(options.proxy) : options.proxy;
|
|
888
|
+
const proxyHeaders = {};
|
|
889
|
+
if (from === "header" && header) {
|
|
890
|
+
proxyHeaders[headerName] = header;
|
|
817
891
|
} else {
|
|
818
|
-
|
|
819
|
-
{ error: 'Provide "goal" (with model) or "workflow"/"nodes"' },
|
|
820
|
-
{ status: 400 }
|
|
821
|
-
);
|
|
892
|
+
proxyUrl.searchParams.set("access_token", token);
|
|
822
893
|
}
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
return Response.json({ workflowId, eventsUrl: `${path}/${workflowId}/events` });
|
|
894
|
+
for (const name of ["x-forwarded-for", "x-real-ip", "user-agent", "content-type"]) {
|
|
895
|
+
const v = req.headers.get(name);
|
|
896
|
+
if (v) proxyHeaders[name] = v;
|
|
827
897
|
}
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return this;
|
|
832
|
-
}
|
|
833
|
-
handler() {
|
|
834
|
-
return (req, ctx) => {
|
|
835
|
-
const url = new URL(req.url);
|
|
836
|
-
return this.handle(req, ctx, this.splitPath(url.pathname), Object.fromEntries(url.searchParams));
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
websocketHandler() {
|
|
840
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
841
|
-
const wsRoot = this.wsRoot;
|
|
842
|
-
const router = this;
|
|
843
|
-
return (req, socket, head) => {
|
|
844
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
845
|
-
const segments = url.pathname.split("/").filter(Boolean);
|
|
846
|
-
const query = Object.fromEntries(url.searchParams);
|
|
847
|
-
const match = router.matchWsTrie(wsRoot, segments);
|
|
848
|
-
if (!match) {
|
|
849
|
-
socket.destroy();
|
|
850
|
-
return;
|
|
898
|
+
const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders });
|
|
899
|
+
if (proxyRes.status >= 400) {
|
|
900
|
+
return new Response(await proxyRes.text() || "Forbidden", { status: proxyRes.status });
|
|
851
901
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return;
|
|
902
|
+
let userData = void 0;
|
|
903
|
+
if (proxyRes.status === 200) {
|
|
904
|
+
const ct = proxyRes.headers.get("content-type");
|
|
905
|
+
if (ct?.includes("application/json")) {
|
|
906
|
+
try {
|
|
907
|
+
userData = await proxyRes.json();
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
862
911
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
912
|
+
ctx.user = userData;
|
|
913
|
+
return next(req, ctx);
|
|
914
|
+
}
|
|
915
|
+
if (options.token) {
|
|
916
|
+
if (token !== options.token) {
|
|
917
|
+
return new Response("Forbidden", { status: 403 });
|
|
918
|
+
}
|
|
919
|
+
return next(req, ctx);
|
|
920
|
+
}
|
|
921
|
+
if (options.verify) {
|
|
922
|
+
const result = await options.verify(token, req);
|
|
923
|
+
if (!result) {
|
|
924
|
+
return new Response("Forbidden", { status: 403 });
|
|
925
|
+
}
|
|
926
|
+
if (typeof result === "object" && result !== null) {
|
|
927
|
+
ctx.user = result;
|
|
928
|
+
}
|
|
929
|
+
return next(req, ctx);
|
|
930
|
+
}
|
|
931
|
+
return next(req, ctx);
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// static.ts
|
|
936
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
937
|
+
import { open } from "node:fs/promises";
|
|
938
|
+
import { extname, resolve as resolve2, normalize, sep as sep2 } from "node:path";
|
|
939
|
+
function serveStatic(root, options) {
|
|
940
|
+
const rootDir = resolve2(root);
|
|
941
|
+
const opts = options ?? {};
|
|
942
|
+
return async (req, ctx) => {
|
|
943
|
+
const relativePath = ctx.params["*"] ?? new URL(req.url).pathname.slice(1);
|
|
944
|
+
const decoded = decodeURIComponent(relativePath);
|
|
945
|
+
if (decoded.includes("..") || decoded.includes("\0")) {
|
|
946
|
+
return new Response("Forbidden", { status: 403 });
|
|
947
|
+
}
|
|
948
|
+
let filePath = normalize(resolve2(rootDir, decoded));
|
|
949
|
+
if (!filePath.startsWith(rootDir + sep2) && filePath !== rootDir) {
|
|
950
|
+
return new Response("Forbidden", { status: 403 });
|
|
951
|
+
}
|
|
952
|
+
let fileHandle;
|
|
953
|
+
try {
|
|
954
|
+
fileHandle = await open(filePath, "r");
|
|
955
|
+
const stat = await fileHandle.stat();
|
|
956
|
+
if (stat.isDirectory()) {
|
|
957
|
+
await fileHandle.close();
|
|
958
|
+
const indexFile = opts.index ?? "index.html";
|
|
959
|
+
filePath = resolve2(filePath, indexFile);
|
|
960
|
+
if (!filePath.startsWith(rootDir + sep2)) {
|
|
961
|
+
return new Response("Forbidden", { status: 403 });
|
|
868
962
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
875
|
-
if (result.status !== 101) {
|
|
876
|
-
sendHttpResponseOnSocket(socket, result);
|
|
963
|
+
fileHandle = await open(filePath, "r");
|
|
964
|
+
const dirStat = await fileHandle.stat();
|
|
965
|
+
if (!dirStat.isFile()) {
|
|
966
|
+
await fileHandle.close();
|
|
967
|
+
return new Response("Not Found", { status: 404 });
|
|
877
968
|
}
|
|
878
|
-
}
|
|
879
|
-
|
|
969
|
+
}
|
|
970
|
+
const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
971
|
+
const etag = `"${createHash2("md5").update(`${stat.size}-${stat.mtimeMs}`).digest("hex")}"`;
|
|
972
|
+
const ifNoneMatch = req.headers.get("if-none-match");
|
|
973
|
+
if (ifNoneMatch === etag) {
|
|
974
|
+
await fileHandle.close();
|
|
975
|
+
return new Response(null, { status: 304 });
|
|
976
|
+
}
|
|
977
|
+
const ifModifiedSince = req.headers.get("if-modified-since");
|
|
978
|
+
if (ifModifiedSince && stat.mtimeMs <= new Date(ifModifiedSince).getTime()) {
|
|
979
|
+
await fileHandle.close();
|
|
980
|
+
return new Response(null, { status: 304 });
|
|
981
|
+
}
|
|
982
|
+
const headers = {
|
|
983
|
+
"Content-Type": mimeType,
|
|
984
|
+
"Content-Length": String(stat.size),
|
|
985
|
+
"ETag": etag,
|
|
986
|
+
"Last-Modified": stat.mtime.toUTCString(),
|
|
987
|
+
"Cache-Control": opts.immutable ? `public, max-age=${opts.maxAge ?? 31536e3}, immutable` : `public, max-age=${opts.maxAge ?? 0}`
|
|
988
|
+
};
|
|
989
|
+
const stream = fileHandle.readableWebStream();
|
|
990
|
+
return new Response(stream, { headers });
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (fileHandle) await fileHandle.close().catch(() => {
|
|
880
993
|
});
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
splitPath(path) {
|
|
884
|
-
return path.split("/").filter(Boolean);
|
|
885
|
-
}
|
|
886
|
-
matchTrie(method, segments) {
|
|
887
|
-
let node = this.root;
|
|
888
|
-
const params = {};
|
|
889
|
-
const pathMws = [...this.root.pathMws];
|
|
890
|
-
let wildcardHandler = null;
|
|
891
|
-
let wildcardMws = [];
|
|
892
|
-
let wildcardIdx = -1;
|
|
893
|
-
for (let i = 0; i < segments.length; i++) {
|
|
894
|
-
pathMws.push(...node.pathMws);
|
|
895
|
-
if (node.wildcard) {
|
|
896
|
-
const h = node.handlers.get("*") || node.handlers.get(method);
|
|
897
|
-
if (h) {
|
|
898
|
-
wildcardHandler = h;
|
|
899
|
-
wildcardMws = node.middlewares.get(method) || node.middlewares.get("*") || [];
|
|
900
|
-
wildcardIdx = i;
|
|
901
|
-
}
|
|
994
|
+
if (err?.code === "ENOENT") {
|
|
995
|
+
return new Response("Not Found", { status: 404 });
|
|
902
996
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
997
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
var MIME_TYPES = {
|
|
1002
|
+
".html": "text/html; charset=utf-8",
|
|
1003
|
+
".htm": "text/html; charset=utf-8",
|
|
1004
|
+
".css": "text/css; charset=utf-8",
|
|
1005
|
+
".js": "application/javascript; charset=utf-8",
|
|
1006
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1007
|
+
".json": "application/json",
|
|
1008
|
+
".png": "image/png",
|
|
1009
|
+
".jpg": "image/jpeg",
|
|
1010
|
+
".jpeg": "image/jpeg",
|
|
1011
|
+
".gif": "image/gif",
|
|
1012
|
+
".svg": "image/svg+xml",
|
|
1013
|
+
".ico": "image/x-icon",
|
|
1014
|
+
".webp": "image/webp",
|
|
1015
|
+
".avif": "image/avif",
|
|
1016
|
+
".woff": "font/woff",
|
|
1017
|
+
".woff2": "font/woff2",
|
|
1018
|
+
".ttf": "font/ttf",
|
|
1019
|
+
".otf": "font/otf",
|
|
1020
|
+
".eot": "application/vnd.ms-fontobject",
|
|
1021
|
+
".txt": "text/plain; charset=utf-8",
|
|
1022
|
+
".xml": "application/xml",
|
|
1023
|
+
".pdf": "application/pdf",
|
|
1024
|
+
".zip": "application/zip",
|
|
1025
|
+
".wasm": "application/wasm",
|
|
1026
|
+
".map": "application/json"
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// validate.ts
|
|
1030
|
+
function validate(schemas) {
|
|
1031
|
+
return async (req, ctx, next) => {
|
|
1032
|
+
const parsed = {};
|
|
1033
|
+
const issues = [];
|
|
1034
|
+
if (schemas.params) {
|
|
1035
|
+
const result = schemas.params.safeParse(ctx.params);
|
|
1036
|
+
if (result.success) {
|
|
1037
|
+
parsed.params = result.data;
|
|
1038
|
+
} else {
|
|
1039
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1040
|
+
path: ["params", ...i.path.map(String)],
|
|
1041
|
+
message: i.message
|
|
1042
|
+
})));
|
|
920
1043
|
}
|
|
921
|
-
node = next;
|
|
922
|
-
}
|
|
923
|
-
if (node.subRouter) {
|
|
924
|
-
return {
|
|
925
|
-
pathMws,
|
|
926
|
-
params,
|
|
927
|
-
middlewares: [],
|
|
928
|
-
subRouter: { router: node.subRouter, remainingIdx: segments.length }
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
pathMws.push(...node.pathMws);
|
|
932
|
-
const handler = node.handlers.get(method) || node.handlers.get("*");
|
|
933
|
-
if (handler) {
|
|
934
|
-
if (node.wildcard) params["*"] = segments.slice(segments.length).join("/");
|
|
935
|
-
return {
|
|
936
|
-
handler,
|
|
937
|
-
middlewares: node.middlewares.get(method) || node.middlewares.get("*") || [],
|
|
938
|
-
pathMws,
|
|
939
|
-
params
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
if (wildcardHandler) {
|
|
943
|
-
params["*"] = segments.slice(wildcardIdx).join("/");
|
|
944
|
-
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
|
|
945
|
-
}
|
|
946
|
-
return null;
|
|
947
|
-
}
|
|
948
|
-
matchWsTrie(root, segments) {
|
|
949
|
-
let node = root;
|
|
950
|
-
const params = {};
|
|
951
|
-
for (const segment of segments) {
|
|
952
|
-
const next = matchWsNode(node, segment, params);
|
|
953
|
-
if (!next) return null;
|
|
954
|
-
node = next;
|
|
955
1044
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
try {
|
|
966
|
-
return await this.runChain(allMws, delegate, req, { ...ctx, params: { ...ctx.params, ...match.params } });
|
|
967
|
-
} catch (e) {
|
|
968
|
-
return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
|
|
1045
|
+
if (schemas.query) {
|
|
1046
|
+
const result = schemas.query.safeParse(ctx.query);
|
|
1047
|
+
if (result.success) {
|
|
1048
|
+
parsed.query = result.data;
|
|
1049
|
+
} else {
|
|
1050
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1051
|
+
path: ["query", ...i.path.map(String)],
|
|
1052
|
+
message: i.message
|
|
1053
|
+
})));
|
|
969
1054
|
}
|
|
970
1055
|
}
|
|
971
|
-
if (
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1056
|
+
if (schemas.headers) {
|
|
1057
|
+
const rawHeaders = {};
|
|
1058
|
+
req.headers.forEach((v, k) => {
|
|
1059
|
+
rawHeaders[k] = v;
|
|
1060
|
+
});
|
|
1061
|
+
const result = schemas.headers.safeParse(rawHeaders);
|
|
1062
|
+
if (result.success) {
|
|
1063
|
+
parsed.headers = result.data;
|
|
1064
|
+
} else {
|
|
1065
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1066
|
+
path: ["headers", ...i.path.map(String)],
|
|
1067
|
+
message: i.message
|
|
1068
|
+
})));
|
|
979
1069
|
}
|
|
980
1070
|
}
|
|
981
|
-
if (
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1071
|
+
if (schemas.body) {
|
|
1072
|
+
if (req.body === null) {
|
|
1073
|
+
issues.push({ path: ["body"], message: "Request body is required" });
|
|
1074
|
+
} else {
|
|
1075
|
+
const bodyText = await req.text();
|
|
1076
|
+
if (!bodyText && req.method !== "GET" && req.method !== "HEAD") {
|
|
1077
|
+
issues.push({ path: ["body"], message: "Request body is required" });
|
|
1078
|
+
} else {
|
|
1079
|
+
let bodyValue;
|
|
1080
|
+
try {
|
|
1081
|
+
bodyValue = JSON.parse(bodyText);
|
|
1082
|
+
} catch {
|
|
1083
|
+
bodyValue = bodyText;
|
|
1084
|
+
}
|
|
1085
|
+
const result = schemas.body.safeParse(bodyValue);
|
|
1086
|
+
if (result.success) {
|
|
1087
|
+
parsed.body = result.data;
|
|
1088
|
+
} else {
|
|
1089
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1090
|
+
path: ["body", ...i.path.map(String)],
|
|
1091
|
+
message: i.message
|
|
1092
|
+
})));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
987
1095
|
}
|
|
988
1096
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
async runChain(middlewares, finalHandler, req, ctx) {
|
|
992
|
-
let index = 0;
|
|
993
|
-
const dispatch = async (req2, ctx2) => {
|
|
994
|
-
if (index < middlewares.length) {
|
|
995
|
-
const mw = middlewares[index++];
|
|
996
|
-
return mw ? await mw(req2, ctx2, dispatch) : new Response("Middleware error", { status: 500 });
|
|
997
|
-
}
|
|
998
|
-
return await finalHandler(req2, ctx2);
|
|
999
|
-
};
|
|
1000
|
-
return dispatch(req, ctx);
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
1003
|
-
function upgradeSocket(wss, req, socket, head, handler, ctx) {
|
|
1004
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1005
|
-
if (handler.open) {
|
|
1006
|
-
handler.open(ws, ctx);
|
|
1097
|
+
if (issues.length > 0) {
|
|
1098
|
+
return Response.json({ error: "Validation failed", issues }, { status: 400 });
|
|
1007
1099
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
ws.on("close", () => {
|
|
1012
|
-
handler.close?.(ws, ctx);
|
|
1013
|
-
});
|
|
1014
|
-
ws.on("error", (err) => {
|
|
1015
|
-
handler.error?.(ws, ctx, err);
|
|
1016
|
-
});
|
|
1017
|
-
});
|
|
1100
|
+
ctx.parsed = parsed;
|
|
1101
|
+
return next(req, ctx);
|
|
1102
|
+
};
|
|
1018
1103
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1104
|
+
|
|
1105
|
+
// cookie.ts
|
|
1106
|
+
function getCookies(req) {
|
|
1107
|
+
const header = req.headers.get("cookie");
|
|
1108
|
+
if (!header) return {};
|
|
1109
|
+
const cookies = {};
|
|
1110
|
+
for (const pair of header.split(";")) {
|
|
1111
|
+
const idx = pair.indexOf("=");
|
|
1112
|
+
if (idx === -1) continue;
|
|
1113
|
+
const name = pair.slice(0, idx).trim();
|
|
1114
|
+
const value = pair.slice(idx + 1).trim();
|
|
1115
|
+
if (name) {
|
|
1116
|
+
cookies[name] = decodeURIComponent(value);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return cookies;
|
|
1120
|
+
}
|
|
1121
|
+
function serializeCookie(name, value, options) {
|
|
1122
|
+
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
1123
|
+
if (options?.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
|
|
1124
|
+
if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
1125
|
+
if (options?.domain) parts.push(`Domain=${options.domain}`);
|
|
1126
|
+
if (options?.path) parts.push(`Path=${options.path}`);
|
|
1127
|
+
if (options?.httpOnly) parts.push("HttpOnly");
|
|
1128
|
+
if (options?.secure) parts.push("Secure");
|
|
1129
|
+
if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`);
|
|
1130
|
+
return parts.join("; ");
|
|
1131
|
+
}
|
|
1132
|
+
function setCookie(res, name, value, options) {
|
|
1133
|
+
const headers = new Headers(res.headers);
|
|
1134
|
+
headers.append("Set-Cookie", serializeCookie(name, value, options));
|
|
1135
|
+
return new Response(res.body, {
|
|
1136
|
+
status: res.status,
|
|
1137
|
+
statusText: res.statusText,
|
|
1138
|
+
headers
|
|
1024
1139
|
});
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
socket.write(headerStr + "\r\n");
|
|
1034
|
-
socket.end();
|
|
1140
|
+
}
|
|
1141
|
+
function deleteCookie(res, name, options) {
|
|
1142
|
+
const headers = new Headers(res.headers);
|
|
1143
|
+
headers.append("Set-Cookie", serializeCookie(name, "", { ...options, maxAge: 0 }));
|
|
1144
|
+
return new Response(res.body, {
|
|
1145
|
+
status: res.status,
|
|
1146
|
+
statusText: res.statusText,
|
|
1147
|
+
headers
|
|
1035
1148
|
});
|
|
1036
1149
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1150
|
+
|
|
1151
|
+
// upload.ts
|
|
1152
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
1153
|
+
import { randomUUID } from "node:crypto";
|
|
1154
|
+
import { join as join2 } from "node:path";
|
|
1155
|
+
function upload(options) {
|
|
1156
|
+
const saveDir = options?.dir;
|
|
1157
|
+
return async (req, ctx, next) => {
|
|
1158
|
+
const ct = req.headers.get("content-type") ?? "";
|
|
1159
|
+
if (!ct.includes("multipart/form-data")) {
|
|
1160
|
+
return next(req, ctx);
|
|
1161
|
+
}
|
|
1162
|
+
const match = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
1163
|
+
if (!match) {
|
|
1164
|
+
return Response.json({ error: "Missing boundary" }, { status: 400 });
|
|
1165
|
+
}
|
|
1166
|
+
const boundary = match[1] ?? match[2];
|
|
1167
|
+
const body = await req.text();
|
|
1168
|
+
const rawParts = body.split(`--${boundary}`).filter((p) => p && !p.startsWith("--") && !p.startsWith("\r\n--"));
|
|
1169
|
+
const files = {};
|
|
1170
|
+
const fields = {};
|
|
1171
|
+
for (const raw of rawParts) {
|
|
1172
|
+
const trimmed = raw.replace(/^\r?\n/, "");
|
|
1173
|
+
const lines = trimmed.split(/\r?\n/);
|
|
1174
|
+
let i = 0;
|
|
1175
|
+
const headers = {};
|
|
1176
|
+
while (i < lines.length && lines[i].length > 0) {
|
|
1177
|
+
const sep3 = lines[i].indexOf(": ");
|
|
1178
|
+
if (sep3 !== -1) headers[lines[i].slice(0, sep3).toLowerCase()] = lines[i].slice(sep3 + 2);
|
|
1179
|
+
i++;
|
|
1180
|
+
}
|
|
1181
|
+
i++;
|
|
1182
|
+
const bodyValue = lines.slice(i).join("\r\n");
|
|
1183
|
+
const disposition = headers["content-disposition"] ?? "";
|
|
1184
|
+
const nameMatch = disposition.match(/name="([^"]*)"/);
|
|
1185
|
+
if (!nameMatch) continue;
|
|
1186
|
+
const name = nameMatch[1];
|
|
1187
|
+
const filenameMatch = disposition.match(/filename="([^"]*)"/);
|
|
1188
|
+
const filename = filenameMatch?.[1];
|
|
1189
|
+
if (filename) {
|
|
1190
|
+
const buf = Buffer.from(bodyValue.replace(/\r?\n$/, ""), "binary");
|
|
1191
|
+
if (options?.allowedTypes) {
|
|
1192
|
+
const mime = headers["content-type"] ?? "application/octet-stream";
|
|
1193
|
+
if (!options.allowedTypes.includes(mime)) {
|
|
1194
|
+
return Response.json({ error: `File type not allowed: ${mime}` }, { status: 415 });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (options?.maxFileSize && buf.byteLength > options.maxFileSize) {
|
|
1198
|
+
return Response.json({ error: `File too large: ${filename}` }, { status: 413 });
|
|
1199
|
+
}
|
|
1200
|
+
const uf = {
|
|
1201
|
+
name: filename,
|
|
1202
|
+
type: headers["content-type"] ?? "application/octet-stream",
|
|
1203
|
+
size: buf.byteLength,
|
|
1204
|
+
buffer: saveDir ? void 0 : buf
|
|
1205
|
+
};
|
|
1206
|
+
if (saveDir) {
|
|
1207
|
+
const filePath = join2(saveDir, `${randomUUID()}-${filename}`);
|
|
1208
|
+
await mkdir(saveDir, { recursive: true });
|
|
1209
|
+
await writeFile(filePath, buf);
|
|
1210
|
+
uf.path = filePath;
|
|
1211
|
+
}
|
|
1212
|
+
if (files[name]) {
|
|
1213
|
+
const existing = files[name];
|
|
1214
|
+
files[name] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
|
|
1215
|
+
} else {
|
|
1216
|
+
files[name] = uf;
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
fields[name] = bodyValue.replace(/\r?\n$/, "");
|
|
1220
|
+
}
|
|
1047
1221
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
query,
|
|
1051
|
-
variables,
|
|
1052
|
-
operationName: url.searchParams.get("operationName") || void 0
|
|
1222
|
+
ctx.parsed = { ...ctx.parsed, files, fields };
|
|
1223
|
+
return next(req, ctx);
|
|
1053
1224
|
};
|
|
1054
1225
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
} catch {
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
async function executeGraphQLQuery(schema, params, options, req, ctx) {
|
|
1069
|
-
const contextValue = options.context ? await options.context(req, ctx) : ctx;
|
|
1070
|
-
const result = await graphql({
|
|
1071
|
-
schema,
|
|
1072
|
-
source: params.query,
|
|
1073
|
-
rootValue: options.rootValue,
|
|
1074
|
-
contextValue,
|
|
1075
|
-
variableValues: params.variables,
|
|
1076
|
-
operationName: params.operationName
|
|
1226
|
+
|
|
1227
|
+
// rate-limit.ts
|
|
1228
|
+
function rateLimit(options) {
|
|
1229
|
+
const max = options?.max ?? 100;
|
|
1230
|
+
const window = options?.window ?? 6e4;
|
|
1231
|
+
const getKey = options?.key ?? ((req) => {
|
|
1232
|
+
const forwarded = req.headers.get("x-forwarded-for");
|
|
1233
|
+
if (forwarded) return forwarded.split(",")[0].trim();
|
|
1234
|
+
return new URL(req.url).hostname;
|
|
1077
1235
|
});
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1236
|
+
const message = options?.message ?? "Too Many Requests";
|
|
1237
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1238
|
+
const interval = setInterval(() => {
|
|
1239
|
+
const now = Date.now();
|
|
1240
|
+
for (const [key, entry] of hits) {
|
|
1241
|
+
if (entry.reset < now) hits.delete(key);
|
|
1242
|
+
}
|
|
1243
|
+
}, window);
|
|
1244
|
+
if (interval.unref) interval.unref();
|
|
1245
|
+
return async (req, ctx, next) => {
|
|
1246
|
+
const key = getKey(req);
|
|
1247
|
+
const now = Date.now();
|
|
1248
|
+
const entry = hits.get(key);
|
|
1249
|
+
if (!entry || entry.reset < now) {
|
|
1250
|
+
hits.set(key, { count: 1, reset: now + window });
|
|
1251
|
+
const res2 = await next(req, ctx);
|
|
1252
|
+
const headers2 = new Headers(res2.headers);
|
|
1253
|
+
headers2.set("X-RateLimit-Limit", String(max));
|
|
1254
|
+
headers2.set("X-RateLimit-Remaining", String(max - 1));
|
|
1255
|
+
headers2.set("X-RateLimit-Reset", String(Math.ceil((now + window) / 1e3)));
|
|
1256
|
+
return new Response(res2.body, { status: res2.status, statusText: res2.statusText, headers: headers2 });
|
|
1257
|
+
}
|
|
1258
|
+
entry.count++;
|
|
1259
|
+
const remaining = Math.max(0, max - entry.count);
|
|
1260
|
+
if (entry.count > max) {
|
|
1261
|
+
return new Response(message, {
|
|
1262
|
+
status: 429,
|
|
1263
|
+
headers: {
|
|
1264
|
+
"Retry-After": String(Math.ceil((entry.reset - now) / 1e3)),
|
|
1265
|
+
"X-RateLimit-Limit": String(max),
|
|
1266
|
+
"X-RateLimit-Remaining": "0",
|
|
1267
|
+
"X-RateLimit-Reset": String(Math.ceil(entry.reset / 1e3))
|
|
1105
1268
|
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const fetcher = createGraphiQLFetcher({ url: "${endpoint}" });
|
|
1116
|
-
|
|
1117
|
-
function App() {
|
|
1118
|
-
return React.createElement(GraphiQL, { fetcher });
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const container = document.getElementById('graphiql');
|
|
1122
|
-
const root = ReactDOM.createRoot(container);
|
|
1123
|
-
root.render(React.createElement(App));
|
|
1124
|
-
</script>
|
|
1125
|
-
</head>
|
|
1126
|
-
<body>
|
|
1127
|
-
<div id="graphiql">Loading\u2026</div>
|
|
1128
|
-
</body>
|
|
1129
|
-
</html>`;
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const res = await next(req, ctx);
|
|
1272
|
+
const headers = new Headers(res.headers);
|
|
1273
|
+
headers.set("X-RateLimit-Limit", String(max));
|
|
1274
|
+
headers.set("X-RateLimit-Remaining", String(remaining));
|
|
1275
|
+
headers.set("X-RateLimit-Reset", String(Math.ceil(entry.reset / 1e3)));
|
|
1276
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
1277
|
+
};
|
|
1130
1278
|
}
|
|
1131
1279
|
|
|
1132
|
-
//
|
|
1133
|
-
import {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
function id(s) {
|
|
1145
|
-
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
1146
|
-
}
|
|
1147
|
-
function concatUint8(chunks) {
|
|
1148
|
-
const len = chunks.reduce((a, c) => a + c.length, 0);
|
|
1149
|
-
const out = new Uint8Array(len);
|
|
1150
|
-
let off = 0;
|
|
1151
|
-
for (const c of chunks) {
|
|
1152
|
-
out.set(c, off);
|
|
1153
|
-
off += c.length;
|
|
1154
|
-
}
|
|
1155
|
-
return out;
|
|
1156
|
-
}
|
|
1157
|
-
async function readStream(stream) {
|
|
1158
|
-
const chunks = [];
|
|
1159
|
-
const reader = stream.getReader();
|
|
1160
|
-
while (true) {
|
|
1161
|
-
const { done, value } = await reader.read();
|
|
1162
|
-
if (done) break;
|
|
1163
|
-
chunks.push(value);
|
|
1164
|
-
}
|
|
1165
|
-
return new TextDecoder().decode(concatUint8(chunks));
|
|
1166
|
-
}
|
|
1167
|
-
function scanPages(dir) {
|
|
1168
|
-
const pages = [];
|
|
1169
|
-
function walk(current) {
|
|
1170
|
-
let entries;
|
|
1171
|
-
try {
|
|
1172
|
-
entries = readdirSync(current);
|
|
1173
|
-
} catch {
|
|
1174
|
-
return;
|
|
1280
|
+
// compress.ts
|
|
1281
|
+
import { gzipSync, brotliCompressSync, constants } from "node:zlib";
|
|
1282
|
+
function compress(options) {
|
|
1283
|
+
const level = options?.level ?? 6;
|
|
1284
|
+
const threshold = options?.threshold ?? 1024;
|
|
1285
|
+
return async (req, ctx, next) => {
|
|
1286
|
+
const accept = req.headers.get("accept-encoding") ?? "";
|
|
1287
|
+
const useBrotli = accept.includes("br");
|
|
1288
|
+
const useGzip = !useBrotli && accept.includes("gzip");
|
|
1289
|
+
const useDeflate = !useBrotli && !useGzip && accept.includes("deflate");
|
|
1290
|
+
if (!useBrotli && !useGzip && !useDeflate) {
|
|
1291
|
+
return next(req, ctx);
|
|
1175
1292
|
}
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const st = statSync(full);
|
|
1180
|
-
if (st.isDirectory()) {
|
|
1181
|
-
if (!name.startsWith(".")) dirs.push(full);
|
|
1182
|
-
}
|
|
1293
|
+
const res = await next(req, ctx);
|
|
1294
|
+
if (res.status === 304 || res.status === 204 || res.status < 200 || res.status >= 300) {
|
|
1295
|
+
return res;
|
|
1183
1296
|
}
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1189
|
-
} else if (existsSync(tsPagePath)) {
|
|
1190
|
-
entryPath = tsPagePath;
|
|
1297
|
+
const ce = res.headers.get("content-encoding");
|
|
1298
|
+
if (ce) return res;
|
|
1299
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
1300
|
+
if (!ct || ct.startsWith("audio/") || ct.startsWith("video/") || ct.startsWith("image/") || ct === "application/zip") {
|
|
1301
|
+
return res;
|
|
1191
1302
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const rPath = existsSync(join(current, "route.ts")) ? join(current, "route.ts") : void 0;
|
|
1200
|
-
pages.push({
|
|
1201
|
-
route,
|
|
1202
|
-
entryPath,
|
|
1203
|
-
loadPath,
|
|
1204
|
-
layouts,
|
|
1205
|
-
routePath: rPath
|
|
1303
|
+
const body = await res.bytes();
|
|
1304
|
+
if (body.byteLength < threshold) return res;
|
|
1305
|
+
let compressed;
|
|
1306
|
+
let encoding;
|
|
1307
|
+
if (useBrotli) {
|
|
1308
|
+
compressed = brotliCompressSync(body, {
|
|
1309
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) }
|
|
1206
1310
|
});
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
route,
|
|
1215
|
-
entryPath: "",
|
|
1216
|
-
layouts: [],
|
|
1217
|
-
routePath: rPath,
|
|
1218
|
-
routeOnly: true
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1311
|
+
encoding = "br";
|
|
1312
|
+
} else if (useGzip) {
|
|
1313
|
+
compressed = gzipSync(body, { level: Math.min(level, 9) });
|
|
1314
|
+
encoding = "gzip";
|
|
1315
|
+
} else {
|
|
1316
|
+
compressed = gzipSync(body, { level: Math.min(level, 9) });
|
|
1317
|
+
encoding = "deflate";
|
|
1221
1318
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1319
|
+
const headers = new Headers(res.headers);
|
|
1320
|
+
headers.set("Content-Encoding", encoding);
|
|
1321
|
+
headers.set("Content-Length", String(compressed.byteLength));
|
|
1322
|
+
headers.delete("Content-Range");
|
|
1323
|
+
headers.set("Vary", "Accept-Encoding");
|
|
1324
|
+
return new Response(compressed, {
|
|
1325
|
+
status: res.status,
|
|
1326
|
+
statusText: res.statusText,
|
|
1327
|
+
headers
|
|
1328
|
+
});
|
|
1329
|
+
};
|
|
1232
1330
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1331
|
+
|
|
1332
|
+
// graphql.ts
|
|
1333
|
+
import { buildSchema, graphql as executeGraphQL } from "graphql";
|
|
1334
|
+
import { makeExecutableSchema } from "@graphql-tools/schema";
|
|
1335
|
+
function parseParamsFromGet(url) {
|
|
1336
|
+
const query = url.searchParams.get("query");
|
|
1337
|
+
if (!query) return null;
|
|
1338
|
+
let variables = {};
|
|
1339
|
+
const variablesStr = url.searchParams.get("variables");
|
|
1340
|
+
if (variablesStr) {
|
|
1341
|
+
try {
|
|
1342
|
+
variables = JSON.parse(variablesStr);
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1240
1345
|
}
|
|
1241
|
-
const parent = dirname(current);
|
|
1242
|
-
if (parent === current) break;
|
|
1243
|
-
current = parent;
|
|
1244
1346
|
}
|
|
1245
|
-
return
|
|
1347
|
+
return { query, variables, operationName: url.searchParams.get("operationName") || void 0 };
|
|
1246
1348
|
}
|
|
1247
|
-
async function
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1349
|
+
async function parseParamsFromPost(req) {
|
|
1350
|
+
try {
|
|
1351
|
+
const body = await req.json();
|
|
1352
|
+
if (!body.query) return null;
|
|
1353
|
+
return { query: body.query, variables: body.variables || {}, operationName: body.operationName };
|
|
1354
|
+
} catch {
|
|
1355
|
+
return null;
|
|
1251
1356
|
}
|
|
1252
|
-
const isBrowser = platform === "browser";
|
|
1253
|
-
await esbuild.build({
|
|
1254
|
-
entryPoints,
|
|
1255
|
-
outdir: outDir,
|
|
1256
|
-
format: "esm",
|
|
1257
|
-
platform: "node",
|
|
1258
|
-
jsx: "automatic",
|
|
1259
|
-
jsxImportSource: "react",
|
|
1260
|
-
bundle: true,
|
|
1261
|
-
external: isBrowser ? void 0 : [
|
|
1262
|
-
"react",
|
|
1263
|
-
"react-dom",
|
|
1264
|
-
"esbuild",
|
|
1265
|
-
"graphql",
|
|
1266
|
-
"ws",
|
|
1267
|
-
"zod",
|
|
1268
|
-
"@graphql-tools/schema",
|
|
1269
|
-
"ai"
|
|
1270
|
-
],
|
|
1271
|
-
write: true,
|
|
1272
|
-
allowOverwrite: true
|
|
1273
|
-
});
|
|
1274
1357
|
}
|
|
1275
|
-
function
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1358
|
+
function buildSchemaFromOptions(options) {
|
|
1359
|
+
if (typeof options.schema === "string") {
|
|
1360
|
+
return options.resolvers ? makeExecutableSchema({ typeDefs: options.schema, resolvers: options.resolvers }) : buildSchema(options.schema);
|
|
1361
|
+
}
|
|
1362
|
+
return options.schema;
|
|
1279
1363
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
});
|
|
1316
|
-
buf = result.outputFiles[0].contents;
|
|
1317
|
-
clientBundleCache.set(key, buf);
|
|
1318
|
-
} catch (err) {
|
|
1319
|
-
console.error("hydration bundle failed:", err);
|
|
1320
|
-
return null;
|
|
1364
|
+
async function executeQuery(schema, params, options, req, ctx) {
|
|
1365
|
+
const contextValue = options.context ? await options.context(req, ctx) : ctx;
|
|
1366
|
+
const result = await executeGraphQL({
|
|
1367
|
+
schema,
|
|
1368
|
+
source: params.query,
|
|
1369
|
+
rootValue: options.rootValue,
|
|
1370
|
+
contextValue,
|
|
1371
|
+
variableValues: params.variables,
|
|
1372
|
+
operationName: params.operationName
|
|
1373
|
+
});
|
|
1374
|
+
return Response.json(result, { status: result.errors ? 400 : 200 });
|
|
1375
|
+
}
|
|
1376
|
+
function graphiqlHTML(endpoint) {
|
|
1377
|
+
return `<!doctype html>
|
|
1378
|
+
<html lang="en">
|
|
1379
|
+
<head>
|
|
1380
|
+
<meta charset="UTF-8" />
|
|
1381
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1382
|
+
<title>GraphiQL</title>
|
|
1383
|
+
<style>body { margin: 0; } #graphiql { height: 100dvh; }</style>
|
|
1384
|
+
<link rel="stylesheet" href="https://esm.sh/graphiql@5.2.2/dist/style.css" />
|
|
1385
|
+
<script type="importmap">
|
|
1386
|
+
{
|
|
1387
|
+
"imports": {
|
|
1388
|
+
"react": "https://esm.sh/react@19.2.5",
|
|
1389
|
+
"react/": "https://esm.sh/react@19.2.5/",
|
|
1390
|
+
"react-dom": "https://esm.sh/react-dom@19.2.5",
|
|
1391
|
+
"react-dom/": "https://esm.sh/react-dom@19.2.5/",
|
|
1392
|
+
"graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
|
|
1393
|
+
"graphiql/": "https://esm.sh/graphiql@5.2.2/",
|
|
1394
|
+
"@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
|
|
1395
|
+
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
|
|
1396
|
+
"graphql": "https://esm.sh/graphql@16.13.2",
|
|
1397
|
+
"@emotion/is-prop-valid": "data:text/javascript,"
|
|
1398
|
+
}
|
|
1321
1399
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1400
|
+
</script>
|
|
1401
|
+
<script type="module">
|
|
1402
|
+
import React from 'react';
|
|
1403
|
+
import ReactDOM from 'react-dom/client';
|
|
1404
|
+
import { GraphiQL } from 'graphiql';
|
|
1405
|
+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
|
1406
|
+
import 'graphiql/setup-workers/esm.sh';
|
|
1407
|
+
|
|
1408
|
+
const fetcher = createGraphiQLFetcher({ url: "${endpoint}" });
|
|
1409
|
+
|
|
1410
|
+
function App() {
|
|
1411
|
+
return React.createElement(GraphiQL, { fetcher });
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const container = document.getElementById('graphiql');
|
|
1415
|
+
const root = ReactDOM.createRoot(container);
|
|
1416
|
+
root.render(React.createElement(App));
|
|
1417
|
+
</script>
|
|
1418
|
+
</head>
|
|
1419
|
+
<body>
|
|
1420
|
+
<div id="graphiql">Loading\u2026</div>
|
|
1421
|
+
</body>
|
|
1422
|
+
</html>`;
|
|
1331
1423
|
}
|
|
1332
|
-
function
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
);
|
|
1424
|
+
function graphql(handler) {
|
|
1425
|
+
const r = new Router();
|
|
1426
|
+
r.get("/", async (req, ctx) => {
|
|
1427
|
+
const options = await handler(req, ctx);
|
|
1428
|
+
const schema = buildSchemaFromOptions(options);
|
|
1429
|
+
const url = new URL(req.url);
|
|
1430
|
+
if (options.graphiql && !url.searchParams.has("query")) {
|
|
1431
|
+
return new Response(graphiqlHTML(url.pathname), {
|
|
1432
|
+
status: 200,
|
|
1433
|
+
headers: { "Content-Type": "text/html" }
|
|
1434
|
+
});
|
|
1343
1435
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
const stream = await renderToReadableStream(element);
|
|
1348
|
-
const body = await readStream(stream);
|
|
1349
|
-
const scripts = [];
|
|
1350
|
-
scripts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(allProps)}</script>`);
|
|
1351
|
-
const bundle = await getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router);
|
|
1352
|
-
if (bundle) {
|
|
1353
|
-
scripts.push(`<script type="module" src="${bundle.url}"></script>`);
|
|
1436
|
+
const params = parseParamsFromGet(url);
|
|
1437
|
+
if (!params) {
|
|
1438
|
+
return Response.json({ errors: [{ message: "Missing query" }] }, { status: 400 });
|
|
1354
1439
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1440
|
+
return executeQuery(schema, params, options, req, ctx);
|
|
1441
|
+
});
|
|
1442
|
+
r.post("/", async (req, ctx) => {
|
|
1443
|
+
const options = await handler(req, ctx);
|
|
1444
|
+
const schema = buildSchemaFromOptions(options);
|
|
1445
|
+
const params = await parseParamsFromPost(req);
|
|
1446
|
+
if (!params) {
|
|
1447
|
+
return Response.json({ errors: [{ message: "Missing query" }] }, { status: 400 });
|
|
1448
|
+
}
|
|
1449
|
+
return executeQuery(schema, params, options, req, ctx);
|
|
1450
|
+
});
|
|
1451
|
+
return r;
|
|
1362
1452
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
continue;
|
|
1453
|
+
|
|
1454
|
+
// ai.ts
|
|
1455
|
+
import { streamText } from "ai";
|
|
1456
|
+
function ai(handler) {
|
|
1457
|
+
const r = new Router();
|
|
1458
|
+
r.post("/", async (req, ctx) => {
|
|
1459
|
+
const options = await handler(req, ctx);
|
|
1460
|
+
const result = streamText(options);
|
|
1461
|
+
return result.toTextStreamResponse();
|
|
1462
|
+
});
|
|
1463
|
+
return r;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// workflow/tool.ts
|
|
1467
|
+
function tool(def) {
|
|
1468
|
+
return {
|
|
1469
|
+
name: def.name ?? "",
|
|
1470
|
+
description: def.description,
|
|
1471
|
+
inputSchema: def.inputSchema,
|
|
1472
|
+
execute: def.execute
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// workflow/reference.ts
|
|
1477
|
+
function getByPath(obj, path) {
|
|
1478
|
+
let current = obj;
|
|
1479
|
+
for (const key of path) {
|
|
1480
|
+
if (current === null || current === void 0) return void 0;
|
|
1481
|
+
if (typeof current === "object" && key in current) {
|
|
1482
|
+
current = current[key];
|
|
1483
|
+
} else {
|
|
1484
|
+
return void 0;
|
|
1396
1485
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1486
|
+
}
|
|
1487
|
+
return current;
|
|
1488
|
+
}
|
|
1489
|
+
function resolveRef(path, ctx) {
|
|
1490
|
+
if (path.startsWith("$nodes.")) {
|
|
1491
|
+
const afterNodes = path.slice(7);
|
|
1492
|
+
const dotIdx = afterNodes.indexOf(".");
|
|
1493
|
+
if (dotIdx === -1) {
|
|
1494
|
+
return ctx.nodeOutputs.get(afterNodes);
|
|
1405
1495
|
}
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1496
|
+
const id2 = afterNodes.slice(0, dotIdx);
|
|
1497
|
+
const propPath = afterNodes.slice(dotIdx + 1);
|
|
1498
|
+
const output = ctx.nodeOutputs.get(id2);
|
|
1499
|
+
if (output === void 0) {
|
|
1500
|
+
throw new Error(`Node "${id2}" has no output yet`);
|
|
1411
1501
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
loadFn,
|
|
1415
|
-
layoutComponents,
|
|
1416
|
-
p.entryPath,
|
|
1417
|
-
p.layouts,
|
|
1418
|
-
pagesDir,
|
|
1419
|
-
router
|
|
1420
|
-
);
|
|
1421
|
-
router.get(p.route, handler);
|
|
1422
|
-
if (p.routePath) {
|
|
1423
|
-
const rUrl = compiledUrl(p.routePath, outDir);
|
|
1424
|
-
const modR = await import(rUrl);
|
|
1425
|
-
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
1426
|
-
for (const method of methods) {
|
|
1427
|
-
if (modR[method]) {
|
|
1428
|
-
router.route(method, p.route, modR[method]);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1502
|
+
if (propPath.startsWith("output")) {
|
|
1503
|
+
return getByPath(output, propPath.slice(7).split(".").filter(Boolean));
|
|
1431
1504
|
}
|
|
1505
|
+
return getByPath(output, propPath.split("."));
|
|
1432
1506
|
}
|
|
1433
|
-
if (
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const nfLayouts = [];
|
|
1438
|
-
const rootLayouts = resolveLayouts(pagesDir, pagesDir);
|
|
1439
|
-
for (const lp of rootLayouts) {
|
|
1440
|
-
const lUrl = compiledUrl(lp, outDir);
|
|
1441
|
-
const modL = await import(lUrl);
|
|
1442
|
-
nfLayouts.push(modL.default);
|
|
1507
|
+
if (path.startsWith("$var.")) {
|
|
1508
|
+
const name = path.slice(5);
|
|
1509
|
+
if (!ctx.variables.has(name)) {
|
|
1510
|
+
throw new Error(`Variable "${name}" is not defined`);
|
|
1443
1511
|
}
|
|
1444
|
-
|
|
1445
|
-
let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
|
|
1446
|
-
for (let i = nfLayouts.length - 1; i >= 0; i--) {
|
|
1447
|
-
element = createElement(nfLayouts[i], { children: element });
|
|
1448
|
-
}
|
|
1449
|
-
element = createElement(TsxContext.Provider, {
|
|
1450
|
-
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
1451
|
-
}, element);
|
|
1452
|
-
const stream = await renderToReadableStream(element);
|
|
1453
|
-
const body = await readStream(stream);
|
|
1454
|
-
const html = `<!DOCTYPE html>
|
|
1455
|
-
${body}`;
|
|
1456
|
-
return new Response(html, {
|
|
1457
|
-
status: 404,
|
|
1458
|
-
headers: { "content-type": "text/html; charset=utf-8" }
|
|
1459
|
-
});
|
|
1460
|
-
};
|
|
1461
|
-
router.all("/*", handler);
|
|
1512
|
+
return ctx.variables.get(name);
|
|
1462
1513
|
}
|
|
1463
|
-
|
|
1514
|
+
if (path.startsWith("$input.")) {
|
|
1515
|
+
const key = path.slice(7);
|
|
1516
|
+
return ctx.input[key];
|
|
1517
|
+
}
|
|
1518
|
+
if (path === "true") return true;
|
|
1519
|
+
if (path === "false") return false;
|
|
1520
|
+
if (path === "null") return null;
|
|
1521
|
+
const num = Number(path);
|
|
1522
|
+
if (!isNaN(num) && path.trim() !== "") return num;
|
|
1523
|
+
return path;
|
|
1464
1524
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`);
|
|
1525
|
+
function resolveValue(v, ctx) {
|
|
1526
|
+
if (typeof v === "string" && v.startsWith("$")) {
|
|
1527
|
+
return resolveRef(v, ctx);
|
|
1528
|
+
}
|
|
1529
|
+
if (Array.isArray(v)) {
|
|
1530
|
+
return v.map((item) => resolveValue(item, ctx));
|
|
1531
|
+
}
|
|
1532
|
+
if (typeof v === "object" && v !== null) {
|
|
1533
|
+
const result = {};
|
|
1534
|
+
for (const [k, val] of Object.entries(v)) {
|
|
1535
|
+
result[k] = resolveValue(val, ctx);
|
|
1477
1536
|
}
|
|
1478
|
-
return
|
|
1479
|
-
}
|
|
1537
|
+
return result;
|
|
1538
|
+
}
|
|
1539
|
+
return v;
|
|
1480
1540
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1541
|
+
|
|
1542
|
+
// workflow/nodes.ts
|
|
1543
|
+
function evaluateExpression(expr, ctx) {
|
|
1544
|
+
const operators = [
|
|
1545
|
+
{ op: "===", fn: (a, b) => a === b },
|
|
1546
|
+
{ op: "!==", fn: (a, b) => a !== b },
|
|
1547
|
+
{ op: ">=", fn: (a, b) => Number(a) >= Number(b) },
|
|
1548
|
+
{ op: "<=", fn: (a, b) => Number(a) <= Number(b) },
|
|
1549
|
+
{ op: ">", fn: (a, b) => Number(a) > Number(b) },
|
|
1550
|
+
{ op: "<", fn: (a, b) => Number(a) < Number(b) },
|
|
1551
|
+
{ op: "==", fn: (a, b) => a == b },
|
|
1552
|
+
{ op: "!=", fn: (a, b) => a != b },
|
|
1553
|
+
{ op: "+", fn: (a, b) => Number(a) + Number(b) },
|
|
1554
|
+
{ op: "-", fn: (a, b) => Number(a) - Number(b) },
|
|
1555
|
+
{ op: "*", fn: (a, b) => Number(a) * Number(b) },
|
|
1556
|
+
{ op: "/", fn: (a, b) => Number(a) / Number(b) },
|
|
1557
|
+
{ op: "%", fn: (a, b) => Number(a) % Number(b) },
|
|
1558
|
+
{ op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
|
|
1559
|
+
{ op: "||", fn: (a, b) => Boolean(a) || Boolean(b) }
|
|
1560
|
+
];
|
|
1561
|
+
for (const { op, fn } of operators) {
|
|
1562
|
+
const idx = expr.indexOf(op);
|
|
1563
|
+
if (idx > 0) {
|
|
1564
|
+
const leftRaw = expr.slice(0, idx).trim();
|
|
1565
|
+
const rightRaw = expr.slice(idx + op.length).trim();
|
|
1566
|
+
const left = resolveValue(leftRaw, ctx);
|
|
1567
|
+
const right = resolveValue(rightRaw, ctx);
|
|
1568
|
+
return fn(left, right);
|
|
1492
1569
|
}
|
|
1493
|
-
const result = opts.origin(requestOrigin);
|
|
1494
|
-
if (typeof result === "boolean") return result ? requestOrigin : "";
|
|
1495
|
-
if (typeof result === "string") return result;
|
|
1496
|
-
return "";
|
|
1497
1570
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1571
|
+
const trimmed = expr.trim();
|
|
1572
|
+
if (trimmed === "true") return true;
|
|
1573
|
+
if (trimmed === "false") return false;
|
|
1574
|
+
if (trimmed === "null") return null;
|
|
1575
|
+
const num = Number(trimmed);
|
|
1576
|
+
if (!isNaN(num) && trimmed !== "") return num;
|
|
1577
|
+
return resolveValue(expr, ctx);
|
|
1578
|
+
}
|
|
1579
|
+
async function executeEval(node, ctx) {
|
|
1580
|
+
const expression = node.input.expression;
|
|
1581
|
+
if (!expression) throw new Error('eval node requires "expression" field');
|
|
1582
|
+
const result = evaluateExpression(expression, ctx);
|
|
1583
|
+
return { result };
|
|
1584
|
+
}
|
|
1585
|
+
async function executeSet(node, ctx) {
|
|
1586
|
+
const name = node.input.name;
|
|
1587
|
+
const value = node.input.value;
|
|
1588
|
+
if (!name) throw new Error('set node requires "name" field');
|
|
1589
|
+
let resolved;
|
|
1590
|
+
if (typeof value === "string") {
|
|
1591
|
+
resolved = evaluateExpression(value, ctx);
|
|
1592
|
+
} else {
|
|
1593
|
+
resolved = resolveValue(value ?? null, ctx);
|
|
1506
1594
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const acao = resolveOrigin(requestOrigin);
|
|
1510
|
-
if (req.method === "OPTIONS" && acao) {
|
|
1511
|
-
const headers = new Headers();
|
|
1512
|
-
headers.set("Access-Control-Allow-Origin", acao);
|
|
1513
|
-
headers.set("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
1514
|
-
headers.set("Access-Control-Allow-Headers", opts.allowedHeaders.join(", "));
|
|
1515
|
-
if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
|
|
1516
|
-
if (opts.maxAge != null) headers.set("Access-Control-Max-Age", String(opts.maxAge));
|
|
1517
|
-
headers.set("Vary", "Origin");
|
|
1518
|
-
return new Response(null, { status: 204, headers });
|
|
1519
|
-
}
|
|
1520
|
-
if (!acao) return next(req, ctx);
|
|
1521
|
-
return Promise.resolve(next(req, ctx)).then((res) => setCORSHeaders(res, acao));
|
|
1522
|
-
};
|
|
1595
|
+
ctx.variables.set(name, resolved);
|
|
1596
|
+
return resolved;
|
|
1523
1597
|
}
|
|
1524
|
-
function
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
const qsToken = url.searchParams.get("access_token");
|
|
1541
|
-
if (qsToken) {
|
|
1542
|
-
token = qsToken;
|
|
1543
|
-
from = "query";
|
|
1598
|
+
async function executeGet(node, ctx) {
|
|
1599
|
+
const name = node.input.name;
|
|
1600
|
+
if (!name) throw new Error('get node requires "name" field');
|
|
1601
|
+
if (!ctx.variables.has(name)) {
|
|
1602
|
+
throw new Error(`Variable "${name}" is not defined`);
|
|
1603
|
+
}
|
|
1604
|
+
return ctx.variables.get(name);
|
|
1605
|
+
}
|
|
1606
|
+
async function executeIf(node, ctx) {
|
|
1607
|
+
const conditions = node.conditions ?? [];
|
|
1608
|
+
for (const condition of conditions) {
|
|
1609
|
+
const test = typeof condition.test === "string" ? Boolean(resolveValue(condition.test, ctx)) : condition.test;
|
|
1610
|
+
if (test && condition.body) {
|
|
1611
|
+
let lastOutput = void 0;
|
|
1612
|
+
for (const bodyNode of condition.body) {
|
|
1613
|
+
lastOutput = await executeNode(bodyNode, ctx);
|
|
1544
1614
|
}
|
|
1615
|
+
return lastOutput;
|
|
1545
1616
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1617
|
+
}
|
|
1618
|
+
return void 0;
|
|
1619
|
+
}
|
|
1620
|
+
async function executeWhile(node, ctx) {
|
|
1621
|
+
const conditionExpr = node.input.condition;
|
|
1622
|
+
if (!conditionExpr) throw new Error('while node requires "condition" field');
|
|
1623
|
+
let lastOutput = void 0;
|
|
1624
|
+
let iterations = 0;
|
|
1625
|
+
const maxIterations = 1e3;
|
|
1626
|
+
while (iterations < maxIterations) {
|
|
1627
|
+
iterations++;
|
|
1628
|
+
ctx.stepCount++;
|
|
1629
|
+
if (ctx.stepCount > ctx.maxSteps) {
|
|
1630
|
+
throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
|
|
1551
1631
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
proxyHeaders[headerName] = header;
|
|
1557
|
-
} else {
|
|
1558
|
-
proxyUrl.searchParams.set("access_token", token);
|
|
1559
|
-
}
|
|
1560
|
-
for (const name of ["x-forwarded-for", "x-real-ip", "user-agent", "content-type"]) {
|
|
1561
|
-
const v = req.headers.get(name);
|
|
1562
|
-
if (v) proxyHeaders[name] = v;
|
|
1563
|
-
}
|
|
1564
|
-
const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders });
|
|
1565
|
-
if (proxyRes.status >= 400) {
|
|
1566
|
-
return new Response(await proxyRes.text() || "Forbidden", { status: proxyRes.status });
|
|
1567
|
-
}
|
|
1568
|
-
let userData = void 0;
|
|
1569
|
-
if (proxyRes.status === 200) {
|
|
1570
|
-
const ct = proxyRes.headers.get("content-type");
|
|
1571
|
-
if (ct?.includes("application/json")) {
|
|
1572
|
-
try {
|
|
1573
|
-
userData = await proxyRes.json();
|
|
1574
|
-
} catch {
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
ctx.user = userData;
|
|
1579
|
-
return next(req, ctx);
|
|
1632
|
+
const condition = Boolean(evaluateExpression(conditionExpr, ctx));
|
|
1633
|
+
if (!condition) break;
|
|
1634
|
+
for (const bodyNode of node.body ?? []) {
|
|
1635
|
+
lastOutput = await executeNode(bodyNode, ctx);
|
|
1580
1636
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1637
|
+
}
|
|
1638
|
+
return lastOutput;
|
|
1639
|
+
}
|
|
1640
|
+
async function executeCall(node, ctx) {
|
|
1641
|
+
const toolName = node.input.tool;
|
|
1642
|
+
const args = node.input.args ?? {};
|
|
1643
|
+
if (toolName && ctx.toolRegistry.has(toolName)) {
|
|
1644
|
+
const tool3 = ctx.toolRegistry.get(toolName);
|
|
1645
|
+
const resolvedInput = resolveValue(args, ctx);
|
|
1646
|
+
const parsed = tool3.inputSchema.parse(resolvedInput);
|
|
1647
|
+
return tool3.execute(parsed, {
|
|
1648
|
+
nodeId: node.id,
|
|
1649
|
+
workflowId: ctx.workflowId,
|
|
1650
|
+
onStream: async (event) => {
|
|
1651
|
+
if (ctx.sseManager && ctx.workflowId) {
|
|
1652
|
+
ctx.sseManager.send(ctx.workflowId, { event: "llm-stream", data: { nodeId: node.id, ...event } });
|
|
1653
|
+
}
|
|
1584
1654
|
}
|
|
1585
|
-
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
const functionName = node.input.function;
|
|
1658
|
+
if (functionName && ctx.functions[functionName]) {
|
|
1659
|
+
const fn = ctx.functions[functionName];
|
|
1660
|
+
const prevFunctions = ctx.functions;
|
|
1661
|
+
const prevInput = ctx.input;
|
|
1662
|
+
ctx.input = resolveValue(args, ctx);
|
|
1663
|
+
let lastOutput = void 0;
|
|
1664
|
+
for (const bodyNode of fn.workflow.nodes) {
|
|
1665
|
+
lastOutput = await executeNode(bodyNode, ctx);
|
|
1586
1666
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1667
|
+
ctx.input = prevInput;
|
|
1668
|
+
return lastOutput;
|
|
1669
|
+
}
|
|
1670
|
+
throw new Error(`call node: tool "${toolName ?? functionName}" not found`);
|
|
1671
|
+
}
|
|
1672
|
+
async function executeHttp(node, ctx) {
|
|
1673
|
+
const input = resolveValue(node.input, ctx);
|
|
1674
|
+
const url = input.url;
|
|
1675
|
+
if (!url) throw new Error('http node requires "url" field');
|
|
1676
|
+
const controller = new AbortController();
|
|
1677
|
+
const timeout = input.timeout ?? 3e4;
|
|
1678
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
1679
|
+
try {
|
|
1680
|
+
const fetchInit = {
|
|
1681
|
+
method: input.method ?? "GET",
|
|
1682
|
+
headers: input.headers ?? {},
|
|
1683
|
+
signal: controller.signal
|
|
1684
|
+
};
|
|
1685
|
+
if (input.body && fetchInit.method !== "GET") {
|
|
1686
|
+
fetchInit.body = JSON.stringify(input.body);
|
|
1596
1687
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1688
|
+
const response = await fetch(url, fetchInit);
|
|
1689
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1690
|
+
const body = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
1691
|
+
return {
|
|
1692
|
+
status: response.status,
|
|
1693
|
+
statusText: response.statusText,
|
|
1694
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1695
|
+
body
|
|
1696
|
+
};
|
|
1697
|
+
} finally {
|
|
1698
|
+
clearTimeout(timer);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
var executors = {
|
|
1702
|
+
eval: executeEval,
|
|
1703
|
+
set: executeSet,
|
|
1704
|
+
get: executeGet,
|
|
1705
|
+
if: executeIf,
|
|
1706
|
+
while: executeWhile,
|
|
1707
|
+
call: executeCall,
|
|
1708
|
+
http: executeHttp
|
|
1709
|
+
};
|
|
1710
|
+
async function executeNode(node, ctx) {
|
|
1711
|
+
const executor = executors[node.tool];
|
|
1712
|
+
if (!executor) {
|
|
1713
|
+
throw new Error(`Unknown node type: "${node.tool}"`);
|
|
1714
|
+
}
|
|
1715
|
+
return executor(node, ctx);
|
|
1599
1716
|
}
|
|
1600
1717
|
|
|
1601
|
-
//
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1718
|
+
// workflow/llm.ts
|
|
1719
|
+
function buildToolsDescription(tools) {
|
|
1720
|
+
return Object.entries(tools).map(([key, t]) => {
|
|
1721
|
+
const name = t.name || key;
|
|
1722
|
+
const schema = t.inputSchema;
|
|
1723
|
+
return `- ${name}: ${t.description}
|
|
1724
|
+
Input schema: describe as JSON object fields`;
|
|
1725
|
+
}).join("\n");
|
|
1726
|
+
}
|
|
1727
|
+
var SYSTEM_PROMPT_TEMPLATE = `You are a workflow generator. Given a user goal and available tools, output a workflow JSON.
|
|
1728
|
+
|
|
1729
|
+
Available tools:
|
|
1730
|
+
{{TOOLS}}
|
|
1731
|
+
|
|
1732
|
+
Workflow format:
|
|
1733
|
+
{
|
|
1734
|
+
"name": "workflow name",
|
|
1735
|
+
"nodes": [
|
|
1736
|
+
{
|
|
1737
|
+
"id": "step1",
|
|
1738
|
+
"tool": "set",
|
|
1739
|
+
"input": { "name": "varName", "value": "initialValue" }
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
"id": "step2",
|
|
1743
|
+
"tool": "call",
|
|
1744
|
+
"input": { "tool": "toolName", "args": { "param1": "$var.varName" } }
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
"id": "step3",
|
|
1748
|
+
"tool": "if",
|
|
1749
|
+
"input": {},
|
|
1750
|
+
"conditions": [
|
|
1751
|
+
{ "test": "$nodes.step2.output.someField", "body": [
|
|
1752
|
+
{ "id": "step4", "tool": "call", "input": { "tool": "toolName", "args": {} } }
|
|
1753
|
+
]}
|
|
1754
|
+
]
|
|
1617
1755
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const stream = fileHandle.readableWebStream();
|
|
1656
|
-
return new Response(stream, { headers });
|
|
1657
|
-
} catch (err) {
|
|
1658
|
-
if (fileHandle) await fileHandle.close().catch(() => {
|
|
1659
|
-
});
|
|
1660
|
-
if (err?.code === "ENOENT") {
|
|
1661
|
-
return new Response("Not Found", { status: 404 });
|
|
1662
|
-
}
|
|
1663
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
1756
|
+
]
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
Node types:
|
|
1760
|
+
- eval: evaluate an expression. input: { expression: "..." }
|
|
1761
|
+
- set: assign a variable. input: { name, value }
|
|
1762
|
+
- get: read a variable. input: { name }
|
|
1763
|
+
- if: conditional branch. input: {}, conditions: [{ test, body }]
|
|
1764
|
+
- while: loop. input: { condition }, body: [nodes]
|
|
1765
|
+
- call: call a registered tool. input: { tool, args }
|
|
1766
|
+
- http: HTTP request. input: { url, method?, headers?, body? }
|
|
1767
|
+
|
|
1768
|
+
Reference syntax:
|
|
1769
|
+
- $var.name - read a variable
|
|
1770
|
+
- $nodes.id.output - output of a previous node
|
|
1771
|
+
- $nodes.id.output.field - specific field of a node's output
|
|
1772
|
+
- $input.field - workflow input parameter
|
|
1773
|
+
|
|
1774
|
+
Output ONLY valid JSON. No explanation, no markdown.`;
|
|
1775
|
+
async function generateWorkflow(goal, tools, generateFn) {
|
|
1776
|
+
const toolsDesc = buildToolsDescription(tools);
|
|
1777
|
+
const system = SYSTEM_PROMPT_TEMPLATE.replace("{{TOOLS}}", toolsDesc);
|
|
1778
|
+
const result = await generateFn({
|
|
1779
|
+
system,
|
|
1780
|
+
messages: [{ role: "user", content: goal }]
|
|
1781
|
+
});
|
|
1782
|
+
const text = result.text.trim();
|
|
1783
|
+
const jsonStart = text.indexOf("{");
|
|
1784
|
+
const jsonEnd = text.lastIndexOf("}");
|
|
1785
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
1786
|
+
throw new Error(`LLM output is not valid JSON: ${text.slice(0, 200)}`);
|
|
1787
|
+
}
|
|
1788
|
+
const jsonStr = text.slice(jsonStart, jsonEnd + 1);
|
|
1789
|
+
try {
|
|
1790
|
+
const workflow2 = JSON.parse(jsonStr);
|
|
1791
|
+
if (!workflow2.nodes || !Array.isArray(workflow2.nodes)) {
|
|
1792
|
+
throw new Error("Generated workflow has no nodes array");
|
|
1664
1793
|
}
|
|
1665
|
-
|
|
1794
|
+
return workflow2;
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
if (err instanceof SyntaxError) {
|
|
1797
|
+
throw new Error(`Failed to parse LLM output as JSON: ${err.message}`);
|
|
1798
|
+
}
|
|
1799
|
+
throw err;
|
|
1800
|
+
}
|
|
1666
1801
|
}
|
|
1667
|
-
var MIME_TYPES = {
|
|
1668
|
-
".html": "text/html; charset=utf-8",
|
|
1669
|
-
".htm": "text/html; charset=utf-8",
|
|
1670
|
-
".css": "text/css; charset=utf-8",
|
|
1671
|
-
".js": "application/javascript; charset=utf-8",
|
|
1672
|
-
".mjs": "application/javascript; charset=utf-8",
|
|
1673
|
-
".json": "application/json",
|
|
1674
|
-
".png": "image/png",
|
|
1675
|
-
".jpg": "image/jpeg",
|
|
1676
|
-
".jpeg": "image/jpeg",
|
|
1677
|
-
".gif": "image/gif",
|
|
1678
|
-
".svg": "image/svg+xml",
|
|
1679
|
-
".ico": "image/x-icon",
|
|
1680
|
-
".webp": "image/webp",
|
|
1681
|
-
".avif": "image/avif",
|
|
1682
|
-
".woff": "font/woff",
|
|
1683
|
-
".woff2": "font/woff2",
|
|
1684
|
-
".ttf": "font/ttf",
|
|
1685
|
-
".otf": "font/otf",
|
|
1686
|
-
".eot": "application/vnd.ms-fontobject",
|
|
1687
|
-
".txt": "text/plain; charset=utf-8",
|
|
1688
|
-
".xml": "application/xml",
|
|
1689
|
-
".pdf": "application/pdf",
|
|
1690
|
-
".zip": "application/zip",
|
|
1691
|
-
".wasm": "application/wasm",
|
|
1692
|
-
".map": "application/json"
|
|
1693
|
-
};
|
|
1694
1802
|
|
|
1695
|
-
//
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1803
|
+
// workflow/engine.ts
|
|
1804
|
+
import { generateText } from "ai";
|
|
1805
|
+
function createWorkflowEngine(options) {
|
|
1806
|
+
const toolRegistry = /* @__PURE__ */ new Map();
|
|
1807
|
+
for (const [key, t] of Object.entries(options.tools)) {
|
|
1808
|
+
t.name = t.name || key;
|
|
1809
|
+
toolRegistry.set(t.name, t);
|
|
1810
|
+
}
|
|
1811
|
+
const states = /* @__PURE__ */ new Map();
|
|
1812
|
+
async function execute(workflow2, opts) {
|
|
1813
|
+
const ctx = {
|
|
1814
|
+
variables: /* @__PURE__ */ new Map(),
|
|
1815
|
+
nodeOutputs: /* @__PURE__ */ new Map(),
|
|
1816
|
+
functions: workflow2.functions ?? {},
|
|
1817
|
+
stepCount: 0,
|
|
1818
|
+
maxSteps: opts?.maxSteps ?? 1e3,
|
|
1819
|
+
input: opts?.initialInput ?? {},
|
|
1820
|
+
toolRegistry,
|
|
1821
|
+
sseManager: options.sseManager,
|
|
1822
|
+
workflowId: opts?.workflowId
|
|
1823
|
+
};
|
|
1824
|
+
let lastOutput = void 0;
|
|
1825
|
+
for (const node of workflow2.nodes) {
|
|
1826
|
+
ctx.stepCount++;
|
|
1827
|
+
if (ctx.stepCount > ctx.maxSteps) {
|
|
1828
|
+
throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
|
|
1709
1829
|
}
|
|
1830
|
+
options.sseManager?.send(ctx.workflowId ?? "", { event: "node-start", data: { nodeId: node.id, tool: node.tool, input: node.input } });
|
|
1831
|
+
const output = await executeNode(node, ctx);
|
|
1832
|
+
ctx.nodeOutputs.set(node.id, output);
|
|
1833
|
+
lastOutput = output;
|
|
1834
|
+
options.sseManager?.send(ctx.workflowId ?? "", { event: "node-end", data: { nodeId: node.id, output } });
|
|
1710
1835
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1836
|
+
return lastOutput;
|
|
1837
|
+
}
|
|
1838
|
+
async function runAsync(workflowId, workflow2, opts) {
|
|
1839
|
+
const state = {
|
|
1840
|
+
workflowId,
|
|
1841
|
+
status: "running",
|
|
1842
|
+
goal: workflow2.name ?? "",
|
|
1843
|
+
startTime: Date.now()
|
|
1844
|
+
};
|
|
1845
|
+
states.set(workflowId, state);
|
|
1846
|
+
const sse = options.sseManager;
|
|
1847
|
+
sse?.send(workflowId, { event: "workflow-start", data: { workflowId, goal: state.goal } });
|
|
1848
|
+
try {
|
|
1849
|
+
const result = await execute(workflow2, { ...opts, workflowId });
|
|
1850
|
+
state.status = "completed";
|
|
1851
|
+
state.result = result;
|
|
1852
|
+
state.endTime = Date.now();
|
|
1853
|
+
sse?.send(workflowId, { event: "complete", data: { result, duration: state.endTime - state.startTime } });
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
state.status = "error";
|
|
1856
|
+
state.error = err instanceof Error ? err.message : String(err);
|
|
1857
|
+
state.endTime = Date.now();
|
|
1858
|
+
sse?.send(workflowId, { event: "error", data: { error: state.error } });
|
|
1859
|
+
} finally {
|
|
1860
|
+
sse?.close(workflowId);
|
|
1721
1861
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1862
|
+
}
|
|
1863
|
+
async function generateWorkflow2(goal) {
|
|
1864
|
+
if (!options.model) {
|
|
1865
|
+
throw new Error('LLM model is required for generateWorkflow. Pass "model" to createWorkflowEngine.');
|
|
1866
|
+
}
|
|
1867
|
+
return generateWorkflow(goal, options.tools, async (prompt) => {
|
|
1868
|
+
const result = await generateText({
|
|
1869
|
+
model: options.model,
|
|
1870
|
+
system: prompt.system,
|
|
1871
|
+
messages: prompt.messages
|
|
1726
1872
|
});
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1873
|
+
return { text: result.text };
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
execute,
|
|
1878
|
+
runAsync,
|
|
1879
|
+
generateWorkflow: generateWorkflow2,
|
|
1880
|
+
getState(workflowId) {
|
|
1881
|
+
return states.get(workflowId);
|
|
1736
1882
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// workflow/sse.ts
|
|
1887
|
+
function createSSEManager() {
|
|
1888
|
+
const streams = /* @__PURE__ */ new Map();
|
|
1889
|
+
const encoder = new TextEncoder();
|
|
1890
|
+
function createStream(workflowId) {
|
|
1891
|
+
const state = {
|
|
1892
|
+
controller: null,
|
|
1893
|
+
encoder,
|
|
1894
|
+
closed: false,
|
|
1895
|
+
buffer: []
|
|
1896
|
+
};
|
|
1897
|
+
const stream = new ReadableStream({
|
|
1898
|
+
start(controller) {
|
|
1899
|
+
state.controller = controller;
|
|
1900
|
+
streams.set(workflowId, state);
|
|
1901
|
+
for (const event of state.buffer) {
|
|
1746
1902
|
try {
|
|
1747
|
-
|
|
1903
|
+
controller.enqueue(encoder.encode(event));
|
|
1748
1904
|
} catch {
|
|
1749
|
-
|
|
1750
|
-
}
|
|
1751
|
-
const result = schemas.body.safeParse(bodyValue);
|
|
1752
|
-
if (result.success) {
|
|
1753
|
-
parsed.body = result.data;
|
|
1754
|
-
} else {
|
|
1755
|
-
issues.push(...result.error.issues.map((i) => ({
|
|
1756
|
-
path: ["body", ...i.path.map(String)],
|
|
1757
|
-
message: i.message
|
|
1758
|
-
})));
|
|
1905
|
+
break;
|
|
1759
1906
|
}
|
|
1760
1907
|
}
|
|
1908
|
+
state.buffer = [];
|
|
1909
|
+
},
|
|
1910
|
+
cancel() {
|
|
1911
|
+
state.closed = true;
|
|
1912
|
+
streams.delete(workflowId);
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
return stream;
|
|
1916
|
+
}
|
|
1917
|
+
function send(workflowId, event) {
|
|
1918
|
+
const state = streams.get(workflowId);
|
|
1919
|
+
if (!state || state.closed) return;
|
|
1920
|
+
const data = `event: ${event.event}
|
|
1921
|
+
data: ${JSON.stringify(event.data)}
|
|
1922
|
+
|
|
1923
|
+
`;
|
|
1924
|
+
if (state.controller) {
|
|
1925
|
+
try {
|
|
1926
|
+
state.controller.enqueue(encoder.encode(data));
|
|
1927
|
+
} catch {
|
|
1928
|
+
state.closed = true;
|
|
1929
|
+
streams.delete(workflowId);
|
|
1930
|
+
}
|
|
1931
|
+
} else {
|
|
1932
|
+
state.buffer.push(data);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
function close(workflowId) {
|
|
1936
|
+
const state = streams.get(workflowId);
|
|
1937
|
+
if (!state) return;
|
|
1938
|
+
state.closed = true;
|
|
1939
|
+
streams.delete(workflowId);
|
|
1940
|
+
try {
|
|
1941
|
+
state.controller?.close();
|
|
1942
|
+
} catch {
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return { createStream, send, close };
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// workflow/route.ts
|
|
1949
|
+
function workflow(handler) {
|
|
1950
|
+
const r = new Router();
|
|
1951
|
+
const sseManager = createSSEManager();
|
|
1952
|
+
r.get("/:workflowId/events", async (req, ctx) => {
|
|
1953
|
+
const stream = sseManager.createStream(ctx.params.workflowId);
|
|
1954
|
+
return new Response(stream, {
|
|
1955
|
+
headers: {
|
|
1956
|
+
"Content-Type": "text/event-stream",
|
|
1957
|
+
"Cache-Control": "no-cache",
|
|
1958
|
+
"Connection": "keep-alive"
|
|
1761
1959
|
}
|
|
1960
|
+
});
|
|
1961
|
+
});
|
|
1962
|
+
r.post("/", async (req, ctx) => {
|
|
1963
|
+
const options = await handler(req, ctx);
|
|
1964
|
+
const engine = createWorkflowEngine({
|
|
1965
|
+
tools: options.tools,
|
|
1966
|
+
model: options.model,
|
|
1967
|
+
sseManager: options.stream ? sseManager : void 0
|
|
1968
|
+
});
|
|
1969
|
+
const body = await req.json();
|
|
1970
|
+
let wf;
|
|
1971
|
+
if (body.goal && options.model) {
|
|
1972
|
+
wf = await engine.generateWorkflow(body.goal);
|
|
1973
|
+
} else if (body.workflow) {
|
|
1974
|
+
wf = body.workflow;
|
|
1975
|
+
} else if (body.nodes) {
|
|
1976
|
+
wf = { nodes: body.nodes };
|
|
1977
|
+
} else {
|
|
1978
|
+
return Response.json(
|
|
1979
|
+
{ error: 'Provide "goal" (with model) or "workflow"/"nodes"' },
|
|
1980
|
+
{ status: 400 }
|
|
1981
|
+
);
|
|
1762
1982
|
}
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1983
|
+
if (options.stream && sseManager) {
|
|
1984
|
+
const workflowId = crypto.randomUUID();
|
|
1985
|
+
engine.runAsync(workflowId, wf);
|
|
1986
|
+
return Response.json({ workflowId, eventsUrl: `/${workflowId}/events` });
|
|
1765
1987
|
}
|
|
1766
|
-
|
|
1767
|
-
return
|
|
1768
|
-
};
|
|
1988
|
+
const result = await engine.execute(wf);
|
|
1989
|
+
return Response.json({ workflow: wf, result });
|
|
1990
|
+
});
|
|
1991
|
+
return r;
|
|
1769
1992
|
}
|
|
1770
1993
|
|
|
1771
|
-
//
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1994
|
+
// postgres/client.ts
|
|
1995
|
+
import postgresFactory from "postgres";
|
|
1996
|
+
|
|
1997
|
+
// postgres/table.ts
|
|
1998
|
+
import { z } from "zod";
|
|
1999
|
+
function unwrap(field) {
|
|
2000
|
+
let inner = field;
|
|
2001
|
+
while (inner instanceof z.ZodOptional || inner instanceof z.ZodNullable || inner instanceof z.ZodDefault || (inner.constructor.name === "ZodTransform" || inner._def?.type === "transform")) {
|
|
2002
|
+
if (inner._def?.innerType) {
|
|
2003
|
+
inner = inner._def.innerType;
|
|
2004
|
+
} else {
|
|
2005
|
+
break;
|
|
1783
2006
|
}
|
|
1784
2007
|
}
|
|
1785
|
-
return
|
|
2008
|
+
return inner;
|
|
1786
2009
|
}
|
|
1787
|
-
function
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2010
|
+
function isOptional(field) {
|
|
2011
|
+
let inner = field;
|
|
2012
|
+
while (true) {
|
|
2013
|
+
if (inner instanceof z.ZodOptional) return true;
|
|
2014
|
+
if (inner instanceof z.ZodNullable) return true;
|
|
2015
|
+
if (inner instanceof z.ZodDefault) {
|
|
2016
|
+
inner = inner._def.innerType;
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
if (inner._def?.type === "transform") {
|
|
2020
|
+
inner = inner._def.innerType;
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
1797
2026
|
}
|
|
1798
|
-
function
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
return new Response(res.body, {
|
|
1802
|
-
status: res.status,
|
|
1803
|
-
statusText: res.statusText,
|
|
1804
|
-
headers
|
|
1805
|
-
});
|
|
2027
|
+
function hasUUIDCheck(field) {
|
|
2028
|
+
const checks = field._def?.checks ?? [];
|
|
2029
|
+
return checks.some((c) => c.type === "string" && c.def?.format === "uuid");
|
|
1806
2030
|
}
|
|
1807
|
-
function
|
|
1808
|
-
const
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2031
|
+
function detectSqlType(name, field, isPk) {
|
|
2032
|
+
const inner = unwrap(field);
|
|
2033
|
+
const nullable = isOptional(field);
|
|
2034
|
+
const autoGenerate = isPk && name === "id";
|
|
2035
|
+
if (isPk && name === "id" && inner instanceof z.ZodNumber) {
|
|
2036
|
+
return { sqlType: "SERIAL", nullable: false, defaultExpr: null, autoGenerate: true };
|
|
2037
|
+
}
|
|
2038
|
+
if (isPk && name === "id" && typeof BigInt !== "undefined" && inner instanceof z.ZodBigInt) {
|
|
2039
|
+
return { sqlType: "BIGSERIAL", nullable: false, defaultExpr: null, autoGenerate: true };
|
|
2040
|
+
}
|
|
2041
|
+
if (isPk && name === "id" && inner instanceof z.ZodString) {
|
|
2042
|
+
if (hasUUIDCheck(inner)) {
|
|
2043
|
+
return { sqlType: "UUID", nullable: false, defaultExpr: "gen_random_uuid()", autoGenerate: true };
|
|
2044
|
+
}
|
|
2045
|
+
return { sqlType: "TEXT", nullable: false, defaultExpr: null, autoGenerate: false };
|
|
2046
|
+
}
|
|
2047
|
+
let sqlType;
|
|
2048
|
+
if (inner instanceof z.ZodNumber) {
|
|
2049
|
+
sqlType = "INTEGER";
|
|
2050
|
+
} else if (inner instanceof z.ZodString) {
|
|
2051
|
+
sqlType = "TEXT";
|
|
2052
|
+
} else if (inner instanceof z.ZodBoolean) {
|
|
2053
|
+
sqlType = "BOOLEAN";
|
|
2054
|
+
} else if (inner instanceof z.ZodDate) {
|
|
2055
|
+
sqlType = "TIMESTAMPTZ";
|
|
2056
|
+
} else if (inner instanceof z.ZodEnum) {
|
|
2057
|
+
sqlType = "TEXT";
|
|
2058
|
+
} else if (inner instanceof z.ZodArray) {
|
|
2059
|
+
sqlType = "JSONB";
|
|
2060
|
+
} else if (inner instanceof z.ZodObject) {
|
|
2061
|
+
sqlType = "JSONB";
|
|
2062
|
+
} else {
|
|
2063
|
+
sqlType = "TEXT";
|
|
2064
|
+
}
|
|
2065
|
+
return { sqlType, nullable: nullable || autoGenerate, defaultExpr: null, autoGenerate };
|
|
2066
|
+
}
|
|
2067
|
+
function parseColumns(schema) {
|
|
2068
|
+
const pkField = Object.keys(schema).find((k) => k === "id");
|
|
2069
|
+
return Object.entries(schema).map(([name, field]) => {
|
|
2070
|
+
const isPk = name === pkField;
|
|
2071
|
+
const { sqlType, nullable, defaultExpr, autoGenerate } = detectSqlType(name, field, isPk);
|
|
2072
|
+
return { name, sqlType, nullable: nullable || autoGenerate, isPrimaryKey: isPk, defaultExpr, autoGenerate };
|
|
1814
2073
|
});
|
|
1815
2074
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
const boundary = match[1] ?? match[2];
|
|
1833
|
-
const body = await req.text();
|
|
1834
|
-
const rawParts = body.split(`--${boundary}`).filter((p) => p && !p.startsWith("--") && !p.startsWith("\r\n--"));
|
|
1835
|
-
const files = {};
|
|
1836
|
-
const fields = {};
|
|
1837
|
-
for (const raw of rawParts) {
|
|
1838
|
-
const trimmed = raw.replace(/^\r?\n/, "");
|
|
1839
|
-
const lines = trimmed.split(/\r?\n/);
|
|
1840
|
-
let i = 0;
|
|
1841
|
-
const headers = {};
|
|
1842
|
-
while (i < lines.length && lines[i].length > 0) {
|
|
1843
|
-
const sep3 = lines[i].indexOf(": ");
|
|
1844
|
-
if (sep3 !== -1) headers[lines[i].slice(0, sep3).toLowerCase()] = lines[i].slice(sep3 + 2);
|
|
1845
|
-
i++;
|
|
2075
|
+
function buildGet(sql, name, pk) {
|
|
2076
|
+
return async function get(id2) {
|
|
2077
|
+
if (!pk) throw new Error(`Table "${name}" has no primary key`);
|
|
2078
|
+
const [row] = await sql`SELECT * FROM ${sql(name)} WHERE ${sql(pk.name)} = ${id2} LIMIT 1`;
|
|
2079
|
+
return row ?? void 0;
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
function buildList(sql, name, columns) {
|
|
2083
|
+
return async function list(filter = {}, opts = {}) {
|
|
2084
|
+
const colNames = new Set(columns.map((c) => c.name));
|
|
2085
|
+
const whereClauses = [];
|
|
2086
|
+
const whereValues = [];
|
|
2087
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
2088
|
+
if (colNames.has(key)) {
|
|
2089
|
+
whereClauses.push(`"${key}" = $${whereValues.length + 1}`);
|
|
2090
|
+
whereValues.push(value);
|
|
1846
2091
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
const filename = filenameMatch?.[1];
|
|
1855
|
-
if (filename) {
|
|
1856
|
-
const buf = Buffer.from(bodyValue.replace(/\r?\n$/, ""), "binary");
|
|
1857
|
-
if (options?.allowedTypes) {
|
|
1858
|
-
const mime = headers["content-type"] ?? "application/octet-stream";
|
|
1859
|
-
if (!options.allowedTypes.includes(mime)) {
|
|
1860
|
-
return Response.json({ error: `File type not allowed: ${mime}` }, { status: 415 });
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
if (options?.maxFileSize && buf.byteLength > options.maxFileSize) {
|
|
1864
|
-
return Response.json({ error: `File too large: ${filename}` }, { status: 413 });
|
|
1865
|
-
}
|
|
1866
|
-
const uf = {
|
|
1867
|
-
name: filename,
|
|
1868
|
-
type: headers["content-type"] ?? "application/octet-stream",
|
|
1869
|
-
size: buf.byteLength,
|
|
1870
|
-
buffer: saveDir ? void 0 : buf
|
|
1871
|
-
};
|
|
1872
|
-
if (saveDir) {
|
|
1873
|
-
const filePath = join2(saveDir, `${randomUUID()}-${filename}`);
|
|
1874
|
-
await mkdir(saveDir, { recursive: true });
|
|
1875
|
-
await writeFile(filePath, buf);
|
|
1876
|
-
uf.path = filePath;
|
|
1877
|
-
}
|
|
1878
|
-
if (files[name]) {
|
|
1879
|
-
const existing = files[name];
|
|
1880
|
-
files[name] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
|
|
1881
|
-
} else {
|
|
1882
|
-
files[name] = uf;
|
|
2092
|
+
}
|
|
2093
|
+
const where = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
2094
|
+
const sortClauses = [];
|
|
2095
|
+
if (opts.sort) {
|
|
2096
|
+
for (const [key, dir] of Object.entries(opts.sort)) {
|
|
2097
|
+
if (colNames.has(key)) {
|
|
2098
|
+
sortClauses.push(`"${key}" ${dir.toUpperCase()}`);
|
|
1883
2099
|
}
|
|
1884
|
-
} else {
|
|
1885
|
-
fields[name] = bodyValue.replace(/\r?\n$/, "");
|
|
1886
2100
|
}
|
|
1887
2101
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
2102
|
+
const orderBy = sortClauses.length > 0 ? `ORDER BY ${sortClauses.join(", ")}` : "";
|
|
2103
|
+
const limitClause = opts.limit != null ? `LIMIT ${opts.limit}` : "";
|
|
2104
|
+
const offsetClause = opts.offset != null ? `OFFSET ${opts.offset}` : "";
|
|
2105
|
+
const [rows, countResult] = await Promise.all([
|
|
2106
|
+
sql.unsafe(`SELECT * FROM "${name}" ${where} ${orderBy} ${limitClause} ${offsetClause}`.trim(), whereValues),
|
|
2107
|
+
sql.unsafe(`SELECT count(*) as count FROM "${name}" ${where}`.trim(), whereValues)
|
|
2108
|
+
]);
|
|
2109
|
+
return { rows, count: Number(countResult[0]?.count ?? 0) };
|
|
1890
2110
|
};
|
|
1891
2111
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
const
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1937
|
-
const res = await next(req, ctx);
|
|
1938
|
-
const headers = new Headers(res.headers);
|
|
1939
|
-
headers.set("X-RateLimit-Limit", String(max));
|
|
1940
|
-
headers.set("X-RateLimit-Remaining", String(remaining));
|
|
1941
|
-
headers.set("X-RateLimit-Reset", String(Math.ceil(entry.reset / 1e3)));
|
|
1942
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
2112
|
+
function buildCreate(sql, name, pk, zodSchema) {
|
|
2113
|
+
return async function create(data) {
|
|
2114
|
+
const validated = zodSchema.parse(data);
|
|
2115
|
+
if (pk?.autoGenerate) {
|
|
2116
|
+
delete validated[pk.name];
|
|
2117
|
+
}
|
|
2118
|
+
const [row] = await sql`INSERT INTO ${sql(name)} ${sql(validated)} RETURNING *`;
|
|
2119
|
+
return row;
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
function buildPatch(sql, name, pk, zodSchema) {
|
|
2123
|
+
return async function patch(id2, data) {
|
|
2124
|
+
if (!pk) throw new Error(`Table "${name}" has no primary key`);
|
|
2125
|
+
const validated = zodSchema.partial().parse(data);
|
|
2126
|
+
delete validated[pk.name];
|
|
2127
|
+
if (Object.keys(validated).length === 0) {
|
|
2128
|
+
const [row2] = await sql`SELECT * FROM ${sql(name)} WHERE ${sql(pk.name)} = ${id2} LIMIT 1`;
|
|
2129
|
+
return row2 ?? void 0;
|
|
2130
|
+
}
|
|
2131
|
+
const [row] = await sql`UPDATE ${sql(name)} SET ${sql(validated)} WHERE ${sql(pk.name)} = ${id2} RETURNING *`;
|
|
2132
|
+
return row ?? void 0;
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function buildRemove(sql, name, pk) {
|
|
2136
|
+
return async function remove(id2) {
|
|
2137
|
+
if (!pk) throw new Error(`Table "${name}" has no primary key`);
|
|
2138
|
+
const rows = await sql`DELETE FROM ${sql(name)} WHERE ${sql(pk.name)} = ${id2} RETURNING 1`;
|
|
2139
|
+
return rows.length > 0;
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function buildTable(sql, tables) {
|
|
2143
|
+
return function table(name, schema) {
|
|
2144
|
+
const zodSchema = z.object(schema);
|
|
2145
|
+
const columns = parseColumns(schema);
|
|
2146
|
+
const pk = columns.find((c) => c.isPrimaryKey) ?? void 0;
|
|
2147
|
+
tables.push({ name, columns });
|
|
2148
|
+
return {
|
|
2149
|
+
$type: void 0,
|
|
2150
|
+
$insert: void 0,
|
|
2151
|
+
get: buildGet(sql, name, pk),
|
|
2152
|
+
list: buildList(sql, name, columns),
|
|
2153
|
+
create: buildCreate(sql, name, pk, zodSchema),
|
|
2154
|
+
patch: buildPatch(sql, name, pk, zodSchema),
|
|
2155
|
+
remove: buildRemove(sql, name, pk)
|
|
2156
|
+
};
|
|
1943
2157
|
};
|
|
1944
2158
|
}
|
|
1945
2159
|
|
|
1946
|
-
//
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
let compressed;
|
|
1972
|
-
let encoding;
|
|
1973
|
-
if (useBrotli) {
|
|
1974
|
-
compressed = brotliCompressSync(body, {
|
|
1975
|
-
params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) }
|
|
1976
|
-
});
|
|
1977
|
-
encoding = "br";
|
|
1978
|
-
} else if (useGzip) {
|
|
1979
|
-
compressed = gzipSync(body, { level: Math.min(level, 9) });
|
|
1980
|
-
encoding = "gzip";
|
|
2160
|
+
// postgres/migrate.ts
|
|
2161
|
+
function toDDL(col) {
|
|
2162
|
+
const parts = [`"${col.name}"`, col.sqlType];
|
|
2163
|
+
if (col.isPrimaryKey) parts.push("PRIMARY KEY");
|
|
2164
|
+
if (!col.isPrimaryKey && !col.nullable) parts.push("NOT NULL");
|
|
2165
|
+
if (col.defaultExpr) parts.push(`DEFAULT ${col.defaultExpr}`);
|
|
2166
|
+
return parts.join(" ");
|
|
2167
|
+
}
|
|
2168
|
+
function createTableSQL(name, columns) {
|
|
2169
|
+
const cols = columns.map(toDDL);
|
|
2170
|
+
return `CREATE TABLE IF NOT EXISTS "${name}" (
|
|
2171
|
+
${cols.join(",\n ")}
|
|
2172
|
+
)`;
|
|
2173
|
+
}
|
|
2174
|
+
function addColumnSQL(name, col) {
|
|
2175
|
+
return `ALTER TABLE "${name}" ADD COLUMN IF NOT EXISTS ${toDDL(col)}`;
|
|
2176
|
+
}
|
|
2177
|
+
async function runMigrations(sql, tables) {
|
|
2178
|
+
for (const table of tables) {
|
|
2179
|
+
const existing = await sql`
|
|
2180
|
+
SELECT column_name FROM information_schema.columns
|
|
2181
|
+
WHERE table_schema = 'public' AND table_name = ${table.name}
|
|
2182
|
+
`;
|
|
2183
|
+
if (existing.length === 0) {
|
|
2184
|
+
await sql.unsafe(createTableSQL(table.name, table.columns));
|
|
1981
2185
|
} else {
|
|
1982
|
-
|
|
1983
|
-
|
|
2186
|
+
const colNames = new Set(existing.map((r) => r.column_name));
|
|
2187
|
+
for (const col of table.columns) {
|
|
2188
|
+
if (!colNames.has(col.name)) {
|
|
2189
|
+
await sql.unsafe(addColumnSQL(table.name, col));
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
1984
2192
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// postgres/client.ts
|
|
2197
|
+
function postgres(opts) {
|
|
2198
|
+
const options = typeof opts === "string" ? { connection: opts } : opts ?? {};
|
|
2199
|
+
const connection = options.connection ?? process.env.DATABASE_URL;
|
|
2200
|
+
if (!connection) {
|
|
2201
|
+
throw new Error(
|
|
2202
|
+
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
const sql = postgresFactory(connection);
|
|
2206
|
+
const tables = [];
|
|
2207
|
+
if (options.signal) {
|
|
2208
|
+
options.signal.addEventListener("abort", () => {
|
|
2209
|
+
sql.end();
|
|
2210
|
+
}, { once: true });
|
|
2211
|
+
}
|
|
2212
|
+
const mw = ((req, ctx, next) => {
|
|
2213
|
+
ctx.sql = sql;
|
|
2214
|
+
return next(req, ctx);
|
|
2215
|
+
});
|
|
2216
|
+
mw.sql = sql;
|
|
2217
|
+
mw.table = buildTable(sql, tables);
|
|
2218
|
+
mw.migrate = () => runMigrations(sql, tables);
|
|
2219
|
+
mw.close = () => sql.end({ timeout: 5 });
|
|
2220
|
+
return mw;
|
|
1996
2221
|
}
|
|
1997
2222
|
export {
|
|
1998
2223
|
Router,
|
|
1999
2224
|
TsxContext,
|
|
2225
|
+
ai,
|
|
2000
2226
|
auth,
|
|
2001
2227
|
compress,
|
|
2002
2228
|
cors,
|
|
@@ -2005,7 +2231,9 @@ export {
|
|
|
2005
2231
|
deleteCookie,
|
|
2006
2232
|
generateWorkflow,
|
|
2007
2233
|
getCookies,
|
|
2234
|
+
graphql,
|
|
2008
2235
|
logger,
|
|
2236
|
+
postgres,
|
|
2009
2237
|
rateLimit,
|
|
2010
2238
|
serve,
|
|
2011
2239
|
serveStatic,
|
|
@@ -2014,5 +2242,6 @@ export {
|
|
|
2014
2242
|
tsx,
|
|
2015
2243
|
upload,
|
|
2016
2244
|
useTsx,
|
|
2017
|
-
validate
|
|
2245
|
+
validate,
|
|
2246
|
+
workflow
|
|
2018
2247
|
};
|