weifuwu 0.22.2 → 0.23.0
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 +521 -76
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +112 -0
- package/cli/template/app.ts +3 -2
- package/cli/template/index.ts +2 -1
- package/dist/agent/run.d.ts +4 -3
- package/dist/agent/types.d.ts +3 -0
- package/dist/ai/provider.d.ts +36 -0
- package/dist/ai/utils.d.ts +5 -0
- package/dist/ai/workflow.d.ts +3 -0
- package/dist/ai.d.ts +9 -1
- package/dist/auth.d.ts +14 -0
- package/dist/client-locale.d.ts +1 -1
- package/dist/client-router.d.ts +3 -3
- package/dist/client-theme.d.ts +1 -1
- package/dist/compile.d.ts +6 -0
- package/dist/cron-utils.d.ts +8 -0
- package/dist/flash.d.ts +24 -0
- package/dist/i18n.d.ts +14 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1597 -495
- package/dist/kb/index.d.ts +3 -0
- package/dist/kb/types.d.ts +64 -0
- package/dist/kb.d.ts +70 -0
- package/dist/oauth-client.d.ts +41 -0
- package/dist/permissions.d.ts +49 -0
- package/dist/queue/types.d.ts +12 -6
- package/dist/react.d.ts +1 -1
- package/dist/react.js +91 -86
- package/dist/s3.d.ts +68 -0
- package/dist/serve.d.ts +1 -1
- package/dist/session.d.ts +12 -1
- package/dist/ssr.d.ts +0 -1
- package/dist/stream.d.ts +5 -5
- package/dist/theme.d.ts +8 -0
- package/dist/tsx-context.d.ts +7 -1
- package/dist/types.d.ts +5 -3
- package/dist/user/index.d.ts +1 -1
- package/dist/user/oauth-login.d.ts +21 -0
- package/dist/user/types.d.ts +31 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -186,7 +186,14 @@ function serve(handler, options) {
|
|
|
186
186
|
if (shuttingDown) return;
|
|
187
187
|
shuttingDown = true;
|
|
188
188
|
server.close();
|
|
189
|
-
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
server.closeAllConnections();
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}, 1e4);
|
|
193
|
+
server.on("close", () => {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
});
|
|
190
197
|
};
|
|
191
198
|
shutdownHandler = shutdown;
|
|
192
199
|
process.on("SIGTERM", shutdown);
|
|
@@ -232,7 +239,7 @@ function serve(handler, options) {
|
|
|
232
239
|
console.log(`weifuwu listening on http://${displayHost}:${_cachedPort}`);
|
|
233
240
|
});
|
|
234
241
|
return {
|
|
235
|
-
stop: () => {
|
|
242
|
+
stop: (timeoutMs = 1e4) => {
|
|
236
243
|
if (shutdownHandler) {
|
|
237
244
|
process.off("SIGTERM", shutdownHandler);
|
|
238
245
|
process.off("SIGINT", shutdownHandler);
|
|
@@ -243,9 +250,16 @@ function serve(handler, options) {
|
|
|
243
250
|
resolve14();
|
|
244
251
|
return;
|
|
245
252
|
}
|
|
253
|
+
server.close();
|
|
246
254
|
server.closeIdleConnections();
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
const timer = setTimeout(() => {
|
|
256
|
+
server.closeAllConnections();
|
|
257
|
+
resolve14();
|
|
258
|
+
}, timeoutMs);
|
|
259
|
+
server.on("close", () => {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
resolve14();
|
|
262
|
+
});
|
|
249
263
|
});
|
|
250
264
|
},
|
|
251
265
|
ready,
|
|
@@ -837,7 +851,7 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
837
851
|
|
|
838
852
|
// tsx-context.ts
|
|
839
853
|
import { useSyncExternalStore, createContext } from "react";
|
|
840
|
-
var DEFAULT_CTX = { params: {}, query: {}, parsed: {},
|
|
854
|
+
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, loaderData: {}, env: {}, user: {} };
|
|
841
855
|
var KEY = "__WEIFUWU_CTX_STORE";
|
|
842
856
|
function getStore() {
|
|
843
857
|
if (typeof globalThis !== "undefined" && globalThis[KEY]) {
|
|
@@ -845,8 +859,9 @@ function getStore() {
|
|
|
845
859
|
}
|
|
846
860
|
const s = {
|
|
847
861
|
_ctx: DEFAULT_CTX,
|
|
848
|
-
_snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed,
|
|
862
|
+
_snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed, theme: DEFAULT_CTX.theme, i18n: DEFAULT_CTX.i18n, loaderData: DEFAULT_CTX.loaderData, env: DEFAULT_CTX.env },
|
|
849
863
|
_listeners: /* @__PURE__ */ new Set(),
|
|
864
|
+
_rebuilders: [],
|
|
850
865
|
_alsGetStore: null
|
|
851
866
|
};
|
|
852
867
|
if (typeof globalThis !== "undefined") {
|
|
@@ -859,8 +874,18 @@ function __registerAls(getStore2) {
|
|
|
859
874
|
store._alsGetStore = getStore2;
|
|
860
875
|
}
|
|
861
876
|
function setCtx(value) {
|
|
877
|
+
if (typeof window !== "undefined") {
|
|
878
|
+
for (const r of store._rebuilders) {
|
|
879
|
+
const rebuilt = r(value);
|
|
880
|
+
if (rebuilt) Object.assign(value, rebuilt);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
862
883
|
store._ctx = { ...store._ctx, ...value };
|
|
863
|
-
store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed,
|
|
884
|
+
store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, theme: store._ctx.theme, i18n: store._ctx.i18n, loaderData: store._ctx.loaderData, env: store._ctx.env };
|
|
885
|
+
if (typeof window !== "undefined") {
|
|
886
|
+
;
|
|
887
|
+
window.__WEIFUWU_CTX = { ...window.__WEIFUWU_CTX, ...value };
|
|
888
|
+
}
|
|
864
889
|
store._listeners.forEach((fn) => fn());
|
|
865
890
|
}
|
|
866
891
|
var TsxContext = createContext(DEFAULT_CTX);
|
|
@@ -965,10 +990,30 @@ function cors(options) {
|
|
|
965
990
|
|
|
966
991
|
// auth.ts
|
|
967
992
|
function auth(options) {
|
|
968
|
-
if (!options.token && !options.verify && !options.proxy) {
|
|
969
|
-
throw new Error("auth() requires at least one of: token, verify, or
|
|
993
|
+
if (!options.token && !options.verify && !options.proxy && !options.session) {
|
|
994
|
+
throw new Error("auth() requires at least one of: token, verify, proxy, or session");
|
|
970
995
|
}
|
|
971
996
|
return async (req, ctx, next) => {
|
|
997
|
+
if (options.session) {
|
|
998
|
+
const sessionUserId = ctx.session?.userId;
|
|
999
|
+
if (sessionUserId !== void 0 && sessionUserId !== null) {
|
|
1000
|
+
if (options.resolveUser) {
|
|
1001
|
+
const userData = await options.resolveUser(sessionUserId);
|
|
1002
|
+
if (userData) {
|
|
1003
|
+
ctx.user = userData;
|
|
1004
|
+
return next(req, ctx);
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof ctx.session?.destroy === "function") {
|
|
1007
|
+
;
|
|
1008
|
+
ctx.session.destroy();
|
|
1009
|
+
}
|
|
1010
|
+
console.warn(`[${currentTraceId()}] auth: session userId ${sessionUserId} resolved to null`);
|
|
1011
|
+
} else {
|
|
1012
|
+
ctx.user = { id: sessionUserId };
|
|
1013
|
+
return next(req, ctx);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
972
1017
|
const headerName = options.header ?? "Authorization";
|
|
973
1018
|
let from = "header";
|
|
974
1019
|
let header = req.headers.get(headerName);
|
|
@@ -2093,18 +2138,21 @@ async function getStreamObject() {
|
|
|
2093
2138
|
if (!_ai.streamObject) _ai.streamObject = (await import("ai")).streamObject;
|
|
2094
2139
|
return _ai.streamObject;
|
|
2095
2140
|
}
|
|
2096
|
-
async function aiStream(handler) {
|
|
2141
|
+
async function aiStream(handler, provider) {
|
|
2097
2142
|
const r = new Router();
|
|
2098
2143
|
r.post("/", async (req, ctx) => {
|
|
2099
2144
|
const options = await handler(req, ctx);
|
|
2145
|
+
if (provider && !options.model) {
|
|
2146
|
+
options.model = provider.model();
|
|
2147
|
+
}
|
|
2100
2148
|
if (options.schema) {
|
|
2101
2149
|
const streamObject2 = await getStreamObject();
|
|
2102
2150
|
const { schema, ...params } = options;
|
|
2103
2151
|
const result2 = streamObject2({ ...params, schema, output: "object" });
|
|
2104
2152
|
return result2.toTextStreamResponse();
|
|
2105
2153
|
}
|
|
2106
|
-
const
|
|
2107
|
-
const result =
|
|
2154
|
+
const streamText3 = await getStreamText();
|
|
2155
|
+
const result = streamText3(options);
|
|
2108
2156
|
return result.toTextStreamResponse();
|
|
2109
2157
|
});
|
|
2110
2158
|
return r;
|
|
@@ -2316,22 +2364,21 @@ function runWorkflow(opts = {}) {
|
|
|
2316
2364
|
let nodes;
|
|
2317
2365
|
if (input.nodes && input.nodes.length > 0) {
|
|
2318
2366
|
nodes = input.nodes;
|
|
2319
|
-
} else
|
|
2367
|
+
} else {
|
|
2368
|
+
if (!opts.provider && !opts.model) throw new Error('Provide either "nodes", a "model", or a "provider" with a model to generate the workflow from "goal"');
|
|
2320
2369
|
const toolsDesc = Object.entries(opts.tools ?? {}).map(([k, t]) => `- ${k}: ${t.description}`).join("\n");
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
messages: [{ role: "user", content: input.goal }]
|
|
2334
|
-
});
|
|
2370
|
+
const system = [
|
|
2371
|
+
"You are a workflow generator. Given a user goal and available tools, output a workflow JSON.",
|
|
2372
|
+
"",
|
|
2373
|
+
"Available tools:",
|
|
2374
|
+
toolsDesc,
|
|
2375
|
+
"",
|
|
2376
|
+
"Node types: eval (expression), set (variable), get (variable), if (condition), while (loop), call (tool), http (request).",
|
|
2377
|
+
"Reference syntax: $var.name, $nodes.id.output, $nodes.id.output.field, $input.field",
|
|
2378
|
+
"Output ONLY valid JSON. No explanation, no markdown."
|
|
2379
|
+
].filter(Boolean).join("\n");
|
|
2380
|
+
const genParams = { system, messages: [{ role: "user", content: input.goal }] };
|
|
2381
|
+
const result = opts.provider ? await opts.provider.generateText(genParams) : await generateText({ ...genParams, model: opts.model });
|
|
2335
2382
|
const text2 = result.text.trim();
|
|
2336
2383
|
const jsonStart = text2.indexOf("{");
|
|
2337
2384
|
const jsonEnd = text2.lastIndexOf("}");
|
|
@@ -2339,8 +2386,6 @@ function runWorkflow(opts = {}) {
|
|
|
2339
2386
|
const parsed = JSON.parse(text2.slice(jsonStart, jsonEnd + 1));
|
|
2340
2387
|
nodes = parsed.nodes ?? parsed.workflow?.nodes ?? [];
|
|
2341
2388
|
if (!Array.isArray(nodes)) throw new Error("Generated workflow has no nodes array");
|
|
2342
|
-
} else {
|
|
2343
|
-
throw new Error('Provide either "nodes" or a "model" to generate the workflow from "goal"');
|
|
2344
2389
|
}
|
|
2345
2390
|
const ctx = {
|
|
2346
2391
|
variables: /* @__PURE__ */ new Map(),
|
|
@@ -2360,6 +2405,54 @@ function runWorkflow(opts = {}) {
|
|
|
2360
2405
|
});
|
|
2361
2406
|
}
|
|
2362
2407
|
|
|
2408
|
+
// ai/provider.ts
|
|
2409
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
2410
|
+
import {
|
|
2411
|
+
embed as aiEmbed,
|
|
2412
|
+
embedMany as aiEmbedMany,
|
|
2413
|
+
generateText as aiGenerateText,
|
|
2414
|
+
streamText as aiStreamText
|
|
2415
|
+
} from "ai";
|
|
2416
|
+
function aiProvider(options) {
|
|
2417
|
+
const baseURL = options?.baseURL ?? process.env.OPENAI_BASE_URL ?? "http://localhost:11434/v1";
|
|
2418
|
+
const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY ?? "ollama";
|
|
2419
|
+
const modelName = options?.model ?? process.env.OPENAI_MODEL ?? "qwen3:0.6b";
|
|
2420
|
+
const embedModelName = options?.embeddingModel ?? process.env.OPENAI_EMBEDDING_MODEL ?? "qwen3-embedding:0.6b";
|
|
2421
|
+
const dimension = options?.embeddingDimension ?? parseInt(process.env.EMBEDDING_DIMENSION || "1024", 10);
|
|
2422
|
+
const client = createOpenAI({ baseURL, apiKey });
|
|
2423
|
+
let _model;
|
|
2424
|
+
let _embedModel;
|
|
2425
|
+
return {
|
|
2426
|
+
get dimension() {
|
|
2427
|
+
return dimension;
|
|
2428
|
+
},
|
|
2429
|
+
model(name) {
|
|
2430
|
+
const m = name ?? modelName;
|
|
2431
|
+
if (!_model) _model = client(m);
|
|
2432
|
+
return _model;
|
|
2433
|
+
},
|
|
2434
|
+
embeddingModel(name) {
|
|
2435
|
+
const m = name ?? embedModelName;
|
|
2436
|
+
if (!_embedModel) _embedModel = client.embedding(m);
|
|
2437
|
+
return _embedModel;
|
|
2438
|
+
},
|
|
2439
|
+
async embed(text2) {
|
|
2440
|
+
const result = await aiEmbed({ model: this.embeddingModel(), value: text2 });
|
|
2441
|
+
return result.embedding;
|
|
2442
|
+
},
|
|
2443
|
+
async embedMany(texts) {
|
|
2444
|
+
const result = await aiEmbedMany({ model: this.embeddingModel(), value: texts });
|
|
2445
|
+
return result.embeddings;
|
|
2446
|
+
},
|
|
2447
|
+
generateText(params) {
|
|
2448
|
+
return aiGenerateText({ ...params, model: this.model() });
|
|
2449
|
+
},
|
|
2450
|
+
streamText(params) {
|
|
2451
|
+
return aiStreamText({ ...params, model: this.model() });
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2363
2456
|
// ai-sdk.ts
|
|
2364
2457
|
import {
|
|
2365
2458
|
streamText,
|
|
@@ -2373,7 +2466,7 @@ import {
|
|
|
2373
2466
|
} from "ai";
|
|
2374
2467
|
import {
|
|
2375
2468
|
openai,
|
|
2376
|
-
createOpenAI
|
|
2469
|
+
createOpenAI as createOpenAI2
|
|
2377
2470
|
} from "@ai-sdk/openai";
|
|
2378
2471
|
|
|
2379
2472
|
// postgres/client.ts
|
|
@@ -3293,6 +3386,226 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3293
3386
|
return { authorizeHandler, consentHandler, tokenHandler, registerClient, getClient, revokeClient };
|
|
3294
3387
|
}
|
|
3295
3388
|
|
|
3389
|
+
// user/oauth-login.ts
|
|
3390
|
+
import crypto4 from "node:crypto";
|
|
3391
|
+
var BUILTIN_PROVIDERS = {
|
|
3392
|
+
google: {
|
|
3393
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
3394
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
3395
|
+
userUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
3396
|
+
scope: "openid email profile",
|
|
3397
|
+
parseUser: (data) => ({
|
|
3398
|
+
id: data.id,
|
|
3399
|
+
email: data.email,
|
|
3400
|
+
name: data.name,
|
|
3401
|
+
avatarUrl: data.picture
|
|
3402
|
+
})
|
|
3403
|
+
},
|
|
3404
|
+
github: {
|
|
3405
|
+
authUrl: "https://github.com/login/oauth/authorize",
|
|
3406
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
3407
|
+
userUrl: "https://api.github.com/user",
|
|
3408
|
+
scope: "read:user user:email",
|
|
3409
|
+
parseUser: (data) => ({
|
|
3410
|
+
id: String(data.id),
|
|
3411
|
+
email: data.email ?? "",
|
|
3412
|
+
name: data.name ?? data.login,
|
|
3413
|
+
avatarUrl: data.avatar_url
|
|
3414
|
+
})
|
|
3415
|
+
}
|
|
3416
|
+
};
|
|
3417
|
+
function registerOAuthLoginRoutes(router, deps, providers) {
|
|
3418
|
+
const { sql: sql2, providerTable, signToken, redirectUrl } = deps;
|
|
3419
|
+
let tableReady = null;
|
|
3420
|
+
async function ensureTable() {
|
|
3421
|
+
if (tableReady) return tableReady;
|
|
3422
|
+
tableReady = (async () => {
|
|
3423
|
+
await sql2.unsafe(`
|
|
3424
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent(providerTable)} (
|
|
3425
|
+
id SERIAL PRIMARY KEY,
|
|
3426
|
+
user_id INTEGER NOT NULL REFERENCES ${escapeIdent(deps.usersTable)}(id) ON DELETE CASCADE,
|
|
3427
|
+
provider TEXT NOT NULL,
|
|
3428
|
+
provider_id TEXT NOT NULL,
|
|
3429
|
+
email TEXT NOT NULL DEFAULT '',
|
|
3430
|
+
name TEXT NOT NULL DEFAULT '',
|
|
3431
|
+
avatar_url TEXT NOT NULL DEFAULT '',
|
|
3432
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
3433
|
+
UNIQUE(provider, provider_id)
|
|
3434
|
+
)
|
|
3435
|
+
`);
|
|
3436
|
+
await sql2.unsafe(`
|
|
3437
|
+
CREATE INDEX IF NOT EXISTS ${escapeIdent(providerTable + "_user_idx")}
|
|
3438
|
+
ON ${escapeIdent(providerTable)}(user_id)
|
|
3439
|
+
`);
|
|
3440
|
+
})();
|
|
3441
|
+
return tableReady;
|
|
3442
|
+
}
|
|
3443
|
+
async function findUserByProvider(provider, providerId) {
|
|
3444
|
+
const [row] = await sql2.unsafe(
|
|
3445
|
+
`SELECT * FROM ${escapeIdent(providerTable)} WHERE provider = $1 AND provider_id = $2 LIMIT 1`,
|
|
3446
|
+
[provider, providerId]
|
|
3447
|
+
);
|
|
3448
|
+
return row ?? null;
|
|
3449
|
+
}
|
|
3450
|
+
async function linkProvider(userId, provider, providerId, email, name, avatarUrl) {
|
|
3451
|
+
await sql2.unsafe(
|
|
3452
|
+
`INSERT INTO ${escapeIdent(providerTable)} (user_id, provider, provider_id, email, name, avatar_url)
|
|
3453
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
3454
|
+
ON CONFLICT (provider, provider_id) DO NOTHING`,
|
|
3455
|
+
[userId, provider, providerId, email, name, avatarUrl]
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
async function findOrCreateUser(provider, providerId, email, name, avatarUrl) {
|
|
3459
|
+
const link = await findUserByProvider(provider, providerId);
|
|
3460
|
+
if (link) {
|
|
3461
|
+
const user2 = await deps.findUserById(link.user_id);
|
|
3462
|
+
if (user2) return user2;
|
|
3463
|
+
}
|
|
3464
|
+
if (email) {
|
|
3465
|
+
const existingUser = await deps.findUserByEmail(email);
|
|
3466
|
+
if (existingUser) {
|
|
3467
|
+
await linkProvider(existingUser.id, provider, providerId, email, name, avatarUrl);
|
|
3468
|
+
return existingUser;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
const newUser = await deps.createPlaceholderUser(
|
|
3472
|
+
email || `${provider}_${providerId}@oauth.local`,
|
|
3473
|
+
name || provider
|
|
3474
|
+
);
|
|
3475
|
+
await linkProvider(newUser.id, provider, providerId, email, name, avatarUrl);
|
|
3476
|
+
return newUser;
|
|
3477
|
+
}
|
|
3478
|
+
function getProviderMeta(providerName) {
|
|
3479
|
+
const config = providers[providerName];
|
|
3480
|
+
if (!config) return null;
|
|
3481
|
+
const builtin = BUILTIN_PROVIDERS[providerName];
|
|
3482
|
+
const parseUser = config.parseUser ?? builtin?.parseUser;
|
|
3483
|
+
if (!parseUser) return null;
|
|
3484
|
+
const meta = {
|
|
3485
|
+
authUrl: config.authUrl ?? builtin?.authUrl ?? "",
|
|
3486
|
+
tokenUrl: config.tokenUrl ?? builtin?.tokenUrl ?? "",
|
|
3487
|
+
userUrl: config.userUrl ?? builtin?.userUrl ?? "",
|
|
3488
|
+
scope: config.scope ?? builtin?.scope ?? "openid",
|
|
3489
|
+
parseUser
|
|
3490
|
+
};
|
|
3491
|
+
if (!meta.authUrl || !meta.tokenUrl || !meta.userUrl) return null;
|
|
3492
|
+
return { config, meta };
|
|
3493
|
+
}
|
|
3494
|
+
router.get("/auth/:provider", async (req, ctx) => {
|
|
3495
|
+
await ensureTable();
|
|
3496
|
+
const providerName = ctx.params.provider;
|
|
3497
|
+
const resolved = getProviderMeta(providerName);
|
|
3498
|
+
if (!resolved) {
|
|
3499
|
+
return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
|
|
3500
|
+
}
|
|
3501
|
+
const { config, meta } = resolved;
|
|
3502
|
+
const state = crypto4.randomUUID();
|
|
3503
|
+
const redirectUri = new URL(req.url);
|
|
3504
|
+
redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
|
|
3505
|
+
if (ctx.session) {
|
|
3506
|
+
ctx.session.oauthState = { state, provider: providerName };
|
|
3507
|
+
}
|
|
3508
|
+
const scope = config.scope ?? meta.scope;
|
|
3509
|
+
const params = new URLSearchParams({
|
|
3510
|
+
client_id: config.clientId,
|
|
3511
|
+
redirect_uri: redirectUri.origin + redirectUri.pathname,
|
|
3512
|
+
response_type: "code",
|
|
3513
|
+
scope,
|
|
3514
|
+
state,
|
|
3515
|
+
access_type: "offline",
|
|
3516
|
+
prompt: "consent"
|
|
3517
|
+
});
|
|
3518
|
+
return Response.redirect(`${meta.authUrl}?${params.toString()}`, 302);
|
|
3519
|
+
});
|
|
3520
|
+
router.get("/auth/:provider/callback", async (req, ctx) => {
|
|
3521
|
+
const providerName = ctx.params.provider;
|
|
3522
|
+
const resolved = getProviderMeta(providerName);
|
|
3523
|
+
if (!resolved) {
|
|
3524
|
+
return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
|
|
3525
|
+
}
|
|
3526
|
+
const { config, meta } = resolved;
|
|
3527
|
+
const url = new URL(req.url);
|
|
3528
|
+
const code = url.searchParams.get("code");
|
|
3529
|
+
const state = url.searchParams.get("state");
|
|
3530
|
+
if (!code || !state) {
|
|
3531
|
+
return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
|
|
3532
|
+
}
|
|
3533
|
+
const savedState = ctx.session?.oauthState;
|
|
3534
|
+
if (!savedState || savedState.state !== state || savedState.provider !== providerName) {
|
|
3535
|
+
return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
|
|
3536
|
+
}
|
|
3537
|
+
if (ctx.session) delete ctx.session.oauthState;
|
|
3538
|
+
const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
|
|
3539
|
+
let tokenRes;
|
|
3540
|
+
try {
|
|
3541
|
+
tokenRes = await fetch(meta.tokenUrl, {
|
|
3542
|
+
method: "POST",
|
|
3543
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
3544
|
+
body: JSON.stringify({
|
|
3545
|
+
code,
|
|
3546
|
+
client_id: config.clientId,
|
|
3547
|
+
client_secret: config.clientSecret,
|
|
3548
|
+
redirect_uri: redirectUri,
|
|
3549
|
+
grant_type: "authorization_code"
|
|
3550
|
+
})
|
|
3551
|
+
});
|
|
3552
|
+
} catch (err) {
|
|
3553
|
+
console.error(`[oauth] token exchange network error for ${providerName}:`, err.message);
|
|
3554
|
+
return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
|
|
3555
|
+
}
|
|
3556
|
+
if (!tokenRes.ok) {
|
|
3557
|
+
const errBody = await tokenRes.text();
|
|
3558
|
+
console.error(`[oauth] token exchange failed for ${providerName}:`, errBody);
|
|
3559
|
+
return Response.json({ error: "Failed to exchange authorization code" }, { status: 502 });
|
|
3560
|
+
}
|
|
3561
|
+
const tokenData = await tokenRes.json();
|
|
3562
|
+
const accessToken = tokenData.access_token;
|
|
3563
|
+
if (!accessToken) {
|
|
3564
|
+
return Response.json({ error: "No access_token in response" }, { status: 502 });
|
|
3565
|
+
}
|
|
3566
|
+
let userRes;
|
|
3567
|
+
try {
|
|
3568
|
+
userRes = await fetch(meta.userUrl, { headers: { Authorization: "Bearer " + accessToken } });
|
|
3569
|
+
} catch (err) {
|
|
3570
|
+
console.error("[oauth] user info network error for " + providerName + ":", err.message);
|
|
3571
|
+
return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
|
|
3572
|
+
}
|
|
3573
|
+
if (!userRes.ok) {
|
|
3574
|
+
return Response.json({ error: "Failed to fetch user profile" }, { status: 502 });
|
|
3575
|
+
}
|
|
3576
|
+
const userData = await userRes.json();
|
|
3577
|
+
const providerUser = meta.parseUser(userData, accessToken);
|
|
3578
|
+
const user2 = await findOrCreateUser(
|
|
3579
|
+
providerName,
|
|
3580
|
+
providerUser.id,
|
|
3581
|
+
providerUser.email,
|
|
3582
|
+
providerUser.name,
|
|
3583
|
+
providerUser.avatarUrl ?? ""
|
|
3584
|
+
);
|
|
3585
|
+
if (!user2) {
|
|
3586
|
+
return Response.json({ error: "Failed to create/link user" }, { status: 500 });
|
|
3587
|
+
}
|
|
3588
|
+
const token = signToken(user2);
|
|
3589
|
+
if (ctx.session) {
|
|
3590
|
+
ctx.session.userId = user2.id;
|
|
3591
|
+
ctx.session.role = user2.role;
|
|
3592
|
+
}
|
|
3593
|
+
const accept = req.headers.get("accept") ?? "";
|
|
3594
|
+
if (accept.includes("application/json")) {
|
|
3595
|
+
return Response.json({
|
|
3596
|
+
token,
|
|
3597
|
+
user: { id: user2.id, email: user2.email, name: user2.name, role: user2.role }
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
const finalUrl = new URL(redirectUrl, url.origin);
|
|
3601
|
+
finalUrl.searchParams.set("token", token);
|
|
3602
|
+
return Response.redirect(finalUrl.toString(), 302);
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
function escapeIdent(s) {
|
|
3606
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3296
3609
|
// user/client.ts
|
|
3297
3610
|
var RegisterSchema = z2.object({
|
|
3298
3611
|
email: z2.string().email(),
|
|
@@ -3303,6 +3616,9 @@ var LoginSchema = z2.object({
|
|
|
3303
3616
|
email: z2.string().email(),
|
|
3304
3617
|
password: z2.string().min(1)
|
|
3305
3618
|
});
|
|
3619
|
+
function escapeIdent2(s) {
|
|
3620
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
3621
|
+
}
|
|
3306
3622
|
function hashPassword(password) {
|
|
3307
3623
|
const salt = randomBytes(16).toString("hex");
|
|
3308
3624
|
const hash = scryptSync(password, salt, 64).toString("hex");
|
|
@@ -3336,6 +3652,25 @@ function user(options) {
|
|
|
3336
3652
|
}
|
|
3337
3653
|
async function migrate() {
|
|
3338
3654
|
await users.create();
|
|
3655
|
+
if (options.oauthLogin) {
|
|
3656
|
+
await pg.sql.unsafe(`
|
|
3657
|
+
CREATE TABLE IF NOT EXISTS "_auth_providers" (
|
|
3658
|
+
id SERIAL PRIMARY KEY,
|
|
3659
|
+
user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
|
|
3660
|
+
provider TEXT NOT NULL,
|
|
3661
|
+
provider_id TEXT NOT NULL,
|
|
3662
|
+
email TEXT NOT NULL DEFAULT '',
|
|
3663
|
+
name TEXT NOT NULL DEFAULT '',
|
|
3664
|
+
avatar_url TEXT NOT NULL DEFAULT '',
|
|
3665
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
3666
|
+
UNIQUE(provider, provider_id)
|
|
3667
|
+
)
|
|
3668
|
+
`);
|
|
3669
|
+
await pg.sql.unsafe(`
|
|
3670
|
+
CREATE INDEX IF NOT EXISTS "_auth_providers_user_idx"
|
|
3671
|
+
ON "_auth_providers"(user_id)
|
|
3672
|
+
`);
|
|
3673
|
+
}
|
|
3339
3674
|
if (!oauth2Enabled) return;
|
|
3340
3675
|
const clients3 = pg.table("_oauth2_clients", {
|
|
3341
3676
|
id: serial("id").primaryKey(),
|
|
@@ -3389,6 +3724,11 @@ function user(options) {
|
|
|
3389
3724
|
async function findById(id2) {
|
|
3390
3725
|
return await users.read(id2);
|
|
3391
3726
|
}
|
|
3727
|
+
async function createPlaceholderUser(email, name) {
|
|
3728
|
+
const randomPassword = randomBytes(32).toString("hex");
|
|
3729
|
+
const row = await users.insert({ email, password: randomPassword, name });
|
|
3730
|
+
return row;
|
|
3731
|
+
}
|
|
3392
3732
|
async function register(data) {
|
|
3393
3733
|
const { email, password, name } = RegisterSchema.parse(data);
|
|
3394
3734
|
const existing = await findByEmail(email);
|
|
@@ -3505,6 +3845,20 @@ function user(options) {
|
|
|
3505
3845
|
return r2;
|
|
3506
3846
|
}
|
|
3507
3847
|
const r = router();
|
|
3848
|
+
if (options.oauthLogin) {
|
|
3849
|
+
registerOAuthLoginRoutes(r, {
|
|
3850
|
+
sql: pg.sql,
|
|
3851
|
+
jwtSecret: secret,
|
|
3852
|
+
expiresIn,
|
|
3853
|
+
usersTable: table,
|
|
3854
|
+
providerTable: "_auth_providers",
|
|
3855
|
+
redirectUrl: options.oauthLogin.redirectUrl || "/",
|
|
3856
|
+
signToken,
|
|
3857
|
+
createPlaceholderUser,
|
|
3858
|
+
findUserById: findById,
|
|
3859
|
+
findUserByEmail: findByEmail
|
|
3860
|
+
}, options.oauthLogin.providers);
|
|
3861
|
+
}
|
|
3508
3862
|
const mod = r;
|
|
3509
3863
|
mod.middleware = middleware;
|
|
3510
3864
|
mod.migrate = migrate;
|
|
@@ -3542,30 +3896,9 @@ function redis(opts) {
|
|
|
3542
3896
|
|
|
3543
3897
|
// queue/index.ts
|
|
3544
3898
|
import { Redis as IORedis2 } from "ioredis";
|
|
3545
|
-
import
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
|
|
3549
|
-
const fields = parts.map((f, i) => {
|
|
3550
|
-
const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
|
|
3551
|
-
const [min, max] = ranges[i];
|
|
3552
|
-
return parseField(f, min, max);
|
|
3553
|
-
});
|
|
3554
|
-
let candidate = new Date(from.getTime() + 6e4);
|
|
3555
|
-
candidate.setSeconds(0, 0);
|
|
3556
|
-
for (let i = 0; i < 525600; i++) {
|
|
3557
|
-
const m = candidate.getMonth() + 1;
|
|
3558
|
-
const d = candidate.getDate();
|
|
3559
|
-
const h = candidate.getHours();
|
|
3560
|
-
const min = candidate.getMinutes();
|
|
3561
|
-
const dw = candidate.getDay();
|
|
3562
|
-
if (fields[4].has(dw) && fields[3].has(m) && fields[2].has(d) && fields[1].has(h) && fields[0].has(min)) {
|
|
3563
|
-
return candidate.getTime();
|
|
3564
|
-
}
|
|
3565
|
-
candidate.setTime(candidate.getTime() + 6e4);
|
|
3566
|
-
}
|
|
3567
|
-
throw new Error(`No future date found for cron expression "${expr}"`);
|
|
3568
|
-
}
|
|
3899
|
+
import crypto5 from "node:crypto";
|
|
3900
|
+
|
|
3901
|
+
// cron-utils.ts
|
|
3569
3902
|
function parseField(field, min, max) {
|
|
3570
3903
|
const values = /* @__PURE__ */ new Set();
|
|
3571
3904
|
for (const part of field.split(",")) {
|
|
@@ -3574,19 +3907,23 @@ function parseField(field, min, max) {
|
|
|
3574
3907
|
} else if (part.includes("/")) {
|
|
3575
3908
|
const [range, stepStr] = part.split("/");
|
|
3576
3909
|
const step = parseInt(stepStr, 10);
|
|
3910
|
+
if (isNaN(step) || step < 1) throw new Error(`Invalid cron step: ${part}`);
|
|
3577
3911
|
let start = min;
|
|
3578
3912
|
let end = max;
|
|
3579
3913
|
if (range !== "*") {
|
|
3580
|
-
const
|
|
3581
|
-
start = parseInt(
|
|
3582
|
-
end =
|
|
3914
|
+
const rangeParts = range.split("-");
|
|
3915
|
+
start = parseInt(rangeParts[0], 10);
|
|
3916
|
+
end = rangeParts.length > 1 ? parseInt(rangeParts[1], 10) : max;
|
|
3583
3917
|
}
|
|
3584
3918
|
for (let i = start; i <= end; i += step) values.add(i);
|
|
3585
3919
|
} else if (part.includes("-")) {
|
|
3586
3920
|
const [s, e] = part.split("-").map(Number);
|
|
3921
|
+
if (isNaN(s) || isNaN(e)) throw new Error(`Invalid cron range: ${part}`);
|
|
3587
3922
|
for (let i = s; i <= e; i++) values.add(i);
|
|
3588
3923
|
} else {
|
|
3589
|
-
|
|
3924
|
+
const val = parseInt(part, 10);
|
|
3925
|
+
if (isNaN(val)) throw new Error(`Invalid cron value: ${part}`);
|
|
3926
|
+
values.add(val);
|
|
3590
3927
|
}
|
|
3591
3928
|
}
|
|
3592
3929
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -3595,41 +3932,333 @@ function parseField(field, min, max) {
|
|
|
3595
3932
|
}
|
|
3596
3933
|
return result;
|
|
3597
3934
|
}
|
|
3935
|
+
var FIELD_RANGES = [
|
|
3936
|
+
[0, 59],
|
|
3937
|
+
// minute
|
|
3938
|
+
[0, 23],
|
|
3939
|
+
// hour
|
|
3940
|
+
[1, 31],
|
|
3941
|
+
// day of month
|
|
3942
|
+
[1, 12],
|
|
3943
|
+
// month
|
|
3944
|
+
[0, 6]
|
|
3945
|
+
// day of week (0=Sunday)
|
|
3946
|
+
];
|
|
3947
|
+
function parsePattern(pattern) {
|
|
3948
|
+
const fields = pattern.trim().split(/\s+/);
|
|
3949
|
+
if (fields.length !== 5) {
|
|
3950
|
+
throw new Error(
|
|
3951
|
+
`Invalid cron pattern "${pattern}": expected 5 fields, got ${fields.length}`
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
return fields.map((f, i) => parseField(f, FIELD_RANGES[i][0], FIELD_RANGES[i][1]));
|
|
3955
|
+
}
|
|
3956
|
+
function matches(fields, date) {
|
|
3957
|
+
return fields[0].has(date.getMinutes()) && fields[1].has(date.getHours()) && fields[2].has(date.getDate()) && fields[3].has(date.getMonth() + 1) && fields[4].has(date.getDay());
|
|
3958
|
+
}
|
|
3959
|
+
function cronNext(expr, from = /* @__PURE__ */ new Date()) {
|
|
3960
|
+
const fields = parsePattern(expr);
|
|
3961
|
+
let candidate = new Date(from.getTime() + 6e4);
|
|
3962
|
+
candidate.setSeconds(0, 0);
|
|
3963
|
+
for (let i = 0; i < 525600; i++) {
|
|
3964
|
+
if (fields[4].has(candidate.getDay()) && fields[3].has(candidate.getMonth() + 1) && fields[2].has(candidate.getDate()) && fields[1].has(candidate.getHours()) && fields[0].has(candidate.getMinutes())) {
|
|
3965
|
+
return candidate.getTime();
|
|
3966
|
+
}
|
|
3967
|
+
candidate.setTime(candidate.getTime() + 6e4);
|
|
3968
|
+
}
|
|
3969
|
+
throw new Error(`No future date found for cron expression "${expr}"`);
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
// queue/index.ts
|
|
3598
3973
|
function queue(opts) {
|
|
3599
|
-
const
|
|
3600
|
-
|
|
3974
|
+
const store2 = opts?.store ?? "memory";
|
|
3975
|
+
if (store2 === "redis") return createRedisQueue(opts);
|
|
3976
|
+
if (store2 === "pg") return createPgQueue(opts);
|
|
3977
|
+
return createMemoryQueue(opts);
|
|
3978
|
+
}
|
|
3979
|
+
function escapeIdent3(s) {
|
|
3980
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
3981
|
+
}
|
|
3982
|
+
function attachCron(q, handlers) {
|
|
3983
|
+
;
|
|
3984
|
+
q.cron = function(pattern, handler) {
|
|
3985
|
+
const id2 = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto5.randomUUID().slice(0, 8);
|
|
3986
|
+
q.process(id2, async () => {
|
|
3987
|
+
await handler();
|
|
3988
|
+
});
|
|
3989
|
+
q.add(id2, {}, { schedule: pattern });
|
|
3990
|
+
return { stop: () => handlers.delete(id2) };
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
function createMemoryQueue(opts) {
|
|
3601
3994
|
const pollInterval = opts?.pollInterval ?? 200;
|
|
3602
3995
|
const handlers = /* @__PURE__ */ new Map();
|
|
3996
|
+
const jobs = [];
|
|
3997
|
+
const failed = [];
|
|
3998
|
+
const MAX_FAILED = 1e3;
|
|
3603
3999
|
let running = false;
|
|
3604
4000
|
let pollTimer = null;
|
|
3605
|
-
let epoch = 0;
|
|
3606
4001
|
let _processed = 0;
|
|
3607
4002
|
let _failed = 0;
|
|
3608
|
-
|
|
3609
|
-
const
|
|
3610
|
-
|
|
4003
|
+
let inflight = 0;
|
|
4004
|
+
const MAX_CONCURRENT = 16;
|
|
4005
|
+
function insertJob(job) {
|
|
4006
|
+
let i = 0;
|
|
4007
|
+
while (i < jobs.length && jobs[i].runAt <= job.runAt) i++;
|
|
4008
|
+
jobs.splice(i, 0, job);
|
|
4009
|
+
}
|
|
4010
|
+
async function execute(job, handler) {
|
|
4011
|
+
inflight++;
|
|
4012
|
+
try {
|
|
4013
|
+
await handler(job);
|
|
4014
|
+
_processed++;
|
|
4015
|
+
} catch (e) {
|
|
4016
|
+
_failed++;
|
|
4017
|
+
failed.unshift({ ...job, error: e.message, failedAt: Date.now() });
|
|
4018
|
+
if (failed.length > MAX_FAILED) failed.length = MAX_FAILED;
|
|
4019
|
+
} finally {
|
|
4020
|
+
inflight--;
|
|
4021
|
+
}
|
|
4022
|
+
if (job.schedule) {
|
|
4023
|
+
try {
|
|
4024
|
+
insertJob({ ...job, id: crypto5.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
|
|
4025
|
+
} catch (e) {
|
|
4026
|
+
console.error("[queue] cron re-queue failed:", e.message);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
async function poll() {
|
|
4031
|
+
if (!running) return;
|
|
4032
|
+
const now = Date.now();
|
|
4033
|
+
while (running && inflight < MAX_CONCURRENT && jobs.length > 0 && jobs[0].runAt <= now) {
|
|
4034
|
+
const job = jobs.shift();
|
|
4035
|
+
const handler = handlers.get(job.type);
|
|
4036
|
+
if (handler) execute(job, handler);
|
|
4037
|
+
}
|
|
4038
|
+
if (running) pollTimer = setTimeout(poll, pollInterval);
|
|
4039
|
+
}
|
|
3611
4040
|
const mw = ((req, ctx, next) => {
|
|
3612
4041
|
ctx.queue = q;
|
|
3613
4042
|
return next(req, ctx);
|
|
3614
4043
|
});
|
|
3615
4044
|
const q = mw;
|
|
4045
|
+
mw.add = function add(type, payload, opts2) {
|
|
4046
|
+
const id2 = crypto5.randomUUID();
|
|
4047
|
+
let runAt;
|
|
4048
|
+
if (opts2?.schedule) {
|
|
4049
|
+
try {
|
|
4050
|
+
const f = parsePattern(opts2.schedule);
|
|
4051
|
+
runAt = matches(f, /* @__PURE__ */ new Date()) ? Date.now() : cronNext(opts2.schedule);
|
|
4052
|
+
} catch {
|
|
4053
|
+
runAt = cronNext(opts2.schedule);
|
|
4054
|
+
}
|
|
4055
|
+
} else if (opts2?.delay) {
|
|
4056
|
+
runAt = Date.now() + opts2.delay;
|
|
4057
|
+
} else {
|
|
4058
|
+
runAt = Date.now();
|
|
4059
|
+
}
|
|
4060
|
+
const job = { id: id2, type, payload, createdAt: Date.now(), runAt };
|
|
4061
|
+
if (opts2?.schedule) job.schedule = opts2.schedule;
|
|
4062
|
+
insertJob(job);
|
|
4063
|
+
return Promise.resolve(id2);
|
|
4064
|
+
};
|
|
4065
|
+
mw.process = function process2(type, handler) {
|
|
4066
|
+
handlers.set(type, handler);
|
|
4067
|
+
};
|
|
4068
|
+
mw.run = async function run() {
|
|
4069
|
+
if (running) return;
|
|
4070
|
+
running = true;
|
|
4071
|
+
poll();
|
|
4072
|
+
};
|
|
4073
|
+
mw.stop = function stop2() {
|
|
4074
|
+
running = false;
|
|
4075
|
+
if (pollTimer) {
|
|
4076
|
+
clearTimeout(pollTimer);
|
|
4077
|
+
pollTimer = null;
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
mw.close = async function close() {
|
|
4081
|
+
mw.stop();
|
|
4082
|
+
while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
|
|
4083
|
+
};
|
|
4084
|
+
mw.jobs = async function jobs2(limit) {
|
|
4085
|
+
return jobs2.slice(0, limit ?? 50);
|
|
4086
|
+
};
|
|
4087
|
+
mw.failedJobs = async function failedJobs(limit) {
|
|
4088
|
+
return failed.slice(0, limit ?? 50);
|
|
4089
|
+
};
|
|
4090
|
+
mw.retryFailed = async function retry(jobId) {
|
|
4091
|
+
const idx = failed.findIndex((j) => j.id === jobId);
|
|
4092
|
+
if (idx < 0) return false;
|
|
4093
|
+
const [entry] = failed.splice(idx, 1);
|
|
4094
|
+
_failed--;
|
|
4095
|
+
insertJob({ ...entry, runAt: Date.now() });
|
|
4096
|
+
return true;
|
|
4097
|
+
};
|
|
4098
|
+
mw.retryAllFailed = async function retryAll(type) {
|
|
4099
|
+
let count = 0;
|
|
4100
|
+
for (let i = failed.length - 1; i >= 0; i--) {
|
|
4101
|
+
if (type && failed[i].type !== type) continue;
|
|
4102
|
+
const [entry] = failed.splice(i, 1);
|
|
4103
|
+
_failed--;
|
|
4104
|
+
insertJob({ ...entry, runAt: Date.now() });
|
|
4105
|
+
count++;
|
|
4106
|
+
}
|
|
4107
|
+
return count;
|
|
4108
|
+
};
|
|
4109
|
+
mw.dashboard = function dashboard() {
|
|
4110
|
+
return buildDashboard(q);
|
|
4111
|
+
};
|
|
4112
|
+
mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
|
|
4113
|
+
attachCron(q, handlers);
|
|
4114
|
+
return q;
|
|
4115
|
+
}
|
|
4116
|
+
function createPgQueue(opts) {
|
|
4117
|
+
const sql2 = opts.pg.sql;
|
|
4118
|
+
const pollInterval = opts?.pollInterval ?? 200;
|
|
4119
|
+
const table = (opts?.prefix ?? "queue") + "_jobs";
|
|
4120
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
4121
|
+
let running = false, pollTimer = null;
|
|
4122
|
+
let _processed = 0, _failed = 0, inflight = 0, ready = false;
|
|
3616
4123
|
const MAX_CONCURRENT = 16;
|
|
3617
|
-
|
|
3618
|
-
async function
|
|
4124
|
+
const MAX_FAILED = 1e3;
|
|
4125
|
+
async function ensureTable() {
|
|
4126
|
+
if (ready) return;
|
|
4127
|
+
await sql2.unsafe(`CREATE TABLE IF NOT EXISTS ${escapeIdent3(table)} (id UUID PRIMARY KEY, type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), schedule TEXT, status TEXT NOT NULL DEFAULT 'pending', error TEXT, failed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`);
|
|
4128
|
+
await sql2.unsafe(`CREATE INDEX IF NOT EXISTS ${escapeIdent3(table + "_run_at_idx")} ON ${escapeIdent3(table)} (run_at, status)`);
|
|
4129
|
+
ready = true;
|
|
4130
|
+
}
|
|
4131
|
+
async function processJob(job, handler) {
|
|
3619
4132
|
inflight++;
|
|
3620
4133
|
try {
|
|
3621
|
-
await
|
|
4134
|
+
await handler(job);
|
|
3622
4135
|
_processed++;
|
|
4136
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent3(table)} WHERE id = $1`, [job.id]);
|
|
3623
4137
|
} catch (e) {
|
|
3624
4138
|
_failed++;
|
|
3625
|
-
const
|
|
3626
|
-
console.error("[queue] handler error:",
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
4139
|
+
const msg = e.message;
|
|
4140
|
+
console.error("[queue] handler error:", msg);
|
|
4141
|
+
await sql2.unsafe(`UPDATE ${escapeIdent3(table)} SET status = 'failed', error = $2, failed_at = NOW() WHERE id = $1`, [job.id, msg]);
|
|
4142
|
+
} finally {
|
|
4143
|
+
inflight--;
|
|
4144
|
+
}
|
|
4145
|
+
if (job.schedule) {
|
|
4146
|
+
try {
|
|
4147
|
+
const nextRun = cronNext(job.schedule);
|
|
4148
|
+
await sql2.unsafe(`INSERT INTO ${escapeIdent3(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`, [crypto5.randomUUID(), job.type, JSON.stringify(job.payload), new Date(nextRun).toISOString(), job.schedule]);
|
|
4149
|
+
} catch (e) {
|
|
4150
|
+
console.error("[queue] cron re-queue failed:", e.message);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
async function poll() {
|
|
4155
|
+
if (!running) return;
|
|
4156
|
+
try {
|
|
4157
|
+
while (running && inflight < MAX_CONCURRENT) {
|
|
4158
|
+
const rows = await sql2.unsafe(`UPDATE ${escapeIdent3(table)} SET status = 'running' WHERE id = (SELECT id FROM ${escapeIdent3(table)} WHERE run_at <= NOW() AND status = 'pending' ORDER BY run_at LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING *`);
|
|
4159
|
+
if (rows.length === 0) break;
|
|
4160
|
+
const row = rows[0];
|
|
4161
|
+
const job = { id: row.id, type: row.type, payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload, createdAt: new Date(row.created_at).getTime(), runAt: new Date(row.run_at).getTime(), schedule: row.schedule || void 0 };
|
|
4162
|
+
const handler = handlers.get(job.type);
|
|
4163
|
+
if (handler) processJob(job, handler);
|
|
4164
|
+
}
|
|
4165
|
+
} catch (e) {
|
|
4166
|
+
const msg = e.message;
|
|
4167
|
+
if (msg.includes("CONNECTION_ENDED") || msg.includes("Connection terminated")) {
|
|
4168
|
+
running = false;
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
console.error("[queue] poll error:", msg);
|
|
4172
|
+
}
|
|
4173
|
+
if (running) pollTimer = setTimeout(poll, pollInterval);
|
|
4174
|
+
}
|
|
4175
|
+
const mw = ((req, ctx, next) => {
|
|
4176
|
+
ctx.queue = q;
|
|
4177
|
+
return next(req, ctx);
|
|
4178
|
+
});
|
|
4179
|
+
const q = mw;
|
|
4180
|
+
mw.add = function add(type, payload, opts2) {
|
|
4181
|
+
return (async () => {
|
|
4182
|
+
const id2 = crypto5.randomUUID();
|
|
4183
|
+
let runAt;
|
|
4184
|
+
if (opts2?.schedule) {
|
|
4185
|
+
try {
|
|
4186
|
+
const f = parsePattern(opts2.schedule);
|
|
4187
|
+
runAt = matches(f, /* @__PURE__ */ new Date()) ? /* @__PURE__ */ new Date() : new Date(cronNext(opts2.schedule));
|
|
4188
|
+
} catch {
|
|
4189
|
+
runAt = new Date(cronNext(opts2.schedule));
|
|
4190
|
+
}
|
|
4191
|
+
} else if (opts2?.delay) {
|
|
4192
|
+
runAt = new Date(Date.now() + opts2.delay);
|
|
4193
|
+
} else {
|
|
4194
|
+
runAt = /* @__PURE__ */ new Date();
|
|
4195
|
+
}
|
|
4196
|
+
await sql2.unsafe(`INSERT INTO ${escapeIdent3(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`, [id2, type, JSON.stringify(payload), runAt.toISOString(), opts2?.schedule || null]);
|
|
4197
|
+
return id2;
|
|
4198
|
+
})();
|
|
4199
|
+
};
|
|
4200
|
+
mw.process = function process2(type, handler) {
|
|
4201
|
+
handlers.set(type, handler);
|
|
4202
|
+
};
|
|
4203
|
+
mw.migrate = ensureTable;
|
|
4204
|
+
mw.run = async function run() {
|
|
4205
|
+
if (running) return;
|
|
4206
|
+
await ensureTable();
|
|
4207
|
+
running = true;
|
|
4208
|
+
poll();
|
|
4209
|
+
};
|
|
4210
|
+
mw.stop = function stop2() {
|
|
4211
|
+
running = false;
|
|
4212
|
+
if (pollTimer) {
|
|
4213
|
+
clearTimeout(pollTimer);
|
|
4214
|
+
pollTimer = null;
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
mw.close = async function close() {
|
|
4218
|
+
mw.stop();
|
|
4219
|
+
while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
|
|
4220
|
+
};
|
|
4221
|
+
mw.jobs = async function jobs(limit) {
|
|
4222
|
+
const rows = await sql2.unsafe(`SELECT * FROM ${escapeIdent3(table)} WHERE status = 'pending' ORDER BY run_at LIMIT $1`, [limit ?? 50]);
|
|
4223
|
+
return rows.map((r) => ({ id: r.id, type: r.type, payload: typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload, createdAt: new Date(r.created_at).getTime(), runAt: new Date(r.run_at).getTime(), schedule: r.schedule || void 0 }));
|
|
4224
|
+
};
|
|
4225
|
+
mw.failedJobs = async function failedJobs(limit) {
|
|
4226
|
+
const rows = await sql2.unsafe(`SELECT * FROM ${escapeIdent3(table)} WHERE status = 'failed' ORDER BY failed_at DESC LIMIT $1`, [limit ?? 50]);
|
|
4227
|
+
return rows.map((r) => ({ id: r.id, type: r.type, payload: typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload, createdAt: new Date(r.created_at).getTime(), runAt: new Date(r.run_at).getTime(), schedule: r.schedule || void 0, error: r.error || "", failedAt: new Date(r.failed_at).getTime() }));
|
|
4228
|
+
};
|
|
4229
|
+
mw.retryFailed = async function retryFailed(jobId) {
|
|
4230
|
+
const result = await sql2.unsafe(`UPDATE ${escapeIdent3(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE id = $1 AND status = 'failed' RETURNING id`, [jobId]);
|
|
4231
|
+
return result.length > 0;
|
|
4232
|
+
};
|
|
4233
|
+
mw.retryAllFailed = async function retryAllFailed(type) {
|
|
4234
|
+
const result = await sql2.unsafe(type ? `UPDATE ${escapeIdent3(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' AND type = $1 RETURNING id` : `UPDATE ${escapeIdent3(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' RETURNING id`, type ? [type] : []);
|
|
4235
|
+
return result.length;
|
|
4236
|
+
};
|
|
4237
|
+
mw.dashboard = function dashboard() {
|
|
4238
|
+
return buildDashboard(q);
|
|
4239
|
+
};
|
|
4240
|
+
mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
|
|
4241
|
+
attachCron(q, handlers);
|
|
4242
|
+
return q;
|
|
4243
|
+
}
|
|
4244
|
+
function createRedisQueue(opts) {
|
|
4245
|
+
const redis2 = opts?.redis ?? new IORedis2(opts?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379");
|
|
4246
|
+
const prefix = opts?.prefix ?? "queue";
|
|
4247
|
+
const pollInterval = opts?.pollInterval ?? 200;
|
|
4248
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
4249
|
+
let running = false, pollTimer = null, epoch = 0;
|
|
4250
|
+
let _processed = 0, _failed = 0, inflight = 0;
|
|
4251
|
+
const jobKey = prefix + ":jobs", failedKey = prefix + ":failed", MAX_FAILED = 1e3, MAX_CONCURRENT = 16;
|
|
4252
|
+
async function processJob(job, handler) {
|
|
4253
|
+
inflight++;
|
|
4254
|
+
try {
|
|
4255
|
+
await handler(job);
|
|
4256
|
+
_processed++;
|
|
4257
|
+
} catch (e) {
|
|
4258
|
+
_failed++;
|
|
4259
|
+
const msg = e.message;
|
|
4260
|
+
console.error("[queue] handler error:", msg);
|
|
4261
|
+
await redis2.lpush(failedKey, JSON.stringify({ ...job, error: msg, failedAt: Date.now() }));
|
|
3633
4262
|
await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
|
|
3634
4263
|
} finally {
|
|
3635
4264
|
inflight--;
|
|
@@ -3637,8 +4266,7 @@ function queue(opts) {
|
|
|
3637
4266
|
if (job.schedule) {
|
|
3638
4267
|
try {
|
|
3639
4268
|
const nextRun = cronNext(job.schedule);
|
|
3640
|
-
|
|
3641
|
-
await redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob));
|
|
4269
|
+
await redis2.zadd(jobKey, nextRun, JSON.stringify({ ...job, id: crypto5.randomUUID(), runAt: nextRun, createdAt: Date.now() }));
|
|
3642
4270
|
} catch (e) {
|
|
3643
4271
|
console.error("[queue] cron re-queue failed:", e.message);
|
|
3644
4272
|
}
|
|
@@ -3652,8 +4280,7 @@ function queue(opts) {
|
|
|
3652
4280
|
while (running && inflight < MAX_CONCURRENT) {
|
|
3653
4281
|
const result = await redis2.zpopmin(jobKey);
|
|
3654
4282
|
if (result.length < 2) break;
|
|
3655
|
-
const raw = result[0];
|
|
3656
|
-
const score = parseInt(result[1], 10);
|
|
4283
|
+
const raw = result[0], score = parseInt(result[1], 10);
|
|
3657
4284
|
if (score > now) {
|
|
3658
4285
|
await redis2.zadd(jobKey, score, raw);
|
|
3659
4286
|
break;
|
|
@@ -3664,20 +4291,21 @@ function queue(opts) {
|
|
|
3664
4291
|
} catch {
|
|
3665
4292
|
continue;
|
|
3666
4293
|
}
|
|
3667
|
-
const
|
|
3668
|
-
if (
|
|
3669
|
-
processJob(job, jobHandler);
|
|
3670
|
-
}
|
|
4294
|
+
const handler = handlers.get(job.type);
|
|
4295
|
+
if (handler) processJob(job, handler);
|
|
3671
4296
|
}
|
|
3672
4297
|
} catch (e) {
|
|
3673
4298
|
console.error("[queue] poll error:", e.message);
|
|
3674
4299
|
}
|
|
3675
|
-
if (running && currentEpoch === epoch)
|
|
3676
|
-
pollTimer = setTimeout(poll, pollInterval);
|
|
3677
|
-
}
|
|
4300
|
+
if (running && currentEpoch === epoch) pollTimer = setTimeout(poll, pollInterval);
|
|
3678
4301
|
}
|
|
4302
|
+
const mw = ((req, ctx, next) => {
|
|
4303
|
+
ctx.queue = q;
|
|
4304
|
+
return next(req, ctx);
|
|
4305
|
+
});
|
|
4306
|
+
const q = mw;
|
|
3679
4307
|
mw.add = function add(type, payload, opts2) {
|
|
3680
|
-
const id2 =
|
|
4308
|
+
const id2 = crypto5.randomUUID();
|
|
3681
4309
|
let runAt;
|
|
3682
4310
|
if (opts2?.schedule) {
|
|
3683
4311
|
runAt = cronNext(opts2.schedule);
|
|
@@ -3751,8 +4379,8 @@ function queue(opts) {
|
|
|
3751
4379
|
return false;
|
|
3752
4380
|
};
|
|
3753
4381
|
mw.retryAllFailed = async function retryAllFailed(type) {
|
|
3754
|
-
const raw = await redis2.lrange(failedKey, 0, -1);
|
|
3755
4382
|
let count = 0;
|
|
4383
|
+
const raw = await redis2.lrange(failedKey, 0, -1);
|
|
3756
4384
|
for (const entry of raw) {
|
|
3757
4385
|
try {
|
|
3758
4386
|
const job = JSON.parse(entry);
|
|
@@ -3770,52 +4398,43 @@ function queue(opts) {
|
|
|
3770
4398
|
return count;
|
|
3771
4399
|
};
|
|
3772
4400
|
mw.dashboard = function dashboard() {
|
|
3773
|
-
|
|
3774
|
-
r.get("/", async (req, ctx) => {
|
|
3775
|
-
const s = q.stats();
|
|
3776
|
-
const pending = await q.jobs(100);
|
|
3777
|
-
const byType = {};
|
|
3778
|
-
for (const job of pending) {
|
|
3779
|
-
if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
|
|
3780
|
-
byType[job.type].pending++;
|
|
3781
|
-
}
|
|
3782
|
-
const failed = await q.failedJobs(1e3);
|
|
3783
|
-
for (const job of failed) {
|
|
3784
|
-
if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
|
|
3785
|
-
byType[job.type].failed++;
|
|
3786
|
-
}
|
|
3787
|
-
return Response.json({
|
|
3788
|
-
stats: s,
|
|
3789
|
-
types: byType,
|
|
3790
|
-
failedCount: failed.length
|
|
3791
|
-
});
|
|
3792
|
-
});
|
|
3793
|
-
r.get("/:type/failed", async (req, ctx) => {
|
|
3794
|
-
const failed = await q.failedJobs(100);
|
|
3795
|
-
const filtered = failed.filter((j) => j.type === ctx.params.type);
|
|
3796
|
-
return Response.json({ jobs: filtered, count: filtered.length });
|
|
3797
|
-
});
|
|
3798
|
-
r.post("/:type/retry", async (req, ctx) => {
|
|
3799
|
-
const count = await q.retryAllFailed(ctx.params.type);
|
|
3800
|
-
return Response.json({ retried: count });
|
|
3801
|
-
});
|
|
3802
|
-
r.post("/retry/:id", async (req, ctx) => {
|
|
3803
|
-
const ok = await q.retryFailed(ctx.params.id);
|
|
3804
|
-
if (!ok) return new Response("Job not found", { status: 404 });
|
|
3805
|
-
return Response.json({ retried: true });
|
|
3806
|
-
});
|
|
3807
|
-
return r;
|
|
4401
|
+
return buildDashboard(q);
|
|
3808
4402
|
};
|
|
3809
|
-
mw.stats = () => ({
|
|
3810
|
-
|
|
3811
|
-
inflight,
|
|
3812
|
-
processed: _processed,
|
|
3813
|
-
failed: _failed,
|
|
3814
|
-
handlers: handlers.size,
|
|
3815
|
-
maxConcurrent: MAX_CONCURRENT
|
|
3816
|
-
});
|
|
4403
|
+
mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
|
|
4404
|
+
attachCron(q, handlers);
|
|
3817
4405
|
return q;
|
|
3818
4406
|
}
|
|
4407
|
+
function buildDashboard(q) {
|
|
4408
|
+
const r = new Router();
|
|
4409
|
+
r.get("/", async () => {
|
|
4410
|
+
const s = q.stats();
|
|
4411
|
+
const pending = await q.jobs(100);
|
|
4412
|
+
const byType = {};
|
|
4413
|
+
for (const j of pending) {
|
|
4414
|
+
if (!byType[j.type]) byType[j.type] = { pending: 0, failed: 0 };
|
|
4415
|
+
byType[j.type].pending++;
|
|
4416
|
+
}
|
|
4417
|
+
const failed = await q.failedJobs(1e3);
|
|
4418
|
+
for (const j of failed) {
|
|
4419
|
+
if (!byType[j.type]) byType[j.type] = { pending: 0, failed: 0 };
|
|
4420
|
+
byType[j.type].failed++;
|
|
4421
|
+
}
|
|
4422
|
+
return Response.json({ stats: s, types: byType, failedCount: failed.length });
|
|
4423
|
+
});
|
|
4424
|
+
r.get("/:type/failed", async (req, ctx) => {
|
|
4425
|
+
const failed = await q.failedJobs(100);
|
|
4426
|
+
return Response.json({ jobs: failed.filter((j) => j.type === ctx.params.type) });
|
|
4427
|
+
});
|
|
4428
|
+
r.post("/:type/retry", async (req, ctx) => {
|
|
4429
|
+
return Response.json({ retried: await q.retryAllFailed(ctx.params.type) });
|
|
4430
|
+
});
|
|
4431
|
+
r.post("/retry/:id", async (req, ctx) => {
|
|
4432
|
+
const ok = await q.retryFailed(ctx.params.id);
|
|
4433
|
+
if (!ok) return new Response("Not found", { status: 404 });
|
|
4434
|
+
return Response.json({ ok: true });
|
|
4435
|
+
});
|
|
4436
|
+
return r;
|
|
4437
|
+
}
|
|
3819
4438
|
|
|
3820
4439
|
// tenant/rest.ts
|
|
3821
4440
|
import { z as z3 } from "zod";
|
|
@@ -4723,9 +5342,6 @@ function tenant(options) {
|
|
|
4723
5342
|
return mod;
|
|
4724
5343
|
}
|
|
4725
5344
|
|
|
4726
|
-
// agent/client.ts
|
|
4727
|
-
import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
4728
|
-
|
|
4729
5345
|
// agent/rest.ts
|
|
4730
5346
|
function buildRouter2(deps) {
|
|
4731
5347
|
const { agents: agentsTable, runs: runsTable, knowledge, runner } = deps;
|
|
@@ -4879,12 +5495,10 @@ function buildRouter2(deps) {
|
|
|
4879
5495
|
}
|
|
4880
5496
|
|
|
4881
5497
|
// agent/run.ts
|
|
4882
|
-
import { streamText as streamText2, generateText as generateText3, embed as embed2 } from "ai";
|
|
4883
5498
|
import { z as z4 } from "zod";
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
function chunkContent(content, chunkSize = 512, overlap = 64) {
|
|
5499
|
+
|
|
5500
|
+
// ai/utils.ts
|
|
5501
|
+
function chunkContent(content, chunkSize, overlap) {
|
|
4888
5502
|
const paragraphs = content.split(/\n\n+/);
|
|
4889
5503
|
const chunks = [];
|
|
4890
5504
|
let current = "";
|
|
@@ -4898,8 +5512,13 @@ function chunkContent(content, chunkSize = 512, overlap = 64) {
|
|
|
4898
5512
|
if (current) chunks.push(current);
|
|
4899
5513
|
return chunks;
|
|
4900
5514
|
}
|
|
4901
|
-
|
|
4902
|
-
|
|
5515
|
+
|
|
5516
|
+
// agent/run.ts
|
|
5517
|
+
function hasKnowledgeDocs(sql2, agentId) {
|
|
5518
|
+
return sql2`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then((r) => r.length > 0);
|
|
5519
|
+
}
|
|
5520
|
+
async function searchKnowledge(sql2, provider, agentId, query, limit = 5) {
|
|
5521
|
+
const embedding = await provider.embed(query);
|
|
4903
5522
|
const vec = `[${embedding.join(",")}]`;
|
|
4904
5523
|
const docs = await sql2.unsafe(
|
|
4905
5524
|
`SELECT id, title, content, metadata, embedding <=> $1::vector AS _score FROM "_knowledge_documents" WHERE agent_id = $2 ORDER BY embedding <=> $1::vector LIMIT $3`,
|
|
@@ -4912,7 +5531,7 @@ async function loadAgent(agents, agentId) {
|
|
|
4912
5531
|
return row ?? null;
|
|
4913
5532
|
}
|
|
4914
5533
|
function createRunner(deps) {
|
|
4915
|
-
const { sql: sql2, agents, runs,
|
|
5534
|
+
const { sql: sql2, agents, runs, provider, modelName, userTools } = deps;
|
|
4916
5535
|
function truncate(s, max = 200) {
|
|
4917
5536
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
4918
5537
|
}
|
|
@@ -4937,8 +5556,6 @@ function createRunner(deps) {
|
|
|
4937
5556
|
async function run(agentId, params) {
|
|
4938
5557
|
const agent2 = await loadAgent(agents, agentId);
|
|
4939
5558
|
if (!agent2 || !agent2.active) throw new Error("Agent not found or inactive");
|
|
4940
|
-
const model = getModel();
|
|
4941
|
-
const embedModel = getEmbeddingModel();
|
|
4942
5559
|
const start = Date.now();
|
|
4943
5560
|
const hasKB = await hasKnowledgeDocs(sql2, agentId);
|
|
4944
5561
|
const messages2 = params.messages ?? [];
|
|
@@ -4954,7 +5571,7 @@ function createRunner(deps) {
|
|
|
4954
5571
|
limit: z4.number().default(5).describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF")
|
|
4955
5572
|
}),
|
|
4956
5573
|
execute: async ({ query, limit }) => {
|
|
4957
|
-
return searchKnowledge(sql2,
|
|
5574
|
+
return searchKnowledge(sql2, provider, agentId, query, limit);
|
|
4958
5575
|
}
|
|
4959
5576
|
};
|
|
4960
5577
|
}
|
|
@@ -4965,8 +5582,7 @@ function createRunner(deps) {
|
|
|
4965
5582
|
}
|
|
4966
5583
|
const system = agent2.system_prompt || void 0;
|
|
4967
5584
|
if (params.stream) {
|
|
4968
|
-
const result =
|
|
4969
|
-
model,
|
|
5585
|
+
const result = provider.streamText({
|
|
4970
5586
|
system,
|
|
4971
5587
|
messages: messages2,
|
|
4972
5588
|
tools: Object.keys(tools).length > 0 ? tools : void 0
|
|
@@ -4991,8 +5607,7 @@ function createRunner(deps) {
|
|
|
4991
5607
|
});
|
|
4992
5608
|
return { stream: sseStream };
|
|
4993
5609
|
} else {
|
|
4994
|
-
const result = await
|
|
4995
|
-
model,
|
|
5610
|
+
const result = await provider.generateText({
|
|
4996
5611
|
system,
|
|
4997
5612
|
messages: messages2,
|
|
4998
5613
|
tools: Object.keys(tools).length > 0 ? tools : void 0
|
|
@@ -5011,20 +5626,20 @@ function createRunner(deps) {
|
|
|
5011
5626
|
}
|
|
5012
5627
|
}
|
|
5013
5628
|
async function addKnowledge(agentId, title, content) {
|
|
5014
|
-
const embedModel = getEmbeddingModel();
|
|
5015
5629
|
const chunks = chunkContent(content);
|
|
5016
5630
|
const [first] = chunks;
|
|
5017
|
-
const
|
|
5631
|
+
const embedding = await provider.embed(first);
|
|
5018
5632
|
const vec = `[${embedding.join(",")}]`;
|
|
5019
5633
|
const [doc] = await sql2.unsafe(
|
|
5020
5634
|
`INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector) RETURNING *`,
|
|
5021
5635
|
[agentId, title, first, vec]
|
|
5022
5636
|
);
|
|
5023
5637
|
for (let i = 1; i < chunks.length; i++) {
|
|
5024
|
-
const
|
|
5638
|
+
const emb = await provider.embed(chunks[i]);
|
|
5639
|
+
const vec2 = `[${emb.join(",")}]`;
|
|
5025
5640
|
await sql2.unsafe(
|
|
5026
5641
|
`INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector)`,
|
|
5027
|
-
[agentId, `${title} (${i + 1})`, chunks[i],
|
|
5642
|
+
[agentId, `${title} (${i + 1})`, chunks[i], vec2]
|
|
5028
5643
|
);
|
|
5029
5644
|
}
|
|
5030
5645
|
return doc;
|
|
@@ -5033,31 +5648,11 @@ function createRunner(deps) {
|
|
|
5033
5648
|
}
|
|
5034
5649
|
|
|
5035
5650
|
// agent/client.ts
|
|
5036
|
-
function createModelsFromEnv() {
|
|
5037
|
-
const baseURL = process.env.OPENAI_BASE_URL || "http://localhost:11434/v1";
|
|
5038
|
-
const apiKey = process.env.OPENAI_API_KEY || "ollama";
|
|
5039
|
-
const modelName = process.env.OPENAI_MODEL || "qwen3:0.6b";
|
|
5040
|
-
const embedModelName = process.env.OPENAI_EMBEDDING_MODEL || "qwen3-embedding:0.6b";
|
|
5041
|
-
const provider = createOpenAI2({ baseURL, apiKey });
|
|
5042
|
-
return {
|
|
5043
|
-
model: provider(modelName),
|
|
5044
|
-
embeddingModel: provider.embedding(embedModelName),
|
|
5045
|
-
dimension: parseInt(process.env.EMBEDDING_DIMENSION || "1024", 10)
|
|
5046
|
-
};
|
|
5047
|
-
}
|
|
5048
5651
|
function agent(options) {
|
|
5049
5652
|
const pg = options.pg;
|
|
5050
5653
|
const sql2 = pg.sql;
|
|
5051
|
-
const
|
|
5052
|
-
const
|
|
5053
|
-
const dimension = options.embeddingDimension ?? 1024;
|
|
5054
|
-
const defaultModels = !model || !embeddingModel ? createModelsFromEnv() : null;
|
|
5055
|
-
function getModel() {
|
|
5056
|
-
return model ?? defaultModels.model;
|
|
5057
|
-
}
|
|
5058
|
-
function getEmbeddingModel() {
|
|
5059
|
-
return embeddingModel ?? defaultModels.embeddingModel;
|
|
5060
|
-
}
|
|
5654
|
+
const resolvedProvider = options.provider ?? aiProvider();
|
|
5655
|
+
const dimension = options.embeddingDimension ?? resolvedProvider.dimension;
|
|
5061
5656
|
const agentsTable = pg.table("_agents", {
|
|
5062
5657
|
id: serial("id").primaryKey(),
|
|
5063
5658
|
tenant_id: text("tenant_id"),
|
|
@@ -5094,7 +5689,7 @@ function agent(options) {
|
|
|
5094
5689
|
trace_id: text("trace_id"),
|
|
5095
5690
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
5096
5691
|
});
|
|
5097
|
-
const runner = createRunner({ sql: sql2, agents: agentsTable, runs: runsTable, knowledge: knowledgeTable,
|
|
5692
|
+
const runner = createRunner({ sql: sql2, agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, provider: resolvedProvider, userTools: options.tools });
|
|
5098
5693
|
const base = new PgModule(pg);
|
|
5099
5694
|
const r = buildRouter2({ agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, runner });
|
|
5100
5695
|
const mod = r;
|
|
@@ -5635,7 +6230,7 @@ function createGateway(config, getPort) {
|
|
|
5635
6230
|
}
|
|
5636
6231
|
|
|
5637
6232
|
// deploy/manager.ts
|
|
5638
|
-
import
|
|
6233
|
+
import crypto6 from "node:crypto";
|
|
5639
6234
|
|
|
5640
6235
|
// deploy/process.ts
|
|
5641
6236
|
import { fork } from "node:child_process";
|
|
@@ -5694,7 +6289,7 @@ function createManager(config, apps, manager) {
|
|
|
5694
6289
|
const token = header.replace("Bearer ", "");
|
|
5695
6290
|
const tokenBuf = Buffer.from(token);
|
|
5696
6291
|
const secretBuf = Buffer.from(config.deployToken);
|
|
5697
|
-
if (tokenBuf.length !== secretBuf.length || !
|
|
6292
|
+
if (tokenBuf.length !== secretBuf.length || !crypto6.timingSafeEqual(tokenBuf, secretBuf)) {
|
|
5698
6293
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5699
6294
|
}
|
|
5700
6295
|
return next(req, ctx);
|
|
@@ -5990,7 +6585,7 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
|
5990
6585
|
// ssr.ts
|
|
5991
6586
|
import { createElement as createElement3 } from "react";
|
|
5992
6587
|
import { createHash as createHash3 } from "node:crypto";
|
|
5993
|
-
import { existsSync as existsSync4, readdirSync } from "node:fs";
|
|
6588
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
|
|
5994
6589
|
import { readdir, stat } from "node:fs/promises";
|
|
5995
6590
|
import { dirname as dirname3, join as join5, resolve as resolve6, relative as relative2 } from "node:path";
|
|
5996
6591
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
@@ -6107,6 +6702,7 @@ function compile(path2) {
|
|
|
6107
6702
|
return isDev() ? compileTsxDev(path2) : compileTsx(path2);
|
|
6108
6703
|
}
|
|
6109
6704
|
var vendorBundle = null;
|
|
6705
|
+
var vendorHash = "";
|
|
6110
6706
|
async function compileVendorBundle() {
|
|
6111
6707
|
if (vendorBundle) return vendorBundle;
|
|
6112
6708
|
if (!_userRequire) _userRequire = createRequire(join2(process.cwd(), "package.json"));
|
|
@@ -6121,8 +6717,18 @@ async function compileVendorBundle() {
|
|
|
6121
6717
|
const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
|
|
6122
6718
|
modules[request] = keys;
|
|
6123
6719
|
}
|
|
6124
|
-
const
|
|
6125
|
-
const
|
|
6720
|
+
const reactTsPath = resolve3(import.meta.dirname ?? __dirname, "react.ts");
|
|
6721
|
+
const reactSrc = readFileSync2(reactTsPath, "utf-8");
|
|
6722
|
+
const wfwKeys = [];
|
|
6723
|
+
for (const line of reactSrc.split("\n")) {
|
|
6724
|
+
const m = line.match(/^export\s+\{[^}]+\}\s*from/);
|
|
6725
|
+
if (m) {
|
|
6726
|
+
const names = line.slice(line.indexOf("{") + 1, line.indexOf("}")).split(",").map((s) => s.trim()).filter(Boolean);
|
|
6727
|
+
for (const n of names) {
|
|
6728
|
+
if (!n.startsWith("type ") && !wfwKeys.includes(n)) wfwKeys.push(n);
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
}
|
|
6126
6732
|
const used = /* @__PURE__ */ new Set();
|
|
6127
6733
|
const stmts = [""];
|
|
6128
6734
|
for (const [request, keys] of Object.entries(modules)) {
|
|
@@ -6130,7 +6736,7 @@ async function compileVendorBundle() {
|
|
|
6130
6736
|
if (unique.length > 0) stmts.push(`export { ${unique.join(", ")} } from ${JSON.stringify(request)};`);
|
|
6131
6737
|
}
|
|
6132
6738
|
const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
|
|
6133
|
-
if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from
|
|
6739
|
+
if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from ${JSON.stringify(reactTsPath)};`);
|
|
6134
6740
|
const result = await esbuild.build({
|
|
6135
6741
|
stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
|
|
6136
6742
|
format: "esm",
|
|
@@ -6138,13 +6744,68 @@ async function compileVendorBundle() {
|
|
|
6138
6744
|
write: false
|
|
6139
6745
|
});
|
|
6140
6746
|
vendorBundle = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
6747
|
+
const hashBytes = new TextEncoder().encode(vendorBundle);
|
|
6748
|
+
const hashBuffer = await crypto.subtle.digest("SHA-1", hashBytes);
|
|
6749
|
+
vendorHash = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
|
|
6141
6750
|
return vendorBundle;
|
|
6142
6751
|
}
|
|
6752
|
+
async function compileBrowser(path2, outDir) {
|
|
6753
|
+
const absPath = resolve3(path2);
|
|
6754
|
+
const h = id(absPath);
|
|
6755
|
+
outDir = outDir ?? resolve3(OUT_DIR);
|
|
6756
|
+
const outPath = join2(outDir, h + ".js");
|
|
6757
|
+
if (!isDev() && existsSync(outPath)) return h;
|
|
6758
|
+
mkdirSync(outDir, { recursive: true });
|
|
6759
|
+
const wfwDir = resolve3(import.meta.dirname ?? __dirname);
|
|
6760
|
+
const plugin = {
|
|
6761
|
+
name: "wfw-external",
|
|
6762
|
+
setup(build2) {
|
|
6763
|
+
build2.onResolve({ filter: /./ }, (args) => {
|
|
6764
|
+
if (args.kind === "entry-point") return;
|
|
6765
|
+
const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
|
|
6766
|
+
if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
|
|
6767
|
+
const rel = abs.slice(wfwDir.length + 1);
|
|
6768
|
+
if (rel.includes("/")) return;
|
|
6769
|
+
return { path: "weifuwu/react", external: true };
|
|
6770
|
+
}
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6773
|
+
};
|
|
6774
|
+
await esbuild.build({
|
|
6775
|
+
entryPoints: { [h]: absPath },
|
|
6776
|
+
outdir: outDir,
|
|
6777
|
+
format: "esm",
|
|
6778
|
+
platform: "browser",
|
|
6779
|
+
jsx: "automatic",
|
|
6780
|
+
jsxImportSource: "react",
|
|
6781
|
+
bundle: true,
|
|
6782
|
+
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"],
|
|
6783
|
+
plugins: [plugin],
|
|
6784
|
+
write: true,
|
|
6785
|
+
allowOverwrite: true
|
|
6786
|
+
});
|
|
6787
|
+
return h;
|
|
6788
|
+
}
|
|
6143
6789
|
async function compileHotComponent(path2) {
|
|
6144
6790
|
const absPath = resolve3(path2);
|
|
6145
6791
|
const h = id(absPath);
|
|
6146
6792
|
const stdin = `import C from ${JSON.stringify(absPath)};
|
|
6147
6793
|
(window.__WFW_REFRESH||function(){})(C)`;
|
|
6794
|
+
const wfwDir = resolve3(import.meta.dirname ?? __dirname);
|
|
6795
|
+
const plugin = {
|
|
6796
|
+
name: "wfw-external",
|
|
6797
|
+
setup(build2) {
|
|
6798
|
+
build2.onResolve({ filter: /./ }, (args) => {
|
|
6799
|
+
if (args.kind === "entry-point") return;
|
|
6800
|
+
const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
|
|
6801
|
+
if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
|
|
6802
|
+
const rel = abs.slice(wfwDir.length + 1);
|
|
6803
|
+
if (rel.includes("/")) return;
|
|
6804
|
+
return { path: "weifuwu/react", external: true };
|
|
6805
|
+
}
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
};
|
|
6148
6809
|
const result = await esbuild.build({
|
|
6149
6810
|
stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
|
|
6150
6811
|
format: "esm",
|
|
@@ -6152,7 +6813,8 @@ async function compileHotComponent(path2) {
|
|
|
6152
6813
|
jsx: "automatic",
|
|
6153
6814
|
jsxImportSource: "react",
|
|
6154
6815
|
bundle: true,
|
|
6155
|
-
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu/react"],
|
|
6816
|
+
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"],
|
|
6817
|
+
plugins: [plugin],
|
|
6156
6818
|
write: false
|
|
6157
6819
|
});
|
|
6158
6820
|
let code = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
@@ -6177,11 +6839,10 @@ function getPublicEnv() {
|
|
|
6177
6839
|
return _publicEnv;
|
|
6178
6840
|
}
|
|
6179
6841
|
function buildHeadPayload(opts) {
|
|
6180
|
-
const { ctx, base,
|
|
6842
|
+
const { ctx, base, tailwind } = opts;
|
|
6181
6843
|
let result = "";
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
result += `<script type="importmap">{
|
|
6844
|
+
const vUrl = `${base}/__wfw/v/bundle?h=${vendorHash}`;
|
|
6845
|
+
result += `<script type="importmap">{
|
|
6185
6846
|
"imports": {
|
|
6186
6847
|
"react": "${vUrl}",
|
|
6187
6848
|
"react-dom": "${vUrl}",
|
|
@@ -6191,22 +6852,12 @@ function buildHeadPayload(opts) {
|
|
|
6191
6852
|
}
|
|
6192
6853
|
}</script>
|
|
6193
6854
|
`;
|
|
6194
|
-
|
|
6195
|
-
if (ctx.prefs?.theme) {
|
|
6855
|
+
if (ctx.theme?.value) {
|
|
6196
6856
|
result += `<script>!function(){var t=(document.cookie.match(/(?:^|;\\s*)theme=([^;]+)/)||[])[1]||'system';if(t==='system'){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'}document.documentElement.setAttribute('data-theme',t)}()</script>
|
|
6197
6857
|
`;
|
|
6198
6858
|
}
|
|
6199
|
-
if (
|
|
6200
|
-
|
|
6201
|
-
if (cssUrl) result += `<link rel="stylesheet" href="${cssUrl}" />
|
|
6202
|
-
`;
|
|
6203
|
-
}
|
|
6204
|
-
const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
|
|
6205
|
-
if (localeData && Object.keys(localeData).length > 0) {
|
|
6206
|
-
if (!_localeDataCache || _localeDataCache.data !== localeData) {
|
|
6207
|
-
_localeDataCache = { data: localeData, json: JSON.stringify(localeData) };
|
|
6208
|
-
}
|
|
6209
|
-
result += `<script>window.__LOCALE_DATA__=${_localeDataCache.json}</script>
|
|
6859
|
+
if (tailwind?.css) {
|
|
6860
|
+
result += `<link rel="stylesheet" href="${tailwind.url}" />
|
|
6210
6861
|
`;
|
|
6211
6862
|
}
|
|
6212
6863
|
const loaderData = opts.loaderData || {};
|
|
@@ -6214,7 +6865,9 @@ function buildHeadPayload(opts) {
|
|
|
6214
6865
|
params: ctx.params,
|
|
6215
6866
|
query: ctx.query,
|
|
6216
6867
|
parsed: ctx.parsed,
|
|
6217
|
-
|
|
6868
|
+
theme: ctx.theme,
|
|
6869
|
+
i18n: ctx.i18n,
|
|
6870
|
+
flash: ctx.flash,
|
|
6218
6871
|
loaderData
|
|
6219
6872
|
};
|
|
6220
6873
|
if (ctx.user && typeof ctx.user === "object") {
|
|
@@ -6234,72 +6887,97 @@ function buildHeadPayload(opts) {
|
|
|
6234
6887
|
`;
|
|
6235
6888
|
return result;
|
|
6236
6889
|
}
|
|
6237
|
-
function buildBodyScripts(opts) {
|
|
6890
|
+
function buildBodyScripts(opts, hydrationScript) {
|
|
6238
6891
|
const parts = [];
|
|
6239
|
-
if (
|
|
6240
|
-
parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.loaderData)}</script>`);
|
|
6241
|
-
}
|
|
6242
|
-
if (opts.bundle) {
|
|
6243
|
-
parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
|
|
6244
|
-
}
|
|
6892
|
+
if (hydrationScript) parts.push(hydrationScript);
|
|
6245
6893
|
return parts.join("\n");
|
|
6246
6894
|
}
|
|
6247
|
-
|
|
6248
|
-
function streamResponse(reactStream, opts) {
|
|
6895
|
+
function streamResponse(reactStream, opts, hydrationScript) {
|
|
6249
6896
|
const decoder = new TextDecoder2();
|
|
6250
6897
|
const encoder2 = new TextEncoder2();
|
|
6251
|
-
const headPayload = buildHeadPayload(opts);
|
|
6252
|
-
let buffer = "";
|
|
6253
|
-
let headFlushed = false;
|
|
6254
|
-
let extractedHead = "";
|
|
6255
6898
|
const output = new ReadableStream({
|
|
6256
6899
|
async start(controller) {
|
|
6257
6900
|
try {
|
|
6258
6901
|
const reader = reactStream.getReader();
|
|
6259
|
-
|
|
6260
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
6261
|
-
if (!extractedHead) {
|
|
6262
|
-
const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
|
|
6263
|
-
if (m) {
|
|
6264
|
-
extractedHead = m[1];
|
|
6265
|
-
buffer = buffer.replace(m[0], "");
|
|
6266
|
-
}
|
|
6267
|
-
}
|
|
6268
|
-
if (!headFlushed) {
|
|
6269
|
-
const idx = buffer.indexOf("</head>");
|
|
6270
|
-
if (idx !== -1) {
|
|
6271
|
-
const before = buffer.slice(0, idx);
|
|
6272
|
-
let injection = "";
|
|
6273
|
-
if (extractedHead) injection += "\n" + extractedHead;
|
|
6274
|
-
injection += headPayload;
|
|
6275
|
-
controller.enqueue(encoder2.encode(before + injection));
|
|
6276
|
-
buffer = buffer.slice(idx);
|
|
6277
|
-
headFlushed = true;
|
|
6278
|
-
}
|
|
6279
|
-
return;
|
|
6280
|
-
}
|
|
6281
|
-
controller.enqueue(encoder2.encode(buffer));
|
|
6282
|
-
buffer = "";
|
|
6283
|
-
}
|
|
6902
|
+
let html = "";
|
|
6284
6903
|
while (true) {
|
|
6285
6904
|
const { done, value } = await reader.read();
|
|
6286
6905
|
if (done) break;
|
|
6287
|
-
|
|
6906
|
+
html += decoder.decode(value, { stream: true });
|
|
6907
|
+
}
|
|
6908
|
+
html += decoder.decode();
|
|
6909
|
+
const headTmpl = html.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
|
|
6910
|
+
if (headTmpl) {
|
|
6911
|
+
const extractedHead = headTmpl[1];
|
|
6912
|
+
html = html.replace(headTmpl[0], "");
|
|
6913
|
+
const headIdx2 = html.indexOf("</head>");
|
|
6914
|
+
if (headIdx2 !== -1) {
|
|
6915
|
+
html = html.slice(0, headIdx2) + "\n" + extractedHead + html.slice(headIdx2);
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
const headPayload = buildHeadPayload(opts);
|
|
6919
|
+
const headIdx = html.indexOf("</head>");
|
|
6920
|
+
if (headIdx !== -1) {
|
|
6921
|
+
html = html.slice(0, headIdx) + headPayload + html.slice(headIdx);
|
|
6288
6922
|
}
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
if (body) controller.enqueue(encoder2.encode("\n" + body));
|
|
6923
|
+
let bodyScripts = "";
|
|
6924
|
+
const built = buildBodyScripts(opts, hydrationScript);
|
|
6925
|
+
if (built) bodyScripts += built;
|
|
6293
6926
|
if (opts.isDev) {
|
|
6294
6927
|
const wsUrl = `${opts.base}/__weifuwu/livereload`;
|
|
6295
6928
|
const hbUrl = `${opts.base}/__wfw/h/`;
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6929
|
+
bodyScripts += `
|
|
6930
|
+
<script>
|
|
6931
|
+
(function(){
|
|
6932
|
+
var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${wsUrl}');
|
|
6933
|
+
var t=0;
|
|
6934
|
+
ws.onmessage=function(e){
|
|
6935
|
+
try{
|
|
6936
|
+
var m=JSON.parse(e.data);
|
|
6937
|
+
if(m.type==='component'){
|
|
6938
|
+
import('${hbUrl}'+m.hash+'?t='+Date.now()).catch(function(){location.reload()});
|
|
6939
|
+
if(m.css){
|
|
6940
|
+
var s=document.querySelector('style[data-lr]')||function(){
|
|
6941
|
+
var x=document.createElement('style');
|
|
6942
|
+
x.setAttribute('data-lr','');
|
|
6943
|
+
document.head.appendChild(x);
|
|
6944
|
+
return x
|
|
6945
|
+
}();
|
|
6946
|
+
s.textContent=m.css
|
|
6947
|
+
}
|
|
6948
|
+
return
|
|
6949
|
+
}
|
|
6950
|
+
if(m.type==='css'){
|
|
6951
|
+
var s=document.querySelector('style[data-lr]')||function(){
|
|
6952
|
+
var x=document.createElement('style');
|
|
6953
|
+
x.setAttribute('data-lr','');
|
|
6954
|
+
document.head.appendChild(x);
|
|
6955
|
+
return x
|
|
6956
|
+
}();
|
|
6957
|
+
s.textContent=m.css
|
|
6958
|
+
return
|
|
6959
|
+
}
|
|
6960
|
+
}catch(_){}
|
|
6961
|
+
if(e.data==='reload'&&Date.now()-t>1e3){t=Date.now();location.reload()}
|
|
6962
|
+
};
|
|
6963
|
+
ws.onclose=function(){
|
|
6964
|
+
if(Date.now()-t>1e3){
|
|
6965
|
+
t=Date.now();
|
|
6966
|
+
setTimeout(function(){location.reload()},500)
|
|
6967
|
+
}
|
|
6968
|
+
};
|
|
6969
|
+
})();
|
|
6970
|
+
</script>`;
|
|
6971
|
+
}
|
|
6972
|
+
if (bodyScripts) {
|
|
6973
|
+
const bodyIdx = html.lastIndexOf("</body>");
|
|
6974
|
+
if (bodyIdx !== -1) {
|
|
6975
|
+
html = html.slice(0, bodyIdx) + bodyScripts + html.slice(bodyIdx);
|
|
6976
|
+
}
|
|
6300
6977
|
}
|
|
6978
|
+
controller.enqueue(encoder2.encode(html));
|
|
6301
6979
|
} catch {
|
|
6302
|
-
const fallback =
|
|
6980
|
+
const fallback = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500 - Internal Server Error</h1></body></html>';
|
|
6303
6981
|
controller.enqueue(encoder2.encode(fallback));
|
|
6304
6982
|
} finally {
|
|
6305
6983
|
controller.close();
|
|
@@ -6329,9 +7007,9 @@ function tailwindContext(dir) {
|
|
|
6329
7007
|
await compileTailwindCss(cssPath, cssDir);
|
|
6330
7008
|
}
|
|
6331
7009
|
const entry = cssCache.get(cssPath);
|
|
6332
|
-
ctx.compiledTailwindCss = entry.css;
|
|
6333
7010
|
const base = (ctx.mountPath || "").replace(/\/$/, "");
|
|
6334
|
-
|
|
7011
|
+
const url = base ? `${base}/__wfw/style/${entry.hash}.css` : `/__wfw/style/${entry.hash}.css`;
|
|
7012
|
+
ctx.tailwind = { css: entry.css, url };
|
|
6335
7013
|
return next(req, ctx);
|
|
6336
7014
|
};
|
|
6337
7015
|
}
|
|
@@ -6425,6 +7103,8 @@ function liveWs() {
|
|
|
6425
7103
|
}
|
|
6426
7104
|
function liveRouter(dir) {
|
|
6427
7105
|
const r = new Router();
|
|
7106
|
+
compileVendorBundle().catch(() => {
|
|
7107
|
+
});
|
|
6428
7108
|
r.get("/__wfw/v/bundle", async () => {
|
|
6429
7109
|
const code = await compileVendorBundle();
|
|
6430
7110
|
return new Response(code, {
|
|
@@ -6472,7 +7152,6 @@ function liveWatcher(dir) {
|
|
|
6472
7152
|
return broadcastReload();
|
|
6473
7153
|
}
|
|
6474
7154
|
clearCompileCache();
|
|
6475
|
-
markClientBundleDirty();
|
|
6476
7155
|
const targets = existsSync3(entryPath) ? [entryPath] : findEntries(resolve5(filePath));
|
|
6477
7156
|
if (targets.length === 0) return broadcastReload();
|
|
6478
7157
|
try {
|
|
@@ -6578,7 +7257,7 @@ function errorBoundary(errorPath) {
|
|
|
6578
7257
|
ctx,
|
|
6579
7258
|
base,
|
|
6580
7259
|
isDev: isDev(),
|
|
6581
|
-
|
|
7260
|
+
tailwind: ctx.tailwind,
|
|
6582
7261
|
status: 500
|
|
6583
7262
|
});
|
|
6584
7263
|
}
|
|
@@ -6589,25 +7268,6 @@ function errorBoundary(errorPath) {
|
|
|
6589
7268
|
var isDev2 = isDev();
|
|
6590
7269
|
var als2 = new AsyncLocalStorage2();
|
|
6591
7270
|
__registerAls(() => als2.getStore());
|
|
6592
|
-
var bundleCache = /* @__PURE__ */ new Map();
|
|
6593
|
-
var _bundleDirty = false;
|
|
6594
|
-
function markClientBundleDirty() {
|
|
6595
|
-
_bundleDirty = true;
|
|
6596
|
-
}
|
|
6597
|
-
function getBundle(key) {
|
|
6598
|
-
if (_bundleDirty) {
|
|
6599
|
-
bundleCache.clear();
|
|
6600
|
-
_bundleDirty = false;
|
|
6601
|
-
}
|
|
6602
|
-
return bundleCache.get(key);
|
|
6603
|
-
}
|
|
6604
|
-
function setBundle(key, buf) {
|
|
6605
|
-
if (_bundleDirty) {
|
|
6606
|
-
bundleCache.clear();
|
|
6607
|
-
_bundleDirty = false;
|
|
6608
|
-
}
|
|
6609
|
-
bundleCache.set(key, buf);
|
|
6610
|
-
}
|
|
6611
7271
|
function hashId(s) {
|
|
6612
7272
|
return createHash3("md5").update(s).digest("hex").slice(0, 8);
|
|
6613
7273
|
}
|
|
@@ -6722,50 +7382,40 @@ async function resolveRoute(ssrDir, segments, routeCache) {
|
|
|
6722
7382
|
routeCache.set(cacheKey, result);
|
|
6723
7383
|
return result;
|
|
6724
7384
|
}
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
format: "esm",
|
|
6754
|
-
jsx: "automatic",
|
|
6755
|
-
jsxImportSource: "react",
|
|
6756
|
-
banner: { js: "self.process={env:{}};" },
|
|
6757
|
-
loader: { ".node": "empty" },
|
|
6758
|
-
external: isDev2 ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"] : void 0,
|
|
6759
|
-
write: false,
|
|
6760
|
-
minify: !isDev2
|
|
6761
|
-
});
|
|
6762
|
-
return result.outputFiles[0].contents;
|
|
6763
|
-
} catch (err) {
|
|
6764
|
-
console.error("hydration bundle failed:", err);
|
|
6765
|
-
return null;
|
|
6766
|
-
}
|
|
7385
|
+
function buildHydrationScript(entryId, ctxJson, base) {
|
|
7386
|
+
const ssrPrefix = `${base}/__ssr`;
|
|
7387
|
+
return `
|
|
7388
|
+
<script type="module">
|
|
7389
|
+
import { setCtx, TsxContext } from 'weifuwu/react';
|
|
7390
|
+
import { createElement } from 'react';
|
|
7391
|
+
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
7392
|
+
|
|
7393
|
+
const _ctx = ${ctxJson};
|
|
7394
|
+
setCtx(_ctx);
|
|
7395
|
+
|
|
7396
|
+
const _root = document.getElementById('__weifuwu_root');
|
|
7397
|
+
|
|
7398
|
+
async function init() {
|
|
7399
|
+
const { default: Page } = await import('${ssrPrefix}/${entryId}.js');
|
|
7400
|
+
const app = createElement(TsxContext.Provider, { value: _ctx },
|
|
7401
|
+
createElement(Page));
|
|
7402
|
+
${isDev2 ? `
|
|
7403
|
+
const reactRoot = createRoot(_root);
|
|
7404
|
+
reactRoot.render(app);
|
|
7405
|
+
window.__WFW_REFRESH = async (NewComponent) => {
|
|
7406
|
+
const store = globalThis.__WEIFUWU_CTX_STORE?._ctx || _ctx;
|
|
7407
|
+
reactRoot.render(createElement(TsxContext.Provider, { value: store },
|
|
7408
|
+
createElement(NewComponent)));
|
|
7409
|
+
};
|
|
7410
|
+
` : `
|
|
7411
|
+
hydrateRoot(_root, app);
|
|
7412
|
+
`}
|
|
6767
7413
|
}
|
|
6768
|
-
|
|
7414
|
+
|
|
7415
|
+
init();
|
|
7416
|
+
</script>`;
|
|
7417
|
+
}
|
|
7418
|
+
function renderPage(pageFile, outDir) {
|
|
6769
7419
|
const absPath = resolve6(pageFile);
|
|
6770
7420
|
const entryId = hashId(absPath);
|
|
6771
7421
|
ssrEntries.set(entryId, { path: absPath });
|
|
@@ -6791,15 +7441,15 @@ function renderPage(pageFile) {
|
|
|
6791
7441
|
query: ctx.query,
|
|
6792
7442
|
user: ctx.user ?? {},
|
|
6793
7443
|
parsed: ctx.parsed ?? {},
|
|
6794
|
-
|
|
7444
|
+
theme: ctx.theme,
|
|
7445
|
+
i18n: ctx.i18n,
|
|
7446
|
+
flash: ctx.flash,
|
|
6795
7447
|
loaderData,
|
|
6796
7448
|
env: ctx.env ?? {}
|
|
6797
7449
|
};
|
|
6798
7450
|
return als2.run(ctxValue, async () => {
|
|
6799
7451
|
setCtx(ctxValue);
|
|
6800
|
-
|
|
6801
|
-
globalThis.__LOCALE_DATA__ = ctxValue.parsed.__localeData;
|
|
6802
|
-
}
|
|
7452
|
+
await compileBrowser(absPath, outDir);
|
|
6803
7453
|
let element = createElement3(
|
|
6804
7454
|
"div",
|
|
6805
7455
|
{ id: "__weifuwu_root" },
|
|
@@ -6810,24 +7460,15 @@ function renderPage(pageFile) {
|
|
|
6810
7460
|
)
|
|
6811
7461
|
);
|
|
6812
7462
|
element = buildHtmlShell("weifuwu", element, layoutComponents);
|
|
6813
|
-
let bundle = null;
|
|
6814
|
-
if (!getBundle(bundleKey)) {
|
|
6815
|
-
const buf = await buildClientBundle(absPath, layoutPaths);
|
|
6816
|
-
if (buf) setBundle(bundleKey, buf);
|
|
6817
|
-
}
|
|
6818
|
-
if (getBundle(bundleKey)) {
|
|
6819
|
-
bundle = { url: bundleKey };
|
|
6820
|
-
}
|
|
6821
7463
|
const { renderToReadableStream } = await import("react-dom/server");
|
|
6822
7464
|
const stream = await renderToReadableStream(element);
|
|
6823
7465
|
return streamResponse(stream, {
|
|
6824
7466
|
ctx,
|
|
6825
7467
|
base,
|
|
6826
7468
|
isDev: isDev2,
|
|
6827
|
-
bundle,
|
|
6828
7469
|
loaderData,
|
|
6829
|
-
|
|
6830
|
-
});
|
|
7470
|
+
tailwind: ctx.tailwind
|
|
7471
|
+
}, buildHydrationScript(entryId, JSON.stringify(ctxValue), base));
|
|
6831
7472
|
});
|
|
6832
7473
|
};
|
|
6833
7474
|
}
|
|
@@ -6873,11 +7514,23 @@ function discoverRoutes(dir) {
|
|
|
6873
7514
|
function ssr(opts) {
|
|
6874
7515
|
const r = new Router();
|
|
6875
7516
|
const dir = resolve6(opts.dir);
|
|
7517
|
+
const outDir = resolve6(OUT_DIR);
|
|
6876
7518
|
const routeCache = /* @__PURE__ */ new Map();
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
7519
|
+
compileVendorBundle().catch(() => {
|
|
7520
|
+
});
|
|
7521
|
+
r.get("/__ssr/:file", (req, ctx) => {
|
|
7522
|
+
const filePath = join5(outDir, ctx.params.file);
|
|
7523
|
+
if (!filePath.startsWith(outDir) || !existsSync4(filePath)) {
|
|
7524
|
+
return new Response("Not Found", { status: 404 });
|
|
7525
|
+
}
|
|
7526
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
7527
|
+
return new Response(content, {
|
|
7528
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7529
|
+
});
|
|
7530
|
+
});
|
|
7531
|
+
r.get("/__wfw/v/bundle", async () => {
|
|
7532
|
+
const code = await compileVendorBundle();
|
|
7533
|
+
return new Response(code, {
|
|
6881
7534
|
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
6882
7535
|
});
|
|
6883
7536
|
});
|
|
@@ -6904,7 +7557,7 @@ function ssr(opts) {
|
|
|
6904
7557
|
...resolved.layoutFiles.map((f) => layout(f)),
|
|
6905
7558
|
tailwindContext(dir)
|
|
6906
7559
|
];
|
|
6907
|
-
const handler = (req2, ctx2) => renderPage(resolved.pageFile)(req2, ctx2);
|
|
7560
|
+
const handler = (req2, ctx2) => renderPage(resolved.pageFile, outDir)(req2, ctx2);
|
|
6908
7561
|
return runChain(mws, handler, req, ctx);
|
|
6909
7562
|
});
|
|
6910
7563
|
const mod = r;
|
|
@@ -6994,13 +7647,13 @@ async function addToolMessages(sql2, sessionId, toolCalls, toolResults) {
|
|
|
6994
7647
|
}
|
|
6995
7648
|
|
|
6996
7649
|
// opencode/run.ts
|
|
6997
|
-
import { streamText as
|
|
7650
|
+
import { streamText as streamText2, stepCountIs } from "ai";
|
|
6998
7651
|
async function* executeGenerator(opts) {
|
|
6999
7652
|
const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql: sql2, abortSignal } = opts;
|
|
7000
7653
|
const lastStepToolCalls = [];
|
|
7001
7654
|
let currentAssistantText = "";
|
|
7002
7655
|
let currentUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
7003
|
-
const result =
|
|
7656
|
+
const result = streamText2({
|
|
7004
7657
|
model,
|
|
7005
7658
|
system: systemPrompt,
|
|
7006
7659
|
messages: [
|
|
@@ -7167,7 +7820,7 @@ function createBashTool(ctx) {
|
|
|
7167
7820
|
// opencode/tools/read.ts
|
|
7168
7821
|
import { tool as tool4 } from "ai";
|
|
7169
7822
|
import { z as z6 } from "zod";
|
|
7170
|
-
import { readFileSync as
|
|
7823
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
7171
7824
|
import { resolve as resolve7 } from "node:path";
|
|
7172
7825
|
function createReadTool(ctx) {
|
|
7173
7826
|
return tool4({
|
|
@@ -7179,10 +7832,10 @@ function createReadTool(ctx) {
|
|
|
7179
7832
|
}),
|
|
7180
7833
|
execute: async ({ path: path2, offset, limit }) => {
|
|
7181
7834
|
const resolved = resolve7(ctx.workspace, path2);
|
|
7182
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7835
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7183
7836
|
return { error: "Path not allowed", content: null, totalLines: 0 };
|
|
7184
7837
|
}
|
|
7185
|
-
const content =
|
|
7838
|
+
const content = readFileSync5(resolved, "utf-8");
|
|
7186
7839
|
const lines = content.split("\n");
|
|
7187
7840
|
const totalLines = lines.length;
|
|
7188
7841
|
if (offset !== void 0) {
|
|
@@ -7220,7 +7873,7 @@ function createWriteTool(ctx) {
|
|
|
7220
7873
|
}),
|
|
7221
7874
|
execute: async ({ path: path2, content }) => {
|
|
7222
7875
|
const resolved = resolve8(ctx.workspace, path2);
|
|
7223
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7876
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7224
7877
|
return { error: "Path not allowed" };
|
|
7225
7878
|
}
|
|
7226
7879
|
mkdirSync3(dirname4(resolved), { recursive: true });
|
|
@@ -7233,7 +7886,7 @@ function createWriteTool(ctx) {
|
|
|
7233
7886
|
// opencode/tools/edit.ts
|
|
7234
7887
|
import { tool as tool6 } from "ai";
|
|
7235
7888
|
import { z as z8 } from "zod";
|
|
7236
|
-
import { readFileSync as
|
|
7889
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7237
7890
|
import { resolve as resolve9 } from "node:path";
|
|
7238
7891
|
function createEditTool(ctx) {
|
|
7239
7892
|
return tool6({
|
|
@@ -7246,10 +7899,10 @@ function createEditTool(ctx) {
|
|
|
7246
7899
|
}),
|
|
7247
7900
|
execute: async ({ path: path2, oldString, newString, replaceAll }) => {
|
|
7248
7901
|
const resolved = resolve9(ctx.workspace, path2);
|
|
7249
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7902
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7250
7903
|
return { error: "Path not allowed" };
|
|
7251
7904
|
}
|
|
7252
|
-
const content =
|
|
7905
|
+
const content = readFileSync6(resolved, "utf-8");
|
|
7253
7906
|
if (replaceAll) {
|
|
7254
7907
|
if (!content.includes(oldString)) {
|
|
7255
7908
|
return { error: "oldString not found in file", replaced: 0 };
|
|
@@ -7434,7 +8087,7 @@ ${availableList}
|
|
|
7434
8087
|
name: z13.string().describe("The name of the skill to load")
|
|
7435
8088
|
}),
|
|
7436
8089
|
execute: async ({ name }) => {
|
|
7437
|
-
if (!isSkillAllowed(name, ctx.permissions)) {
|
|
8090
|
+
if (!isSkillAllowed(name, ctx.permissions.permissions)) {
|
|
7438
8091
|
return { error: `Skill "${name}" is not permitted` };
|
|
7439
8092
|
}
|
|
7440
8093
|
const skill = ctx.skillsRegistry.get(name);
|
|
@@ -7455,28 +8108,28 @@ ${availableList}
|
|
|
7455
8108
|
// opencode/tools/index.ts
|
|
7456
8109
|
function createTools(ctx) {
|
|
7457
8110
|
const tools = {};
|
|
7458
|
-
if (isToolEnabled("bash", ctx.permissions)) {
|
|
8111
|
+
if (isToolEnabled("bash", ctx.permissions.permissions)) {
|
|
7459
8112
|
tools.bash = createBashTool(ctx);
|
|
7460
8113
|
}
|
|
7461
|
-
if (isToolEnabled("read", ctx.permissions)) {
|
|
8114
|
+
if (isToolEnabled("read", ctx.permissions.permissions)) {
|
|
7462
8115
|
tools.read = createReadTool(ctx);
|
|
7463
8116
|
}
|
|
7464
|
-
if (isToolEnabled("write", ctx.permissions)) {
|
|
8117
|
+
if (isToolEnabled("write", ctx.permissions.permissions)) {
|
|
7465
8118
|
tools.write = createWriteTool(ctx);
|
|
7466
8119
|
}
|
|
7467
|
-
if (isToolEnabled("edit", ctx.permissions)) {
|
|
8120
|
+
if (isToolEnabled("edit", ctx.permissions.permissions)) {
|
|
7468
8121
|
tools.edit = createEditTool(ctx);
|
|
7469
8122
|
}
|
|
7470
|
-
if (isToolEnabled("grep", ctx.permissions)) {
|
|
8123
|
+
if (isToolEnabled("grep", ctx.permissions.permissions)) {
|
|
7471
8124
|
tools.grep = createGrepTool(ctx);
|
|
7472
8125
|
}
|
|
7473
|
-
if (isToolEnabled("glob", ctx.permissions)) {
|
|
8126
|
+
if (isToolEnabled("glob", ctx.permissions.permissions)) {
|
|
7474
8127
|
tools.glob = createGlobTool(ctx);
|
|
7475
8128
|
}
|
|
7476
|
-
if (isToolEnabled("web", ctx.permissions)) {
|
|
8129
|
+
if (isToolEnabled("web", ctx.permissions.permissions)) {
|
|
7477
8130
|
tools.web = createWebTool(ctx);
|
|
7478
8131
|
}
|
|
7479
|
-
if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions)) {
|
|
8132
|
+
if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions.permissions)) {
|
|
7480
8133
|
tools.skill = createSkillTool(ctx);
|
|
7481
8134
|
}
|
|
7482
8135
|
tools.question = createQuestionTool(ctx);
|
|
@@ -7485,7 +8138,7 @@ function createTools(ctx) {
|
|
|
7485
8138
|
|
|
7486
8139
|
// opencode/rest.ts
|
|
7487
8140
|
async function buildRouter4(deps) {
|
|
7488
|
-
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
8141
|
+
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
|
|
7489
8142
|
const router = new Router();
|
|
7490
8143
|
router.post("/sessions", async (req, ctx) => {
|
|
7491
8144
|
const body = await req.json().catch(() => ({}));
|
|
@@ -7520,7 +8173,7 @@ async function buildRouter4(deps) {
|
|
|
7520
8173
|
if (!content) return new Response("Missing content", { status: 400 });
|
|
7521
8174
|
const toolCtx = {
|
|
7522
8175
|
workspace: session2.workspace || workspace,
|
|
7523
|
-
permissions,
|
|
8176
|
+
permissions: permissions2,
|
|
7524
8177
|
pendingQuestions,
|
|
7525
8178
|
skillsRegistry: deps.skillsRegistry
|
|
7526
8179
|
};
|
|
@@ -7578,7 +8231,7 @@ async function buildRouter4(deps) {
|
|
|
7578
8231
|
// opencode/ws.ts
|
|
7579
8232
|
var clients2 = /* @__PURE__ */ new WeakMap();
|
|
7580
8233
|
function createWSHandler2(deps) {
|
|
7581
|
-
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
8234
|
+
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
|
|
7582
8235
|
return {
|
|
7583
8236
|
open(ws, ctx) {
|
|
7584
8237
|
const userId = ctx.user?.id ?? 0;
|
|
@@ -7628,7 +8281,7 @@ function createWSHandler2(deps) {
|
|
|
7628
8281
|
}
|
|
7629
8282
|
const toolCtx = {
|
|
7630
8283
|
workspace: session2.workspace || workspace,
|
|
7631
|
-
permissions,
|
|
8284
|
+
permissions: permissions2,
|
|
7632
8285
|
pendingQuestions,
|
|
7633
8286
|
skillsRegistry
|
|
7634
8287
|
};
|
|
@@ -7790,7 +8443,7 @@ async function opencode(options) {
|
|
|
7790
8443
|
const workspace = options.workspace || process.cwd();
|
|
7791
8444
|
const systemPrompt = options.systemPrompt;
|
|
7792
8445
|
const manualSkills = options.skills || [];
|
|
7793
|
-
const
|
|
8446
|
+
const permissions2 = options.permissions;
|
|
7794
8447
|
const modelName = options.model || "deepseek-v4-flash";
|
|
7795
8448
|
const [discoveredSkills] = await Promise.all([discoverSkills(workspace)]);
|
|
7796
8449
|
const skillsRegistry = buildSkillRegistry(discoveredSkills, manualSkills);
|
|
@@ -7798,7 +8451,7 @@ async function opencode(options) {
|
|
|
7798
8451
|
const model = provider.chat(modelName);
|
|
7799
8452
|
const pendingQuestions = /* @__PURE__ */ new Map();
|
|
7800
8453
|
const base = new PgModule(pg);
|
|
7801
|
-
const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
|
|
8454
|
+
const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
|
|
7802
8455
|
const mod = r;
|
|
7803
8456
|
mod.migrate = async () => {
|
|
7804
8457
|
const sessions2 = pg.table("_opencode_sessions", {
|
|
@@ -7831,7 +8484,7 @@ async function opencode(options) {
|
|
|
7831
8484
|
await messages2.create();
|
|
7832
8485
|
await messages2.createIndex(["session_id", "created_at"]);
|
|
7833
8486
|
};
|
|
7834
|
-
mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
|
|
8487
|
+
mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
|
|
7835
8488
|
mod.close = () => base.close();
|
|
7836
8489
|
return mod;
|
|
7837
8490
|
}
|
|
@@ -8101,12 +8754,49 @@ function analytics(options) {
|
|
|
8101
8754
|
return mod;
|
|
8102
8755
|
}
|
|
8103
8756
|
|
|
8104
|
-
//
|
|
8757
|
+
// theme.ts
|
|
8758
|
+
function makeSetTheme(cookie, location) {
|
|
8759
|
+
return (value, loc) => {
|
|
8760
|
+
const finalLoc = loc ?? location;
|
|
8761
|
+
const c = `${cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8762
|
+
return new Response(null, { status: 302, headers: { Location: finalLoc, "Set-Cookie": c } });
|
|
8763
|
+
};
|
|
8764
|
+
}
|
|
8765
|
+
function theme(options) {
|
|
8766
|
+
const opts = { default: "system", cookie: "theme", ...options };
|
|
8767
|
+
return async (req, ctx, next) => {
|
|
8768
|
+
const url = new URL(req.url);
|
|
8769
|
+
const match = url.pathname.match(/^\/__theme\/([\w-]+)$/);
|
|
8770
|
+
if (match && req.method === "GET") {
|
|
8771
|
+
const value = match[1];
|
|
8772
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8773
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8774
|
+
if (accept.includes("application/json")) {
|
|
8775
|
+
return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
|
|
8776
|
+
}
|
|
8777
|
+
const referer = req.headers.get("referer") || "/";
|
|
8778
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8779
|
+
}
|
|
8780
|
+
let themeValue = opts.default;
|
|
8781
|
+
if (opts.cookie) {
|
|
8782
|
+
const fromCookie = getCookies(req)[opts.cookie];
|
|
8783
|
+
if (fromCookie) themeValue = fromCookie;
|
|
8784
|
+
}
|
|
8785
|
+
ctx.theme = {
|
|
8786
|
+
value: themeValue,
|
|
8787
|
+
set: makeSetTheme(opts.cookie, req.headers.get("referer") || "/")
|
|
8788
|
+
};
|
|
8789
|
+
return next(req, ctx);
|
|
8790
|
+
};
|
|
8791
|
+
}
|
|
8792
|
+
|
|
8793
|
+
// i18n.ts
|
|
8105
8794
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
8106
8795
|
import { join as join7, resolve as resolve13 } from "node:path";
|
|
8107
|
-
var
|
|
8108
|
-
|
|
8109
|
-
|
|
8796
|
+
var DEFAULTS2 = {
|
|
8797
|
+
default: "en",
|
|
8798
|
+
cookie: "locale",
|
|
8799
|
+
fromAcceptLanguage: true
|
|
8110
8800
|
};
|
|
8111
8801
|
function translate(msgs, key, params, fallback) {
|
|
8112
8802
|
const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
|
|
@@ -8118,130 +8808,125 @@ function translate(msgs, key, params, fallback) {
|
|
|
8118
8808
|
}
|
|
8119
8809
|
return result;
|
|
8120
8810
|
}
|
|
8121
|
-
function
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
async function handlePrefSwitch(req, value, cookieName, load) {
|
|
8125
|
-
const isJson = req.headers.get("accept")?.includes("application/json");
|
|
8126
|
-
if (isJson) {
|
|
8127
|
-
const result = { ok: true };
|
|
8128
|
-
if (cookieName === "locale" || cookieName === "lang") {
|
|
8129
|
-
result.locale = value;
|
|
8130
|
-
const messages2 = await load(value);
|
|
8131
|
-
if (Object.keys(messages2).length > 0) result.messages = messages2;
|
|
8132
|
-
} else {
|
|
8133
|
-
result.theme = value;
|
|
8134
|
-
}
|
|
8135
|
-
return Response.json(result, {
|
|
8136
|
-
headers: { "Set-Cookie": prefCookie(cookieName, value) }
|
|
8137
|
-
});
|
|
8138
|
-
}
|
|
8139
|
-
const referer = req.headers.get("referer") || "/";
|
|
8140
|
-
return new Response(null, {
|
|
8141
|
-
status: 302,
|
|
8142
|
-
headers: { Location: referer, "Set-Cookie": prefCookie(cookieName, value) }
|
|
8143
|
-
});
|
|
8144
|
-
}
|
|
8145
|
-
function preferences(options) {
|
|
8146
|
-
const dir = options.dir ? resolve13(options.dir) : void 0;
|
|
8147
|
-
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
8148
|
-
const themeOpts = { ...defaults.theme, ...options.theme };
|
|
8811
|
+
function i18n(options) {
|
|
8812
|
+
const opts = { ...DEFAULTS2, ...options };
|
|
8813
|
+
const dir = opts.dir ? resolve13(opts.dir) : void 0;
|
|
8149
8814
|
const cache3 = /* @__PURE__ */ new Map();
|
|
8150
8815
|
function validLocale(locale) {
|
|
8151
8816
|
return /^[\w-]+$/.test(locale) && !locale.includes("..");
|
|
8152
8817
|
}
|
|
8153
|
-
async function
|
|
8154
|
-
if (
|
|
8155
|
-
|
|
8818
|
+
async function loadMessages(locale) {
|
|
8819
|
+
if (opts.messages?.[locale] && Object.keys(opts.messages[locale]).length > 0) {
|
|
8820
|
+
cache3.set(locale, opts.messages[locale]);
|
|
8821
|
+
return opts.messages[locale];
|
|
8822
|
+
}
|
|
8823
|
+
if (!dir || !validLocale(locale)) return {};
|
|
8156
8824
|
const cached = cache3.get(locale);
|
|
8157
8825
|
if (cached) return cached;
|
|
8158
8826
|
const filePath = join7(dir, `${locale}.json`);
|
|
8159
|
-
let data = null;
|
|
8160
8827
|
try {
|
|
8161
8828
|
await stat2(filePath);
|
|
8162
8829
|
const content = await readFile2(filePath, "utf-8");
|
|
8163
|
-
data = JSON.parse(content);
|
|
8830
|
+
const data = JSON.parse(content);
|
|
8164
8831
|
cache3.set(locale, data);
|
|
8165
8832
|
return data;
|
|
8166
8833
|
} catch {
|
|
8167
8834
|
}
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
return fallback;
|
|
8175
|
-
}
|
|
8835
|
+
const short = locale.split("-")[0];
|
|
8836
|
+
if (short !== locale) {
|
|
8837
|
+
const fallback = cache3.get(short) || await loadMessages(short);
|
|
8838
|
+
if (fallback && Object.keys(fallback).length > 0) {
|
|
8839
|
+
cache3.set(locale, fallback);
|
|
8840
|
+
return fallback;
|
|
8176
8841
|
}
|
|
8177
8842
|
}
|
|
8178
8843
|
return {};
|
|
8179
8844
|
}
|
|
8845
|
+
function detectLocale(req) {
|
|
8846
|
+
if (opts.cookie) {
|
|
8847
|
+
const fromCookie = getCookies(req)[opts.cookie];
|
|
8848
|
+
if (fromCookie && validLocale(fromCookie)) return fromCookie;
|
|
8849
|
+
}
|
|
8850
|
+
if (opts.fromAcceptLanguage) {
|
|
8851
|
+
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
|
|
8852
|
+
if (fromHeader && validLocale(fromHeader)) return fromHeader;
|
|
8853
|
+
}
|
|
8854
|
+
return opts.default;
|
|
8855
|
+
}
|
|
8180
8856
|
return async (req, ctx, next) => {
|
|
8181
8857
|
const url = new URL(req.url);
|
|
8182
|
-
const
|
|
8183
|
-
if (
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
const msgs = await load(locale);
|
|
8195
|
-
ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
|
|
8196
|
-
globalThis.__LOCALE_DATA__ = msgs;
|
|
8197
|
-
ctx.parsed = { ...ctx.parsed, __localeData: msgs };
|
|
8198
|
-
}
|
|
8199
|
-
ctx.setPref = (name, value) => {
|
|
8200
|
-
const cookieOpts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Lax"];
|
|
8858
|
+
const match = url.pathname.match(/^\/__lang\/([\w-]+)$/);
|
|
8859
|
+
if (match && req.method === "GET") {
|
|
8860
|
+
const value = match[1];
|
|
8861
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8862
|
+
const messages2 = await loadMessages(value);
|
|
8863
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8864
|
+
if (accept.includes("application/json")) {
|
|
8865
|
+
return Response.json(
|
|
8866
|
+
{ ok: true, locale: value, messages: Object.keys(messages2).length > 0 ? messages2 : void 0 },
|
|
8867
|
+
{ headers: { "Set-Cookie": cookie } }
|
|
8868
|
+
);
|
|
8869
|
+
}
|
|
8201
8870
|
const referer = req.headers.get("referer") || "/";
|
|
8202
|
-
return new Response(null, {
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8871
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8872
|
+
}
|
|
8873
|
+
const locale = detectLocale(req);
|
|
8874
|
+
const msgs = await loadMessages(locale);
|
|
8875
|
+
ctx.i18n = {
|
|
8876
|
+
locale,
|
|
8877
|
+
messages: msgs,
|
|
8878
|
+
t: (key, params, fallback) => translate(msgs, key, params, fallback),
|
|
8879
|
+
set: (value, loc) => {
|
|
8880
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8881
|
+
const location = loc ?? (req.headers.get("referer") || "/");
|
|
8882
|
+
return new Response(null, { status: 302, headers: { Location: location, "Set-Cookie": cookie } });
|
|
8883
|
+
}
|
|
8209
8884
|
};
|
|
8210
|
-
|
|
8211
|
-
|
|
8885
|
+
return next(req, ctx);
|
|
8886
|
+
};
|
|
8887
|
+
}
|
|
8888
|
+
|
|
8889
|
+
// flash.ts
|
|
8890
|
+
function makeSetFlash(name, location) {
|
|
8891
|
+
return (data, loc) => {
|
|
8892
|
+
const finalLoc = loc ?? location;
|
|
8893
|
+
const value = encodeURIComponent(JSON.stringify(data));
|
|
8894
|
+
return new Response(null, {
|
|
8895
|
+
status: 302,
|
|
8896
|
+
headers: {
|
|
8897
|
+
Location: finalLoc,
|
|
8898
|
+
"Set-Cookie": `${name}=${value}; Path=/; SameSite=Lax`
|
|
8899
|
+
}
|
|
8900
|
+
});
|
|
8901
|
+
};
|
|
8902
|
+
}
|
|
8903
|
+
function flash(options) {
|
|
8904
|
+
const name = options?.name ?? "flash";
|
|
8905
|
+
return async (req, ctx, next) => {
|
|
8906
|
+
const raw = getCookies(req)[name] ?? null;
|
|
8907
|
+
const referer = req.headers.get("referer") || "/";
|
|
8908
|
+
let value = void 0;
|
|
8909
|
+
if (raw) {
|
|
8212
8910
|
try {
|
|
8213
|
-
|
|
8911
|
+
value = JSON.parse(decodeURIComponent(raw));
|
|
8214
8912
|
} catch {
|
|
8215
|
-
|
|
8913
|
+
value = raw;
|
|
8216
8914
|
}
|
|
8217
8915
|
}
|
|
8916
|
+
;
|
|
8917
|
+
ctx.flash = {
|
|
8918
|
+
value,
|
|
8919
|
+
set: makeSetFlash(name, referer)
|
|
8920
|
+
};
|
|
8218
8921
|
const res = await next(req, ctx);
|
|
8219
|
-
if (
|
|
8922
|
+
if (raw) {
|
|
8220
8923
|
const headers = new Headers(res.headers);
|
|
8221
|
-
headers.append("Set-Cookie",
|
|
8924
|
+
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
|
|
8222
8925
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
8223
8926
|
}
|
|
8224
8927
|
return res;
|
|
8225
8928
|
};
|
|
8226
8929
|
}
|
|
8227
|
-
function detectLocale(req, opts) {
|
|
8228
|
-
if (opts.cookie) {
|
|
8229
|
-
const fromCookie = getCookies(req)[opts.cookie];
|
|
8230
|
-
if (fromCookie) return fromCookie;
|
|
8231
|
-
}
|
|
8232
|
-
if (opts.fromAcceptLanguage) {
|
|
8233
|
-
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
|
|
8234
|
-
if (fromHeader) return fromHeader;
|
|
8235
|
-
}
|
|
8236
|
-
return opts.default;
|
|
8237
|
-
}
|
|
8238
|
-
function detectTheme(req, opts) {
|
|
8239
|
-
if (opts.cookie) {
|
|
8240
|
-
const fromCookie = getCookies(req)[opts.cookie];
|
|
8241
|
-
if (fromCookie) return fromCookie;
|
|
8242
|
-
}
|
|
8243
|
-
return opts.default;
|
|
8244
|
-
}
|
|
8245
8930
|
|
|
8246
8931
|
// seo.ts
|
|
8247
8932
|
function escapeXml(s) {
|
|
@@ -8620,7 +9305,7 @@ function logdb(options) {
|
|
|
8620
9305
|
}
|
|
8621
9306
|
|
|
8622
9307
|
// iii/client.ts
|
|
8623
|
-
import
|
|
9308
|
+
import crypto7 from "node:crypto";
|
|
8624
9309
|
|
|
8625
9310
|
// iii/stream.ts
|
|
8626
9311
|
function notify(channels, stream, group, item, event, data) {
|
|
@@ -9149,7 +9834,7 @@ function iii(opts = {}) {
|
|
|
9149
9834
|
registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
|
|
9150
9835
|
registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
|
|
9151
9836
|
function addLocalWorker(worker) {
|
|
9152
|
-
const workerId =
|
|
9837
|
+
const workerId = crypto7.randomUUID();
|
|
9153
9838
|
const reg = {
|
|
9154
9839
|
id: workerId,
|
|
9155
9840
|
name: worker.name,
|
|
@@ -9164,7 +9849,7 @@ function iii(opts = {}) {
|
|
|
9164
9849
|
const triggerIds = [];
|
|
9165
9850
|
for (const t of worker.getTriggers()) {
|
|
9166
9851
|
if (t.input.function_id === fn.id) {
|
|
9167
|
-
const tid =
|
|
9852
|
+
const tid = crypto7.randomUUID();
|
|
9168
9853
|
triggers.set(tid, {
|
|
9169
9854
|
id: tid,
|
|
9170
9855
|
type: t.input.type,
|
|
@@ -9193,7 +9878,7 @@ function iii(opts = {}) {
|
|
|
9193
9878
|
if (!worker) return;
|
|
9194
9879
|
const handler = async (payload) => {
|
|
9195
9880
|
if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
|
|
9196
|
-
const invocationId =
|
|
9881
|
+
const invocationId = crypto7.randomUUID();
|
|
9197
9882
|
return new Promise((resolve14, reject) => {
|
|
9198
9883
|
const timer = setTimeout(() => {
|
|
9199
9884
|
pending.delete(invocationId);
|
|
@@ -9228,7 +9913,7 @@ function iii(opts = {}) {
|
|
|
9228
9913
|
let engineRef = null;
|
|
9229
9914
|
const wsHandler = createWsHandler({
|
|
9230
9915
|
registerRemoteWorker(ws, name) {
|
|
9231
|
-
const id2 =
|
|
9916
|
+
const id2 = crypto7.randomUUID();
|
|
9232
9917
|
workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
|
|
9233
9918
|
return id2;
|
|
9234
9919
|
},
|
|
@@ -9239,7 +9924,7 @@ function iii(opts = {}) {
|
|
|
9239
9924
|
addRemoteFunction(workerId, id2);
|
|
9240
9925
|
},
|
|
9241
9926
|
registerRemoteTrigger(workerId, input) {
|
|
9242
|
-
const tid =
|
|
9927
|
+
const tid = crypto7.randomUUID();
|
|
9243
9928
|
const reg = { id: tid, ...input, workerId };
|
|
9244
9929
|
triggers.set(tid, reg);
|
|
9245
9930
|
const worker = workers.get(workerId);
|
|
@@ -9589,7 +10274,7 @@ function registerWorker(url) {
|
|
|
9589
10274
|
}
|
|
9590
10275
|
|
|
9591
10276
|
// session.ts
|
|
9592
|
-
import
|
|
10277
|
+
import crypto8 from "node:crypto";
|
|
9593
10278
|
var kSaved = /* @__PURE__ */ Symbol("session.saved");
|
|
9594
10279
|
var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
|
|
9595
10280
|
var kId = /* @__PURE__ */ Symbol("session.id");
|
|
@@ -9665,13 +10350,33 @@ var RedisStore = class {
|
|
|
9665
10350
|
await this.redis.del(this.key(sid));
|
|
9666
10351
|
}
|
|
9667
10352
|
};
|
|
9668
|
-
|
|
10353
|
+
var COOKIE_SEPARATOR = ".";
|
|
10354
|
+
function signSessionId(sid, secret) {
|
|
10355
|
+
const hmac = crypto8.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
|
|
10356
|
+
return sid + COOKIE_SEPARATOR + hmac;
|
|
10357
|
+
}
|
|
10358
|
+
function unsignSessionId(value, secret) {
|
|
10359
|
+
const dot = value.lastIndexOf(COOKIE_SEPARATOR);
|
|
10360
|
+
if (dot === -1) return null;
|
|
10361
|
+
const sid = value.slice(0, dot);
|
|
10362
|
+
const sig = value.slice(dot + 1);
|
|
10363
|
+
const expected = crypto8.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
|
|
10364
|
+
if (sig.length !== expected.length) return null;
|
|
10365
|
+
try {
|
|
10366
|
+
return crypto8.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ? sid : null;
|
|
10367
|
+
} catch {
|
|
10368
|
+
return null;
|
|
10369
|
+
}
|
|
10370
|
+
}
|
|
10371
|
+
var kCreatedAt = "__createdAt";
|
|
10372
|
+
function createSessionObject(data, sid, store2, ttl, createdAt) {
|
|
9669
10373
|
const obj = data ?? {};
|
|
9670
10374
|
obj[kSaved] = false;
|
|
9671
10375
|
obj[kDestroyed] = false;
|
|
9672
10376
|
obj[kId] = sid;
|
|
9673
10377
|
obj[kStore] = store2;
|
|
9674
10378
|
obj[kTtl] = ttl;
|
|
10379
|
+
if (createdAt) obj[kCreatedAt] = createdAt;
|
|
9675
10380
|
obj.save = () => {
|
|
9676
10381
|
obj[kSaved] = true;
|
|
9677
10382
|
};
|
|
@@ -9701,6 +10406,8 @@ function isSessionActive(session2) {
|
|
|
9701
10406
|
function session(options) {
|
|
9702
10407
|
const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
|
|
9703
10408
|
const cookieName = options?.cookieName ?? "__session";
|
|
10409
|
+
const secret = options?.secret;
|
|
10410
|
+
const rotateInterval = options?.rotateInterval ?? 9e5;
|
|
9704
10411
|
const cookieOpts = {
|
|
9705
10412
|
path: options?.cookie?.path ?? "/",
|
|
9706
10413
|
domain: options?.cookie?.domain,
|
|
@@ -9720,25 +10427,38 @@ function session(options) {
|
|
|
9720
10427
|
store2 = mem;
|
|
9721
10428
|
closeStore = () => mem.close();
|
|
9722
10429
|
}
|
|
10430
|
+
function writeCookie(res, sid) {
|
|
10431
|
+
const value = secret ? signSessionId(sid, secret) : sid;
|
|
10432
|
+
return setCookie(res, cookieName, value, cookieOpts);
|
|
10433
|
+
}
|
|
9723
10434
|
const mw = (async (req, ctx, next) => {
|
|
9724
10435
|
const cookies = getCookies(req);
|
|
9725
|
-
const
|
|
10436
|
+
const rawSid = cookies[cookieName];
|
|
10437
|
+
let sid;
|
|
10438
|
+
if (rawSid) {
|
|
10439
|
+
sid = secret ? unsignSessionId(rawSid, secret) : rawSid;
|
|
10440
|
+
}
|
|
9726
10441
|
let session2;
|
|
9727
10442
|
let loadedSid = sid ?? null;
|
|
10443
|
+
let needsRotation = false;
|
|
9728
10444
|
if (sid) {
|
|
9729
10445
|
const data = await store2.get(sid);
|
|
9730
10446
|
if (data) {
|
|
9731
|
-
|
|
10447
|
+
const createdAt = data[kCreatedAt] ?? Date.now();
|
|
10448
|
+
session2 = createSessionObject(data, sid, store2, ttl, createdAt);
|
|
10449
|
+
if (rotateInterval > 0 && Date.now() - createdAt > rotateInterval) {
|
|
10450
|
+
needsRotation = true;
|
|
10451
|
+
}
|
|
9732
10452
|
} else {
|
|
9733
10453
|
loadedSid = null;
|
|
9734
|
-
session2 = createSessionObject({},
|
|
10454
|
+
session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
|
|
9735
10455
|
}
|
|
9736
10456
|
} else {
|
|
9737
|
-
|
|
10457
|
+
loadedSid = null;
|
|
10458
|
+
session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
|
|
9738
10459
|
}
|
|
9739
10460
|
const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
|
|
9740
10461
|
ctx.session = session2;
|
|
9741
|
-
ctx.sessionId = session2.id;
|
|
9742
10462
|
const res = await next(req, ctx);
|
|
9743
10463
|
const currentSession = ctx.session;
|
|
9744
10464
|
if (!currentSession || currentSession[kDestroyed]) {
|
|
@@ -9747,14 +10467,22 @@ function session(options) {
|
|
|
9747
10467
|
}
|
|
9748
10468
|
return deleteCookie(res, cookieName, cookieOpts);
|
|
9749
10469
|
}
|
|
10470
|
+
if (needsRotation && loadedSid) {
|
|
10471
|
+
const newId = crypto8.randomUUID();
|
|
10472
|
+
const data = JSON.parse(JSON.stringify(currentSession));
|
|
10473
|
+
data[kCreatedAt] = Date.now();
|
|
10474
|
+
await store2.set(newId, data, ttl);
|
|
10475
|
+
await store2.destroy(loadedSid);
|
|
10476
|
+
loadedSid = newId;
|
|
10477
|
+
currentSession[kId] = newId;
|
|
10478
|
+
currentSession[kCreatedAt] = data[kCreatedAt];
|
|
10479
|
+
}
|
|
9750
10480
|
const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
|
|
9751
10481
|
const wasSaved = currentSession[kSaved];
|
|
9752
|
-
const changed = wasSaved || currentData !== snapshot;
|
|
10482
|
+
const changed = wasSaved || needsRotation || currentData !== snapshot;
|
|
9753
10483
|
if (!changed) {
|
|
9754
|
-
if (loadedSid) {
|
|
9755
|
-
|
|
9756
|
-
await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
|
|
9757
|
-
}
|
|
10484
|
+
if (loadedSid && store2 instanceof RedisStore) {
|
|
10485
|
+
await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
|
|
9758
10486
|
}
|
|
9759
10487
|
return res;
|
|
9760
10488
|
}
|
|
@@ -9763,8 +10491,10 @@ function session(options) {
|
|
|
9763
10491
|
const data = JSON.parse(currentData);
|
|
9764
10492
|
await store2.set(targetSid, data, ttl);
|
|
9765
10493
|
if (!loadedSid) {
|
|
9766
|
-
|
|
9767
|
-
|
|
10494
|
+
return writeCookie(res, targetSid);
|
|
10495
|
+
}
|
|
10496
|
+
if (needsRotation) {
|
|
10497
|
+
return writeCookie(res, targetSid);
|
|
9768
10498
|
}
|
|
9769
10499
|
} else if (loadedSid) {
|
|
9770
10500
|
await store2.destroy(loadedSid);
|
|
@@ -9780,7 +10510,7 @@ function session(options) {
|
|
|
9780
10510
|
}
|
|
9781
10511
|
|
|
9782
10512
|
// cache.ts
|
|
9783
|
-
import
|
|
10513
|
+
import crypto9 from "node:crypto";
|
|
9784
10514
|
var BINARY_PREFIXES = [
|
|
9785
10515
|
"image/",
|
|
9786
10516
|
"audio/",
|
|
@@ -9800,7 +10530,7 @@ function isCacheableStatus(status, allowed) {
|
|
|
9800
10530
|
return allowed.includes(status);
|
|
9801
10531
|
}
|
|
9802
10532
|
function defaultCacheKey(req) {
|
|
9803
|
-
const hash =
|
|
10533
|
+
const hash = crypto9.createHash("sha256");
|
|
9804
10534
|
hash.update(req.method);
|
|
9805
10535
|
hash.update(req.url);
|
|
9806
10536
|
return hash.digest("hex");
|
|
@@ -10015,10 +10745,10 @@ function cache2(options) {
|
|
|
10015
10745
|
}
|
|
10016
10746
|
|
|
10017
10747
|
// webhook.ts
|
|
10018
|
-
import
|
|
10748
|
+
import crypto10 from "node:crypto";
|
|
10019
10749
|
function timingSafeEqual2(a, b) {
|
|
10020
10750
|
try {
|
|
10021
|
-
return
|
|
10751
|
+
return crypto10.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
10022
10752
|
} catch {
|
|
10023
10753
|
return false;
|
|
10024
10754
|
}
|
|
@@ -10036,7 +10766,7 @@ function createStripeVerifier(config) {
|
|
|
10036
10766
|
const signature = parts["v1"];
|
|
10037
10767
|
if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
|
|
10038
10768
|
const signed = `${timestamp}.${body}`;
|
|
10039
|
-
const expected =
|
|
10769
|
+
const expected = crypto10.createHmac("sha256", config.secret).update(signed).digest("hex");
|
|
10040
10770
|
const valid = timingSafeEqual2(signature, expected);
|
|
10041
10771
|
let event = "";
|
|
10042
10772
|
let id2;
|
|
@@ -10053,7 +10783,7 @@ function createGitHubVerifier(config) {
|
|
|
10053
10783
|
return (body, headers) => {
|
|
10054
10784
|
const sig = headers["x-hub-signature-256"];
|
|
10055
10785
|
if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
|
|
10056
|
-
const expected = `sha256=${
|
|
10786
|
+
const expected = `sha256=${crypto10.createHmac("sha256", config.secret).update(body).digest("hex")}`;
|
|
10057
10787
|
const valid = timingSafeEqual2(sig, expected);
|
|
10058
10788
|
let event = headers["x-github-event"] ?? "";
|
|
10059
10789
|
let id2;
|
|
@@ -10076,7 +10806,7 @@ function createSlackVerifier(config) {
|
|
|
10076
10806
|
return { valid: false, provider: "slack", event: "", id: void 0 };
|
|
10077
10807
|
}
|
|
10078
10808
|
const sigBase = `v0:${timestamp}:${body}`;
|
|
10079
|
-
const expected = `v0=${
|
|
10809
|
+
const expected = `v0=${crypto10.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
|
|
10080
10810
|
const valid = timingSafeEqual2(signature, expected);
|
|
10081
10811
|
let event = "";
|
|
10082
10812
|
let id2;
|
|
@@ -10237,7 +10967,7 @@ function resolveTableName(table) {
|
|
|
10237
10967
|
}
|
|
10238
10968
|
return name;
|
|
10239
10969
|
}
|
|
10240
|
-
function
|
|
10970
|
+
function escapeIdent4(s) {
|
|
10241
10971
|
return `"${s.replace(/"/g, '""')}"`;
|
|
10242
10972
|
}
|
|
10243
10973
|
function sqlLit(s) {
|
|
@@ -10247,11 +10977,11 @@ async function createIndex(sql2, table, fields, options) {
|
|
|
10247
10977
|
const language = options?.language ?? "english";
|
|
10248
10978
|
const tableName = resolveTableName(table);
|
|
10249
10979
|
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10250
|
-
const vectorExpr = fields.map((f) => `coalesce(${
|
|
10980
|
+
const vectorExpr = fields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
|
|
10251
10981
|
const indexType = options?.indexType ?? "gin";
|
|
10252
10982
|
await sql2.unsafe(`
|
|
10253
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
10254
|
-
ON ${
|
|
10983
|
+
CREATE INDEX IF NOT EXISTS ${escapeIdent4(indexName)}
|
|
10984
|
+
ON ${escapeIdent4(tableName)}
|
|
10255
10985
|
USING ${indexType}
|
|
10256
10986
|
(to_tsvector(${sqlLit(language)}, ${vectorExpr}))
|
|
10257
10987
|
`);
|
|
@@ -10259,7 +10989,7 @@ async function createIndex(sql2, table, fields, options) {
|
|
|
10259
10989
|
async function dropIndex(sql2, table, options) {
|
|
10260
10990
|
const tableName = resolveTableName(table);
|
|
10261
10991
|
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10262
|
-
await sql2.unsafe(`DROP INDEX IF EXISTS ${
|
|
10992
|
+
await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent4(indexName)}`);
|
|
10263
10993
|
}
|
|
10264
10994
|
async function search(sql2, table, query, options) {
|
|
10265
10995
|
const tableName = resolveTableName(table);
|
|
@@ -10273,13 +11003,13 @@ async function search(sql2, table, query, options) {
|
|
|
10273
11003
|
}
|
|
10274
11004
|
const sanitized = query.trim();
|
|
10275
11005
|
if (!sanitized) return [];
|
|
10276
|
-
const vectorExpr = searchFields.map((f) => `coalesce(${
|
|
11006
|
+
const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
|
|
10277
11007
|
const langLit = sqlLit(language);
|
|
10278
11008
|
const queryLit = sqlLit(sanitized);
|
|
10279
|
-
const rankColId =
|
|
10280
|
-
const tableId =
|
|
11009
|
+
const rankColId = escapeIdent4(rankCol);
|
|
11010
|
+
const tableId = escapeIdent4(tableName);
|
|
10281
11011
|
const headlineExpr = options?.headline ? searchFields.map(
|
|
10282
|
-
(f) => `ts_headline(${langLit}, ${
|
|
11012
|
+
(f) => `ts_headline(${langLit}, ${escapeIdent4(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent4(f + "_headline")}`
|
|
10283
11013
|
).join(",\n ") : "";
|
|
10284
11014
|
const sql_query = `
|
|
10285
11015
|
SELECT
|
|
@@ -10319,14 +11049,380 @@ async function suggest(sql2, table, prefix, options) {
|
|
|
10319
11049
|
const rows = await sql2.unsafe(`
|
|
10320
11050
|
SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
|
|
10321
11051
|
FROM (
|
|
10322
|
-
SELECT regexp_split_to_table(lower(${
|
|
10323
|
-
FROM ${
|
|
11052
|
+
SELECT regexp_split_to_table(lower(${escapeIdent4(field)}), E'\\W+') as word
|
|
11053
|
+
FROM ${escapeIdent4(tableName)}
|
|
10324
11054
|
) words
|
|
10325
11055
|
WHERE word LIKE ${sqlLit(sanitized + "%")}
|
|
10326
11056
|
LIMIT ${limit}
|
|
10327
11057
|
`);
|
|
10328
11058
|
return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
|
|
10329
11059
|
}
|
|
11060
|
+
|
|
11061
|
+
// s3.ts
|
|
11062
|
+
import {
|
|
11063
|
+
S3Client,
|
|
11064
|
+
PutObjectCommand,
|
|
11065
|
+
GetObjectCommand,
|
|
11066
|
+
DeleteObjectCommand,
|
|
11067
|
+
HeadObjectCommand,
|
|
11068
|
+
ListObjectsV2Command
|
|
11069
|
+
} from "@aws-sdk/client-s3";
|
|
11070
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
11071
|
+
function s3(options) {
|
|
11072
|
+
const { bucket, publicUrl } = options;
|
|
11073
|
+
const client = new S3Client({
|
|
11074
|
+
region: options.region ?? "us-east-1",
|
|
11075
|
+
endpoint: options.endpoint,
|
|
11076
|
+
forcePathStyle: options.forcePathStyle,
|
|
11077
|
+
credentials: options.credentials
|
|
11078
|
+
});
|
|
11079
|
+
async function put(key, body, putOpts) {
|
|
11080
|
+
const command = new PutObjectCommand({
|
|
11081
|
+
Bucket: bucket,
|
|
11082
|
+
Key: key,
|
|
11083
|
+
Body: body,
|
|
11084
|
+
ContentType: putOpts?.contentType,
|
|
11085
|
+
CacheControl: putOpts?.cacheControl ?? "public, max-age=31536000",
|
|
11086
|
+
Metadata: putOpts?.metadata
|
|
11087
|
+
});
|
|
11088
|
+
await client.send(command);
|
|
11089
|
+
return key;
|
|
11090
|
+
}
|
|
11091
|
+
async function get(key) {
|
|
11092
|
+
try {
|
|
11093
|
+
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
11094
|
+
const response = await client.send(command);
|
|
11095
|
+
const body = response.Body;
|
|
11096
|
+
if (!body) return null;
|
|
11097
|
+
return Buffer.from(await body.transformToByteArray());
|
|
11098
|
+
} catch (err) {
|
|
11099
|
+
if (err.name === "NoSuchKey") return null;
|
|
11100
|
+
throw err;
|
|
11101
|
+
}
|
|
11102
|
+
}
|
|
11103
|
+
async function del(key) {
|
|
11104
|
+
const command = new DeleteObjectCommand({ Bucket: bucket, Key: key });
|
|
11105
|
+
await client.send(command);
|
|
11106
|
+
}
|
|
11107
|
+
async function exists(key) {
|
|
11108
|
+
try {
|
|
11109
|
+
const command = new HeadObjectCommand({ Bucket: bucket, Key: key });
|
|
11110
|
+
await client.send(command);
|
|
11111
|
+
return true;
|
|
11112
|
+
} catch (err) {
|
|
11113
|
+
if (err.name === "NotFound" || err.name === "NoSuchKey") return false;
|
|
11114
|
+
throw err;
|
|
11115
|
+
}
|
|
11116
|
+
}
|
|
11117
|
+
async function url(key, urlOpts) {
|
|
11118
|
+
const expiresIn = urlOpts?.expiresIn ?? 3600;
|
|
11119
|
+
if (expiresIn === 0) {
|
|
11120
|
+
if (!publicUrl) {
|
|
11121
|
+
throw new Error(
|
|
11122
|
+
"s3.url() with expiresIn=0 requires publicUrl in S3Options. Set publicUrl to enable unsigned public URLs."
|
|
11123
|
+
);
|
|
11124
|
+
}
|
|
11125
|
+
const base = publicUrl.replace(/\/+$/, "");
|
|
11126
|
+
const objectKey = key.startsWith("/") ? key.slice(1) : key;
|
|
11127
|
+
return `${base}/${objectKey}`;
|
|
11128
|
+
}
|
|
11129
|
+
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
11130
|
+
return getSignedUrl(client, command, { expiresIn });
|
|
11131
|
+
}
|
|
11132
|
+
async function list(prefix) {
|
|
11133
|
+
const keys = [];
|
|
11134
|
+
let continuationToken;
|
|
11135
|
+
do {
|
|
11136
|
+
const command = new ListObjectsV2Command({
|
|
11137
|
+
Bucket: bucket,
|
|
11138
|
+
Prefix: prefix,
|
|
11139
|
+
ContinuationToken: continuationToken
|
|
11140
|
+
});
|
|
11141
|
+
const response = await client.send(command);
|
|
11142
|
+
if (response.Contents) {
|
|
11143
|
+
for (const obj of response.Contents) {
|
|
11144
|
+
if (obj.Key) keys.push(obj.Key);
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
continuationToken = response.NextContinuationToken;
|
|
11148
|
+
} while (continuationToken);
|
|
11149
|
+
return keys;
|
|
11150
|
+
}
|
|
11151
|
+
const mod = {
|
|
11152
|
+
put,
|
|
11153
|
+
get,
|
|
11154
|
+
delete: del,
|
|
11155
|
+
exists,
|
|
11156
|
+
url,
|
|
11157
|
+
list,
|
|
11158
|
+
client
|
|
11159
|
+
};
|
|
11160
|
+
const mw = ((req, ctx, next) => {
|
|
11161
|
+
;
|
|
11162
|
+
ctx.s3 = mod;
|
|
11163
|
+
return next(req, ctx);
|
|
11164
|
+
});
|
|
11165
|
+
mw.put = put;
|
|
11166
|
+
mw.get = get;
|
|
11167
|
+
mw.delete = del;
|
|
11168
|
+
mw.exists = exists;
|
|
11169
|
+
mw.url = url;
|
|
11170
|
+
mw.list = list;
|
|
11171
|
+
mw.client = client;
|
|
11172
|
+
return mw;
|
|
11173
|
+
}
|
|
11174
|
+
|
|
11175
|
+
// kb/index.ts
|
|
11176
|
+
function escapeIdent5(s) {
|
|
11177
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
11178
|
+
}
|
|
11179
|
+
function knowledgeBase(options) {
|
|
11180
|
+
const { pg, provider, table = "_kb_docs", chunkSize = 512, chunkOverlap = 64, searchLimit = 5, searchThreshold = 0 } = options;
|
|
11181
|
+
const sql2 = pg.sql;
|
|
11182
|
+
const dimension = provider.dimension;
|
|
11183
|
+
const docsTable = pg.table(table, {
|
|
11184
|
+
id: serial("id").primaryKey(),
|
|
11185
|
+
doc_key: text("doc_key").notNull(),
|
|
11186
|
+
title: text("title").notNull().default(""),
|
|
11187
|
+
content: text("content").notNull(),
|
|
11188
|
+
chunk_index: integer("chunk_index").notNull().default(0),
|
|
11189
|
+
metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
|
|
11190
|
+
embedding: vector("embedding", dimension),
|
|
11191
|
+
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
11192
|
+
});
|
|
11193
|
+
async function migrate() {
|
|
11194
|
+
await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "vector"`);
|
|
11195
|
+
await docsTable.create();
|
|
11196
|
+
await docsTable.createIndex("doc_key");
|
|
11197
|
+
await docsTable.createIndex("embedding", { type: "hnsw", operator: "vector_cosine_ops" });
|
|
11198
|
+
}
|
|
11199
|
+
async function ingest(key, content, ingestOpts) {
|
|
11200
|
+
const title = ingestOpts?.title ?? key;
|
|
11201
|
+
const meta = ingestOpts?.metadata ?? {};
|
|
11202
|
+
const cs = ingestOpts?.chunkSize ?? chunkSize;
|
|
11203
|
+
const co = ingestOpts?.chunkOverlap ?? chunkOverlap;
|
|
11204
|
+
const chunks = chunkContent(content, cs, co);
|
|
11205
|
+
const metaJson = JSON.stringify(meta);
|
|
11206
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
|
|
11207
|
+
const embeddings = await Promise.all(chunks.map((c) => provider.embed(c)));
|
|
11208
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
11209
|
+
const vec = `[${embeddings[i].join(",")}]`;
|
|
11210
|
+
await sql2.unsafe(
|
|
11211
|
+
`INSERT INTO ${escapeIdent5(table)} (doc_key, title, content, chunk_index, metadata, embedding)
|
|
11212
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector)`,
|
|
11213
|
+
[key, title, chunks[i], i, metaJson, vec]
|
|
11214
|
+
);
|
|
11215
|
+
}
|
|
11216
|
+
return chunks.length;
|
|
11217
|
+
}
|
|
11218
|
+
async function search2(query, searchOpts) {
|
|
11219
|
+
const limit = searchOpts?.limit ?? searchLimit;
|
|
11220
|
+
const threshold = searchOpts?.threshold ?? searchThreshold;
|
|
11221
|
+
const embedding = await provider.embed(query);
|
|
11222
|
+
const vec = `[${embedding.join(",")}]`;
|
|
11223
|
+
const whereClause = threshold > 0 ? `WHERE (1 - (embedding <=> $1::vector) / 2) >= ${threshold}` : "";
|
|
11224
|
+
const rows = await sql2.unsafe(
|
|
11225
|
+
`SELECT id, doc_key, title, content, chunk_index, metadata,
|
|
11226
|
+
1 - (embedding <=> $1::vector) / 2 AS _score
|
|
11227
|
+
FROM ${escapeIdent5(table)}
|
|
11228
|
+
${whereClause}
|
|
11229
|
+
ORDER BY embedding <=> $1::vector
|
|
11230
|
+
LIMIT ${limit}`,
|
|
11231
|
+
[vec]
|
|
11232
|
+
);
|
|
11233
|
+
return rows.map((r) => ({
|
|
11234
|
+
id: r.id,
|
|
11235
|
+
key: r.doc_key,
|
|
11236
|
+
title: r.title,
|
|
11237
|
+
content: r.content,
|
|
11238
|
+
score: r._score,
|
|
11239
|
+
metadata: typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata ?? {}
|
|
11240
|
+
}));
|
|
11241
|
+
}
|
|
11242
|
+
async function del(key) {
|
|
11243
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
|
|
11244
|
+
}
|
|
11245
|
+
async function list() {
|
|
11246
|
+
const rows = await sql2.unsafe(`
|
|
11247
|
+
SELECT doc_key, title, COUNT(*) AS chunks
|
|
11248
|
+
FROM ${escapeIdent5(table)}
|
|
11249
|
+
GROUP BY doc_key, title
|
|
11250
|
+
ORDER BY doc_key
|
|
11251
|
+
`);
|
|
11252
|
+
return rows.map((r) => ({
|
|
11253
|
+
key: r.doc_key,
|
|
11254
|
+
title: r.title,
|
|
11255
|
+
chunks: Number(r.chunks)
|
|
11256
|
+
}));
|
|
11257
|
+
}
|
|
11258
|
+
function mw() {
|
|
11259
|
+
return (req, ctx, next) => {
|
|
11260
|
+
;
|
|
11261
|
+
ctx.kb = { search: search2 };
|
|
11262
|
+
return next(req, ctx);
|
|
11263
|
+
};
|
|
11264
|
+
}
|
|
11265
|
+
return {
|
|
11266
|
+
ingest,
|
|
11267
|
+
search: search2,
|
|
11268
|
+
delete: del,
|
|
11269
|
+
list,
|
|
11270
|
+
migrate,
|
|
11271
|
+
middleware: mw
|
|
11272
|
+
};
|
|
11273
|
+
}
|
|
11274
|
+
|
|
11275
|
+
// permissions.ts
|
|
11276
|
+
function escapeIdent6(s) {
|
|
11277
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
11278
|
+
}
|
|
11279
|
+
function permissions(options) {
|
|
11280
|
+
const { pg } = options;
|
|
11281
|
+
const sql2 = pg.sql;
|
|
11282
|
+
const prefix = options.prefix ?? "";
|
|
11283
|
+
const rolesTable = `${prefix}_roles`;
|
|
11284
|
+
const rolePermsTable = `${prefix}_role_permissions`;
|
|
11285
|
+
const userRolesTable = `${prefix}_user_roles`;
|
|
11286
|
+
async function migrate() {
|
|
11287
|
+
await sql2.unsafe(`
|
|
11288
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolesTable)} (
|
|
11289
|
+
id SERIAL PRIMARY KEY,
|
|
11290
|
+
name TEXT UNIQUE NOT NULL,
|
|
11291
|
+
description TEXT NOT NULL DEFAULT '',
|
|
11292
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
11293
|
+
)
|
|
11294
|
+
`);
|
|
11295
|
+
await sql2.unsafe(`
|
|
11296
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolePermsTable)} (
|
|
11297
|
+
id SERIAL PRIMARY KEY,
|
|
11298
|
+
role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
|
|
11299
|
+
permission TEXT NOT NULL,
|
|
11300
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11301
|
+
UNIQUE(role_id, permission)
|
|
11302
|
+
)
|
|
11303
|
+
`);
|
|
11304
|
+
await sql2.unsafe(`
|
|
11305
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(userRolesTable)} (
|
|
11306
|
+
id SERIAL PRIMARY KEY,
|
|
11307
|
+
user_id INTEGER NOT NULL,
|
|
11308
|
+
role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
|
|
11309
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11310
|
+
UNIQUE(user_id, role_id)
|
|
11311
|
+
)
|
|
11312
|
+
`);
|
|
11313
|
+
}
|
|
11314
|
+
async function ensureRole(role) {
|
|
11315
|
+
const [existing] = await sql2.unsafe(
|
|
11316
|
+
`SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1 LIMIT 1`,
|
|
11317
|
+
[role]
|
|
11318
|
+
);
|
|
11319
|
+
if (existing) return existing.id;
|
|
11320
|
+
const [created] = await sql2.unsafe(
|
|
11321
|
+
`INSERT INTO ${escapeIdent6(rolesTable)} (name) VALUES ($1) RETURNING id`,
|
|
11322
|
+
[role]
|
|
11323
|
+
);
|
|
11324
|
+
return created.id;
|
|
11325
|
+
}
|
|
11326
|
+
async function assignRole(userId, role) {
|
|
11327
|
+
const roleId = await ensureRole(role);
|
|
11328
|
+
await sql2.unsafe(
|
|
11329
|
+
`INSERT INTO ${escapeIdent6(userRolesTable)} (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
11330
|
+
[userId, roleId]
|
|
11331
|
+
);
|
|
11332
|
+
}
|
|
11333
|
+
async function removeRole(userId, role) {
|
|
11334
|
+
await sql2.unsafe(
|
|
11335
|
+
`DELETE FROM ${escapeIdent6(userRolesTable)} WHERE user_id = $1 AND role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $2)`,
|
|
11336
|
+
[userId, role]
|
|
11337
|
+
);
|
|
11338
|
+
}
|
|
11339
|
+
async function grantPermission(role, permission) {
|
|
11340
|
+
const roleId = await ensureRole(role);
|
|
11341
|
+
await sql2.unsafe(
|
|
11342
|
+
`INSERT INTO ${escapeIdent6(rolePermsTable)} (role_id, permission) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
11343
|
+
[roleId, permission]
|
|
11344
|
+
);
|
|
11345
|
+
}
|
|
11346
|
+
async function revokePermission(role, permission) {
|
|
11347
|
+
await sql2.unsafe(
|
|
11348
|
+
`DELETE FROM ${escapeIdent6(rolePermsTable)} WHERE role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1) AND permission = $2`,
|
|
11349
|
+
[role, permission]
|
|
11350
|
+
);
|
|
11351
|
+
}
|
|
11352
|
+
async function getUserRoles(userId) {
|
|
11353
|
+
const rows = await sql2.unsafe(
|
|
11354
|
+
`SELECT r.name FROM ${escapeIdent6(userRolesTable)} ur
|
|
11355
|
+
JOIN ${escapeIdent6(rolesTable)} r ON r.id = ur.role_id
|
|
11356
|
+
WHERE ur.user_id = $1 ORDER BY r.name`,
|
|
11357
|
+
[userId]
|
|
11358
|
+
);
|
|
11359
|
+
return rows.map((r) => r.name);
|
|
11360
|
+
}
|
|
11361
|
+
async function getUserPermissions(userId) {
|
|
11362
|
+
const rows = await sql2.unsafe(
|
|
11363
|
+
`SELECT DISTINCT rp.permission FROM ${escapeIdent6(userRolesTable)} ur
|
|
11364
|
+
JOIN ${escapeIdent6(rolePermsTable)} rp ON rp.role_id = ur.role_id
|
|
11365
|
+
WHERE ur.user_id = $1 ORDER BY rp.permission`,
|
|
11366
|
+
[userId]
|
|
11367
|
+
);
|
|
11368
|
+
return rows.map((r) => r.permission);
|
|
11369
|
+
}
|
|
11370
|
+
const mw = (async (req, ctx, next) => {
|
|
11371
|
+
const userId = ctx.user?.id;
|
|
11372
|
+
let roles = /* @__PURE__ */ new Set();
|
|
11373
|
+
let perms = /* @__PURE__ */ new Set();
|
|
11374
|
+
if (userId) {
|
|
11375
|
+
const userRoles = await getUserRoles(userId);
|
|
11376
|
+
const userPerms = userId ? await getUserPermissions(userId) : [];
|
|
11377
|
+
roles = new Set(userRoles);
|
|
11378
|
+
perms = new Set(userPerms);
|
|
11379
|
+
const hasWildcard = userPerms.includes("*");
|
|
11380
|
+
if (hasWildcard) {
|
|
11381
|
+
perms = /* @__PURE__ */ new Set(["*"]);
|
|
11382
|
+
}
|
|
11383
|
+
}
|
|
11384
|
+
ctx.permissions = { roles, permissions: perms };
|
|
11385
|
+
return next(req, ctx);
|
|
11386
|
+
});
|
|
11387
|
+
function requireRole(...roles) {
|
|
11388
|
+
return (req, ctx, next) => {
|
|
11389
|
+
if (!ctx.permissions?.roles || !roles.some((r) => ctx.permissions.roles.has(r))) {
|
|
11390
|
+
return Response.json(
|
|
11391
|
+
{ error: `Forbidden: requires one of roles [${roles.join(", ")}]` },
|
|
11392
|
+
{ status: 403 }
|
|
11393
|
+
);
|
|
11394
|
+
}
|
|
11395
|
+
return next(req, ctx);
|
|
11396
|
+
};
|
|
11397
|
+
}
|
|
11398
|
+
function requirePermission(...perms) {
|
|
11399
|
+
return (req, ctx, next) => {
|
|
11400
|
+
const userPerms = ctx.permissions?.permissions;
|
|
11401
|
+
if (!userPerms) {
|
|
11402
|
+
return Response.json({ error: "Forbidden: no permissions loaded" }, { status: 403 });
|
|
11403
|
+
}
|
|
11404
|
+
if (userPerms.has("*")) return next(req, ctx);
|
|
11405
|
+
const missing = perms.filter((p) => !userPerms.has(p));
|
|
11406
|
+
if (missing.length > 0) {
|
|
11407
|
+
return Response.json(
|
|
11408
|
+
{ error: `Forbidden: missing permissions [${missing.join(", ")}]` },
|
|
11409
|
+
{ status: 403 }
|
|
11410
|
+
);
|
|
11411
|
+
}
|
|
11412
|
+
return next(req, ctx);
|
|
11413
|
+
};
|
|
11414
|
+
}
|
|
11415
|
+
mw.assignRole = assignRole;
|
|
11416
|
+
mw.removeRole = removeRole;
|
|
11417
|
+
mw.grantPermission = grantPermission;
|
|
11418
|
+
mw.revokePermission = revokePermission;
|
|
11419
|
+
mw.getUserRoles = getUserRoles;
|
|
11420
|
+
mw.getUserPermissions = getUserPermissions;
|
|
11421
|
+
mw.requireRole = requireRole;
|
|
11422
|
+
mw.requirePermission = requirePermission;
|
|
11423
|
+
mw.migrate = migrate;
|
|
11424
|
+
return mw;
|
|
11425
|
+
}
|
|
10330
11426
|
export {
|
|
10331
11427
|
DEFAULT_MAX_BODY,
|
|
10332
11428
|
MIGRATIONS_TABLE,
|
|
@@ -10339,6 +11435,7 @@ export {
|
|
|
10339
11435
|
TestRequest,
|
|
10340
11436
|
TsxContext,
|
|
10341
11437
|
agent,
|
|
11438
|
+
aiProvider,
|
|
10342
11439
|
aiStream,
|
|
10343
11440
|
analytics,
|
|
10344
11441
|
auth,
|
|
@@ -10346,7 +11443,7 @@ export {
|
|
|
10346
11443
|
compress,
|
|
10347
11444
|
cors,
|
|
10348
11445
|
createHub,
|
|
10349
|
-
createOpenAI,
|
|
11446
|
+
createOpenAI2 as createOpenAI,
|
|
10350
11447
|
createSSEStream,
|
|
10351
11448
|
createTestDb,
|
|
10352
11449
|
createTestServer,
|
|
@@ -10359,6 +11456,7 @@ export {
|
|
|
10359
11456
|
deploy,
|
|
10360
11457
|
embed,
|
|
10361
11458
|
embedMany,
|
|
11459
|
+
flash,
|
|
10362
11460
|
formatSSE,
|
|
10363
11461
|
formatSSEData,
|
|
10364
11462
|
fts_exports as fts,
|
|
@@ -10368,9 +11466,11 @@ export {
|
|
|
10368
11466
|
graphql,
|
|
10369
11467
|
health,
|
|
10370
11468
|
helmet,
|
|
11469
|
+
i18n,
|
|
10371
11470
|
iii,
|
|
10372
11471
|
isDev,
|
|
10373
11472
|
isProd,
|
|
11473
|
+
knowledgeBase,
|
|
10374
11474
|
loadEnv,
|
|
10375
11475
|
logdb,
|
|
10376
11476
|
logger,
|
|
@@ -10378,8 +11478,8 @@ export {
|
|
|
10378
11478
|
messager,
|
|
10379
11479
|
openai,
|
|
10380
11480
|
opencode,
|
|
11481
|
+
permissions,
|
|
10381
11482
|
postgres,
|
|
10382
|
-
preferences,
|
|
10383
11483
|
queue,
|
|
10384
11484
|
rateLimit,
|
|
10385
11485
|
redis,
|
|
@@ -10387,6 +11487,7 @@ export {
|
|
|
10387
11487
|
requestId,
|
|
10388
11488
|
runWithTrace,
|
|
10389
11489
|
runWorkflow,
|
|
11490
|
+
s3,
|
|
10390
11491
|
seo,
|
|
10391
11492
|
seoMiddleware,
|
|
10392
11493
|
seoTags,
|
|
@@ -10400,6 +11501,7 @@ export {
|
|
|
10400
11501
|
streamText,
|
|
10401
11502
|
tenant,
|
|
10402
11503
|
testApp,
|
|
11504
|
+
theme,
|
|
10403
11505
|
tool2 as tool,
|
|
10404
11506
|
traceElapsed,
|
|
10405
11507
|
upload,
|