weifuwu 0.22.3 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -87
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +112 -0
- package/cli/template/.weifuwu/ssr/560568d7.js +14 -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/client-locale.d.ts +1 -1
- package/dist/client-router.d.ts +3 -3
- 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 +13 -7
- package/dist/index.js +1351 -820
- package/dist/kb/index.d.ts +3 -0
- package/dist/kb/types.d.ts +64 -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/session.d.ts +0 -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 +15 -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 +1 -1
package/dist/index.js
CHANGED
|
@@ -851,7 +851,7 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
851
851
|
|
|
852
852
|
// tsx-context.ts
|
|
853
853
|
import { useSyncExternalStore, createContext } from "react";
|
|
854
|
-
var DEFAULT_CTX = { params: {}, query: {}, parsed: {},
|
|
854
|
+
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, loaderData: {}, env: {}, user: {}, flash: {} };
|
|
855
855
|
var KEY = "__WEIFUWU_CTX_STORE";
|
|
856
856
|
function getStore() {
|
|
857
857
|
if (typeof globalThis !== "undefined" && globalThis[KEY]) {
|
|
@@ -859,8 +859,9 @@ function getStore() {
|
|
|
859
859
|
}
|
|
860
860
|
const s = {
|
|
861
861
|
_ctx: DEFAULT_CTX,
|
|
862
|
-
_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 },
|
|
863
863
|
_listeners: /* @__PURE__ */ new Set(),
|
|
864
|
+
_rebuilders: [],
|
|
864
865
|
_alsGetStore: null
|
|
865
866
|
};
|
|
866
867
|
if (typeof globalThis !== "undefined") {
|
|
@@ -873,8 +874,18 @@ function __registerAls(getStore2) {
|
|
|
873
874
|
store._alsGetStore = getStore2;
|
|
874
875
|
}
|
|
875
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
|
+
}
|
|
876
883
|
store._ctx = { ...store._ctx, ...value };
|
|
877
|
-
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
|
+
}
|
|
878
889
|
store._listeners.forEach((fn) => fn());
|
|
879
890
|
}
|
|
880
891
|
var TsxContext = createContext(DEFAULT_CTX);
|
|
@@ -1085,266 +1096,6 @@ function auth(options) {
|
|
|
1085
1096
|
};
|
|
1086
1097
|
}
|
|
1087
1098
|
|
|
1088
|
-
// oauth-client.ts
|
|
1089
|
-
import crypto2 from "node:crypto";
|
|
1090
|
-
import jwt from "jsonwebtoken";
|
|
1091
|
-
var BUILTIN_PROVIDERS = {
|
|
1092
|
-
google: {
|
|
1093
|
-
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1094
|
-
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1095
|
-
userUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
1096
|
-
scope: "openid email profile",
|
|
1097
|
-
parseUser: (data) => ({
|
|
1098
|
-
id: data.id,
|
|
1099
|
-
email: data.email,
|
|
1100
|
-
name: data.name,
|
|
1101
|
-
avatarUrl: data.picture
|
|
1102
|
-
})
|
|
1103
|
-
},
|
|
1104
|
-
github: {
|
|
1105
|
-
authUrl: "https://github.com/login/oauth/authorize",
|
|
1106
|
-
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
1107
|
-
userUrl: "https://api.github.com/user",
|
|
1108
|
-
scope: "read:user user:email",
|
|
1109
|
-
parseUser: (data) => ({
|
|
1110
|
-
id: String(data.id),
|
|
1111
|
-
email: data.email ?? "",
|
|
1112
|
-
name: data.name ?? data.login,
|
|
1113
|
-
avatarUrl: data.avatar_url
|
|
1114
|
-
})
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
function oauthClient(options) {
|
|
1118
|
-
const {
|
|
1119
|
-
pg,
|
|
1120
|
-
jwtSecret,
|
|
1121
|
-
providers,
|
|
1122
|
-
redirectUrl = "/",
|
|
1123
|
-
expiresIn = "24h"
|
|
1124
|
-
} = options;
|
|
1125
|
-
const providerTable = options.table ?? "_auth_providers";
|
|
1126
|
-
const router = new Router();
|
|
1127
|
-
async function saveOAuthState(ctx, state, provider) {
|
|
1128
|
-
if (ctx.session) {
|
|
1129
|
-
ctx.session.oauthState = { state, provider };
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
function verifyOAuthState(ctx, state, provider) {
|
|
1133
|
-
const saved = ctx.session?.oauthState;
|
|
1134
|
-
if (!saved) return false;
|
|
1135
|
-
if (saved.state !== state || saved.provider !== provider) return false;
|
|
1136
|
-
delete ctx.session.oauthState;
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
async function ensureTable() {
|
|
1140
|
-
await pg.sql`
|
|
1141
|
-
CREATE TABLE IF NOT EXISTS ${pg.sql(providerTable)} (
|
|
1142
|
-
id SERIAL PRIMARY KEY,
|
|
1143
|
-
user_id INTEGER NOT NULL REFERENCES "_users"(id) ON DELETE CASCADE,
|
|
1144
|
-
provider TEXT NOT NULL,
|
|
1145
|
-
provider_id TEXT NOT NULL,
|
|
1146
|
-
email TEXT NOT NULL DEFAULT '',
|
|
1147
|
-
name TEXT NOT NULL DEFAULT '',
|
|
1148
|
-
avatar_url TEXT NOT NULL DEFAULT '',
|
|
1149
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
1150
|
-
UNIQUE(provider, provider_id)
|
|
1151
|
-
)
|
|
1152
|
-
`;
|
|
1153
|
-
await pg.sql`
|
|
1154
|
-
CREATE INDEX IF NOT EXISTS ${pg.sql(providerTable + "_user_idx")}
|
|
1155
|
-
ON ${pg.sql(providerTable)}(user_id)
|
|
1156
|
-
`;
|
|
1157
|
-
}
|
|
1158
|
-
async function findUserByProvider(provider, providerId) {
|
|
1159
|
-
const [row] = await pg.sql`
|
|
1160
|
-
SELECT * FROM ${pg.sql(providerTable)}
|
|
1161
|
-
WHERE provider = ${provider} AND provider_id = ${providerId}
|
|
1162
|
-
LIMIT 1
|
|
1163
|
-
`;
|
|
1164
|
-
return row ?? null;
|
|
1165
|
-
}
|
|
1166
|
-
async function findUserByEmail(email) {
|
|
1167
|
-
const [row] = await pg.sql`
|
|
1168
|
-
SELECT * FROM "_users" WHERE email = ${email} LIMIT 1
|
|
1169
|
-
`;
|
|
1170
|
-
return row ?? null;
|
|
1171
|
-
}
|
|
1172
|
-
async function createUser(email, name) {
|
|
1173
|
-
const randomPassword = crypto2.randomBytes(32).toString("hex");
|
|
1174
|
-
const [row] = await pg.sql`
|
|
1175
|
-
INSERT INTO "_users" (email, password, name, role)
|
|
1176
|
-
VALUES (${email}, ${randomPassword}, ${name}, 'user')
|
|
1177
|
-
RETURNING *
|
|
1178
|
-
`;
|
|
1179
|
-
return row;
|
|
1180
|
-
}
|
|
1181
|
-
async function linkProvider(userId, provider, providerId, email, name, avatarUrl) {
|
|
1182
|
-
await pg.sql`
|
|
1183
|
-
INSERT INTO ${pg.sql(providerTable)} (user_id, provider, provider_id, email, name, avatar_url)
|
|
1184
|
-
VALUES (${userId}, ${provider}, ${providerId}, ${email}, ${name}, ${avatarUrl})
|
|
1185
|
-
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
1186
|
-
`;
|
|
1187
|
-
}
|
|
1188
|
-
async function findOrCreateUser(provider, providerId, email, name, avatarUrl) {
|
|
1189
|
-
const link = await findUserByProvider(provider, providerId);
|
|
1190
|
-
if (link) {
|
|
1191
|
-
const [user2] = await pg.sql`SELECT * FROM "_users" WHERE id = ${link.user_id} LIMIT 1`;
|
|
1192
|
-
return user2 ?? null;
|
|
1193
|
-
}
|
|
1194
|
-
if (email) {
|
|
1195
|
-
const existingUser = await findUserByEmail(email);
|
|
1196
|
-
if (existingUser) {
|
|
1197
|
-
await linkProvider(existingUser.id, provider, providerId, email, name, avatarUrl);
|
|
1198
|
-
return existingUser;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
const newUser = await createUser(email || `${provider}_${providerId}@oauth.local`, name || provider);
|
|
1202
|
-
await linkProvider(newUser.id, provider, providerId, email, name, avatarUrl);
|
|
1203
|
-
return newUser;
|
|
1204
|
-
}
|
|
1205
|
-
function signToken(user2) {
|
|
1206
|
-
return jwt.sign(
|
|
1207
|
-
{ sub: user2.id, email: user2.email, role: user2.role },
|
|
1208
|
-
jwtSecret,
|
|
1209
|
-
{ expiresIn }
|
|
1210
|
-
);
|
|
1211
|
-
}
|
|
1212
|
-
let tableReady = null;
|
|
1213
|
-
function ensureInit() {
|
|
1214
|
-
if (!tableReady) tableReady = ensureTable();
|
|
1215
|
-
return tableReady;
|
|
1216
|
-
}
|
|
1217
|
-
function getProviderMeta(providerName) {
|
|
1218
|
-
const config = providers[providerName];
|
|
1219
|
-
if (!config) return null;
|
|
1220
|
-
const builtin = BUILTIN_PROVIDERS[providerName];
|
|
1221
|
-
const parseUser = config.parseUser ?? builtin?.parseUser;
|
|
1222
|
-
if (!parseUser) return null;
|
|
1223
|
-
const meta = {
|
|
1224
|
-
authUrl: config.authUrl ?? builtin?.authUrl ?? "",
|
|
1225
|
-
tokenUrl: config.tokenUrl ?? builtin?.tokenUrl ?? "",
|
|
1226
|
-
userUrl: config.userUrl ?? builtin?.userUrl ?? "",
|
|
1227
|
-
scope: config.scope ?? builtin?.scope ?? "openid",
|
|
1228
|
-
parseUser
|
|
1229
|
-
};
|
|
1230
|
-
if (!meta.authUrl || !meta.tokenUrl || !meta.userUrl) return null;
|
|
1231
|
-
return { config, meta };
|
|
1232
|
-
}
|
|
1233
|
-
router.get("/:provider", async (req, ctx) => {
|
|
1234
|
-
await ensureInit();
|
|
1235
|
-
const providerName = ctx.params.provider;
|
|
1236
|
-
const resolved = getProviderMeta(providerName);
|
|
1237
|
-
if (!resolved) {
|
|
1238
|
-
return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
|
|
1239
|
-
}
|
|
1240
|
-
const { config, meta } = resolved;
|
|
1241
|
-
const state = crypto2.randomUUID();
|
|
1242
|
-
const redirectUri = new URL(req.url);
|
|
1243
|
-
redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
|
|
1244
|
-
await saveOAuthState(ctx, state, providerName);
|
|
1245
|
-
const scope = config.scope ?? meta.scope;
|
|
1246
|
-
const params = new URLSearchParams({
|
|
1247
|
-
client_id: config.clientId,
|
|
1248
|
-
redirect_uri: redirectUri.origin + redirectUri.pathname,
|
|
1249
|
-
response_type: "code",
|
|
1250
|
-
scope,
|
|
1251
|
-
state,
|
|
1252
|
-
access_type: "offline",
|
|
1253
|
-
prompt: "consent"
|
|
1254
|
-
});
|
|
1255
|
-
return Response.redirect(`${meta.authUrl}?${params.toString()}`, 302);
|
|
1256
|
-
});
|
|
1257
|
-
router.get("/:provider/callback", async (req, ctx) => {
|
|
1258
|
-
await ensureInit();
|
|
1259
|
-
const providerName = ctx.params.provider;
|
|
1260
|
-
const resolved = getProviderMeta(providerName);
|
|
1261
|
-
if (!resolved) {
|
|
1262
|
-
return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
|
|
1263
|
-
}
|
|
1264
|
-
const { config, meta } = resolved;
|
|
1265
|
-
const url = new URL(req.url);
|
|
1266
|
-
const code = url.searchParams.get("code");
|
|
1267
|
-
const state = url.searchParams.get("state");
|
|
1268
|
-
if (!code || !state) {
|
|
1269
|
-
return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
|
|
1270
|
-
}
|
|
1271
|
-
if (!verifyOAuthState(ctx, state, providerName)) {
|
|
1272
|
-
return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
|
|
1273
|
-
}
|
|
1274
|
-
const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
|
|
1275
|
-
let tokenRes;
|
|
1276
|
-
try {
|
|
1277
|
-
tokenRes = await fetch(meta.tokenUrl, {
|
|
1278
|
-
method: "POST",
|
|
1279
|
-
headers: {
|
|
1280
|
-
"Content-Type": "application/json",
|
|
1281
|
-
"Accept": "application/json"
|
|
1282
|
-
},
|
|
1283
|
-
body: JSON.stringify({
|
|
1284
|
-
code,
|
|
1285
|
-
client_id: config.clientId,
|
|
1286
|
-
client_secret: config.clientSecret,
|
|
1287
|
-
redirect_uri: redirectUri,
|
|
1288
|
-
grant_type: "authorization_code"
|
|
1289
|
-
})
|
|
1290
|
-
});
|
|
1291
|
-
} catch (err) {
|
|
1292
|
-
console.error(`[oauth] token exchange network error for ${providerName}:`, err);
|
|
1293
|
-
return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
|
|
1294
|
-
}
|
|
1295
|
-
if (!tokenRes.ok) {
|
|
1296
|
-
const errBody = await tokenRes.text();
|
|
1297
|
-
console.error(`[oauth] token exchange failed for ${providerName}:`, errBody);
|
|
1298
|
-
return Response.json({ error: "Failed to exchange authorization code" }, { status: 502 });
|
|
1299
|
-
}
|
|
1300
|
-
const tokenData = await tokenRes.json();
|
|
1301
|
-
const accessToken = tokenData.access_token;
|
|
1302
|
-
if (!accessToken) {
|
|
1303
|
-
return Response.json({ error: "No access_token in response" }, { status: 502 });
|
|
1304
|
-
}
|
|
1305
|
-
let userRes;
|
|
1306
|
-
try {
|
|
1307
|
-
userRes = await fetch(meta.userUrl, {
|
|
1308
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
1309
|
-
});
|
|
1310
|
-
} catch (err) {
|
|
1311
|
-
console.error(`[oauth] user info network error for ${providerName}:`, err);
|
|
1312
|
-
return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
|
|
1313
|
-
}
|
|
1314
|
-
if (!userRes.ok) {
|
|
1315
|
-
return Response.json({ error: "Failed to fetch user profile" }, { status: 502 });
|
|
1316
|
-
}
|
|
1317
|
-
const userData = await userRes.json();
|
|
1318
|
-
const providerUser = meta.parseUser(userData, accessToken);
|
|
1319
|
-
const user2 = await findOrCreateUser(
|
|
1320
|
-
providerName,
|
|
1321
|
-
providerUser.id,
|
|
1322
|
-
providerUser.email,
|
|
1323
|
-
providerUser.name,
|
|
1324
|
-
providerUser.avatarUrl ?? ""
|
|
1325
|
-
);
|
|
1326
|
-
if (!user2) {
|
|
1327
|
-
return Response.json({ error: "Failed to create/link user" }, { status: 500 });
|
|
1328
|
-
}
|
|
1329
|
-
const token = signToken(user2);
|
|
1330
|
-
if (ctx.session) {
|
|
1331
|
-
ctx.session.userId = user2.id;
|
|
1332
|
-
ctx.session.role = user2.role;
|
|
1333
|
-
}
|
|
1334
|
-
const accept = req.headers.get("accept") ?? "";
|
|
1335
|
-
if (accept.includes("application/json")) {
|
|
1336
|
-
return Response.json({
|
|
1337
|
-
token,
|
|
1338
|
-
user: { id: user2.id, email: user2.email, name: user2.name, role: user2.role }
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
const finalUrl = new URL(redirectUrl, url.origin);
|
|
1342
|
-
finalUrl.searchParams.set("token", token);
|
|
1343
|
-
return Response.redirect(finalUrl.toString(), 302);
|
|
1344
|
-
});
|
|
1345
|
-
return router;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
1099
|
// static.ts
|
|
1349
1100
|
import { open, realpath } from "node:fs/promises";
|
|
1350
1101
|
import { extname, resolve as resolve2, normalize, sep } from "node:path";
|
|
@@ -1918,10 +1669,10 @@ function helmet(options) {
|
|
|
1918
1669
|
}
|
|
1919
1670
|
|
|
1920
1671
|
// request-id.ts
|
|
1921
|
-
import
|
|
1672
|
+
import crypto2 from "node:crypto";
|
|
1922
1673
|
function requestId(options) {
|
|
1923
1674
|
const header = options?.header ?? "X-Request-ID";
|
|
1924
|
-
const gen = options?.generator ?? (() =>
|
|
1675
|
+
const gen = options?.generator ?? (() => crypto2.randomUUID());
|
|
1925
1676
|
return async (req, ctx, next) => {
|
|
1926
1677
|
const existing = req.headers.get(header);
|
|
1927
1678
|
const id2 = existing ?? gen();
|
|
@@ -2387,18 +2138,21 @@ async function getStreamObject() {
|
|
|
2387
2138
|
if (!_ai.streamObject) _ai.streamObject = (await import("ai")).streamObject;
|
|
2388
2139
|
return _ai.streamObject;
|
|
2389
2140
|
}
|
|
2390
|
-
async function aiStream(handler) {
|
|
2141
|
+
async function aiStream(handler, provider) {
|
|
2391
2142
|
const r = new Router();
|
|
2392
2143
|
r.post("/", async (req, ctx) => {
|
|
2393
2144
|
const options = await handler(req, ctx);
|
|
2145
|
+
if (provider && !options.model) {
|
|
2146
|
+
options.model = provider.model();
|
|
2147
|
+
}
|
|
2394
2148
|
if (options.schema) {
|
|
2395
2149
|
const streamObject2 = await getStreamObject();
|
|
2396
2150
|
const { schema, ...params } = options;
|
|
2397
2151
|
const result2 = streamObject2({ ...params, schema, output: "object" });
|
|
2398
2152
|
return result2.toTextStreamResponse();
|
|
2399
2153
|
}
|
|
2400
|
-
const
|
|
2401
|
-
const result =
|
|
2154
|
+
const streamText3 = await getStreamText();
|
|
2155
|
+
const result = streamText3(options);
|
|
2402
2156
|
return result.toTextStreamResponse();
|
|
2403
2157
|
});
|
|
2404
2158
|
return r;
|
|
@@ -2610,22 +2364,21 @@ function runWorkflow(opts = {}) {
|
|
|
2610
2364
|
let nodes;
|
|
2611
2365
|
if (input.nodes && input.nodes.length > 0) {
|
|
2612
2366
|
nodes = input.nodes;
|
|
2613
|
-
} 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"');
|
|
2614
2369
|
const toolsDesc = Object.entries(opts.tools ?? {}).map(([k, t]) => `- ${k}: ${t.description}`).join("\n");
|
|
2615
|
-
const
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
messages: [{ role: "user", content: input.goal }]
|
|
2628
|
-
});
|
|
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 });
|
|
2629
2382
|
const text2 = result.text.trim();
|
|
2630
2383
|
const jsonStart = text2.indexOf("{");
|
|
2631
2384
|
const jsonEnd = text2.lastIndexOf("}");
|
|
@@ -2633,8 +2386,6 @@ function runWorkflow(opts = {}) {
|
|
|
2633
2386
|
const parsed = JSON.parse(text2.slice(jsonStart, jsonEnd + 1));
|
|
2634
2387
|
nodes = parsed.nodes ?? parsed.workflow?.nodes ?? [];
|
|
2635
2388
|
if (!Array.isArray(nodes)) throw new Error("Generated workflow has no nodes array");
|
|
2636
|
-
} else {
|
|
2637
|
-
throw new Error('Provide either "nodes" or a "model" to generate the workflow from "goal"');
|
|
2638
2389
|
}
|
|
2639
2390
|
const ctx = {
|
|
2640
2391
|
variables: /* @__PURE__ */ new Map(),
|
|
@@ -2654,6 +2405,54 @@ function runWorkflow(opts = {}) {
|
|
|
2654
2405
|
});
|
|
2655
2406
|
}
|
|
2656
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
|
+
|
|
2657
2456
|
// ai-sdk.ts
|
|
2658
2457
|
import {
|
|
2659
2458
|
streamText,
|
|
@@ -2667,7 +2466,7 @@ import {
|
|
|
2667
2466
|
} from "ai";
|
|
2668
2467
|
import {
|
|
2669
2468
|
openai,
|
|
2670
|
-
createOpenAI
|
|
2469
|
+
createOpenAI as createOpenAI2
|
|
2671
2470
|
} from "@ai-sdk/openai";
|
|
2672
2471
|
|
|
2673
2472
|
// postgres/client.ts
|
|
@@ -3296,12 +3095,12 @@ var PgModule = class {
|
|
|
3296
3095
|
|
|
3297
3096
|
// user/client.ts
|
|
3298
3097
|
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
3299
|
-
import
|
|
3098
|
+
import jwt2 from "jsonwebtoken";
|
|
3300
3099
|
import { z as z2 } from "zod";
|
|
3301
3100
|
|
|
3302
3101
|
// user/oauth2.ts
|
|
3303
|
-
import
|
|
3304
|
-
import
|
|
3102
|
+
import crypto3 from "node:crypto";
|
|
3103
|
+
import jwt from "jsonwebtoken";
|
|
3305
3104
|
function createOAuth2Server(deps) {
|
|
3306
3105
|
const { pg, users, jwtSecret, expiresIn } = deps;
|
|
3307
3106
|
async function getClient(clientId) {
|
|
@@ -3319,8 +3118,8 @@ function createOAuth2Server(deps) {
|
|
|
3319
3118
|
};
|
|
3320
3119
|
}
|
|
3321
3120
|
async function registerClient(data) {
|
|
3322
|
-
const clientId =
|
|
3323
|
-
const clientSecret =
|
|
3121
|
+
const clientId = crypto3.randomUUID();
|
|
3122
|
+
const clientSecret = crypto3.randomBytes(32).toString("hex");
|
|
3324
3123
|
const [row] = await pg.sql`
|
|
3325
3124
|
INSERT INTO "_oauth2_clients" ("name", "client_id", "client_secret", "redirect_uris")
|
|
3326
3125
|
VALUES (${data.name}, ${clientId}, ${clientSecret}, ${pg.sql.array(data.redirectUris)})
|
|
@@ -3344,7 +3143,7 @@ function createOAuth2Server(deps) {
|
|
|
3344
3143
|
const header = req.headers.get("Authorization");
|
|
3345
3144
|
if (header?.startsWith("Bearer ")) {
|
|
3346
3145
|
try {
|
|
3347
|
-
const payload =
|
|
3146
|
+
const payload = jwt.verify(header.slice(7), jwtSecret);
|
|
3348
3147
|
return { id: payload.sub, email: payload.email, role: payload.role };
|
|
3349
3148
|
} catch {
|
|
3350
3149
|
return null;
|
|
@@ -3354,7 +3153,7 @@ function createOAuth2Server(deps) {
|
|
|
3354
3153
|
const qsToken = url.searchParams.get("access_token");
|
|
3355
3154
|
if (qsToken) {
|
|
3356
3155
|
try {
|
|
3357
|
-
const payload =
|
|
3156
|
+
const payload = jwt.verify(qsToken, jwtSecret);
|
|
3358
3157
|
return { id: payload.sub, email: payload.email, role: payload.role };
|
|
3359
3158
|
} catch {
|
|
3360
3159
|
return null;
|
|
@@ -3365,7 +3164,7 @@ function createOAuth2Server(deps) {
|
|
|
3365
3164
|
const match = cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("session="));
|
|
3366
3165
|
if (match) {
|
|
3367
3166
|
try {
|
|
3368
|
-
const payload =
|
|
3167
|
+
const payload = jwt.verify(match.slice(8), jwtSecret);
|
|
3369
3168
|
return { id: payload.sub, email: payload.email, role: payload.role };
|
|
3370
3169
|
} catch {
|
|
3371
3170
|
return null;
|
|
@@ -3468,7 +3267,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3468
3267
|
const loc2 = `${redirectUri}?error=access_denied${state ? `&state=${state}` : ""}`;
|
|
3469
3268
|
return Response.redirect(loc2, 302);
|
|
3470
3269
|
}
|
|
3471
|
-
const code =
|
|
3270
|
+
const code = crypto3.randomUUID();
|
|
3472
3271
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
3473
3272
|
await pg.sql`
|
|
3474
3273
|
INSERT INTO "_oauth2_codes" ("code", "client_id", "user_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "expires_at")
|
|
@@ -3533,7 +3332,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3533
3332
|
if (stored.code_challenge_method === "plain") {
|
|
3534
3333
|
expected = codeVerifier;
|
|
3535
3334
|
} else {
|
|
3536
|
-
expected =
|
|
3335
|
+
expected = crypto3.createHash("sha256").update(codeVerifier).digest().toString("base64url");
|
|
3537
3336
|
}
|
|
3538
3337
|
if (expected !== stored.code_challenge) {
|
|
3539
3338
|
return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
|
|
@@ -3545,12 +3344,12 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3545
3344
|
return Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
3546
3345
|
}
|
|
3547
3346
|
const scope = stored.scope || "";
|
|
3548
|
-
const accessToken =
|
|
3347
|
+
const accessToken = jwt.sign(
|
|
3549
3348
|
{ sub: user2.id, email: user2.email, role: user2.role, client_id: clientId, scope },
|
|
3550
3349
|
jwtSecret,
|
|
3551
3350
|
{ expiresIn }
|
|
3552
3351
|
);
|
|
3553
|
-
const refreshToken =
|
|
3352
|
+
const refreshToken = crypto3.randomUUID();
|
|
3554
3353
|
const refreshExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
|
|
3555
3354
|
await pg.sql`
|
|
3556
3355
|
INSERT INTO "_oauth2_tokens" ("token", "client_id", "user_id", "scope", "expires_at")
|
|
@@ -3572,7 +3371,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3572
3371
|
if (!client || client.clientSecret !== clientSecret) {
|
|
3573
3372
|
return Response.json({ error: "invalid_client" }, { status: 401 });
|
|
3574
3373
|
}
|
|
3575
|
-
const accessToken =
|
|
3374
|
+
const accessToken = jwt.sign(
|
|
3576
3375
|
{ sub: clientId, client_id: clientId, scope, token_type: "client_credentials" },
|
|
3577
3376
|
jwtSecret,
|
|
3578
3377
|
{ expiresIn }
|
|
@@ -3587,32 +3386,255 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3587
3386
|
return { authorizeHandler, consentHandler, tokenHandler, registerClient, getClient, revokeClient };
|
|
3588
3387
|
}
|
|
3589
3388
|
|
|
3590
|
-
// user/
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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
|
+
|
|
3609
|
+
// user/client.ts
|
|
3610
|
+
var RegisterSchema = z2.object({
|
|
3611
|
+
email: z2.string().email(),
|
|
3612
|
+
password: z2.string().min(6),
|
|
3613
|
+
name: z2.string().min(1)
|
|
3614
|
+
});
|
|
3615
|
+
var LoginSchema = z2.object({
|
|
3616
|
+
email: z2.string().email(),
|
|
3617
|
+
password: z2.string().min(1)
|
|
3618
|
+
});
|
|
3619
|
+
function escapeIdent2(s) {
|
|
3620
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
3621
|
+
}
|
|
3622
|
+
function hashPassword(password) {
|
|
3623
|
+
const salt = randomBytes(16).toString("hex");
|
|
3624
|
+
const hash = scryptSync(password, salt, 64).toString("hex");
|
|
3625
|
+
return `${salt}:${hash}`;
|
|
3626
|
+
}
|
|
3627
|
+
function verifyPassword(password, stored) {
|
|
3628
|
+
const [salt, hash] = stored.split(":");
|
|
3629
|
+
const verify = scryptSync(password, salt, 64).toString("hex");
|
|
3630
|
+
if (hash.length !== verify.length) return false;
|
|
3631
|
+
return timingSafeEqual(Buffer.from(hash), Buffer.from(verify));
|
|
3632
|
+
}
|
|
3633
|
+
function user(options) {
|
|
3634
|
+
const table = options.table ?? "_users";
|
|
3635
|
+
const pg = options.pg;
|
|
3636
|
+
const secret = options.jwtSecret;
|
|
3637
|
+
const expiresIn = options.expiresIn ?? "24h";
|
|
3616
3638
|
const oauth2Enabled = options.oauth2?.server ?? false;
|
|
3617
3639
|
const base = new PgModule(pg);
|
|
3618
3640
|
const users = pg.table(table, {
|
|
@@ -3630,6 +3652,25 @@ function user(options) {
|
|
|
3630
3652
|
}
|
|
3631
3653
|
async function migrate() {
|
|
3632
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
|
+
}
|
|
3633
3674
|
if (!oauth2Enabled) return;
|
|
3634
3675
|
const clients3 = pg.table("_oauth2_clients", {
|
|
3635
3676
|
id: serial("id").primaryKey(),
|
|
@@ -3666,7 +3707,7 @@ function user(options) {
|
|
|
3666
3707
|
await tokens.create();
|
|
3667
3708
|
}
|
|
3668
3709
|
function signToken(user2) {
|
|
3669
|
-
return
|
|
3710
|
+
return jwt2.sign(
|
|
3670
3711
|
{ sub: user2.id, email: user2.email, role: user2.role },
|
|
3671
3712
|
secret,
|
|
3672
3713
|
{ expiresIn }
|
|
@@ -3683,6 +3724,11 @@ function user(options) {
|
|
|
3683
3724
|
async function findById(id2) {
|
|
3684
3725
|
return await users.read(id2);
|
|
3685
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
|
+
}
|
|
3686
3732
|
async function register(data) {
|
|
3687
3733
|
const { email, password, name } = RegisterSchema.parse(data);
|
|
3688
3734
|
const existing = await findByEmail(email);
|
|
@@ -3717,7 +3763,7 @@ function user(options) {
|
|
|
3717
3763
|
}
|
|
3718
3764
|
async function verify(token) {
|
|
3719
3765
|
try {
|
|
3720
|
-
const payload =
|
|
3766
|
+
const payload = jwt2.verify(token, secret);
|
|
3721
3767
|
if (payload.token_type === "client_credentials") return null;
|
|
3722
3768
|
const row = await findById(payload.sub);
|
|
3723
3769
|
if (!row) return null;
|
|
@@ -3799,6 +3845,20 @@ function user(options) {
|
|
|
3799
3845
|
return r2;
|
|
3800
3846
|
}
|
|
3801
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
|
+
}
|
|
3802
3862
|
const mod = r;
|
|
3803
3863
|
mod.middleware = middleware;
|
|
3804
3864
|
mod.migrate = migrate;
|
|
@@ -3837,29 +3897,8 @@ function redis(opts) {
|
|
|
3837
3897
|
// queue/index.ts
|
|
3838
3898
|
import { Redis as IORedis2 } from "ioredis";
|
|
3839
3899
|
import crypto5 from "node:crypto";
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
|
|
3843
|
-
const fields = parts.map((f, i) => {
|
|
3844
|
-
const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
|
|
3845
|
-
const [min, max] = ranges[i];
|
|
3846
|
-
return parseField(f, min, max);
|
|
3847
|
-
});
|
|
3848
|
-
let candidate = new Date(from.getTime() + 6e4);
|
|
3849
|
-
candidate.setSeconds(0, 0);
|
|
3850
|
-
for (let i = 0; i < 525600; i++) {
|
|
3851
|
-
const m = candidate.getMonth() + 1;
|
|
3852
|
-
const d = candidate.getDate();
|
|
3853
|
-
const h = candidate.getHours();
|
|
3854
|
-
const min = candidate.getMinutes();
|
|
3855
|
-
const dw = candidate.getDay();
|
|
3856
|
-
if (fields[4].has(dw) && fields[3].has(m) && fields[2].has(d) && fields[1].has(h) && fields[0].has(min)) {
|
|
3857
|
-
return candidate.getTime();
|
|
3858
|
-
}
|
|
3859
|
-
candidate.setTime(candidate.getTime() + 6e4);
|
|
3860
|
-
}
|
|
3861
|
-
throw new Error(`No future date found for cron expression "${expr}"`);
|
|
3862
|
-
}
|
|
3900
|
+
|
|
3901
|
+
// cron-utils.ts
|
|
3863
3902
|
function parseField(field, min, max) {
|
|
3864
3903
|
const values = /* @__PURE__ */ new Set();
|
|
3865
3904
|
for (const part of field.split(",")) {
|
|
@@ -3868,19 +3907,23 @@ function parseField(field, min, max) {
|
|
|
3868
3907
|
} else if (part.includes("/")) {
|
|
3869
3908
|
const [range, stepStr] = part.split("/");
|
|
3870
3909
|
const step = parseInt(stepStr, 10);
|
|
3910
|
+
if (isNaN(step) || step < 1) throw new Error(`Invalid cron step: ${part}`);
|
|
3871
3911
|
let start = min;
|
|
3872
3912
|
let end = max;
|
|
3873
3913
|
if (range !== "*") {
|
|
3874
|
-
const
|
|
3875
|
-
start = parseInt(
|
|
3876
|
-
end =
|
|
3914
|
+
const rangeParts = range.split("-");
|
|
3915
|
+
start = parseInt(rangeParts[0], 10);
|
|
3916
|
+
end = rangeParts.length > 1 ? parseInt(rangeParts[1], 10) : max;
|
|
3877
3917
|
}
|
|
3878
3918
|
for (let i = start; i <= end; i += step) values.add(i);
|
|
3879
3919
|
} else if (part.includes("-")) {
|
|
3880
3920
|
const [s, e] = part.split("-").map(Number);
|
|
3921
|
+
if (isNaN(s) || isNaN(e)) throw new Error(`Invalid cron range: ${part}`);
|
|
3881
3922
|
for (let i = s; i <= e; i++) values.add(i);
|
|
3882
3923
|
} else {
|
|
3883
|
-
|
|
3924
|
+
const val = parseInt(part, 10);
|
|
3925
|
+
if (isNaN(val)) throw new Error(`Invalid cron value: ${part}`);
|
|
3926
|
+
values.add(val);
|
|
3884
3927
|
}
|
|
3885
3928
|
}
|
|
3886
3929
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -3889,41 +3932,333 @@ function parseField(field, min, max) {
|
|
|
3889
3932
|
}
|
|
3890
3933
|
return result;
|
|
3891
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
|
|
3892
3973
|
function queue(opts) {
|
|
3893
|
-
const
|
|
3894
|
-
|
|
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) {
|
|
3895
3994
|
const pollInterval = opts?.pollInterval ?? 200;
|
|
3896
3995
|
const handlers = /* @__PURE__ */ new Map();
|
|
3996
|
+
const jobs = [];
|
|
3997
|
+
const failed = [];
|
|
3998
|
+
const MAX_FAILED = 1e3;
|
|
3897
3999
|
let running = false;
|
|
3898
4000
|
let pollTimer = null;
|
|
3899
|
-
let epoch = 0;
|
|
3900
4001
|
let _processed = 0;
|
|
3901
4002
|
let _failed = 0;
|
|
3902
|
-
|
|
3903
|
-
const
|
|
3904
|
-
|
|
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
|
+
}
|
|
3905
4040
|
const mw = ((req, ctx, next) => {
|
|
3906
4041
|
ctx.queue = q;
|
|
3907
4042
|
return next(req, ctx);
|
|
3908
4043
|
});
|
|
3909
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;
|
|
3910
4123
|
const MAX_CONCURRENT = 16;
|
|
3911
|
-
|
|
3912
|
-
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) {
|
|
3913
4132
|
inflight++;
|
|
3914
4133
|
try {
|
|
3915
|
-
await
|
|
4134
|
+
await handler(job);
|
|
3916
4135
|
_processed++;
|
|
4136
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent3(table)} WHERE id = $1`, [job.id]);
|
|
3917
4137
|
} catch (e) {
|
|
3918
4138
|
_failed++;
|
|
3919
|
-
const
|
|
3920
|
-
console.error("[queue] handler error:",
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
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() }));
|
|
3927
4262
|
await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
|
|
3928
4263
|
} finally {
|
|
3929
4264
|
inflight--;
|
|
@@ -3931,8 +4266,7 @@ function queue(opts) {
|
|
|
3931
4266
|
if (job.schedule) {
|
|
3932
4267
|
try {
|
|
3933
4268
|
const nextRun = cronNext(job.schedule);
|
|
3934
|
-
|
|
3935
|
-
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() }));
|
|
3936
4270
|
} catch (e) {
|
|
3937
4271
|
console.error("[queue] cron re-queue failed:", e.message);
|
|
3938
4272
|
}
|
|
@@ -3946,8 +4280,7 @@ function queue(opts) {
|
|
|
3946
4280
|
while (running && inflight < MAX_CONCURRENT) {
|
|
3947
4281
|
const result = await redis2.zpopmin(jobKey);
|
|
3948
4282
|
if (result.length < 2) break;
|
|
3949
|
-
const raw = result[0];
|
|
3950
|
-
const score = parseInt(result[1], 10);
|
|
4283
|
+
const raw = result[0], score = parseInt(result[1], 10);
|
|
3951
4284
|
if (score > now) {
|
|
3952
4285
|
await redis2.zadd(jobKey, score, raw);
|
|
3953
4286
|
break;
|
|
@@ -3958,18 +4291,19 @@ function queue(opts) {
|
|
|
3958
4291
|
} catch {
|
|
3959
4292
|
continue;
|
|
3960
4293
|
}
|
|
3961
|
-
const
|
|
3962
|
-
if (
|
|
3963
|
-
processJob(job, jobHandler);
|
|
3964
|
-
}
|
|
4294
|
+
const handler = handlers.get(job.type);
|
|
4295
|
+
if (handler) processJob(job, handler);
|
|
3965
4296
|
}
|
|
3966
4297
|
} catch (e) {
|
|
3967
4298
|
console.error("[queue] poll error:", e.message);
|
|
3968
4299
|
}
|
|
3969
|
-
if (running && currentEpoch === epoch)
|
|
3970
|
-
pollTimer = setTimeout(poll, pollInterval);
|
|
3971
|
-
}
|
|
4300
|
+
if (running && currentEpoch === epoch) pollTimer = setTimeout(poll, pollInterval);
|
|
3972
4301
|
}
|
|
4302
|
+
const mw = ((req, ctx, next) => {
|
|
4303
|
+
ctx.queue = q;
|
|
4304
|
+
return next(req, ctx);
|
|
4305
|
+
});
|
|
4306
|
+
const q = mw;
|
|
3973
4307
|
mw.add = function add(type, payload, opts2) {
|
|
3974
4308
|
const id2 = crypto5.randomUUID();
|
|
3975
4309
|
let runAt;
|
|
@@ -4045,8 +4379,8 @@ function queue(opts) {
|
|
|
4045
4379
|
return false;
|
|
4046
4380
|
};
|
|
4047
4381
|
mw.retryAllFailed = async function retryAllFailed(type) {
|
|
4048
|
-
const raw = await redis2.lrange(failedKey, 0, -1);
|
|
4049
4382
|
let count = 0;
|
|
4383
|
+
const raw = await redis2.lrange(failedKey, 0, -1);
|
|
4050
4384
|
for (const entry of raw) {
|
|
4051
4385
|
try {
|
|
4052
4386
|
const job = JSON.parse(entry);
|
|
@@ -4064,52 +4398,43 @@ function queue(opts) {
|
|
|
4064
4398
|
return count;
|
|
4065
4399
|
};
|
|
4066
4400
|
mw.dashboard = function dashboard() {
|
|
4067
|
-
|
|
4068
|
-
r.get("/", async (req, ctx) => {
|
|
4069
|
-
const s = q.stats();
|
|
4070
|
-
const pending = await q.jobs(100);
|
|
4071
|
-
const byType = {};
|
|
4072
|
-
for (const job of pending) {
|
|
4073
|
-
if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
|
|
4074
|
-
byType[job.type].pending++;
|
|
4075
|
-
}
|
|
4076
|
-
const failed = await q.failedJobs(1e3);
|
|
4077
|
-
for (const job of failed) {
|
|
4078
|
-
if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
|
|
4079
|
-
byType[job.type].failed++;
|
|
4080
|
-
}
|
|
4081
|
-
return Response.json({
|
|
4082
|
-
stats: s,
|
|
4083
|
-
types: byType,
|
|
4084
|
-
failedCount: failed.length
|
|
4085
|
-
});
|
|
4086
|
-
});
|
|
4087
|
-
r.get("/:type/failed", async (req, ctx) => {
|
|
4088
|
-
const failed = await q.failedJobs(100);
|
|
4089
|
-
const filtered = failed.filter((j) => j.type === ctx.params.type);
|
|
4090
|
-
return Response.json({ jobs: filtered, count: filtered.length });
|
|
4091
|
-
});
|
|
4092
|
-
r.post("/:type/retry", async (req, ctx) => {
|
|
4093
|
-
const count = await q.retryAllFailed(ctx.params.type);
|
|
4094
|
-
return Response.json({ retried: count });
|
|
4095
|
-
});
|
|
4096
|
-
r.post("/retry/:id", async (req, ctx) => {
|
|
4097
|
-
const ok = await q.retryFailed(ctx.params.id);
|
|
4098
|
-
if (!ok) return new Response("Job not found", { status: 404 });
|
|
4099
|
-
return Response.json({ retried: true });
|
|
4100
|
-
});
|
|
4101
|
-
return r;
|
|
4401
|
+
return buildDashboard(q);
|
|
4102
4402
|
};
|
|
4103
|
-
mw.stats = () => ({
|
|
4104
|
-
|
|
4105
|
-
inflight,
|
|
4106
|
-
processed: _processed,
|
|
4107
|
-
failed: _failed,
|
|
4108
|
-
handlers: handlers.size,
|
|
4109
|
-
maxConcurrent: MAX_CONCURRENT
|
|
4110
|
-
});
|
|
4403
|
+
mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
|
|
4404
|
+
attachCron(q, handlers);
|
|
4111
4405
|
return q;
|
|
4112
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
|
+
}
|
|
4113
4438
|
|
|
4114
4439
|
// tenant/rest.ts
|
|
4115
4440
|
import { z as z3 } from "zod";
|
|
@@ -5017,9 +5342,6 @@ function tenant(options) {
|
|
|
5017
5342
|
return mod;
|
|
5018
5343
|
}
|
|
5019
5344
|
|
|
5020
|
-
// agent/client.ts
|
|
5021
|
-
import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
5022
|
-
|
|
5023
5345
|
// agent/rest.ts
|
|
5024
5346
|
function buildRouter2(deps) {
|
|
5025
5347
|
const { agents: agentsTable, runs: runsTable, knowledge, runner } = deps;
|
|
@@ -5173,12 +5495,10 @@ function buildRouter2(deps) {
|
|
|
5173
5495
|
}
|
|
5174
5496
|
|
|
5175
5497
|
// agent/run.ts
|
|
5176
|
-
import { streamText as streamText2, generateText as generateText3, embed as embed2 } from "ai";
|
|
5177
5498
|
import { z as z4 } from "zod";
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
function chunkContent(content, chunkSize = 512, overlap = 64) {
|
|
5499
|
+
|
|
5500
|
+
// ai/utils.ts
|
|
5501
|
+
function chunkContent(content, chunkSize, overlap) {
|
|
5182
5502
|
const paragraphs = content.split(/\n\n+/);
|
|
5183
5503
|
const chunks = [];
|
|
5184
5504
|
let current = "";
|
|
@@ -5192,8 +5512,13 @@ function chunkContent(content, chunkSize = 512, overlap = 64) {
|
|
|
5192
5512
|
if (current) chunks.push(current);
|
|
5193
5513
|
return chunks;
|
|
5194
5514
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
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);
|
|
5197
5522
|
const vec = `[${embedding.join(",")}]`;
|
|
5198
5523
|
const docs = await sql2.unsafe(
|
|
5199
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`,
|
|
@@ -5206,7 +5531,7 @@ async function loadAgent(agents, agentId) {
|
|
|
5206
5531
|
return row ?? null;
|
|
5207
5532
|
}
|
|
5208
5533
|
function createRunner(deps) {
|
|
5209
|
-
const { sql: sql2, agents, runs,
|
|
5534
|
+
const { sql: sql2, agents, runs, provider, modelName, userTools } = deps;
|
|
5210
5535
|
function truncate(s, max = 200) {
|
|
5211
5536
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
5212
5537
|
}
|
|
@@ -5231,8 +5556,6 @@ function createRunner(deps) {
|
|
|
5231
5556
|
async function run(agentId, params) {
|
|
5232
5557
|
const agent2 = await loadAgent(agents, agentId);
|
|
5233
5558
|
if (!agent2 || !agent2.active) throw new Error("Agent not found or inactive");
|
|
5234
|
-
const model = getModel();
|
|
5235
|
-
const embedModel = getEmbeddingModel();
|
|
5236
5559
|
const start = Date.now();
|
|
5237
5560
|
const hasKB = await hasKnowledgeDocs(sql2, agentId);
|
|
5238
5561
|
const messages2 = params.messages ?? [];
|
|
@@ -5248,7 +5571,7 @@ function createRunner(deps) {
|
|
|
5248
5571
|
limit: z4.number().default(5).describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF")
|
|
5249
5572
|
}),
|
|
5250
5573
|
execute: async ({ query, limit }) => {
|
|
5251
|
-
return searchKnowledge(sql2,
|
|
5574
|
+
return searchKnowledge(sql2, provider, agentId, query, limit);
|
|
5252
5575
|
}
|
|
5253
5576
|
};
|
|
5254
5577
|
}
|
|
@@ -5259,8 +5582,7 @@ function createRunner(deps) {
|
|
|
5259
5582
|
}
|
|
5260
5583
|
const system = agent2.system_prompt || void 0;
|
|
5261
5584
|
if (params.stream) {
|
|
5262
|
-
const result =
|
|
5263
|
-
model,
|
|
5585
|
+
const result = provider.streamText({
|
|
5264
5586
|
system,
|
|
5265
5587
|
messages: messages2,
|
|
5266
5588
|
tools: Object.keys(tools).length > 0 ? tools : void 0
|
|
@@ -5285,8 +5607,7 @@ function createRunner(deps) {
|
|
|
5285
5607
|
});
|
|
5286
5608
|
return { stream: sseStream };
|
|
5287
5609
|
} else {
|
|
5288
|
-
const result = await
|
|
5289
|
-
model,
|
|
5610
|
+
const result = await provider.generateText({
|
|
5290
5611
|
system,
|
|
5291
5612
|
messages: messages2,
|
|
5292
5613
|
tools: Object.keys(tools).length > 0 ? tools : void 0
|
|
@@ -5305,20 +5626,20 @@ function createRunner(deps) {
|
|
|
5305
5626
|
}
|
|
5306
5627
|
}
|
|
5307
5628
|
async function addKnowledge(agentId, title, content) {
|
|
5308
|
-
const embedModel = getEmbeddingModel();
|
|
5309
5629
|
const chunks = chunkContent(content);
|
|
5310
5630
|
const [first] = chunks;
|
|
5311
|
-
const
|
|
5631
|
+
const embedding = await provider.embed(first);
|
|
5312
5632
|
const vec = `[${embedding.join(",")}]`;
|
|
5313
5633
|
const [doc] = await sql2.unsafe(
|
|
5314
5634
|
`INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector) RETURNING *`,
|
|
5315
5635
|
[agentId, title, first, vec]
|
|
5316
5636
|
);
|
|
5317
5637
|
for (let i = 1; i < chunks.length; i++) {
|
|
5318
|
-
const
|
|
5638
|
+
const emb = await provider.embed(chunks[i]);
|
|
5639
|
+
const vec2 = `[${emb.join(",")}]`;
|
|
5319
5640
|
await sql2.unsafe(
|
|
5320
5641
|
`INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector)`,
|
|
5321
|
-
[agentId, `${title} (${i + 1})`, chunks[i],
|
|
5642
|
+
[agentId, `${title} (${i + 1})`, chunks[i], vec2]
|
|
5322
5643
|
);
|
|
5323
5644
|
}
|
|
5324
5645
|
return doc;
|
|
@@ -5327,31 +5648,11 @@ function createRunner(deps) {
|
|
|
5327
5648
|
}
|
|
5328
5649
|
|
|
5329
5650
|
// agent/client.ts
|
|
5330
|
-
function createModelsFromEnv() {
|
|
5331
|
-
const baseURL = process.env.OPENAI_BASE_URL || "http://localhost:11434/v1";
|
|
5332
|
-
const apiKey = process.env.OPENAI_API_KEY || "ollama";
|
|
5333
|
-
const modelName = process.env.OPENAI_MODEL || "qwen3:0.6b";
|
|
5334
|
-
const embedModelName = process.env.OPENAI_EMBEDDING_MODEL || "qwen3-embedding:0.6b";
|
|
5335
|
-
const provider = createOpenAI2({ baseURL, apiKey });
|
|
5336
|
-
return {
|
|
5337
|
-
model: provider(modelName),
|
|
5338
|
-
embeddingModel: provider.embedding(embedModelName),
|
|
5339
|
-
dimension: parseInt(process.env.EMBEDDING_DIMENSION || "1024", 10)
|
|
5340
|
-
};
|
|
5341
|
-
}
|
|
5342
5651
|
function agent(options) {
|
|
5343
5652
|
const pg = options.pg;
|
|
5344
5653
|
const sql2 = pg.sql;
|
|
5345
|
-
const
|
|
5346
|
-
const
|
|
5347
|
-
const dimension = options.embeddingDimension ?? 1024;
|
|
5348
|
-
const defaultModels = !model || !embeddingModel ? createModelsFromEnv() : null;
|
|
5349
|
-
function getModel() {
|
|
5350
|
-
return model ?? defaultModels.model;
|
|
5351
|
-
}
|
|
5352
|
-
function getEmbeddingModel() {
|
|
5353
|
-
return embeddingModel ?? defaultModels.embeddingModel;
|
|
5354
|
-
}
|
|
5654
|
+
const resolvedProvider = options.provider ?? aiProvider();
|
|
5655
|
+
const dimension = options.embeddingDimension ?? resolvedProvider.dimension;
|
|
5355
5656
|
const agentsTable = pg.table("_agents", {
|
|
5356
5657
|
id: serial("id").primaryKey(),
|
|
5357
5658
|
tenant_id: text("tenant_id"),
|
|
@@ -5388,7 +5689,7 @@ function agent(options) {
|
|
|
5388
5689
|
trace_id: text("trace_id"),
|
|
5389
5690
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
5390
5691
|
});
|
|
5391
|
-
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 });
|
|
5392
5693
|
const base = new PgModule(pg);
|
|
5393
5694
|
const r = buildRouter2({ agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, runner });
|
|
5394
5695
|
const mod = r;
|
|
@@ -6284,7 +6585,7 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
|
6284
6585
|
// ssr.ts
|
|
6285
6586
|
import { createElement as createElement3 } from "react";
|
|
6286
6587
|
import { createHash as createHash3 } from "node:crypto";
|
|
6287
|
-
import { existsSync as existsSync4, readdirSync } from "node:fs";
|
|
6588
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
|
|
6288
6589
|
import { readdir, stat } from "node:fs/promises";
|
|
6289
6590
|
import { dirname as dirname3, join as join5, resolve as resolve6, relative as relative2 } from "node:path";
|
|
6290
6591
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
@@ -6401,6 +6702,7 @@ function compile(path2) {
|
|
|
6401
6702
|
return isDev() ? compileTsxDev(path2) : compileTsx(path2);
|
|
6402
6703
|
}
|
|
6403
6704
|
var vendorBundle = null;
|
|
6705
|
+
var vendorHash = "";
|
|
6404
6706
|
async function compileVendorBundle() {
|
|
6405
6707
|
if (vendorBundle) return vendorBundle;
|
|
6406
6708
|
if (!_userRequire) _userRequire = createRequire(join2(process.cwd(), "package.json"));
|
|
@@ -6415,8 +6717,18 @@ async function compileVendorBundle() {
|
|
|
6415
6717
|
const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
|
|
6416
6718
|
modules[request] = keys;
|
|
6417
6719
|
}
|
|
6418
|
-
const
|
|
6419
|
-
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
|
+
}
|
|
6420
6732
|
const used = /* @__PURE__ */ new Set();
|
|
6421
6733
|
const stmts = [""];
|
|
6422
6734
|
for (const [request, keys] of Object.entries(modules)) {
|
|
@@ -6424,7 +6736,7 @@ async function compileVendorBundle() {
|
|
|
6424
6736
|
if (unique.length > 0) stmts.push(`export { ${unique.join(", ")} } from ${JSON.stringify(request)};`);
|
|
6425
6737
|
}
|
|
6426
6738
|
const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
|
|
6427
|
-
if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from
|
|
6739
|
+
if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from ${JSON.stringify(reactTsPath)};`);
|
|
6428
6740
|
const result = await esbuild.build({
|
|
6429
6741
|
stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
|
|
6430
6742
|
format: "esm",
|
|
@@ -6432,13 +6744,68 @@ async function compileVendorBundle() {
|
|
|
6432
6744
|
write: false
|
|
6433
6745
|
});
|
|
6434
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);
|
|
6435
6750
|
return vendorBundle;
|
|
6436
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
|
+
}
|
|
6437
6789
|
async function compileHotComponent(path2) {
|
|
6438
6790
|
const absPath = resolve3(path2);
|
|
6439
6791
|
const h = id(absPath);
|
|
6440
6792
|
const stdin = `import C from ${JSON.stringify(absPath)};
|
|
6441
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
|
+
};
|
|
6442
6809
|
const result = await esbuild.build({
|
|
6443
6810
|
stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
|
|
6444
6811
|
format: "esm",
|
|
@@ -6446,7 +6813,8 @@ async function compileHotComponent(path2) {
|
|
|
6446
6813
|
jsx: "automatic",
|
|
6447
6814
|
jsxImportSource: "react",
|
|
6448
6815
|
bundle: true,
|
|
6449
|
-
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],
|
|
6450
6818
|
write: false
|
|
6451
6819
|
});
|
|
6452
6820
|
let code = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
@@ -6471,11 +6839,10 @@ function getPublicEnv() {
|
|
|
6471
6839
|
return _publicEnv;
|
|
6472
6840
|
}
|
|
6473
6841
|
function buildHeadPayload(opts) {
|
|
6474
|
-
const { ctx, base,
|
|
6842
|
+
const { ctx, base, tailwind } = opts;
|
|
6475
6843
|
let result = "";
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
result += `<script type="importmap">{
|
|
6844
|
+
const vUrl = `${base}/__wfw/v/bundle?h=${vendorHash}`;
|
|
6845
|
+
result += `<script type="importmap">{
|
|
6479
6846
|
"imports": {
|
|
6480
6847
|
"react": "${vUrl}",
|
|
6481
6848
|
"react-dom": "${vUrl}",
|
|
@@ -6485,22 +6852,12 @@ function buildHeadPayload(opts) {
|
|
|
6485
6852
|
}
|
|
6486
6853
|
}</script>
|
|
6487
6854
|
`;
|
|
6488
|
-
|
|
6489
|
-
if (ctx.prefs?.theme) {
|
|
6855
|
+
if (ctx.theme?.value) {
|
|
6490
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>
|
|
6491
6857
|
`;
|
|
6492
6858
|
}
|
|
6493
|
-
if (
|
|
6494
|
-
|
|
6495
|
-
if (cssUrl) result += `<link rel="stylesheet" href="${cssUrl}" />
|
|
6496
|
-
`;
|
|
6497
|
-
}
|
|
6498
|
-
const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
|
|
6499
|
-
if (localeData && Object.keys(localeData).length > 0) {
|
|
6500
|
-
if (!_localeDataCache || _localeDataCache.data !== localeData) {
|
|
6501
|
-
_localeDataCache = { data: localeData, json: JSON.stringify(localeData) };
|
|
6502
|
-
}
|
|
6503
|
-
result += `<script>window.__LOCALE_DATA__=${_localeDataCache.json}</script>
|
|
6859
|
+
if (tailwind?.css) {
|
|
6860
|
+
result += `<link rel="stylesheet" href="${tailwind.url}" />
|
|
6504
6861
|
`;
|
|
6505
6862
|
}
|
|
6506
6863
|
const loaderData = opts.loaderData || {};
|
|
@@ -6508,7 +6865,9 @@ function buildHeadPayload(opts) {
|
|
|
6508
6865
|
params: ctx.params,
|
|
6509
6866
|
query: ctx.query,
|
|
6510
6867
|
parsed: ctx.parsed,
|
|
6511
|
-
|
|
6868
|
+
theme: ctx.theme,
|
|
6869
|
+
i18n: ctx.i18n,
|
|
6870
|
+
flash: ctx.flash,
|
|
6512
6871
|
loaderData
|
|
6513
6872
|
};
|
|
6514
6873
|
if (ctx.user && typeof ctx.user === "object") {
|
|
@@ -6528,72 +6887,97 @@ function buildHeadPayload(opts) {
|
|
|
6528
6887
|
`;
|
|
6529
6888
|
return result;
|
|
6530
6889
|
}
|
|
6531
|
-
function buildBodyScripts(opts) {
|
|
6890
|
+
function buildBodyScripts(opts, hydrationScript) {
|
|
6532
6891
|
const parts = [];
|
|
6533
|
-
if (
|
|
6534
|
-
parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.loaderData)}</script>`);
|
|
6535
|
-
}
|
|
6536
|
-
if (opts.bundle) {
|
|
6537
|
-
parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
|
|
6538
|
-
}
|
|
6892
|
+
if (hydrationScript) parts.push(hydrationScript);
|
|
6539
6893
|
return parts.join("\n");
|
|
6540
6894
|
}
|
|
6541
|
-
|
|
6542
|
-
function streamResponse(reactStream, opts) {
|
|
6895
|
+
function streamResponse(reactStream, opts, hydrationScript) {
|
|
6543
6896
|
const decoder = new TextDecoder2();
|
|
6544
6897
|
const encoder2 = new TextEncoder2();
|
|
6545
|
-
const headPayload = buildHeadPayload(opts);
|
|
6546
|
-
let buffer = "";
|
|
6547
|
-
let headFlushed = false;
|
|
6548
|
-
let extractedHead = "";
|
|
6549
6898
|
const output = new ReadableStream({
|
|
6550
6899
|
async start(controller) {
|
|
6551
6900
|
try {
|
|
6552
6901
|
const reader = reactStream.getReader();
|
|
6553
|
-
|
|
6554
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
6555
|
-
if (!extractedHead) {
|
|
6556
|
-
const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
|
|
6557
|
-
if (m) {
|
|
6558
|
-
extractedHead = m[1];
|
|
6559
|
-
buffer = buffer.replace(m[0], "");
|
|
6560
|
-
}
|
|
6561
|
-
}
|
|
6562
|
-
if (!headFlushed) {
|
|
6563
|
-
const idx = buffer.indexOf("</head>");
|
|
6564
|
-
if (idx !== -1) {
|
|
6565
|
-
const before = buffer.slice(0, idx);
|
|
6566
|
-
let injection = "";
|
|
6567
|
-
if (extractedHead) injection += "\n" + extractedHead;
|
|
6568
|
-
injection += headPayload;
|
|
6569
|
-
controller.enqueue(encoder2.encode(before + injection));
|
|
6570
|
-
buffer = buffer.slice(idx);
|
|
6571
|
-
headFlushed = true;
|
|
6572
|
-
}
|
|
6573
|
-
return;
|
|
6574
|
-
}
|
|
6575
|
-
controller.enqueue(encoder2.encode(buffer));
|
|
6576
|
-
buffer = "";
|
|
6577
|
-
}
|
|
6902
|
+
let html = "";
|
|
6578
6903
|
while (true) {
|
|
6579
6904
|
const { done, value } = await reader.read();
|
|
6580
6905
|
if (done) break;
|
|
6581
|
-
|
|
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);
|
|
6582
6922
|
}
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
if (body) controller.enqueue(encoder2.encode("\n" + body));
|
|
6923
|
+
let bodyScripts = "";
|
|
6924
|
+
const built = buildBodyScripts(opts, hydrationScript);
|
|
6925
|
+
if (built) bodyScripts += built;
|
|
6587
6926
|
if (opts.isDev) {
|
|
6588
6927
|
const wsUrl = `${opts.base}/__weifuwu/livereload`;
|
|
6589
6928
|
const hbUrl = `${opts.base}/__wfw/h/`;
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
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
|
+
}
|
|
6594
6977
|
}
|
|
6978
|
+
controller.enqueue(encoder2.encode(html));
|
|
6595
6979
|
} catch {
|
|
6596
|
-
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>';
|
|
6597
6981
|
controller.enqueue(encoder2.encode(fallback));
|
|
6598
6982
|
} finally {
|
|
6599
6983
|
controller.close();
|
|
@@ -6623,9 +7007,9 @@ function tailwindContext(dir) {
|
|
|
6623
7007
|
await compileTailwindCss(cssPath, cssDir);
|
|
6624
7008
|
}
|
|
6625
7009
|
const entry = cssCache.get(cssPath);
|
|
6626
|
-
ctx.compiledTailwindCss = entry.css;
|
|
6627
7010
|
const base = (ctx.mountPath || "").replace(/\/$/, "");
|
|
6628
|
-
|
|
7011
|
+
const url = base ? `${base}/__wfw/style/${entry.hash}.css` : `/__wfw/style/${entry.hash}.css`;
|
|
7012
|
+
ctx.tailwind = { css: entry.css, url };
|
|
6629
7013
|
return next(req, ctx);
|
|
6630
7014
|
};
|
|
6631
7015
|
}
|
|
@@ -6719,6 +7103,8 @@ function liveWs() {
|
|
|
6719
7103
|
}
|
|
6720
7104
|
function liveRouter(dir) {
|
|
6721
7105
|
const r = new Router();
|
|
7106
|
+
compileVendorBundle().catch(() => {
|
|
7107
|
+
});
|
|
6722
7108
|
r.get("/__wfw/v/bundle", async () => {
|
|
6723
7109
|
const code = await compileVendorBundle();
|
|
6724
7110
|
return new Response(code, {
|
|
@@ -6766,7 +7152,6 @@ function liveWatcher(dir) {
|
|
|
6766
7152
|
return broadcastReload();
|
|
6767
7153
|
}
|
|
6768
7154
|
clearCompileCache();
|
|
6769
|
-
markClientBundleDirty();
|
|
6770
7155
|
const targets = existsSync3(entryPath) ? [entryPath] : findEntries(resolve5(filePath));
|
|
6771
7156
|
if (targets.length === 0) return broadcastReload();
|
|
6772
7157
|
try {
|
|
@@ -6872,7 +7257,7 @@ function errorBoundary(errorPath) {
|
|
|
6872
7257
|
ctx,
|
|
6873
7258
|
base,
|
|
6874
7259
|
isDev: isDev(),
|
|
6875
|
-
|
|
7260
|
+
tailwind: ctx.tailwind,
|
|
6876
7261
|
status: 500
|
|
6877
7262
|
});
|
|
6878
7263
|
}
|
|
@@ -6883,25 +7268,6 @@ function errorBoundary(errorPath) {
|
|
|
6883
7268
|
var isDev2 = isDev();
|
|
6884
7269
|
var als2 = new AsyncLocalStorage2();
|
|
6885
7270
|
__registerAls(() => als2.getStore());
|
|
6886
|
-
var bundleCache = /* @__PURE__ */ new Map();
|
|
6887
|
-
var _bundleDirty = false;
|
|
6888
|
-
function markClientBundleDirty() {
|
|
6889
|
-
_bundleDirty = true;
|
|
6890
|
-
}
|
|
6891
|
-
function getBundle(key) {
|
|
6892
|
-
if (_bundleDirty) {
|
|
6893
|
-
bundleCache.clear();
|
|
6894
|
-
_bundleDirty = false;
|
|
6895
|
-
}
|
|
6896
|
-
return bundleCache.get(key);
|
|
6897
|
-
}
|
|
6898
|
-
function setBundle(key, buf) {
|
|
6899
|
-
if (_bundleDirty) {
|
|
6900
|
-
bundleCache.clear();
|
|
6901
|
-
_bundleDirty = false;
|
|
6902
|
-
}
|
|
6903
|
-
bundleCache.set(key, buf);
|
|
6904
|
-
}
|
|
6905
7271
|
function hashId(s) {
|
|
6906
7272
|
return createHash3("md5").update(s).digest("hex").slice(0, 8);
|
|
6907
7273
|
}
|
|
@@ -7016,50 +7382,56 @@ async function resolveRoute(ssrDir, segments, routeCache) {
|
|
|
7016
7382
|
routeCache.set(cacheKey, result);
|
|
7017
7383
|
return result;
|
|
7018
7384
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
banner: { js: "self.process={env:{}};" },
|
|
7051
|
-
loader: { ".node": "empty" },
|
|
7052
|
-
external: isDev2 ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"] : void 0,
|
|
7053
|
-
write: false,
|
|
7054
|
-
minify: !isDev2
|
|
7055
|
-
});
|
|
7056
|
-
return result.outputFiles[0].contents;
|
|
7057
|
-
} catch (err) {
|
|
7058
|
-
console.error("hydration bundle failed:", err);
|
|
7059
|
-
return null;
|
|
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
|
+
// Stable proxy \u2014 same function ref = React preserves fiber + useState state across HMR
|
|
7404
|
+
const _pageImpl = { current: Page };
|
|
7405
|
+
const _pageProxy = new Proxy(function __wfw_page(){}, {
|
|
7406
|
+
apply(_target, _thisArg, args) {
|
|
7407
|
+
return Reflect.apply(_pageImpl.current, _thisArg, args);
|
|
7408
|
+
},
|
|
7409
|
+
});
|
|
7410
|
+
|
|
7411
|
+
const reactRoot = createRoot(_root);
|
|
7412
|
+
let _tick = 0;
|
|
7413
|
+
function renderPage() {
|
|
7414
|
+
reactRoot.render(createElement(TsxContext.Provider, { value: _ctx },
|
|
7415
|
+
createElement(_pageProxy, { __t: _tick })));
|
|
7060
7416
|
}
|
|
7417
|
+
renderPage();
|
|
7418
|
+
|
|
7419
|
+
window.__WFW_REFRESH = async (NewComponent) => {
|
|
7420
|
+
const store = globalThis.__WEIFUWU_CTX_STORE?._ctx || _ctx;
|
|
7421
|
+
_pageImpl.current = NewComponent;
|
|
7422
|
+
_tick++;
|
|
7423
|
+
reactRoot.render(createElement(TsxContext.Provider, { value: store },
|
|
7424
|
+
createElement(_pageProxy, { __t: _tick })));
|
|
7425
|
+
};
|
|
7426
|
+
` : `
|
|
7427
|
+
hydrateRoot(_root, app);
|
|
7428
|
+
`}
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7431
|
+
init();
|
|
7432
|
+
</script>`;
|
|
7061
7433
|
}
|
|
7062
|
-
function renderPage(pageFile) {
|
|
7434
|
+
function renderPage(pageFile, outDir) {
|
|
7063
7435
|
const absPath = resolve6(pageFile);
|
|
7064
7436
|
const entryId = hashId(absPath);
|
|
7065
7437
|
ssrEntries.set(entryId, { path: absPath });
|
|
@@ -7085,15 +7457,15 @@ function renderPage(pageFile) {
|
|
|
7085
7457
|
query: ctx.query,
|
|
7086
7458
|
user: ctx.user ?? {},
|
|
7087
7459
|
parsed: ctx.parsed ?? {},
|
|
7088
|
-
|
|
7460
|
+
theme: ctx.theme,
|
|
7461
|
+
i18n: ctx.i18n,
|
|
7462
|
+
flash: ctx.flash,
|
|
7089
7463
|
loaderData,
|
|
7090
7464
|
env: ctx.env ?? {}
|
|
7091
7465
|
};
|
|
7092
7466
|
return als2.run(ctxValue, async () => {
|
|
7093
7467
|
setCtx(ctxValue);
|
|
7094
|
-
|
|
7095
|
-
globalThis.__LOCALE_DATA__ = ctxValue.parsed.__localeData;
|
|
7096
|
-
}
|
|
7468
|
+
await compileBrowser(absPath, outDir);
|
|
7097
7469
|
let element = createElement3(
|
|
7098
7470
|
"div",
|
|
7099
7471
|
{ id: "__weifuwu_root" },
|
|
@@ -7104,24 +7476,15 @@ function renderPage(pageFile) {
|
|
|
7104
7476
|
)
|
|
7105
7477
|
);
|
|
7106
7478
|
element = buildHtmlShell("weifuwu", element, layoutComponents);
|
|
7107
|
-
let bundle = null;
|
|
7108
|
-
if (!getBundle(bundleKey)) {
|
|
7109
|
-
const buf = await buildClientBundle(absPath, layoutPaths);
|
|
7110
|
-
if (buf) setBundle(bundleKey, buf);
|
|
7111
|
-
}
|
|
7112
|
-
if (getBundle(bundleKey)) {
|
|
7113
|
-
bundle = { url: bundleKey };
|
|
7114
|
-
}
|
|
7115
7479
|
const { renderToReadableStream } = await import("react-dom/server");
|
|
7116
7480
|
const stream = await renderToReadableStream(element);
|
|
7117
7481
|
return streamResponse(stream, {
|
|
7118
7482
|
ctx,
|
|
7119
7483
|
base,
|
|
7120
7484
|
isDev: isDev2,
|
|
7121
|
-
bundle,
|
|
7122
7485
|
loaderData,
|
|
7123
|
-
|
|
7124
|
-
});
|
|
7486
|
+
tailwind: ctx.tailwind
|
|
7487
|
+
}, buildHydrationScript(entryId, JSON.stringify(ctxValue), base));
|
|
7125
7488
|
});
|
|
7126
7489
|
};
|
|
7127
7490
|
}
|
|
@@ -7167,11 +7530,23 @@ function discoverRoutes(dir) {
|
|
|
7167
7530
|
function ssr(opts) {
|
|
7168
7531
|
const r = new Router();
|
|
7169
7532
|
const dir = resolve6(opts.dir);
|
|
7533
|
+
const outDir = resolve6(OUT_DIR);
|
|
7170
7534
|
const routeCache = /* @__PURE__ */ new Map();
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7535
|
+
compileVendorBundle().catch(() => {
|
|
7536
|
+
});
|
|
7537
|
+
r.get("/__ssr/:file", (req, ctx) => {
|
|
7538
|
+
const filePath = join5(outDir, ctx.params.file);
|
|
7539
|
+
if (!filePath.startsWith(outDir) || !existsSync4(filePath)) {
|
|
7540
|
+
return new Response("Not Found", { status: 404 });
|
|
7541
|
+
}
|
|
7542
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
7543
|
+
return new Response(content, {
|
|
7544
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7545
|
+
});
|
|
7546
|
+
});
|
|
7547
|
+
r.get("/__wfw/v/bundle", async () => {
|
|
7548
|
+
const code = await compileVendorBundle();
|
|
7549
|
+
return new Response(code, {
|
|
7175
7550
|
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7176
7551
|
});
|
|
7177
7552
|
});
|
|
@@ -7198,7 +7573,7 @@ function ssr(opts) {
|
|
|
7198
7573
|
...resolved.layoutFiles.map((f) => layout(f)),
|
|
7199
7574
|
tailwindContext(dir)
|
|
7200
7575
|
];
|
|
7201
|
-
const handler = (req2, ctx2) => renderPage(resolved.pageFile)(req2, ctx2);
|
|
7576
|
+
const handler = (req2, ctx2) => renderPage(resolved.pageFile, outDir)(req2, ctx2);
|
|
7202
7577
|
return runChain(mws, handler, req, ctx);
|
|
7203
7578
|
});
|
|
7204
7579
|
const mod = r;
|
|
@@ -7288,13 +7663,13 @@ async function addToolMessages(sql2, sessionId, toolCalls, toolResults) {
|
|
|
7288
7663
|
}
|
|
7289
7664
|
|
|
7290
7665
|
// opencode/run.ts
|
|
7291
|
-
import { streamText as
|
|
7666
|
+
import { streamText as streamText2, stepCountIs } from "ai";
|
|
7292
7667
|
async function* executeGenerator(opts) {
|
|
7293
7668
|
const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql: sql2, abortSignal } = opts;
|
|
7294
7669
|
const lastStepToolCalls = [];
|
|
7295
7670
|
let currentAssistantText = "";
|
|
7296
7671
|
let currentUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
7297
|
-
const result =
|
|
7672
|
+
const result = streamText2({
|
|
7298
7673
|
model,
|
|
7299
7674
|
system: systemPrompt,
|
|
7300
7675
|
messages: [
|
|
@@ -7461,7 +7836,7 @@ function createBashTool(ctx) {
|
|
|
7461
7836
|
// opencode/tools/read.ts
|
|
7462
7837
|
import { tool as tool4 } from "ai";
|
|
7463
7838
|
import { z as z6 } from "zod";
|
|
7464
|
-
import { readFileSync as
|
|
7839
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
7465
7840
|
import { resolve as resolve7 } from "node:path";
|
|
7466
7841
|
function createReadTool(ctx) {
|
|
7467
7842
|
return tool4({
|
|
@@ -7473,10 +7848,10 @@ function createReadTool(ctx) {
|
|
|
7473
7848
|
}),
|
|
7474
7849
|
execute: async ({ path: path2, offset, limit }) => {
|
|
7475
7850
|
const resolved = resolve7(ctx.workspace, path2);
|
|
7476
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7851
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7477
7852
|
return { error: "Path not allowed", content: null, totalLines: 0 };
|
|
7478
7853
|
}
|
|
7479
|
-
const content =
|
|
7854
|
+
const content = readFileSync5(resolved, "utf-8");
|
|
7480
7855
|
const lines = content.split("\n");
|
|
7481
7856
|
const totalLines = lines.length;
|
|
7482
7857
|
if (offset !== void 0) {
|
|
@@ -7514,7 +7889,7 @@ function createWriteTool(ctx) {
|
|
|
7514
7889
|
}),
|
|
7515
7890
|
execute: async ({ path: path2, content }) => {
|
|
7516
7891
|
const resolved = resolve8(ctx.workspace, path2);
|
|
7517
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7892
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7518
7893
|
return { error: "Path not allowed" };
|
|
7519
7894
|
}
|
|
7520
7895
|
mkdirSync3(dirname4(resolved), { recursive: true });
|
|
@@ -7527,7 +7902,7 @@ function createWriteTool(ctx) {
|
|
|
7527
7902
|
// opencode/tools/edit.ts
|
|
7528
7903
|
import { tool as tool6 } from "ai";
|
|
7529
7904
|
import { z as z8 } from "zod";
|
|
7530
|
-
import { readFileSync as
|
|
7905
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7531
7906
|
import { resolve as resolve9 } from "node:path";
|
|
7532
7907
|
function createEditTool(ctx) {
|
|
7533
7908
|
return tool6({
|
|
@@ -7540,10 +7915,10 @@ function createEditTool(ctx) {
|
|
|
7540
7915
|
}),
|
|
7541
7916
|
execute: async ({ path: path2, oldString, newString, replaceAll }) => {
|
|
7542
7917
|
const resolved = resolve9(ctx.workspace, path2);
|
|
7543
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7918
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7544
7919
|
return { error: "Path not allowed" };
|
|
7545
7920
|
}
|
|
7546
|
-
const content =
|
|
7921
|
+
const content = readFileSync6(resolved, "utf-8");
|
|
7547
7922
|
if (replaceAll) {
|
|
7548
7923
|
if (!content.includes(oldString)) {
|
|
7549
7924
|
return { error: "oldString not found in file", replaced: 0 };
|
|
@@ -7728,7 +8103,7 @@ ${availableList}
|
|
|
7728
8103
|
name: z13.string().describe("The name of the skill to load")
|
|
7729
8104
|
}),
|
|
7730
8105
|
execute: async ({ name }) => {
|
|
7731
|
-
if (!isSkillAllowed(name, ctx.permissions)) {
|
|
8106
|
+
if (!isSkillAllowed(name, ctx.permissions.permissions)) {
|
|
7732
8107
|
return { error: `Skill "${name}" is not permitted` };
|
|
7733
8108
|
}
|
|
7734
8109
|
const skill = ctx.skillsRegistry.get(name);
|
|
@@ -7749,28 +8124,28 @@ ${availableList}
|
|
|
7749
8124
|
// opencode/tools/index.ts
|
|
7750
8125
|
function createTools(ctx) {
|
|
7751
8126
|
const tools = {};
|
|
7752
|
-
if (isToolEnabled("bash", ctx.permissions)) {
|
|
8127
|
+
if (isToolEnabled("bash", ctx.permissions.permissions)) {
|
|
7753
8128
|
tools.bash = createBashTool(ctx);
|
|
7754
8129
|
}
|
|
7755
|
-
if (isToolEnabled("read", ctx.permissions)) {
|
|
8130
|
+
if (isToolEnabled("read", ctx.permissions.permissions)) {
|
|
7756
8131
|
tools.read = createReadTool(ctx);
|
|
7757
8132
|
}
|
|
7758
|
-
if (isToolEnabled("write", ctx.permissions)) {
|
|
8133
|
+
if (isToolEnabled("write", ctx.permissions.permissions)) {
|
|
7759
8134
|
tools.write = createWriteTool(ctx);
|
|
7760
8135
|
}
|
|
7761
|
-
if (isToolEnabled("edit", ctx.permissions)) {
|
|
8136
|
+
if (isToolEnabled("edit", ctx.permissions.permissions)) {
|
|
7762
8137
|
tools.edit = createEditTool(ctx);
|
|
7763
8138
|
}
|
|
7764
|
-
if (isToolEnabled("grep", ctx.permissions)) {
|
|
8139
|
+
if (isToolEnabled("grep", ctx.permissions.permissions)) {
|
|
7765
8140
|
tools.grep = createGrepTool(ctx);
|
|
7766
8141
|
}
|
|
7767
|
-
if (isToolEnabled("glob", ctx.permissions)) {
|
|
8142
|
+
if (isToolEnabled("glob", ctx.permissions.permissions)) {
|
|
7768
8143
|
tools.glob = createGlobTool(ctx);
|
|
7769
8144
|
}
|
|
7770
|
-
if (isToolEnabled("web", ctx.permissions)) {
|
|
8145
|
+
if (isToolEnabled("web", ctx.permissions.permissions)) {
|
|
7771
8146
|
tools.web = createWebTool(ctx);
|
|
7772
8147
|
}
|
|
7773
|
-
if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions)) {
|
|
8148
|
+
if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions.permissions)) {
|
|
7774
8149
|
tools.skill = createSkillTool(ctx);
|
|
7775
8150
|
}
|
|
7776
8151
|
tools.question = createQuestionTool(ctx);
|
|
@@ -7779,7 +8154,7 @@ function createTools(ctx) {
|
|
|
7779
8154
|
|
|
7780
8155
|
// opencode/rest.ts
|
|
7781
8156
|
async function buildRouter4(deps) {
|
|
7782
|
-
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
8157
|
+
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
|
|
7783
8158
|
const router = new Router();
|
|
7784
8159
|
router.post("/sessions", async (req, ctx) => {
|
|
7785
8160
|
const body = await req.json().catch(() => ({}));
|
|
@@ -7814,7 +8189,7 @@ async function buildRouter4(deps) {
|
|
|
7814
8189
|
if (!content) return new Response("Missing content", { status: 400 });
|
|
7815
8190
|
const toolCtx = {
|
|
7816
8191
|
workspace: session2.workspace || workspace,
|
|
7817
|
-
permissions,
|
|
8192
|
+
permissions: permissions2,
|
|
7818
8193
|
pendingQuestions,
|
|
7819
8194
|
skillsRegistry: deps.skillsRegistry
|
|
7820
8195
|
};
|
|
@@ -7872,7 +8247,7 @@ async function buildRouter4(deps) {
|
|
|
7872
8247
|
// opencode/ws.ts
|
|
7873
8248
|
var clients2 = /* @__PURE__ */ new WeakMap();
|
|
7874
8249
|
function createWSHandler2(deps) {
|
|
7875
|
-
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
8250
|
+
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
|
|
7876
8251
|
return {
|
|
7877
8252
|
open(ws, ctx) {
|
|
7878
8253
|
const userId = ctx.user?.id ?? 0;
|
|
@@ -7922,7 +8297,7 @@ function createWSHandler2(deps) {
|
|
|
7922
8297
|
}
|
|
7923
8298
|
const toolCtx = {
|
|
7924
8299
|
workspace: session2.workspace || workspace,
|
|
7925
|
-
permissions,
|
|
8300
|
+
permissions: permissions2,
|
|
7926
8301
|
pendingQuestions,
|
|
7927
8302
|
skillsRegistry
|
|
7928
8303
|
};
|
|
@@ -8084,7 +8459,7 @@ async function opencode(options) {
|
|
|
8084
8459
|
const workspace = options.workspace || process.cwd();
|
|
8085
8460
|
const systemPrompt = options.systemPrompt;
|
|
8086
8461
|
const manualSkills = options.skills || [];
|
|
8087
|
-
const
|
|
8462
|
+
const permissions2 = options.permissions;
|
|
8088
8463
|
const modelName = options.model || "deepseek-v4-flash";
|
|
8089
8464
|
const [discoveredSkills] = await Promise.all([discoverSkills(workspace)]);
|
|
8090
8465
|
const skillsRegistry = buildSkillRegistry(discoveredSkills, manualSkills);
|
|
@@ -8092,7 +8467,7 @@ async function opencode(options) {
|
|
|
8092
8467
|
const model = provider.chat(modelName);
|
|
8093
8468
|
const pendingQuestions = /* @__PURE__ */ new Map();
|
|
8094
8469
|
const base = new PgModule(pg);
|
|
8095
|
-
const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
|
|
8470
|
+
const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
|
|
8096
8471
|
const mod = r;
|
|
8097
8472
|
mod.migrate = async () => {
|
|
8098
8473
|
const sessions2 = pg.table("_opencode_sessions", {
|
|
@@ -8125,7 +8500,7 @@ async function opencode(options) {
|
|
|
8125
8500
|
await messages2.create();
|
|
8126
8501
|
await messages2.createIndex(["session_id", "created_at"]);
|
|
8127
8502
|
};
|
|
8128
|
-
mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
|
|
8503
|
+
mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
|
|
8129
8504
|
mod.close = () => base.close();
|
|
8130
8505
|
return mod;
|
|
8131
8506
|
}
|
|
@@ -8395,12 +8770,49 @@ function analytics(options) {
|
|
|
8395
8770
|
return mod;
|
|
8396
8771
|
}
|
|
8397
8772
|
|
|
8398
|
-
//
|
|
8773
|
+
// theme.ts
|
|
8774
|
+
function makeSetTheme(cookie, location) {
|
|
8775
|
+
return (value, loc) => {
|
|
8776
|
+
const finalLoc = loc ?? location;
|
|
8777
|
+
const c = `${cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8778
|
+
return new Response(null, { status: 302, headers: { Location: finalLoc, "Set-Cookie": c } });
|
|
8779
|
+
};
|
|
8780
|
+
}
|
|
8781
|
+
function theme(options) {
|
|
8782
|
+
const opts = { default: "system", cookie: "theme", ...options };
|
|
8783
|
+
return async (req, ctx, next) => {
|
|
8784
|
+
const url = new URL(req.url);
|
|
8785
|
+
const match = url.pathname.match(/^\/__theme\/([\w-]+)$/);
|
|
8786
|
+
if (match && req.method === "GET") {
|
|
8787
|
+
const value = match[1];
|
|
8788
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8789
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8790
|
+
if (accept.includes("application/json")) {
|
|
8791
|
+
return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
|
|
8792
|
+
}
|
|
8793
|
+
const referer = req.headers.get("referer") || "/";
|
|
8794
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8795
|
+
}
|
|
8796
|
+
let themeValue = opts.default;
|
|
8797
|
+
if (opts.cookie) {
|
|
8798
|
+
const fromCookie = getCookies(req)[opts.cookie];
|
|
8799
|
+
if (fromCookie) themeValue = fromCookie;
|
|
8800
|
+
}
|
|
8801
|
+
ctx.theme = {
|
|
8802
|
+
value: themeValue,
|
|
8803
|
+
set: makeSetTheme(opts.cookie, req.headers.get("referer") || "/")
|
|
8804
|
+
};
|
|
8805
|
+
return next(req, ctx);
|
|
8806
|
+
};
|
|
8807
|
+
}
|
|
8808
|
+
|
|
8809
|
+
// i18n.ts
|
|
8399
8810
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
8400
8811
|
import { join as join7, resolve as resolve13 } from "node:path";
|
|
8401
|
-
var
|
|
8402
|
-
|
|
8403
|
-
|
|
8812
|
+
var DEFAULTS2 = {
|
|
8813
|
+
default: "en",
|
|
8814
|
+
cookie: "locale",
|
|
8815
|
+
fromAcceptLanguage: true
|
|
8404
8816
|
};
|
|
8405
8817
|
function translate(msgs, key, params, fallback) {
|
|
8406
8818
|
const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
|
|
@@ -8412,130 +8824,125 @@ function translate(msgs, key, params, fallback) {
|
|
|
8412
8824
|
}
|
|
8413
8825
|
return result;
|
|
8414
8826
|
}
|
|
8415
|
-
function
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
async function handlePrefSwitch(req, value, cookieName, load) {
|
|
8419
|
-
const isJson = req.headers.get("accept")?.includes("application/json");
|
|
8420
|
-
if (isJson) {
|
|
8421
|
-
const result = { ok: true };
|
|
8422
|
-
if (cookieName === "locale" || cookieName === "lang") {
|
|
8423
|
-
result.locale = value;
|
|
8424
|
-
const messages2 = await load(value);
|
|
8425
|
-
if (Object.keys(messages2).length > 0) result.messages = messages2;
|
|
8426
|
-
} else {
|
|
8427
|
-
result.theme = value;
|
|
8428
|
-
}
|
|
8429
|
-
return Response.json(result, {
|
|
8430
|
-
headers: { "Set-Cookie": prefCookie(cookieName, value) }
|
|
8431
|
-
});
|
|
8432
|
-
}
|
|
8433
|
-
const referer = req.headers.get("referer") || "/";
|
|
8434
|
-
return new Response(null, {
|
|
8435
|
-
status: 302,
|
|
8436
|
-
headers: { Location: referer, "Set-Cookie": prefCookie(cookieName, value) }
|
|
8437
|
-
});
|
|
8438
|
-
}
|
|
8439
|
-
function preferences(options) {
|
|
8440
|
-
const dir = options.dir ? resolve13(options.dir) : void 0;
|
|
8441
|
-
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
8442
|
-
const themeOpts = { ...defaults.theme, ...options.theme };
|
|
8827
|
+
function i18n(options) {
|
|
8828
|
+
const opts = { ...DEFAULTS2, ...options };
|
|
8829
|
+
const dir = opts.dir ? resolve13(opts.dir) : void 0;
|
|
8443
8830
|
const cache3 = /* @__PURE__ */ new Map();
|
|
8444
8831
|
function validLocale(locale) {
|
|
8445
8832
|
return /^[\w-]+$/.test(locale) && !locale.includes("..");
|
|
8446
8833
|
}
|
|
8447
|
-
async function
|
|
8448
|
-
if (
|
|
8449
|
-
|
|
8834
|
+
async function loadMessages(locale) {
|
|
8835
|
+
if (opts.messages?.[locale] && Object.keys(opts.messages[locale]).length > 0) {
|
|
8836
|
+
cache3.set(locale, opts.messages[locale]);
|
|
8837
|
+
return opts.messages[locale];
|
|
8838
|
+
}
|
|
8839
|
+
if (!dir || !validLocale(locale)) return {};
|
|
8450
8840
|
const cached = cache3.get(locale);
|
|
8451
8841
|
if (cached) return cached;
|
|
8452
8842
|
const filePath = join7(dir, `${locale}.json`);
|
|
8453
|
-
let data = null;
|
|
8454
8843
|
try {
|
|
8455
8844
|
await stat2(filePath);
|
|
8456
8845
|
const content = await readFile2(filePath, "utf-8");
|
|
8457
|
-
data = JSON.parse(content);
|
|
8846
|
+
const data = JSON.parse(content);
|
|
8458
8847
|
cache3.set(locale, data);
|
|
8459
8848
|
return data;
|
|
8460
8849
|
} catch {
|
|
8461
8850
|
}
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
return fallback;
|
|
8469
|
-
}
|
|
8851
|
+
const short = locale.split("-")[0];
|
|
8852
|
+
if (short !== locale) {
|
|
8853
|
+
const fallback = cache3.get(short) || await loadMessages(short);
|
|
8854
|
+
if (fallback && Object.keys(fallback).length > 0) {
|
|
8855
|
+
cache3.set(locale, fallback);
|
|
8856
|
+
return fallback;
|
|
8470
8857
|
}
|
|
8471
8858
|
}
|
|
8472
8859
|
return {};
|
|
8473
8860
|
}
|
|
8861
|
+
function detectLocale(req) {
|
|
8862
|
+
if (opts.cookie) {
|
|
8863
|
+
const fromCookie = getCookies(req)[opts.cookie];
|
|
8864
|
+
if (fromCookie && validLocale(fromCookie)) return fromCookie;
|
|
8865
|
+
}
|
|
8866
|
+
if (opts.fromAcceptLanguage) {
|
|
8867
|
+
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
|
|
8868
|
+
if (fromHeader && validLocale(fromHeader)) return fromHeader;
|
|
8869
|
+
}
|
|
8870
|
+
return opts.default;
|
|
8871
|
+
}
|
|
8474
8872
|
return async (req, ctx, next) => {
|
|
8475
8873
|
const url = new URL(req.url);
|
|
8476
|
-
const
|
|
8477
|
-
if (
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
const msgs = await load(locale);
|
|
8489
|
-
ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
|
|
8490
|
-
globalThis.__LOCALE_DATA__ = msgs;
|
|
8491
|
-
ctx.parsed = { ...ctx.parsed, __localeData: msgs };
|
|
8492
|
-
}
|
|
8493
|
-
ctx.setPref = (name, value) => {
|
|
8494
|
-
const cookieOpts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Lax"];
|
|
8874
|
+
const match = url.pathname.match(/^\/__lang\/([\w-]+)$/);
|
|
8875
|
+
if (match && req.method === "GET") {
|
|
8876
|
+
const value = match[1];
|
|
8877
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8878
|
+
const messages2 = await loadMessages(value);
|
|
8879
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8880
|
+
if (accept.includes("application/json")) {
|
|
8881
|
+
return Response.json(
|
|
8882
|
+
{ ok: true, locale: value, messages: Object.keys(messages2).length > 0 ? messages2 : void 0 },
|
|
8883
|
+
{ headers: { "Set-Cookie": cookie } }
|
|
8884
|
+
);
|
|
8885
|
+
}
|
|
8495
8886
|
const referer = req.headers.get("referer") || "/";
|
|
8496
|
-
return new Response(null, {
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8887
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8888
|
+
}
|
|
8889
|
+
const locale = detectLocale(req);
|
|
8890
|
+
const msgs = await loadMessages(locale);
|
|
8891
|
+
ctx.i18n = {
|
|
8892
|
+
locale,
|
|
8893
|
+
messages: msgs,
|
|
8894
|
+
t: (key, params, fallback) => translate(msgs, key, params, fallback),
|
|
8895
|
+
set: (value, loc) => {
|
|
8896
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8897
|
+
const location = loc ?? (req.headers.get("referer") || "/");
|
|
8898
|
+
return new Response(null, { status: 302, headers: { Location: location, "Set-Cookie": cookie } });
|
|
8899
|
+
}
|
|
8503
8900
|
};
|
|
8504
|
-
|
|
8505
|
-
|
|
8901
|
+
return next(req, ctx);
|
|
8902
|
+
};
|
|
8903
|
+
}
|
|
8904
|
+
|
|
8905
|
+
// flash.ts
|
|
8906
|
+
function makeSetFlash(name, location) {
|
|
8907
|
+
return (data, loc) => {
|
|
8908
|
+
const finalLoc = loc ?? location;
|
|
8909
|
+
const value = encodeURIComponent(JSON.stringify(data));
|
|
8910
|
+
return new Response(null, {
|
|
8911
|
+
status: 302,
|
|
8912
|
+
headers: {
|
|
8913
|
+
Location: finalLoc,
|
|
8914
|
+
"Set-Cookie": `${name}=${value}; Path=/; SameSite=Lax`
|
|
8915
|
+
}
|
|
8916
|
+
});
|
|
8917
|
+
};
|
|
8918
|
+
}
|
|
8919
|
+
function flash(options) {
|
|
8920
|
+
const name = options?.name ?? "flash";
|
|
8921
|
+
return async (req, ctx, next) => {
|
|
8922
|
+
const raw = getCookies(req)[name] ?? null;
|
|
8923
|
+
const referer = req.headers.get("referer") || "/";
|
|
8924
|
+
let value = void 0;
|
|
8925
|
+
if (raw) {
|
|
8506
8926
|
try {
|
|
8507
|
-
|
|
8927
|
+
value = JSON.parse(decodeURIComponent(raw));
|
|
8508
8928
|
} catch {
|
|
8509
|
-
|
|
8929
|
+
value = raw;
|
|
8510
8930
|
}
|
|
8511
8931
|
}
|
|
8932
|
+
;
|
|
8933
|
+
ctx.flash = {
|
|
8934
|
+
value,
|
|
8935
|
+
set: makeSetFlash(name, referer)
|
|
8936
|
+
};
|
|
8512
8937
|
const res = await next(req, ctx);
|
|
8513
|
-
if (
|
|
8938
|
+
if (raw) {
|
|
8514
8939
|
const headers = new Headers(res.headers);
|
|
8515
|
-
headers.append("Set-Cookie",
|
|
8940
|
+
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
|
|
8516
8941
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
8517
8942
|
}
|
|
8518
8943
|
return res;
|
|
8519
8944
|
};
|
|
8520
8945
|
}
|
|
8521
|
-
function detectLocale(req, opts) {
|
|
8522
|
-
if (opts.cookie) {
|
|
8523
|
-
const fromCookie = getCookies(req)[opts.cookie];
|
|
8524
|
-
if (fromCookie) return fromCookie;
|
|
8525
|
-
}
|
|
8526
|
-
if (opts.fromAcceptLanguage) {
|
|
8527
|
-
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
|
|
8528
|
-
if (fromHeader) return fromHeader;
|
|
8529
|
-
}
|
|
8530
|
-
return opts.default;
|
|
8531
|
-
}
|
|
8532
|
-
function detectTheme(req, opts) {
|
|
8533
|
-
if (opts.cookie) {
|
|
8534
|
-
const fromCookie = getCookies(req)[opts.cookie];
|
|
8535
|
-
if (fromCookie) return fromCookie;
|
|
8536
|
-
}
|
|
8537
|
-
return opts.default;
|
|
8538
|
-
}
|
|
8539
8946
|
|
|
8540
8947
|
// seo.ts
|
|
8541
8948
|
function escapeXml(s) {
|
|
@@ -10068,7 +10475,6 @@ function session(options) {
|
|
|
10068
10475
|
}
|
|
10069
10476
|
const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
|
|
10070
10477
|
ctx.session = session2;
|
|
10071
|
-
ctx.sessionId = session2.id;
|
|
10072
10478
|
const res = await next(req, ctx);
|
|
10073
10479
|
const currentSession = ctx.session;
|
|
10074
10480
|
if (!currentSession || currentSession[kDestroyed]) {
|
|
@@ -10577,7 +10983,7 @@ function resolveTableName(table) {
|
|
|
10577
10983
|
}
|
|
10578
10984
|
return name;
|
|
10579
10985
|
}
|
|
10580
|
-
function
|
|
10986
|
+
function escapeIdent4(s) {
|
|
10581
10987
|
return `"${s.replace(/"/g, '""')}"`;
|
|
10582
10988
|
}
|
|
10583
10989
|
function sqlLit(s) {
|
|
@@ -10587,11 +10993,11 @@ async function createIndex(sql2, table, fields, options) {
|
|
|
10587
10993
|
const language = options?.language ?? "english";
|
|
10588
10994
|
const tableName = resolveTableName(table);
|
|
10589
10995
|
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10590
|
-
const vectorExpr = fields.map((f) => `coalesce(${
|
|
10996
|
+
const vectorExpr = fields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
|
|
10591
10997
|
const indexType = options?.indexType ?? "gin";
|
|
10592
10998
|
await sql2.unsafe(`
|
|
10593
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
10594
|
-
ON ${
|
|
10999
|
+
CREATE INDEX IF NOT EXISTS ${escapeIdent4(indexName)}
|
|
11000
|
+
ON ${escapeIdent4(tableName)}
|
|
10595
11001
|
USING ${indexType}
|
|
10596
11002
|
(to_tsvector(${sqlLit(language)}, ${vectorExpr}))
|
|
10597
11003
|
`);
|
|
@@ -10599,7 +11005,7 @@ async function createIndex(sql2, table, fields, options) {
|
|
|
10599
11005
|
async function dropIndex(sql2, table, options) {
|
|
10600
11006
|
const tableName = resolveTableName(table);
|
|
10601
11007
|
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10602
|
-
await sql2.unsafe(`DROP INDEX IF EXISTS ${
|
|
11008
|
+
await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent4(indexName)}`);
|
|
10603
11009
|
}
|
|
10604
11010
|
async function search(sql2, table, query, options) {
|
|
10605
11011
|
const tableName = resolveTableName(table);
|
|
@@ -10613,13 +11019,13 @@ async function search(sql2, table, query, options) {
|
|
|
10613
11019
|
}
|
|
10614
11020
|
const sanitized = query.trim();
|
|
10615
11021
|
if (!sanitized) return [];
|
|
10616
|
-
const vectorExpr = searchFields.map((f) => `coalesce(${
|
|
11022
|
+
const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
|
|
10617
11023
|
const langLit = sqlLit(language);
|
|
10618
11024
|
const queryLit = sqlLit(sanitized);
|
|
10619
|
-
const rankColId =
|
|
10620
|
-
const tableId =
|
|
11025
|
+
const rankColId = escapeIdent4(rankCol);
|
|
11026
|
+
const tableId = escapeIdent4(tableName);
|
|
10621
11027
|
const headlineExpr = options?.headline ? searchFields.map(
|
|
10622
|
-
(f) => `ts_headline(${langLit}, ${
|
|
11028
|
+
(f) => `ts_headline(${langLit}, ${escapeIdent4(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent4(f + "_headline")}`
|
|
10623
11029
|
).join(",\n ") : "";
|
|
10624
11030
|
const sql_query = `
|
|
10625
11031
|
SELECT
|
|
@@ -10659,8 +11065,8 @@ async function suggest(sql2, table, prefix, options) {
|
|
|
10659
11065
|
const rows = await sql2.unsafe(`
|
|
10660
11066
|
SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
|
|
10661
11067
|
FROM (
|
|
10662
|
-
SELECT regexp_split_to_table(lower(${
|
|
10663
|
-
FROM ${
|
|
11068
|
+
SELECT regexp_split_to_table(lower(${escapeIdent4(field)}), E'\\W+') as word
|
|
11069
|
+
FROM ${escapeIdent4(tableName)}
|
|
10664
11070
|
) words
|
|
10665
11071
|
WHERE word LIKE ${sqlLit(sanitized + "%")}
|
|
10666
11072
|
LIMIT ${limit}
|
|
@@ -10782,75 +11188,45 @@ function s3(options) {
|
|
|
10782
11188
|
return mw;
|
|
10783
11189
|
}
|
|
10784
11190
|
|
|
10785
|
-
// kb.ts
|
|
10786
|
-
function
|
|
10787
|
-
const paragraphs = content.split(/\n\n+/);
|
|
10788
|
-
const chunks = [];
|
|
10789
|
-
let current = "";
|
|
10790
|
-
for (const p of paragraphs) {
|
|
10791
|
-
if (current.length + p.length > chunkSize && current.length > 0) {
|
|
10792
|
-
chunks.push(current);
|
|
10793
|
-
current = current.slice(-overlap);
|
|
10794
|
-
}
|
|
10795
|
-
current += (current ? "\n\n" : "") + p;
|
|
10796
|
-
}
|
|
10797
|
-
if (current) chunks.push(current);
|
|
10798
|
-
return chunks;
|
|
10799
|
-
}
|
|
10800
|
-
function escapeIdent2(s) {
|
|
11191
|
+
// kb/index.ts
|
|
11192
|
+
function escapeIdent5(s) {
|
|
10801
11193
|
return `"${s.replace(/"/g, '""')}"`;
|
|
10802
11194
|
}
|
|
10803
11195
|
function knowledgeBase(options) {
|
|
10804
|
-
const {
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
11196
|
+
const { pg, provider, table = "_kb_docs", chunkSize = 512, chunkOverlap = 64, searchLimit = 5, searchThreshold = 0 } = options;
|
|
11197
|
+
const sql2 = pg.sql;
|
|
11198
|
+
const dimension = provider.dimension;
|
|
11199
|
+
const docsTable = pg.table(table, {
|
|
11200
|
+
id: serial("id").primaryKey(),
|
|
11201
|
+
doc_key: text("doc_key").notNull(),
|
|
11202
|
+
title: text("title").notNull().default(""),
|
|
11203
|
+
content: text("content").notNull(),
|
|
11204
|
+
chunk_index: integer("chunk_index").notNull().default(0),
|
|
11205
|
+
metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
|
|
11206
|
+
embedding: vector("embedding", dimension),
|
|
11207
|
+
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
11208
|
+
});
|
|
10814
11209
|
async function migrate() {
|
|
10815
11210
|
await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "vector"`);
|
|
10816
|
-
await
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
doc_key TEXT NOT NULL,
|
|
10820
|
-
title TEXT NOT NULL DEFAULT '',
|
|
10821
|
-
content TEXT NOT NULL,
|
|
10822
|
-
chunk_index INTEGER NOT NULL DEFAULT 0,
|
|
10823
|
-
metadata JSONB NOT NULL DEFAULT '{}',
|
|
10824
|
-
embedding vector(${dimensions}),
|
|
10825
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
10826
|
-
)
|
|
10827
|
-
`);
|
|
10828
|
-
await sql2.unsafe(`
|
|
10829
|
-
CREATE INDEX IF NOT EXISTS ${escapeIdent2(table + "_key_idx")}
|
|
10830
|
-
ON ${escapeIdent2(table)}(doc_key)
|
|
10831
|
-
`);
|
|
10832
|
-
await sql2.unsafe(`
|
|
10833
|
-
CREATE INDEX IF NOT EXISTS ${escapeIdent2(table + "_embedding_idx")}
|
|
10834
|
-
ON ${escapeIdent2(table)}
|
|
10835
|
-
USING hnsw (embedding vector_cosine_ops)
|
|
10836
|
-
`);
|
|
11211
|
+
await docsTable.create();
|
|
11212
|
+
await docsTable.createIndex("doc_key");
|
|
11213
|
+
await docsTable.createIndex("embedding", { type: "hnsw", operator: "vector_cosine_ops" });
|
|
10837
11214
|
}
|
|
10838
11215
|
async function ingest(key, content, ingestOpts) {
|
|
10839
|
-
await sql2.unsafe(`DELETE FROM ${escapeIdent2(table)} WHERE doc_key = $1`, [key]);
|
|
10840
11216
|
const title = ingestOpts?.title ?? key;
|
|
10841
11217
|
const meta = ingestOpts?.metadata ?? {};
|
|
10842
11218
|
const cs = ingestOpts?.chunkSize ?? chunkSize;
|
|
10843
11219
|
const co = ingestOpts?.chunkOverlap ?? chunkOverlap;
|
|
10844
|
-
const chunks =
|
|
11220
|
+
const chunks = chunkContent(content, cs, co);
|
|
10845
11221
|
const metaJson = JSON.stringify(meta);
|
|
11222
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
|
|
11223
|
+
const embeddings = await Promise.all(chunks.map((c) => provider.embed(c)));
|
|
10846
11224
|
for (let i = 0; i < chunks.length; i++) {
|
|
10847
|
-
const
|
|
10848
|
-
const embedding = await embedFn(chunk);
|
|
10849
|
-
const vec = `[${embedding.join(",")}]`;
|
|
11225
|
+
const vec = `[${embeddings[i].join(",")}]`;
|
|
10850
11226
|
await sql2.unsafe(
|
|
10851
|
-
`INSERT INTO ${
|
|
11227
|
+
`INSERT INTO ${escapeIdent5(table)} (doc_key, title, content, chunk_index, metadata, embedding)
|
|
10852
11228
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector)`,
|
|
10853
|
-
[key, title,
|
|
11229
|
+
[key, title, chunks[i], i, metaJson, vec]
|
|
10854
11230
|
);
|
|
10855
11231
|
}
|
|
10856
11232
|
return chunks.length;
|
|
@@ -10858,13 +11234,13 @@ function knowledgeBase(options) {
|
|
|
10858
11234
|
async function search2(query, searchOpts) {
|
|
10859
11235
|
const limit = searchOpts?.limit ?? searchLimit;
|
|
10860
11236
|
const threshold = searchOpts?.threshold ?? searchThreshold;
|
|
10861
|
-
const embedding = await
|
|
11237
|
+
const embedding = await provider.embed(query);
|
|
10862
11238
|
const vec = `[${embedding.join(",")}]`;
|
|
10863
11239
|
const whereClause = threshold > 0 ? `WHERE (1 - (embedding <=> $1::vector) / 2) >= ${threshold}` : "";
|
|
10864
11240
|
const rows = await sql2.unsafe(
|
|
10865
11241
|
`SELECT id, doc_key, title, content, chunk_index, metadata,
|
|
10866
11242
|
1 - (embedding <=> $1::vector) / 2 AS _score
|
|
10867
|
-
FROM ${
|
|
11243
|
+
FROM ${escapeIdent5(table)}
|
|
10868
11244
|
${whereClause}
|
|
10869
11245
|
ORDER BY embedding <=> $1::vector
|
|
10870
11246
|
LIMIT ${limit}`,
|
|
@@ -10880,12 +11256,12 @@ function knowledgeBase(options) {
|
|
|
10880
11256
|
}));
|
|
10881
11257
|
}
|
|
10882
11258
|
async function del(key) {
|
|
10883
|
-
await sql2.unsafe(`DELETE FROM ${
|
|
11259
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
|
|
10884
11260
|
}
|
|
10885
11261
|
async function list() {
|
|
10886
11262
|
const rows = await sql2.unsafe(`
|
|
10887
11263
|
SELECT doc_key, title, COUNT(*) AS chunks
|
|
10888
|
-
FROM ${
|
|
11264
|
+
FROM ${escapeIdent5(table)}
|
|
10889
11265
|
GROUP BY doc_key, title
|
|
10890
11266
|
ORDER BY doc_key
|
|
10891
11267
|
`);
|
|
@@ -10911,6 +11287,158 @@ function knowledgeBase(options) {
|
|
|
10911
11287
|
middleware: mw
|
|
10912
11288
|
};
|
|
10913
11289
|
}
|
|
11290
|
+
|
|
11291
|
+
// permissions.ts
|
|
11292
|
+
function escapeIdent6(s) {
|
|
11293
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
11294
|
+
}
|
|
11295
|
+
function permissions(options) {
|
|
11296
|
+
const { pg } = options;
|
|
11297
|
+
const sql2 = pg.sql;
|
|
11298
|
+
const prefix = options.prefix ?? "";
|
|
11299
|
+
const rolesTable = `${prefix}_roles`;
|
|
11300
|
+
const rolePermsTable = `${prefix}_role_permissions`;
|
|
11301
|
+
const userRolesTable = `${prefix}_user_roles`;
|
|
11302
|
+
async function migrate() {
|
|
11303
|
+
await sql2.unsafe(`
|
|
11304
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolesTable)} (
|
|
11305
|
+
id SERIAL PRIMARY KEY,
|
|
11306
|
+
name TEXT UNIQUE NOT NULL,
|
|
11307
|
+
description TEXT NOT NULL DEFAULT '',
|
|
11308
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
11309
|
+
)
|
|
11310
|
+
`);
|
|
11311
|
+
await sql2.unsafe(`
|
|
11312
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolePermsTable)} (
|
|
11313
|
+
id SERIAL PRIMARY KEY,
|
|
11314
|
+
role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
|
|
11315
|
+
permission TEXT NOT NULL,
|
|
11316
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11317
|
+
UNIQUE(role_id, permission)
|
|
11318
|
+
)
|
|
11319
|
+
`);
|
|
11320
|
+
await sql2.unsafe(`
|
|
11321
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(userRolesTable)} (
|
|
11322
|
+
id SERIAL PRIMARY KEY,
|
|
11323
|
+
user_id INTEGER NOT NULL,
|
|
11324
|
+
role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
|
|
11325
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11326
|
+
UNIQUE(user_id, role_id)
|
|
11327
|
+
)
|
|
11328
|
+
`);
|
|
11329
|
+
}
|
|
11330
|
+
async function ensureRole(role) {
|
|
11331
|
+
const [existing] = await sql2.unsafe(
|
|
11332
|
+
`SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1 LIMIT 1`,
|
|
11333
|
+
[role]
|
|
11334
|
+
);
|
|
11335
|
+
if (existing) return existing.id;
|
|
11336
|
+
const [created] = await sql2.unsafe(
|
|
11337
|
+
`INSERT INTO ${escapeIdent6(rolesTable)} (name) VALUES ($1) RETURNING id`,
|
|
11338
|
+
[role]
|
|
11339
|
+
);
|
|
11340
|
+
return created.id;
|
|
11341
|
+
}
|
|
11342
|
+
async function assignRole(userId, role) {
|
|
11343
|
+
const roleId = await ensureRole(role);
|
|
11344
|
+
await sql2.unsafe(
|
|
11345
|
+
`INSERT INTO ${escapeIdent6(userRolesTable)} (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
11346
|
+
[userId, roleId]
|
|
11347
|
+
);
|
|
11348
|
+
}
|
|
11349
|
+
async function removeRole(userId, role) {
|
|
11350
|
+
await sql2.unsafe(
|
|
11351
|
+
`DELETE FROM ${escapeIdent6(userRolesTable)} WHERE user_id = $1 AND role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $2)`,
|
|
11352
|
+
[userId, role]
|
|
11353
|
+
);
|
|
11354
|
+
}
|
|
11355
|
+
async function grantPermission(role, permission) {
|
|
11356
|
+
const roleId = await ensureRole(role);
|
|
11357
|
+
await sql2.unsafe(
|
|
11358
|
+
`INSERT INTO ${escapeIdent6(rolePermsTable)} (role_id, permission) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
11359
|
+
[roleId, permission]
|
|
11360
|
+
);
|
|
11361
|
+
}
|
|
11362
|
+
async function revokePermission(role, permission) {
|
|
11363
|
+
await sql2.unsafe(
|
|
11364
|
+
`DELETE FROM ${escapeIdent6(rolePermsTable)} WHERE role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1) AND permission = $2`,
|
|
11365
|
+
[role, permission]
|
|
11366
|
+
);
|
|
11367
|
+
}
|
|
11368
|
+
async function getUserRoles(userId) {
|
|
11369
|
+
const rows = await sql2.unsafe(
|
|
11370
|
+
`SELECT r.name FROM ${escapeIdent6(userRolesTable)} ur
|
|
11371
|
+
JOIN ${escapeIdent6(rolesTable)} r ON r.id = ur.role_id
|
|
11372
|
+
WHERE ur.user_id = $1 ORDER BY r.name`,
|
|
11373
|
+
[userId]
|
|
11374
|
+
);
|
|
11375
|
+
return rows.map((r) => r.name);
|
|
11376
|
+
}
|
|
11377
|
+
async function getUserPermissions(userId) {
|
|
11378
|
+
const rows = await sql2.unsafe(
|
|
11379
|
+
`SELECT DISTINCT rp.permission FROM ${escapeIdent6(userRolesTable)} ur
|
|
11380
|
+
JOIN ${escapeIdent6(rolePermsTable)} rp ON rp.role_id = ur.role_id
|
|
11381
|
+
WHERE ur.user_id = $1 ORDER BY rp.permission`,
|
|
11382
|
+
[userId]
|
|
11383
|
+
);
|
|
11384
|
+
return rows.map((r) => r.permission);
|
|
11385
|
+
}
|
|
11386
|
+
const mw = (async (req, ctx, next) => {
|
|
11387
|
+
const userId = ctx.user?.id;
|
|
11388
|
+
let roles = /* @__PURE__ */ new Set();
|
|
11389
|
+
let perms = /* @__PURE__ */ new Set();
|
|
11390
|
+
if (userId) {
|
|
11391
|
+
const userRoles = await getUserRoles(userId);
|
|
11392
|
+
const userPerms = userId ? await getUserPermissions(userId) : [];
|
|
11393
|
+
roles = new Set(userRoles);
|
|
11394
|
+
perms = new Set(userPerms);
|
|
11395
|
+
const hasWildcard = userPerms.includes("*");
|
|
11396
|
+
if (hasWildcard) {
|
|
11397
|
+
perms = /* @__PURE__ */ new Set(["*"]);
|
|
11398
|
+
}
|
|
11399
|
+
}
|
|
11400
|
+
ctx.permissions = { roles, permissions: perms };
|
|
11401
|
+
return next(req, ctx);
|
|
11402
|
+
});
|
|
11403
|
+
function requireRole(...roles) {
|
|
11404
|
+
return (req, ctx, next) => {
|
|
11405
|
+
if (!ctx.permissions?.roles || !roles.some((r) => ctx.permissions.roles.has(r))) {
|
|
11406
|
+
return Response.json(
|
|
11407
|
+
{ error: `Forbidden: requires one of roles [${roles.join(", ")}]` },
|
|
11408
|
+
{ status: 403 }
|
|
11409
|
+
);
|
|
11410
|
+
}
|
|
11411
|
+
return next(req, ctx);
|
|
11412
|
+
};
|
|
11413
|
+
}
|
|
11414
|
+
function requirePermission(...perms) {
|
|
11415
|
+
return (req, ctx, next) => {
|
|
11416
|
+
const userPerms = ctx.permissions?.permissions;
|
|
11417
|
+
if (!userPerms) {
|
|
11418
|
+
return Response.json({ error: "Forbidden: no permissions loaded" }, { status: 403 });
|
|
11419
|
+
}
|
|
11420
|
+
if (userPerms.has("*")) return next(req, ctx);
|
|
11421
|
+
const missing = perms.filter((p) => !userPerms.has(p));
|
|
11422
|
+
if (missing.length > 0) {
|
|
11423
|
+
return Response.json(
|
|
11424
|
+
{ error: `Forbidden: missing permissions [${missing.join(", ")}]` },
|
|
11425
|
+
{ status: 403 }
|
|
11426
|
+
);
|
|
11427
|
+
}
|
|
11428
|
+
return next(req, ctx);
|
|
11429
|
+
};
|
|
11430
|
+
}
|
|
11431
|
+
mw.assignRole = assignRole;
|
|
11432
|
+
mw.removeRole = removeRole;
|
|
11433
|
+
mw.grantPermission = grantPermission;
|
|
11434
|
+
mw.revokePermission = revokePermission;
|
|
11435
|
+
mw.getUserRoles = getUserRoles;
|
|
11436
|
+
mw.getUserPermissions = getUserPermissions;
|
|
11437
|
+
mw.requireRole = requireRole;
|
|
11438
|
+
mw.requirePermission = requirePermission;
|
|
11439
|
+
mw.migrate = migrate;
|
|
11440
|
+
return mw;
|
|
11441
|
+
}
|
|
10914
11442
|
export {
|
|
10915
11443
|
DEFAULT_MAX_BODY,
|
|
10916
11444
|
MIGRATIONS_TABLE,
|
|
@@ -10923,6 +11451,7 @@ export {
|
|
|
10923
11451
|
TestRequest,
|
|
10924
11452
|
TsxContext,
|
|
10925
11453
|
agent,
|
|
11454
|
+
aiProvider,
|
|
10926
11455
|
aiStream,
|
|
10927
11456
|
analytics,
|
|
10928
11457
|
auth,
|
|
@@ -10930,7 +11459,7 @@ export {
|
|
|
10930
11459
|
compress,
|
|
10931
11460
|
cors,
|
|
10932
11461
|
createHub,
|
|
10933
|
-
createOpenAI,
|
|
11462
|
+
createOpenAI2 as createOpenAI,
|
|
10934
11463
|
createSSEStream,
|
|
10935
11464
|
createTestDb,
|
|
10936
11465
|
createTestServer,
|
|
@@ -10943,6 +11472,7 @@ export {
|
|
|
10943
11472
|
deploy,
|
|
10944
11473
|
embed,
|
|
10945
11474
|
embedMany,
|
|
11475
|
+
flash,
|
|
10946
11476
|
formatSSE,
|
|
10947
11477
|
formatSSEData,
|
|
10948
11478
|
fts_exports as fts,
|
|
@@ -10952,6 +11482,7 @@ export {
|
|
|
10952
11482
|
graphql,
|
|
10953
11483
|
health,
|
|
10954
11484
|
helmet,
|
|
11485
|
+
i18n,
|
|
10955
11486
|
iii,
|
|
10956
11487
|
isDev,
|
|
10957
11488
|
isProd,
|
|
@@ -10961,11 +11492,10 @@ export {
|
|
|
10961
11492
|
logger,
|
|
10962
11493
|
mailer,
|
|
10963
11494
|
messager,
|
|
10964
|
-
oauthClient,
|
|
10965
11495
|
openai,
|
|
10966
11496
|
opencode,
|
|
11497
|
+
permissions,
|
|
10967
11498
|
postgres,
|
|
10968
|
-
preferences,
|
|
10969
11499
|
queue,
|
|
10970
11500
|
rateLimit,
|
|
10971
11501
|
redis,
|
|
@@ -10987,6 +11517,7 @@ export {
|
|
|
10987
11517
|
streamText,
|
|
10988
11518
|
tenant,
|
|
10989
11519
|
testApp,
|
|
11520
|
+
theme,
|
|
10990
11521
|
tool2 as tool,
|
|
10991
11522
|
traceElapsed,
|
|
10992
11523
|
upload,
|