weifuwu 0.22.3 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -87
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +112 -0
- package/cli/template/app.ts +3 -2
- package/cli/template/index.ts +2 -1
- package/dist/agent/run.d.ts +4 -3
- package/dist/agent/types.d.ts +3 -0
- package/dist/ai/provider.d.ts +36 -0
- package/dist/ai/utils.d.ts +5 -0
- package/dist/ai/workflow.d.ts +3 -0
- package/dist/ai.d.ts +9 -1
- package/dist/client-locale.d.ts +1 -1
- package/dist/client-router.d.ts +3 -3
- package/dist/client-theme.d.ts +1 -1
- package/dist/compile.d.ts +6 -0
- package/dist/cron-utils.d.ts +8 -0
- package/dist/flash.d.ts +24 -0
- package/dist/i18n.d.ts +14 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +1336 -821
- 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 +7 -1
- package/dist/types.d.ts +5 -3
- package/dist/user/index.d.ts +1 -1
- package/dist/user/oauth-login.d.ts +21 -0
- package/dist/user/types.d.ts +31 -0
- package/package.json +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: {} };
|
|
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) {
|
|
4132
|
+
inflight++;
|
|
4133
|
+
try {
|
|
4134
|
+
await handler(job);
|
|
4135
|
+
_processed++;
|
|
4136
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent3(table)} WHERE id = $1`, [job.id]);
|
|
4137
|
+
} catch (e) {
|
|
4138
|
+
_failed++;
|
|
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) {
|
|
3913
4253
|
inflight++;
|
|
3914
4254
|
try {
|
|
3915
|
-
await
|
|
4255
|
+
await handler(job);
|
|
3916
4256
|
_processed++;
|
|
3917
4257
|
} catch (e) {
|
|
3918
4258
|
_failed++;
|
|
3919
|
-
const
|
|
3920
|
-
console.error("[queue] handler error:",
|
|
3921
|
-
|
|
3922
|
-
...job,
|
|
3923
|
-
error: errMsg,
|
|
3924
|
-
failedAt: Date.now()
|
|
3925
|
-
});
|
|
3926
|
-
await redis2.lpush(failedKey, failedEntry);
|
|
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
|
+
}
|
|
6582
6917
|
}
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
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);
|
|
6922
|
+
}
|
|
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>`;
|
|
6594
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
|
+
}
|
|
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,40 @@ 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
|
-
|
|
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;
|
|
7060
|
-
}
|
|
7385
|
+
function buildHydrationScript(entryId, ctxJson, base) {
|
|
7386
|
+
const ssrPrefix = `${base}/__ssr`;
|
|
7387
|
+
return `
|
|
7388
|
+
<script type="module">
|
|
7389
|
+
import { setCtx, TsxContext } from 'weifuwu/react';
|
|
7390
|
+
import { createElement } from 'react';
|
|
7391
|
+
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
7392
|
+
|
|
7393
|
+
const _ctx = ${ctxJson};
|
|
7394
|
+
setCtx(_ctx);
|
|
7395
|
+
|
|
7396
|
+
const _root = document.getElementById('__weifuwu_root');
|
|
7397
|
+
|
|
7398
|
+
async function init() {
|
|
7399
|
+
const { default: Page } = await import('${ssrPrefix}/${entryId}.js');
|
|
7400
|
+
const app = createElement(TsxContext.Provider, { value: _ctx },
|
|
7401
|
+
createElement(Page));
|
|
7402
|
+
${isDev2 ? `
|
|
7403
|
+
const reactRoot = createRoot(_root);
|
|
7404
|
+
reactRoot.render(app);
|
|
7405
|
+
window.__WFW_REFRESH = async (NewComponent) => {
|
|
7406
|
+
const store = globalThis.__WEIFUWU_CTX_STORE?._ctx || _ctx;
|
|
7407
|
+
reactRoot.render(createElement(TsxContext.Provider, { value: store },
|
|
7408
|
+
createElement(NewComponent)));
|
|
7409
|
+
};
|
|
7410
|
+
` : `
|
|
7411
|
+
hydrateRoot(_root, app);
|
|
7412
|
+
`}
|
|
7413
|
+
}
|
|
7414
|
+
|
|
7415
|
+
init();
|
|
7416
|
+
</script>`;
|
|
7061
7417
|
}
|
|
7062
|
-
function renderPage(pageFile) {
|
|
7418
|
+
function renderPage(pageFile, outDir) {
|
|
7063
7419
|
const absPath = resolve6(pageFile);
|
|
7064
7420
|
const entryId = hashId(absPath);
|
|
7065
7421
|
ssrEntries.set(entryId, { path: absPath });
|
|
@@ -7085,15 +7441,15 @@ function renderPage(pageFile) {
|
|
|
7085
7441
|
query: ctx.query,
|
|
7086
7442
|
user: ctx.user ?? {},
|
|
7087
7443
|
parsed: ctx.parsed ?? {},
|
|
7088
|
-
|
|
7444
|
+
theme: ctx.theme,
|
|
7445
|
+
i18n: ctx.i18n,
|
|
7446
|
+
flash: ctx.flash,
|
|
7089
7447
|
loaderData,
|
|
7090
7448
|
env: ctx.env ?? {}
|
|
7091
7449
|
};
|
|
7092
7450
|
return als2.run(ctxValue, async () => {
|
|
7093
7451
|
setCtx(ctxValue);
|
|
7094
|
-
|
|
7095
|
-
globalThis.__LOCALE_DATA__ = ctxValue.parsed.__localeData;
|
|
7096
|
-
}
|
|
7452
|
+
await compileBrowser(absPath, outDir);
|
|
7097
7453
|
let element = createElement3(
|
|
7098
7454
|
"div",
|
|
7099
7455
|
{ id: "__weifuwu_root" },
|
|
@@ -7104,24 +7460,15 @@ function renderPage(pageFile) {
|
|
|
7104
7460
|
)
|
|
7105
7461
|
);
|
|
7106
7462
|
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
7463
|
const { renderToReadableStream } = await import("react-dom/server");
|
|
7116
7464
|
const stream = await renderToReadableStream(element);
|
|
7117
7465
|
return streamResponse(stream, {
|
|
7118
7466
|
ctx,
|
|
7119
7467
|
base,
|
|
7120
7468
|
isDev: isDev2,
|
|
7121
|
-
bundle,
|
|
7122
7469
|
loaderData,
|
|
7123
|
-
|
|
7124
|
-
});
|
|
7470
|
+
tailwind: ctx.tailwind
|
|
7471
|
+
}, buildHydrationScript(entryId, JSON.stringify(ctxValue), base));
|
|
7125
7472
|
});
|
|
7126
7473
|
};
|
|
7127
7474
|
}
|
|
@@ -7167,11 +7514,23 @@ function discoverRoutes(dir) {
|
|
|
7167
7514
|
function ssr(opts) {
|
|
7168
7515
|
const r = new Router();
|
|
7169
7516
|
const dir = resolve6(opts.dir);
|
|
7517
|
+
const outDir = resolve6(OUT_DIR);
|
|
7170
7518
|
const routeCache = /* @__PURE__ */ new Map();
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7519
|
+
compileVendorBundle().catch(() => {
|
|
7520
|
+
});
|
|
7521
|
+
r.get("/__ssr/:file", (req, ctx) => {
|
|
7522
|
+
const filePath = join5(outDir, ctx.params.file);
|
|
7523
|
+
if (!filePath.startsWith(outDir) || !existsSync4(filePath)) {
|
|
7524
|
+
return new Response("Not Found", { status: 404 });
|
|
7525
|
+
}
|
|
7526
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
7527
|
+
return new Response(content, {
|
|
7528
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7529
|
+
});
|
|
7530
|
+
});
|
|
7531
|
+
r.get("/__wfw/v/bundle", async () => {
|
|
7532
|
+
const code = await compileVendorBundle();
|
|
7533
|
+
return new Response(code, {
|
|
7175
7534
|
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7176
7535
|
});
|
|
7177
7536
|
});
|
|
@@ -7198,7 +7557,7 @@ function ssr(opts) {
|
|
|
7198
7557
|
...resolved.layoutFiles.map((f) => layout(f)),
|
|
7199
7558
|
tailwindContext(dir)
|
|
7200
7559
|
];
|
|
7201
|
-
const handler = (req2, ctx2) => renderPage(resolved.pageFile)(req2, ctx2);
|
|
7560
|
+
const handler = (req2, ctx2) => renderPage(resolved.pageFile, outDir)(req2, ctx2);
|
|
7202
7561
|
return runChain(mws, handler, req, ctx);
|
|
7203
7562
|
});
|
|
7204
7563
|
const mod = r;
|
|
@@ -7288,13 +7647,13 @@ async function addToolMessages(sql2, sessionId, toolCalls, toolResults) {
|
|
|
7288
7647
|
}
|
|
7289
7648
|
|
|
7290
7649
|
// opencode/run.ts
|
|
7291
|
-
import { streamText as
|
|
7650
|
+
import { streamText as streamText2, stepCountIs } from "ai";
|
|
7292
7651
|
async function* executeGenerator(opts) {
|
|
7293
7652
|
const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql: sql2, abortSignal } = opts;
|
|
7294
7653
|
const lastStepToolCalls = [];
|
|
7295
7654
|
let currentAssistantText = "";
|
|
7296
7655
|
let currentUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
7297
|
-
const result =
|
|
7656
|
+
const result = streamText2({
|
|
7298
7657
|
model,
|
|
7299
7658
|
system: systemPrompt,
|
|
7300
7659
|
messages: [
|
|
@@ -7461,7 +7820,7 @@ function createBashTool(ctx) {
|
|
|
7461
7820
|
// opencode/tools/read.ts
|
|
7462
7821
|
import { tool as tool4 } from "ai";
|
|
7463
7822
|
import { z as z6 } from "zod";
|
|
7464
|
-
import { readFileSync as
|
|
7823
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
7465
7824
|
import { resolve as resolve7 } from "node:path";
|
|
7466
7825
|
function createReadTool(ctx) {
|
|
7467
7826
|
return tool4({
|
|
@@ -7473,10 +7832,10 @@ function createReadTool(ctx) {
|
|
|
7473
7832
|
}),
|
|
7474
7833
|
execute: async ({ path: path2, offset, limit }) => {
|
|
7475
7834
|
const resolved = resolve7(ctx.workspace, path2);
|
|
7476
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7835
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7477
7836
|
return { error: "Path not allowed", content: null, totalLines: 0 };
|
|
7478
7837
|
}
|
|
7479
|
-
const content =
|
|
7838
|
+
const content = readFileSync5(resolved, "utf-8");
|
|
7480
7839
|
const lines = content.split("\n");
|
|
7481
7840
|
const totalLines = lines.length;
|
|
7482
7841
|
if (offset !== void 0) {
|
|
@@ -7514,7 +7873,7 @@ function createWriteTool(ctx) {
|
|
|
7514
7873
|
}),
|
|
7515
7874
|
execute: async ({ path: path2, content }) => {
|
|
7516
7875
|
const resolved = resolve8(ctx.workspace, path2);
|
|
7517
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7876
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7518
7877
|
return { error: "Path not allowed" };
|
|
7519
7878
|
}
|
|
7520
7879
|
mkdirSync3(dirname4(resolved), { recursive: true });
|
|
@@ -7527,7 +7886,7 @@ function createWriteTool(ctx) {
|
|
|
7527
7886
|
// opencode/tools/edit.ts
|
|
7528
7887
|
import { tool as tool6 } from "ai";
|
|
7529
7888
|
import { z as z8 } from "zod";
|
|
7530
|
-
import { readFileSync as
|
|
7889
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7531
7890
|
import { resolve as resolve9 } from "node:path";
|
|
7532
7891
|
function createEditTool(ctx) {
|
|
7533
7892
|
return tool6({
|
|
@@ -7540,10 +7899,10 @@ function createEditTool(ctx) {
|
|
|
7540
7899
|
}),
|
|
7541
7900
|
execute: async ({ path: path2, oldString, newString, replaceAll }) => {
|
|
7542
7901
|
const resolved = resolve9(ctx.workspace, path2);
|
|
7543
|
-
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
7902
|
+
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
7544
7903
|
return { error: "Path not allowed" };
|
|
7545
7904
|
}
|
|
7546
|
-
const content =
|
|
7905
|
+
const content = readFileSync6(resolved, "utf-8");
|
|
7547
7906
|
if (replaceAll) {
|
|
7548
7907
|
if (!content.includes(oldString)) {
|
|
7549
7908
|
return { error: "oldString not found in file", replaced: 0 };
|
|
@@ -7728,7 +8087,7 @@ ${availableList}
|
|
|
7728
8087
|
name: z13.string().describe("The name of the skill to load")
|
|
7729
8088
|
}),
|
|
7730
8089
|
execute: async ({ name }) => {
|
|
7731
|
-
if (!isSkillAllowed(name, ctx.permissions)) {
|
|
8090
|
+
if (!isSkillAllowed(name, ctx.permissions.permissions)) {
|
|
7732
8091
|
return { error: `Skill "${name}" is not permitted` };
|
|
7733
8092
|
}
|
|
7734
8093
|
const skill = ctx.skillsRegistry.get(name);
|
|
@@ -7749,28 +8108,28 @@ ${availableList}
|
|
|
7749
8108
|
// opencode/tools/index.ts
|
|
7750
8109
|
function createTools(ctx) {
|
|
7751
8110
|
const tools = {};
|
|
7752
|
-
if (isToolEnabled("bash", ctx.permissions)) {
|
|
8111
|
+
if (isToolEnabled("bash", ctx.permissions.permissions)) {
|
|
7753
8112
|
tools.bash = createBashTool(ctx);
|
|
7754
8113
|
}
|
|
7755
|
-
if (isToolEnabled("read", ctx.permissions)) {
|
|
8114
|
+
if (isToolEnabled("read", ctx.permissions.permissions)) {
|
|
7756
8115
|
tools.read = createReadTool(ctx);
|
|
7757
8116
|
}
|
|
7758
|
-
if (isToolEnabled("write", ctx.permissions)) {
|
|
8117
|
+
if (isToolEnabled("write", ctx.permissions.permissions)) {
|
|
7759
8118
|
tools.write = createWriteTool(ctx);
|
|
7760
8119
|
}
|
|
7761
|
-
if (isToolEnabled("edit", ctx.permissions)) {
|
|
8120
|
+
if (isToolEnabled("edit", ctx.permissions.permissions)) {
|
|
7762
8121
|
tools.edit = createEditTool(ctx);
|
|
7763
8122
|
}
|
|
7764
|
-
if (isToolEnabled("grep", ctx.permissions)) {
|
|
8123
|
+
if (isToolEnabled("grep", ctx.permissions.permissions)) {
|
|
7765
8124
|
tools.grep = createGrepTool(ctx);
|
|
7766
8125
|
}
|
|
7767
|
-
if (isToolEnabled("glob", ctx.permissions)) {
|
|
8126
|
+
if (isToolEnabled("glob", ctx.permissions.permissions)) {
|
|
7768
8127
|
tools.glob = createGlobTool(ctx);
|
|
7769
8128
|
}
|
|
7770
|
-
if (isToolEnabled("web", ctx.permissions)) {
|
|
8129
|
+
if (isToolEnabled("web", ctx.permissions.permissions)) {
|
|
7771
8130
|
tools.web = createWebTool(ctx);
|
|
7772
8131
|
}
|
|
7773
|
-
if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions)) {
|
|
8132
|
+
if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions.permissions)) {
|
|
7774
8133
|
tools.skill = createSkillTool(ctx);
|
|
7775
8134
|
}
|
|
7776
8135
|
tools.question = createQuestionTool(ctx);
|
|
@@ -7779,7 +8138,7 @@ function createTools(ctx) {
|
|
|
7779
8138
|
|
|
7780
8139
|
// opencode/rest.ts
|
|
7781
8140
|
async function buildRouter4(deps) {
|
|
7782
|
-
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
8141
|
+
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
|
|
7783
8142
|
const router = new Router();
|
|
7784
8143
|
router.post("/sessions", async (req, ctx) => {
|
|
7785
8144
|
const body = await req.json().catch(() => ({}));
|
|
@@ -7814,7 +8173,7 @@ async function buildRouter4(deps) {
|
|
|
7814
8173
|
if (!content) return new Response("Missing content", { status: 400 });
|
|
7815
8174
|
const toolCtx = {
|
|
7816
8175
|
workspace: session2.workspace || workspace,
|
|
7817
|
-
permissions,
|
|
8176
|
+
permissions: permissions2,
|
|
7818
8177
|
pendingQuestions,
|
|
7819
8178
|
skillsRegistry: deps.skillsRegistry
|
|
7820
8179
|
};
|
|
@@ -7872,7 +8231,7 @@ async function buildRouter4(deps) {
|
|
|
7872
8231
|
// opencode/ws.ts
|
|
7873
8232
|
var clients2 = /* @__PURE__ */ new WeakMap();
|
|
7874
8233
|
function createWSHandler2(deps) {
|
|
7875
|
-
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
8234
|
+
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
|
|
7876
8235
|
return {
|
|
7877
8236
|
open(ws, ctx) {
|
|
7878
8237
|
const userId = ctx.user?.id ?? 0;
|
|
@@ -7922,7 +8281,7 @@ function createWSHandler2(deps) {
|
|
|
7922
8281
|
}
|
|
7923
8282
|
const toolCtx = {
|
|
7924
8283
|
workspace: session2.workspace || workspace,
|
|
7925
|
-
permissions,
|
|
8284
|
+
permissions: permissions2,
|
|
7926
8285
|
pendingQuestions,
|
|
7927
8286
|
skillsRegistry
|
|
7928
8287
|
};
|
|
@@ -8084,7 +8443,7 @@ async function opencode(options) {
|
|
|
8084
8443
|
const workspace = options.workspace || process.cwd();
|
|
8085
8444
|
const systemPrompt = options.systemPrompt;
|
|
8086
8445
|
const manualSkills = options.skills || [];
|
|
8087
|
-
const
|
|
8446
|
+
const permissions2 = options.permissions;
|
|
8088
8447
|
const modelName = options.model || "deepseek-v4-flash";
|
|
8089
8448
|
const [discoveredSkills] = await Promise.all([discoverSkills(workspace)]);
|
|
8090
8449
|
const skillsRegistry = buildSkillRegistry(discoveredSkills, manualSkills);
|
|
@@ -8092,7 +8451,7 @@ async function opencode(options) {
|
|
|
8092
8451
|
const model = provider.chat(modelName);
|
|
8093
8452
|
const pendingQuestions = /* @__PURE__ */ new Map();
|
|
8094
8453
|
const base = new PgModule(pg);
|
|
8095
|
-
const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
|
|
8454
|
+
const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
|
|
8096
8455
|
const mod = r;
|
|
8097
8456
|
mod.migrate = async () => {
|
|
8098
8457
|
const sessions2 = pg.table("_opencode_sessions", {
|
|
@@ -8125,7 +8484,7 @@ async function opencode(options) {
|
|
|
8125
8484
|
await messages2.create();
|
|
8126
8485
|
await messages2.createIndex(["session_id", "created_at"]);
|
|
8127
8486
|
};
|
|
8128
|
-
mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
|
|
8487
|
+
mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
|
|
8129
8488
|
mod.close = () => base.close();
|
|
8130
8489
|
return mod;
|
|
8131
8490
|
}
|
|
@@ -8395,12 +8754,49 @@ function analytics(options) {
|
|
|
8395
8754
|
return mod;
|
|
8396
8755
|
}
|
|
8397
8756
|
|
|
8398
|
-
//
|
|
8757
|
+
// theme.ts
|
|
8758
|
+
function makeSetTheme(cookie, location) {
|
|
8759
|
+
return (value, loc) => {
|
|
8760
|
+
const finalLoc = loc ?? location;
|
|
8761
|
+
const c = `${cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8762
|
+
return new Response(null, { status: 302, headers: { Location: finalLoc, "Set-Cookie": c } });
|
|
8763
|
+
};
|
|
8764
|
+
}
|
|
8765
|
+
function theme(options) {
|
|
8766
|
+
const opts = { default: "system", cookie: "theme", ...options };
|
|
8767
|
+
return async (req, ctx, next) => {
|
|
8768
|
+
const url = new URL(req.url);
|
|
8769
|
+
const match = url.pathname.match(/^\/__theme\/([\w-]+)$/);
|
|
8770
|
+
if (match && req.method === "GET") {
|
|
8771
|
+
const value = match[1];
|
|
8772
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8773
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8774
|
+
if (accept.includes("application/json")) {
|
|
8775
|
+
return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
|
|
8776
|
+
}
|
|
8777
|
+
const referer = req.headers.get("referer") || "/";
|
|
8778
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8779
|
+
}
|
|
8780
|
+
let themeValue = opts.default;
|
|
8781
|
+
if (opts.cookie) {
|
|
8782
|
+
const fromCookie = getCookies(req)[opts.cookie];
|
|
8783
|
+
if (fromCookie) themeValue = fromCookie;
|
|
8784
|
+
}
|
|
8785
|
+
ctx.theme = {
|
|
8786
|
+
value: themeValue,
|
|
8787
|
+
set: makeSetTheme(opts.cookie, req.headers.get("referer") || "/")
|
|
8788
|
+
};
|
|
8789
|
+
return next(req, ctx);
|
|
8790
|
+
};
|
|
8791
|
+
}
|
|
8792
|
+
|
|
8793
|
+
// i18n.ts
|
|
8399
8794
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
8400
8795
|
import { join as join7, resolve as resolve13 } from "node:path";
|
|
8401
|
-
var
|
|
8402
|
-
|
|
8403
|
-
|
|
8796
|
+
var DEFAULTS2 = {
|
|
8797
|
+
default: "en",
|
|
8798
|
+
cookie: "locale",
|
|
8799
|
+
fromAcceptLanguage: true
|
|
8404
8800
|
};
|
|
8405
8801
|
function translate(msgs, key, params, fallback) {
|
|
8406
8802
|
const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
|
|
@@ -8412,130 +8808,125 @@ function translate(msgs, key, params, fallback) {
|
|
|
8412
8808
|
}
|
|
8413
8809
|
return result;
|
|
8414
8810
|
}
|
|
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 };
|
|
8811
|
+
function i18n(options) {
|
|
8812
|
+
const opts = { ...DEFAULTS2, ...options };
|
|
8813
|
+
const dir = opts.dir ? resolve13(opts.dir) : void 0;
|
|
8443
8814
|
const cache3 = /* @__PURE__ */ new Map();
|
|
8444
8815
|
function validLocale(locale) {
|
|
8445
8816
|
return /^[\w-]+$/.test(locale) && !locale.includes("..");
|
|
8446
8817
|
}
|
|
8447
|
-
async function
|
|
8448
|
-
if (
|
|
8449
|
-
|
|
8818
|
+
async function loadMessages(locale) {
|
|
8819
|
+
if (opts.messages?.[locale] && Object.keys(opts.messages[locale]).length > 0) {
|
|
8820
|
+
cache3.set(locale, opts.messages[locale]);
|
|
8821
|
+
return opts.messages[locale];
|
|
8822
|
+
}
|
|
8823
|
+
if (!dir || !validLocale(locale)) return {};
|
|
8450
8824
|
const cached = cache3.get(locale);
|
|
8451
8825
|
if (cached) return cached;
|
|
8452
8826
|
const filePath = join7(dir, `${locale}.json`);
|
|
8453
|
-
let data = null;
|
|
8454
8827
|
try {
|
|
8455
8828
|
await stat2(filePath);
|
|
8456
8829
|
const content = await readFile2(filePath, "utf-8");
|
|
8457
|
-
data = JSON.parse(content);
|
|
8830
|
+
const data = JSON.parse(content);
|
|
8458
8831
|
cache3.set(locale, data);
|
|
8459
8832
|
return data;
|
|
8460
8833
|
} catch {
|
|
8461
8834
|
}
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
return fallback;
|
|
8469
|
-
}
|
|
8835
|
+
const short = locale.split("-")[0];
|
|
8836
|
+
if (short !== locale) {
|
|
8837
|
+
const fallback = cache3.get(short) || await loadMessages(short);
|
|
8838
|
+
if (fallback && Object.keys(fallback).length > 0) {
|
|
8839
|
+
cache3.set(locale, fallback);
|
|
8840
|
+
return fallback;
|
|
8470
8841
|
}
|
|
8471
8842
|
}
|
|
8472
8843
|
return {};
|
|
8473
8844
|
}
|
|
8845
|
+
function detectLocale(req) {
|
|
8846
|
+
if (opts.cookie) {
|
|
8847
|
+
const fromCookie = getCookies(req)[opts.cookie];
|
|
8848
|
+
if (fromCookie && validLocale(fromCookie)) return fromCookie;
|
|
8849
|
+
}
|
|
8850
|
+
if (opts.fromAcceptLanguage) {
|
|
8851
|
+
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
|
|
8852
|
+
if (fromHeader && validLocale(fromHeader)) return fromHeader;
|
|
8853
|
+
}
|
|
8854
|
+
return opts.default;
|
|
8855
|
+
}
|
|
8474
8856
|
return async (req, ctx, next) => {
|
|
8475
8857
|
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"];
|
|
8858
|
+
const match = url.pathname.match(/^\/__lang\/([\w-]+)$/);
|
|
8859
|
+
if (match && req.method === "GET") {
|
|
8860
|
+
const value = match[1];
|
|
8861
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8862
|
+
const messages2 = await loadMessages(value);
|
|
8863
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8864
|
+
if (accept.includes("application/json")) {
|
|
8865
|
+
return Response.json(
|
|
8866
|
+
{ ok: true, locale: value, messages: Object.keys(messages2).length > 0 ? messages2 : void 0 },
|
|
8867
|
+
{ headers: { "Set-Cookie": cookie } }
|
|
8868
|
+
);
|
|
8869
|
+
}
|
|
8495
8870
|
const referer = req.headers.get("referer") || "/";
|
|
8496
|
-
return new Response(null, {
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8871
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8872
|
+
}
|
|
8873
|
+
const locale = detectLocale(req);
|
|
8874
|
+
const msgs = await loadMessages(locale);
|
|
8875
|
+
ctx.i18n = {
|
|
8876
|
+
locale,
|
|
8877
|
+
messages: msgs,
|
|
8878
|
+
t: (key, params, fallback) => translate(msgs, key, params, fallback),
|
|
8879
|
+
set: (value, loc) => {
|
|
8880
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8881
|
+
const location = loc ?? (req.headers.get("referer") || "/");
|
|
8882
|
+
return new Response(null, { status: 302, headers: { Location: location, "Set-Cookie": cookie } });
|
|
8883
|
+
}
|
|
8503
8884
|
};
|
|
8504
|
-
|
|
8505
|
-
|
|
8885
|
+
return next(req, ctx);
|
|
8886
|
+
};
|
|
8887
|
+
}
|
|
8888
|
+
|
|
8889
|
+
// flash.ts
|
|
8890
|
+
function makeSetFlash(name, location) {
|
|
8891
|
+
return (data, loc) => {
|
|
8892
|
+
const finalLoc = loc ?? location;
|
|
8893
|
+
const value = encodeURIComponent(JSON.stringify(data));
|
|
8894
|
+
return new Response(null, {
|
|
8895
|
+
status: 302,
|
|
8896
|
+
headers: {
|
|
8897
|
+
Location: finalLoc,
|
|
8898
|
+
"Set-Cookie": `${name}=${value}; Path=/; SameSite=Lax`
|
|
8899
|
+
}
|
|
8900
|
+
});
|
|
8901
|
+
};
|
|
8902
|
+
}
|
|
8903
|
+
function flash(options) {
|
|
8904
|
+
const name = options?.name ?? "flash";
|
|
8905
|
+
return async (req, ctx, next) => {
|
|
8906
|
+
const raw = getCookies(req)[name] ?? null;
|
|
8907
|
+
const referer = req.headers.get("referer") || "/";
|
|
8908
|
+
let value = void 0;
|
|
8909
|
+
if (raw) {
|
|
8506
8910
|
try {
|
|
8507
|
-
|
|
8911
|
+
value = JSON.parse(decodeURIComponent(raw));
|
|
8508
8912
|
} catch {
|
|
8509
|
-
|
|
8913
|
+
value = raw;
|
|
8510
8914
|
}
|
|
8511
8915
|
}
|
|
8916
|
+
;
|
|
8917
|
+
ctx.flash = {
|
|
8918
|
+
value,
|
|
8919
|
+
set: makeSetFlash(name, referer)
|
|
8920
|
+
};
|
|
8512
8921
|
const res = await next(req, ctx);
|
|
8513
|
-
if (
|
|
8922
|
+
if (raw) {
|
|
8514
8923
|
const headers = new Headers(res.headers);
|
|
8515
|
-
headers.append("Set-Cookie",
|
|
8924
|
+
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
|
|
8516
8925
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
8517
8926
|
}
|
|
8518
8927
|
return res;
|
|
8519
8928
|
};
|
|
8520
8929
|
}
|
|
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
8930
|
|
|
8540
8931
|
// seo.ts
|
|
8541
8932
|
function escapeXml(s) {
|
|
@@ -10068,7 +10459,6 @@ function session(options) {
|
|
|
10068
10459
|
}
|
|
10069
10460
|
const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
|
|
10070
10461
|
ctx.session = session2;
|
|
10071
|
-
ctx.sessionId = session2.id;
|
|
10072
10462
|
const res = await next(req, ctx);
|
|
10073
10463
|
const currentSession = ctx.session;
|
|
10074
10464
|
if (!currentSession || currentSession[kDestroyed]) {
|
|
@@ -10577,7 +10967,7 @@ function resolveTableName(table) {
|
|
|
10577
10967
|
}
|
|
10578
10968
|
return name;
|
|
10579
10969
|
}
|
|
10580
|
-
function
|
|
10970
|
+
function escapeIdent4(s) {
|
|
10581
10971
|
return `"${s.replace(/"/g, '""')}"`;
|
|
10582
10972
|
}
|
|
10583
10973
|
function sqlLit(s) {
|
|
@@ -10587,11 +10977,11 @@ async function createIndex(sql2, table, fields, options) {
|
|
|
10587
10977
|
const language = options?.language ?? "english";
|
|
10588
10978
|
const tableName = resolveTableName(table);
|
|
10589
10979
|
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10590
|
-
const vectorExpr = fields.map((f) => `coalesce(${
|
|
10980
|
+
const vectorExpr = fields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
|
|
10591
10981
|
const indexType = options?.indexType ?? "gin";
|
|
10592
10982
|
await sql2.unsafe(`
|
|
10593
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
10594
|
-
ON ${
|
|
10983
|
+
CREATE INDEX IF NOT EXISTS ${escapeIdent4(indexName)}
|
|
10984
|
+
ON ${escapeIdent4(tableName)}
|
|
10595
10985
|
USING ${indexType}
|
|
10596
10986
|
(to_tsvector(${sqlLit(language)}, ${vectorExpr}))
|
|
10597
10987
|
`);
|
|
@@ -10599,7 +10989,7 @@ async function createIndex(sql2, table, fields, options) {
|
|
|
10599
10989
|
async function dropIndex(sql2, table, options) {
|
|
10600
10990
|
const tableName = resolveTableName(table);
|
|
10601
10991
|
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10602
|
-
await sql2.unsafe(`DROP INDEX IF EXISTS ${
|
|
10992
|
+
await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent4(indexName)}`);
|
|
10603
10993
|
}
|
|
10604
10994
|
async function search(sql2, table, query, options) {
|
|
10605
10995
|
const tableName = resolveTableName(table);
|
|
@@ -10613,13 +11003,13 @@ async function search(sql2, table, query, options) {
|
|
|
10613
11003
|
}
|
|
10614
11004
|
const sanitized = query.trim();
|
|
10615
11005
|
if (!sanitized) return [];
|
|
10616
|
-
const vectorExpr = searchFields.map((f) => `coalesce(${
|
|
11006
|
+
const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
|
|
10617
11007
|
const langLit = sqlLit(language);
|
|
10618
11008
|
const queryLit = sqlLit(sanitized);
|
|
10619
|
-
const rankColId =
|
|
10620
|
-
const tableId =
|
|
11009
|
+
const rankColId = escapeIdent4(rankCol);
|
|
11010
|
+
const tableId = escapeIdent4(tableName);
|
|
10621
11011
|
const headlineExpr = options?.headline ? searchFields.map(
|
|
10622
|
-
(f) => `ts_headline(${langLit}, ${
|
|
11012
|
+
(f) => `ts_headline(${langLit}, ${escapeIdent4(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent4(f + "_headline")}`
|
|
10623
11013
|
).join(",\n ") : "";
|
|
10624
11014
|
const sql_query = `
|
|
10625
11015
|
SELECT
|
|
@@ -10659,8 +11049,8 @@ async function suggest(sql2, table, prefix, options) {
|
|
|
10659
11049
|
const rows = await sql2.unsafe(`
|
|
10660
11050
|
SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
|
|
10661
11051
|
FROM (
|
|
10662
|
-
SELECT regexp_split_to_table(lower(${
|
|
10663
|
-
FROM ${
|
|
11052
|
+
SELECT regexp_split_to_table(lower(${escapeIdent4(field)}), E'\\W+') as word
|
|
11053
|
+
FROM ${escapeIdent4(tableName)}
|
|
10664
11054
|
) words
|
|
10665
11055
|
WHERE word LIKE ${sqlLit(sanitized + "%")}
|
|
10666
11056
|
LIMIT ${limit}
|
|
@@ -10782,75 +11172,45 @@ function s3(options) {
|
|
|
10782
11172
|
return mw;
|
|
10783
11173
|
}
|
|
10784
11174
|
|
|
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) {
|
|
11175
|
+
// kb/index.ts
|
|
11176
|
+
function escapeIdent5(s) {
|
|
10801
11177
|
return `"${s.replace(/"/g, '""')}"`;
|
|
10802
11178
|
}
|
|
10803
11179
|
function knowledgeBase(options) {
|
|
10804
|
-
const {
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
11180
|
+
const { pg, provider, table = "_kb_docs", chunkSize = 512, chunkOverlap = 64, searchLimit = 5, searchThreshold = 0 } = options;
|
|
11181
|
+
const sql2 = pg.sql;
|
|
11182
|
+
const dimension = provider.dimension;
|
|
11183
|
+
const docsTable = pg.table(table, {
|
|
11184
|
+
id: serial("id").primaryKey(),
|
|
11185
|
+
doc_key: text("doc_key").notNull(),
|
|
11186
|
+
title: text("title").notNull().default(""),
|
|
11187
|
+
content: text("content").notNull(),
|
|
11188
|
+
chunk_index: integer("chunk_index").notNull().default(0),
|
|
11189
|
+
metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
|
|
11190
|
+
embedding: vector("embedding", dimension),
|
|
11191
|
+
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
11192
|
+
});
|
|
10814
11193
|
async function migrate() {
|
|
10815
11194
|
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
|
-
`);
|
|
11195
|
+
await docsTable.create();
|
|
11196
|
+
await docsTable.createIndex("doc_key");
|
|
11197
|
+
await docsTable.createIndex("embedding", { type: "hnsw", operator: "vector_cosine_ops" });
|
|
10837
11198
|
}
|
|
10838
11199
|
async function ingest(key, content, ingestOpts) {
|
|
10839
|
-
await sql2.unsafe(`DELETE FROM ${escapeIdent2(table)} WHERE doc_key = $1`, [key]);
|
|
10840
11200
|
const title = ingestOpts?.title ?? key;
|
|
10841
11201
|
const meta = ingestOpts?.metadata ?? {};
|
|
10842
11202
|
const cs = ingestOpts?.chunkSize ?? chunkSize;
|
|
10843
11203
|
const co = ingestOpts?.chunkOverlap ?? chunkOverlap;
|
|
10844
|
-
const chunks =
|
|
11204
|
+
const chunks = chunkContent(content, cs, co);
|
|
10845
11205
|
const metaJson = JSON.stringify(meta);
|
|
11206
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
|
|
11207
|
+
const embeddings = await Promise.all(chunks.map((c) => provider.embed(c)));
|
|
10846
11208
|
for (let i = 0; i < chunks.length; i++) {
|
|
10847
|
-
const
|
|
10848
|
-
const embedding = await embedFn(chunk);
|
|
10849
|
-
const vec = `[${embedding.join(",")}]`;
|
|
11209
|
+
const vec = `[${embeddings[i].join(",")}]`;
|
|
10850
11210
|
await sql2.unsafe(
|
|
10851
|
-
`INSERT INTO ${
|
|
11211
|
+
`INSERT INTO ${escapeIdent5(table)} (doc_key, title, content, chunk_index, metadata, embedding)
|
|
10852
11212
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector)`,
|
|
10853
|
-
[key, title,
|
|
11213
|
+
[key, title, chunks[i], i, metaJson, vec]
|
|
10854
11214
|
);
|
|
10855
11215
|
}
|
|
10856
11216
|
return chunks.length;
|
|
@@ -10858,13 +11218,13 @@ function knowledgeBase(options) {
|
|
|
10858
11218
|
async function search2(query, searchOpts) {
|
|
10859
11219
|
const limit = searchOpts?.limit ?? searchLimit;
|
|
10860
11220
|
const threshold = searchOpts?.threshold ?? searchThreshold;
|
|
10861
|
-
const embedding = await
|
|
11221
|
+
const embedding = await provider.embed(query);
|
|
10862
11222
|
const vec = `[${embedding.join(",")}]`;
|
|
10863
11223
|
const whereClause = threshold > 0 ? `WHERE (1 - (embedding <=> $1::vector) / 2) >= ${threshold}` : "";
|
|
10864
11224
|
const rows = await sql2.unsafe(
|
|
10865
11225
|
`SELECT id, doc_key, title, content, chunk_index, metadata,
|
|
10866
11226
|
1 - (embedding <=> $1::vector) / 2 AS _score
|
|
10867
|
-
FROM ${
|
|
11227
|
+
FROM ${escapeIdent5(table)}
|
|
10868
11228
|
${whereClause}
|
|
10869
11229
|
ORDER BY embedding <=> $1::vector
|
|
10870
11230
|
LIMIT ${limit}`,
|
|
@@ -10880,12 +11240,12 @@ function knowledgeBase(options) {
|
|
|
10880
11240
|
}));
|
|
10881
11241
|
}
|
|
10882
11242
|
async function del(key) {
|
|
10883
|
-
await sql2.unsafe(`DELETE FROM ${
|
|
11243
|
+
await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
|
|
10884
11244
|
}
|
|
10885
11245
|
async function list() {
|
|
10886
11246
|
const rows = await sql2.unsafe(`
|
|
10887
11247
|
SELECT doc_key, title, COUNT(*) AS chunks
|
|
10888
|
-
FROM ${
|
|
11248
|
+
FROM ${escapeIdent5(table)}
|
|
10889
11249
|
GROUP BY doc_key, title
|
|
10890
11250
|
ORDER BY doc_key
|
|
10891
11251
|
`);
|
|
@@ -10911,6 +11271,158 @@ function knowledgeBase(options) {
|
|
|
10911
11271
|
middleware: mw
|
|
10912
11272
|
};
|
|
10913
11273
|
}
|
|
11274
|
+
|
|
11275
|
+
// permissions.ts
|
|
11276
|
+
function escapeIdent6(s) {
|
|
11277
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
11278
|
+
}
|
|
11279
|
+
function permissions(options) {
|
|
11280
|
+
const { pg } = options;
|
|
11281
|
+
const sql2 = pg.sql;
|
|
11282
|
+
const prefix = options.prefix ?? "";
|
|
11283
|
+
const rolesTable = `${prefix}_roles`;
|
|
11284
|
+
const rolePermsTable = `${prefix}_role_permissions`;
|
|
11285
|
+
const userRolesTable = `${prefix}_user_roles`;
|
|
11286
|
+
async function migrate() {
|
|
11287
|
+
await sql2.unsafe(`
|
|
11288
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolesTable)} (
|
|
11289
|
+
id SERIAL PRIMARY KEY,
|
|
11290
|
+
name TEXT UNIQUE NOT NULL,
|
|
11291
|
+
description TEXT NOT NULL DEFAULT '',
|
|
11292
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
11293
|
+
)
|
|
11294
|
+
`);
|
|
11295
|
+
await sql2.unsafe(`
|
|
11296
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolePermsTable)} (
|
|
11297
|
+
id SERIAL PRIMARY KEY,
|
|
11298
|
+
role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
|
|
11299
|
+
permission TEXT NOT NULL,
|
|
11300
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11301
|
+
UNIQUE(role_id, permission)
|
|
11302
|
+
)
|
|
11303
|
+
`);
|
|
11304
|
+
await sql2.unsafe(`
|
|
11305
|
+
CREATE TABLE IF NOT EXISTS ${escapeIdent6(userRolesTable)} (
|
|
11306
|
+
id SERIAL PRIMARY KEY,
|
|
11307
|
+
user_id INTEGER NOT NULL,
|
|
11308
|
+
role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
|
|
11309
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11310
|
+
UNIQUE(user_id, role_id)
|
|
11311
|
+
)
|
|
11312
|
+
`);
|
|
11313
|
+
}
|
|
11314
|
+
async function ensureRole(role) {
|
|
11315
|
+
const [existing] = await sql2.unsafe(
|
|
11316
|
+
`SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1 LIMIT 1`,
|
|
11317
|
+
[role]
|
|
11318
|
+
);
|
|
11319
|
+
if (existing) return existing.id;
|
|
11320
|
+
const [created] = await sql2.unsafe(
|
|
11321
|
+
`INSERT INTO ${escapeIdent6(rolesTable)} (name) VALUES ($1) RETURNING id`,
|
|
11322
|
+
[role]
|
|
11323
|
+
);
|
|
11324
|
+
return created.id;
|
|
11325
|
+
}
|
|
11326
|
+
async function assignRole(userId, role) {
|
|
11327
|
+
const roleId = await ensureRole(role);
|
|
11328
|
+
await sql2.unsafe(
|
|
11329
|
+
`INSERT INTO ${escapeIdent6(userRolesTable)} (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
11330
|
+
[userId, roleId]
|
|
11331
|
+
);
|
|
11332
|
+
}
|
|
11333
|
+
async function removeRole(userId, role) {
|
|
11334
|
+
await sql2.unsafe(
|
|
11335
|
+
`DELETE FROM ${escapeIdent6(userRolesTable)} WHERE user_id = $1 AND role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $2)`,
|
|
11336
|
+
[userId, role]
|
|
11337
|
+
);
|
|
11338
|
+
}
|
|
11339
|
+
async function grantPermission(role, permission) {
|
|
11340
|
+
const roleId = await ensureRole(role);
|
|
11341
|
+
await sql2.unsafe(
|
|
11342
|
+
`INSERT INTO ${escapeIdent6(rolePermsTable)} (role_id, permission) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
11343
|
+
[roleId, permission]
|
|
11344
|
+
);
|
|
11345
|
+
}
|
|
11346
|
+
async function revokePermission(role, permission) {
|
|
11347
|
+
await sql2.unsafe(
|
|
11348
|
+
`DELETE FROM ${escapeIdent6(rolePermsTable)} WHERE role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1) AND permission = $2`,
|
|
11349
|
+
[role, permission]
|
|
11350
|
+
);
|
|
11351
|
+
}
|
|
11352
|
+
async function getUserRoles(userId) {
|
|
11353
|
+
const rows = await sql2.unsafe(
|
|
11354
|
+
`SELECT r.name FROM ${escapeIdent6(userRolesTable)} ur
|
|
11355
|
+
JOIN ${escapeIdent6(rolesTable)} r ON r.id = ur.role_id
|
|
11356
|
+
WHERE ur.user_id = $1 ORDER BY r.name`,
|
|
11357
|
+
[userId]
|
|
11358
|
+
);
|
|
11359
|
+
return rows.map((r) => r.name);
|
|
11360
|
+
}
|
|
11361
|
+
async function getUserPermissions(userId) {
|
|
11362
|
+
const rows = await sql2.unsafe(
|
|
11363
|
+
`SELECT DISTINCT rp.permission FROM ${escapeIdent6(userRolesTable)} ur
|
|
11364
|
+
JOIN ${escapeIdent6(rolePermsTable)} rp ON rp.role_id = ur.role_id
|
|
11365
|
+
WHERE ur.user_id = $1 ORDER BY rp.permission`,
|
|
11366
|
+
[userId]
|
|
11367
|
+
);
|
|
11368
|
+
return rows.map((r) => r.permission);
|
|
11369
|
+
}
|
|
11370
|
+
const mw = (async (req, ctx, next) => {
|
|
11371
|
+
const userId = ctx.user?.id;
|
|
11372
|
+
let roles = /* @__PURE__ */ new Set();
|
|
11373
|
+
let perms = /* @__PURE__ */ new Set();
|
|
11374
|
+
if (userId) {
|
|
11375
|
+
const userRoles = await getUserRoles(userId);
|
|
11376
|
+
const userPerms = userId ? await getUserPermissions(userId) : [];
|
|
11377
|
+
roles = new Set(userRoles);
|
|
11378
|
+
perms = new Set(userPerms);
|
|
11379
|
+
const hasWildcard = userPerms.includes("*");
|
|
11380
|
+
if (hasWildcard) {
|
|
11381
|
+
perms = /* @__PURE__ */ new Set(["*"]);
|
|
11382
|
+
}
|
|
11383
|
+
}
|
|
11384
|
+
ctx.permissions = { roles, permissions: perms };
|
|
11385
|
+
return next(req, ctx);
|
|
11386
|
+
});
|
|
11387
|
+
function requireRole(...roles) {
|
|
11388
|
+
return (req, ctx, next) => {
|
|
11389
|
+
if (!ctx.permissions?.roles || !roles.some((r) => ctx.permissions.roles.has(r))) {
|
|
11390
|
+
return Response.json(
|
|
11391
|
+
{ error: `Forbidden: requires one of roles [${roles.join(", ")}]` },
|
|
11392
|
+
{ status: 403 }
|
|
11393
|
+
);
|
|
11394
|
+
}
|
|
11395
|
+
return next(req, ctx);
|
|
11396
|
+
};
|
|
11397
|
+
}
|
|
11398
|
+
function requirePermission(...perms) {
|
|
11399
|
+
return (req, ctx, next) => {
|
|
11400
|
+
const userPerms = ctx.permissions?.permissions;
|
|
11401
|
+
if (!userPerms) {
|
|
11402
|
+
return Response.json({ error: "Forbidden: no permissions loaded" }, { status: 403 });
|
|
11403
|
+
}
|
|
11404
|
+
if (userPerms.has("*")) return next(req, ctx);
|
|
11405
|
+
const missing = perms.filter((p) => !userPerms.has(p));
|
|
11406
|
+
if (missing.length > 0) {
|
|
11407
|
+
return Response.json(
|
|
11408
|
+
{ error: `Forbidden: missing permissions [${missing.join(", ")}]` },
|
|
11409
|
+
{ status: 403 }
|
|
11410
|
+
);
|
|
11411
|
+
}
|
|
11412
|
+
return next(req, ctx);
|
|
11413
|
+
};
|
|
11414
|
+
}
|
|
11415
|
+
mw.assignRole = assignRole;
|
|
11416
|
+
mw.removeRole = removeRole;
|
|
11417
|
+
mw.grantPermission = grantPermission;
|
|
11418
|
+
mw.revokePermission = revokePermission;
|
|
11419
|
+
mw.getUserRoles = getUserRoles;
|
|
11420
|
+
mw.getUserPermissions = getUserPermissions;
|
|
11421
|
+
mw.requireRole = requireRole;
|
|
11422
|
+
mw.requirePermission = requirePermission;
|
|
11423
|
+
mw.migrate = migrate;
|
|
11424
|
+
return mw;
|
|
11425
|
+
}
|
|
10914
11426
|
export {
|
|
10915
11427
|
DEFAULT_MAX_BODY,
|
|
10916
11428
|
MIGRATIONS_TABLE,
|
|
@@ -10923,6 +11435,7 @@ export {
|
|
|
10923
11435
|
TestRequest,
|
|
10924
11436
|
TsxContext,
|
|
10925
11437
|
agent,
|
|
11438
|
+
aiProvider,
|
|
10926
11439
|
aiStream,
|
|
10927
11440
|
analytics,
|
|
10928
11441
|
auth,
|
|
@@ -10930,7 +11443,7 @@ export {
|
|
|
10930
11443
|
compress,
|
|
10931
11444
|
cors,
|
|
10932
11445
|
createHub,
|
|
10933
|
-
createOpenAI,
|
|
11446
|
+
createOpenAI2 as createOpenAI,
|
|
10934
11447
|
createSSEStream,
|
|
10935
11448
|
createTestDb,
|
|
10936
11449
|
createTestServer,
|
|
@@ -10943,6 +11456,7 @@ export {
|
|
|
10943
11456
|
deploy,
|
|
10944
11457
|
embed,
|
|
10945
11458
|
embedMany,
|
|
11459
|
+
flash,
|
|
10946
11460
|
formatSSE,
|
|
10947
11461
|
formatSSEData,
|
|
10948
11462
|
fts_exports as fts,
|
|
@@ -10952,6 +11466,7 @@ export {
|
|
|
10952
11466
|
graphql,
|
|
10953
11467
|
health,
|
|
10954
11468
|
helmet,
|
|
11469
|
+
i18n,
|
|
10955
11470
|
iii,
|
|
10956
11471
|
isDev,
|
|
10957
11472
|
isProd,
|
|
@@ -10961,11 +11476,10 @@ export {
|
|
|
10961
11476
|
logger,
|
|
10962
11477
|
mailer,
|
|
10963
11478
|
messager,
|
|
10964
|
-
oauthClient,
|
|
10965
11479
|
openai,
|
|
10966
11480
|
opencode,
|
|
11481
|
+
permissions,
|
|
10967
11482
|
postgres,
|
|
10968
|
-
preferences,
|
|
10969
11483
|
queue,
|
|
10970
11484
|
rateLimit,
|
|
10971
11485
|
redis,
|
|
@@ -10987,6 +11501,7 @@ export {
|
|
|
10987
11501
|
streamText,
|
|
10988
11502
|
tenant,
|
|
10989
11503
|
testApp,
|
|
11504
|
+
theme,
|
|
10990
11505
|
tool2 as tool,
|
|
10991
11506
|
traceElapsed,
|
|
10992
11507
|
upload,
|