weifuwu 0.76.0 → 0.78.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 +38 -41
- package/dist/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/components/List/List.d.ts +3 -0
- package/dist/components/Tour/Tour.d.ts +1 -1
- package/dist/components/index.js +12 -12
- package/dist/components/style.css +17 -0
- package/dist/index.js +1272 -1228
- package/dist/scheduler/index.d.ts +7 -2
- package/dist/ui-dom/hooks/index.d.ts +0 -1
- package/dist/ui-dom/hooks/popup.d.ts +1 -10
- package/dist/ui-dom/hooks/stable.d.ts +1 -1
- package/dist/ui-dom/hooks/types.d.ts +0 -1
- package/dist/ui-dom/index.d.ts +1 -1
- package/dist/ui-dom/index.js +10 -10
- package/dist/ui-dom/jsx-runtime.js +1 -1
- package/dist/ui-dom/testing.js +1 -1
- package/dist/ui-dom/types.d.ts +31 -41
- package/dist/ui-dom/vdom/audit.d.ts +20 -0
- package/dist/ui-dom/vdom/build.d.ts +10 -3
- package/dist/ui-dom/vdom/diff.d.ts +7 -8
- package/dist/ui-dom/vdom/index.d.ts +1 -1
- package/dist/ui-dom/vdom/mount.d.ts +14 -5
- package/dist/ui-dom/vdom/render.d.ts +4 -0
- package/dist/ui-dom/vdom/serve.d.ts +1 -1
- package/dist/ui-dom/vdom/transform.d.ts +32 -0
- package/dist/ui-dom/vnode.d.ts +18 -10
- package/docs/components.md +5 -5
- package/docs/custom-components.md +86 -54
- package/docs/examples.md +35 -41
- package/docs/frontend-middleware.md +3 -4
- package/docs/frontend-ui-dom.md +22 -18
- package/docs/frontend.md +243 -168
- package/docs/mobile.md +2 -2
- package/docs/realtime.md +8 -3
- package/package.json +1 -1
- package/dist/ui-dom/focus-trap.d.ts +0 -4
- package/dist/ui-dom/scroll-lock.d.ts +0 -5
- package/dist/ui-dom/vdom/scheduler.d.ts +0 -13
package/dist/index.js
CHANGED
|
@@ -2315,1140 +2315,577 @@ function isConnClosed(e) {
|
|
|
2315
2315
|
// src/scheduler/index.ts
|
|
2316
2316
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2317
2317
|
|
|
2318
|
-
// src/
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
//
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
super("connection", message, { cause });
|
|
2344
|
-
this.name = "ConnectionError";
|
|
2345
|
-
this.attempts = attempts;
|
|
2346
|
-
}
|
|
2347
|
-
};
|
|
2348
|
-
var TimeoutError = class extends DbError {
|
|
2349
|
-
operation;
|
|
2350
|
-
ms;
|
|
2351
|
-
constructor(operation, ms) {
|
|
2352
|
-
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
2353
|
-
this.name = "TimeoutError";
|
|
2354
|
-
this.operation = operation;
|
|
2355
|
-
this.ms = ms;
|
|
2356
|
-
}
|
|
2357
|
-
};
|
|
2358
|
-
var ValidationError = class extends DbError {
|
|
2359
|
-
constructor(message) {
|
|
2360
|
-
super("validation", message);
|
|
2361
|
-
this.name = "ValidationError";
|
|
2362
|
-
}
|
|
2363
|
-
};
|
|
2364
|
-
|
|
2365
|
-
// src/db/redis/resp.ts
|
|
2366
|
-
var RespError = class extends DbError {
|
|
2367
|
-
constructor(message) {
|
|
2368
|
-
super("protocol", message, { code: "RESP" });
|
|
2369
|
-
this.name = "RespError";
|
|
2370
|
-
}
|
|
2371
|
-
};
|
|
2372
|
-
var IncompleteError = class extends Error {
|
|
2373
|
-
constructor() {
|
|
2374
|
-
super("incomplete RESP message");
|
|
2375
|
-
this.name = "IncompleteError";
|
|
2376
|
-
}
|
|
2377
|
-
};
|
|
2378
|
-
function decodeValue(v, decodeBytes) {
|
|
2379
|
-
if (v instanceof Uint8Array) return decodeBytes ? _decoder.decode(v) : v;
|
|
2380
|
-
if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
|
|
2381
|
-
return v;
|
|
2382
|
-
}
|
|
2383
|
-
function encodeCommand(args) {
|
|
2384
|
-
const lens = new Array(args.length);
|
|
2385
|
-
let total = headerLen(args.length);
|
|
2386
|
-
for (let i = 0; i < args.length; i++) {
|
|
2387
|
-
const arg = args[i];
|
|
2388
|
-
const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
|
|
2389
|
-
lens[i] = len;
|
|
2390
|
-
total += headerLen(len) + len + 2;
|
|
2318
|
+
// src/scheduler/cron.ts
|
|
2319
|
+
var RANGES = [
|
|
2320
|
+
[0, 59],
|
|
2321
|
+
// 分
|
|
2322
|
+
[0, 23],
|
|
2323
|
+
// 时
|
|
2324
|
+
[1, 31],
|
|
2325
|
+
// 日
|
|
2326
|
+
[1, 12],
|
|
2327
|
+
// 月
|
|
2328
|
+
[0, 6]
|
|
2329
|
+
// 周
|
|
2330
|
+
];
|
|
2331
|
+
function parseField(field, idx) {
|
|
2332
|
+
const [min, max] = RANGES[idx];
|
|
2333
|
+
const name = ["minute", "hour", "day", "month", "weekday"][idx];
|
|
2334
|
+
if (field === "*") return null;
|
|
2335
|
+
if (field.startsWith("*/")) {
|
|
2336
|
+
const n = Number(field.slice(2));
|
|
2337
|
+
if (!Number.isInteger(n) || n <= 0 || n > max) {
|
|
2338
|
+
throw new Error(`cron: invalid step '${field}' in ${name} field (must be 1..${max})`);
|
|
2339
|
+
}
|
|
2340
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
2341
|
+
for (let v = min; v <= max; v += n) set2.add(v);
|
|
2342
|
+
return set2;
|
|
2391
2343
|
}
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
off += lh.length;
|
|
2405
|
-
if (typeof arg === "string") {
|
|
2406
|
-
out.set(_encoder.encode(arg), off);
|
|
2407
|
-
} else if (arg instanceof Buffer) {
|
|
2408
|
-
out.set(arg, off);
|
|
2344
|
+
const set = /* @__PURE__ */ new Set();
|
|
2345
|
+
for (const part of field.split(",")) {
|
|
2346
|
+
if (part === "") throw new Error(`cron: empty item in '${field}' (${name})`);
|
|
2347
|
+
if (part.includes("-")) {
|
|
2348
|
+
const [rangePart, stepPart] = part.split("/");
|
|
2349
|
+
const step = stepPart !== void 0 ? Number(stepPart) : 1;
|
|
2350
|
+
const [a, b] = rangePart.split("-").map(Number);
|
|
2351
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a < min || b > max || a > b) {
|
|
2352
|
+
throw new Error(`cron: invalid range '${part}' in ${name} field (${min}..${max})`);
|
|
2353
|
+
}
|
|
2354
|
+
if (!Number.isInteger(step) || step <= 0) throw new Error(`cron: invalid step in '${part}'`);
|
|
2355
|
+
for (let v = a; v <= b; v += step) set.add(v);
|
|
2409
2356
|
} else {
|
|
2410
|
-
|
|
2357
|
+
const v = Number(part);
|
|
2358
|
+
if (!Number.isInteger(v) || v < min || v > max) {
|
|
2359
|
+
throw new Error(`cron: value '${part}' out of range ${min}..${max} in ${name} field`);
|
|
2360
|
+
}
|
|
2361
|
+
set.add(v);
|
|
2411
2362
|
}
|
|
2412
|
-
off += len;
|
|
2413
|
-
out[off] = 13;
|
|
2414
|
-
out[off + 1] = 10;
|
|
2415
|
-
off += 2;
|
|
2416
2363
|
}
|
|
2417
|
-
return
|
|
2364
|
+
return set;
|
|
2418
2365
|
}
|
|
2419
|
-
function
|
|
2420
|
-
|
|
2421
|
-
if (
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2366
|
+
function parseCron(expr) {
|
|
2367
|
+
const parts = expr.trim().split(/\s+/);
|
|
2368
|
+
if (parts.length !== 5) {
|
|
2369
|
+
throw new Error(`cron: expected 5 fields (min hour dom month dow), got ${parts.length}: '${expr}'`);
|
|
2370
|
+
}
|
|
2371
|
+
const fields = parts.map((p, i) => parseField(p, i));
|
|
2372
|
+
return {
|
|
2373
|
+
fields: fields.map((values) => ({ values })),
|
|
2374
|
+
domExplicit: fields[2] !== null,
|
|
2375
|
+
dowExplicit: fields[4] !== null
|
|
2376
|
+
};
|
|
2425
2377
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2378
|
+
function fieldMatches(values, v) {
|
|
2379
|
+
if (values === null) return true;
|
|
2380
|
+
return values.has(v);
|
|
2381
|
+
}
|
|
2382
|
+
function dayMatches(expr, dom, dow) {
|
|
2383
|
+
const domOk = fieldMatches(expr.fields[2].values, dom);
|
|
2384
|
+
const dowOk = fieldMatches(expr.fields[4].values, dow);
|
|
2385
|
+
if (expr.domExplicit && expr.dowExplicit) return domOk || dowOk;
|
|
2386
|
+
if (expr.domExplicit) return domOk;
|
|
2387
|
+
if (expr.dowExplicit) return dowOk;
|
|
2388
|
+
return true;
|
|
2389
|
+
}
|
|
2390
|
+
function nextMatch(values, v, min, max) {
|
|
2391
|
+
if (values === null) return v;
|
|
2392
|
+
for (let x = v; x <= max; x++) if (values.has(x)) return x;
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
function nextRun(expr, from) {
|
|
2396
|
+
const d = new Date(from.getTime());
|
|
2397
|
+
d.setSeconds(0, 0);
|
|
2398
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
2399
|
+
const deadline = from.getTime() + 5 * 366 * 24 * 3600 * 1e3;
|
|
2400
|
+
while (d.getTime() <= deadline) {
|
|
2401
|
+
const year = d.getFullYear();
|
|
2402
|
+
const month = d.getMonth() + 1;
|
|
2403
|
+
const dom = d.getDate();
|
|
2404
|
+
const dow = d.getDay();
|
|
2405
|
+
if (!fieldMatches(expr.fields[3].values, month)) {
|
|
2406
|
+
d.setMonth(month, 1);
|
|
2407
|
+
d.setHours(0, 0, 0, 0);
|
|
2408
|
+
d.setMonth(month);
|
|
2409
|
+
continue;
|
|
2443
2410
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
const out = [];
|
|
2449
|
-
while (true) {
|
|
2450
|
-
const saved = this.off;
|
|
2451
|
-
try {
|
|
2452
|
-
out.push(this.parseValue());
|
|
2453
|
-
} catch (e) {
|
|
2454
|
-
this.off = saved;
|
|
2455
|
-
if (e instanceof IncompleteError) break;
|
|
2456
|
-
throw e;
|
|
2457
|
-
}
|
|
2411
|
+
if (!dayMatches(expr, dom, dow)) {
|
|
2412
|
+
d.setDate(dom + 1);
|
|
2413
|
+
d.setHours(0, 0, 0, 0);
|
|
2414
|
+
continue;
|
|
2458
2415
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
const rest = this.buf.length - this.off;
|
|
2466
|
-
if (rest === 0) {
|
|
2467
|
-
this.buf = chunk;
|
|
2468
|
-
this.off = 0;
|
|
2469
|
-
return;
|
|
2416
|
+
const hour = d.getHours();
|
|
2417
|
+
const nextHour = nextMatch(expr.fields[1].values, hour, 0, 23);
|
|
2418
|
+
if (nextHour === null) {
|
|
2419
|
+
d.setDate(dom + 1);
|
|
2420
|
+
d.setHours(0, 0, 0, 0);
|
|
2421
|
+
continue;
|
|
2470
2422
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
this.buf = merged;
|
|
2475
|
-
this.off = 0;
|
|
2476
|
-
}
|
|
2477
|
-
/** 全部消费后压缩(释放底层) */
|
|
2478
|
-
compact() {
|
|
2479
|
-
if (this.off === this.buf.length) {
|
|
2480
|
-
this.buf = new Uint8Array(0);
|
|
2481
|
-
this.off = 0;
|
|
2482
|
-
} else if (this.off > 0) {
|
|
2483
|
-
this.buf = this.buf.subarray(this.off);
|
|
2484
|
-
this.off = 0;
|
|
2423
|
+
if (nextHour !== hour) {
|
|
2424
|
+
d.setHours(nextHour, 0, 0, 0);
|
|
2425
|
+
continue;
|
|
2485
2426
|
}
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
if (
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
switch (type) {
|
|
2492
|
-
case "+": {
|
|
2493
|
-
const line = this.readLine();
|
|
2494
|
-
return line;
|
|
2495
|
-
}
|
|
2496
|
-
case "-": {
|
|
2497
|
-
const line = this.readLine();
|
|
2498
|
-
return new RespError(line);
|
|
2499
|
-
}
|
|
2500
|
-
case ":": {
|
|
2501
|
-
return this.readInt();
|
|
2502
|
-
}
|
|
2503
|
-
case "$": {
|
|
2504
|
-
const len = this.readInt();
|
|
2505
|
-
if (len === -1) return null;
|
|
2506
|
-
return this.readBulkBytes(len);
|
|
2507
|
-
}
|
|
2508
|
-
case "*": {
|
|
2509
|
-
const count = this.readInt();
|
|
2510
|
-
if (count === -1) return null;
|
|
2511
|
-
const items = [];
|
|
2512
|
-
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
2513
|
-
return items;
|
|
2514
|
-
}
|
|
2515
|
-
default:
|
|
2516
|
-
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
/**
|
|
2520
|
-
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
2521
|
-
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
2522
|
-
*/
|
|
2523
|
-
readInt() {
|
|
2524
|
-
const buf = this.buf;
|
|
2525
|
-
let i = this.off;
|
|
2526
|
-
let neg = false;
|
|
2527
|
-
if (i < buf.length && buf[i] === 45) {
|
|
2528
|
-
neg = true;
|
|
2529
|
-
i++;
|
|
2530
|
-
}
|
|
2531
|
-
let n = 0;
|
|
2532
|
-
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
|
|
2533
|
-
n = n * 10 + (buf[i] - 48);
|
|
2534
|
-
i++;
|
|
2427
|
+
const minute = d.getMinutes();
|
|
2428
|
+
const nextMinute = nextMatch(expr.fields[0].values, minute, 0, 59);
|
|
2429
|
+
if (nextMinute === null) {
|
|
2430
|
+
d.setHours(hour + 1, 0, 0, 0);
|
|
2431
|
+
continue;
|
|
2535
2432
|
}
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2538
|
-
|
|
2433
|
+
if (nextMinute !== minute) {
|
|
2434
|
+
d.setMinutes(nextMinute, 0, 0);
|
|
2435
|
+
continue;
|
|
2539
2436
|
}
|
|
2540
|
-
|
|
2541
|
-
}
|
|
2542
|
-
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
2543
|
-
readLine() {
|
|
2544
|
-
const idx = indexOfCRLF(this.buf, this.off);
|
|
2545
|
-
if (idx === -1) throw new IncompleteError();
|
|
2546
|
-
const line = _decoder.decode(this.buf.subarray(this.off, idx));
|
|
2547
|
-
this.off = idx + 2;
|
|
2548
|
-
return line;
|
|
2549
|
-
}
|
|
2550
|
-
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
2551
|
-
readBulkBytes(len) {
|
|
2552
|
-
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
2553
|
-
const value = this.buf.subarray(this.off, this.off + len);
|
|
2554
|
-
this.off += len + 2;
|
|
2555
|
-
return value;
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
function indexOfCRLF(buf, from = 0) {
|
|
2559
|
-
let i = buf.indexOf(13, from);
|
|
2560
|
-
while (i !== -1) {
|
|
2561
|
-
if (i + 1 < buf.length && buf[i + 1] === 10) return i;
|
|
2562
|
-
i = buf.indexOf(13, i + 1);
|
|
2437
|
+
return d;
|
|
2563
2438
|
}
|
|
2564
|
-
|
|
2439
|
+
throw new Error("cron: no next run within 5 years (invalid expression?)");
|
|
2565
2440
|
}
|
|
2566
2441
|
|
|
2567
|
-
// src/
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
offlineQueue = [];
|
|
2579
|
-
subs = /* @__PURE__ */ new Map();
|
|
2580
|
-
psubs = /* @__PURE__ */ new Map();
|
|
2581
|
-
status = "idle";
|
|
2582
|
-
retries = 0;
|
|
2583
|
-
reconnectTimer = null;
|
|
2584
|
-
socketTimeoutTimer = null;
|
|
2585
|
-
connectPromise = null;
|
|
2586
|
-
closed = false;
|
|
2587
|
-
connectedOnce = false;
|
|
2588
|
-
constructor(options = {}) {
|
|
2589
|
-
this.opts = {
|
|
2590
|
-
host: options.host ?? "127.0.0.1",
|
|
2591
|
-
port: options.port ?? 6379,
|
|
2592
|
-
retryDelayMs: options.retryDelayMs ?? 100,
|
|
2593
|
-
maxRetries: options.maxRetries ?? 10,
|
|
2594
|
-
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
2595
|
-
maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
|
|
2596
|
-
commandTimeoutMs: options.commandTimeoutMs ?? 0,
|
|
2597
|
-
socketTimeoutMs: options.socketTimeoutMs ?? 0,
|
|
2598
|
-
onCommand: options.onCommand
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
2602
|
-
connect() {
|
|
2603
|
-
if (this.connectPromise) return this.connectPromise;
|
|
2604
|
-
this.closed = false;
|
|
2605
|
-
this.connectPromise = new Promise((resolve3, reject) => {
|
|
2606
|
-
this.openSocket();
|
|
2607
|
-
this.onceReady = () => resolve3();
|
|
2608
|
-
this.onceFailed = (err) => reject(err);
|
|
2609
|
-
}).finally(() => {
|
|
2610
|
-
this.connectPromise = null;
|
|
2611
|
-
this.onceReady = void 0;
|
|
2612
|
-
this.onceFailed = void 0;
|
|
2613
|
-
});
|
|
2614
|
-
return this.connectPromise;
|
|
2442
|
+
// src/scheduler/index.ts
|
|
2443
|
+
function scheduler(options) {
|
|
2444
|
+
const prefix = options.prefix ?? "wf:sched:";
|
|
2445
|
+
const tickMs = options.tickMs ?? 1e3;
|
|
2446
|
+
const delayedKey = `${prefix}delayed`;
|
|
2447
|
+
const cronsKey = `${prefix}crons`;
|
|
2448
|
+
const queueModule = options.queue;
|
|
2449
|
+
let connPromise = null;
|
|
2450
|
+
function getConn() {
|
|
2451
|
+
if (!connPromise) connPromise = options.redis.createConnection();
|
|
2452
|
+
return connPromise;
|
|
2615
2453
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
this.onceReady?.();
|
|
2633
|
-
});
|
|
2634
|
-
sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
|
|
2635
|
-
sock.on("error", (err) => {
|
|
2636
|
-
if (this.status === "connecting") {
|
|
2637
|
-
this.handleDisconnect(err);
|
|
2454
|
+
let running = false;
|
|
2455
|
+
let tickTimer = null;
|
|
2456
|
+
async function tick() {
|
|
2457
|
+
await tickCrons();
|
|
2458
|
+
let due;
|
|
2459
|
+
try {
|
|
2460
|
+
due = await (await getConn()).command("ZRANGEBYSCORE", delayedKey, 0, Date.now());
|
|
2461
|
+
} catch {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
for (const member of due) {
|
|
2465
|
+
let removed;
|
|
2466
|
+
try {
|
|
2467
|
+
removed = await (await getConn()).command("ZREM", delayedKey, member);
|
|
2468
|
+
} catch {
|
|
2469
|
+
return;
|
|
2638
2470
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2471
|
+
if (removed !== 1) continue;
|
|
2472
|
+
try {
|
|
2473
|
+
const task = JSON.parse(member);
|
|
2474
|
+
await queueModule.queue.add(task.name, task.data);
|
|
2475
|
+
} catch (e) {
|
|
2476
|
+
console.error("[scheduler] enqueue:", e instanceof Error ? e.message : e);
|
|
2644
2477
|
}
|
|
2645
|
-
});
|
|
2646
|
-
}
|
|
2647
|
-
handleDisconnect(err) {
|
|
2648
|
-
const queue2 = this.pending.slice(this.pendingHead);
|
|
2649
|
-
this.pending = [];
|
|
2650
|
-
this.pendingHead = 0;
|
|
2651
|
-
for (const p of queue2) p.reject(err);
|
|
2652
|
-
this.clearSocketTimeout();
|
|
2653
|
-
if (this.closed || this.status === "closed") return;
|
|
2654
|
-
this.retries++;
|
|
2655
|
-
this.status = "connecting";
|
|
2656
|
-
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
2657
|
-
this.status = "closed";
|
|
2658
|
-
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
2659
|
-
this.onceFailed?.(failErr);
|
|
2660
|
-
return;
|
|
2661
2478
|
}
|
|
2662
|
-
const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
|
|
2663
|
-
this.reconnectTimer = setTimeout(() => {
|
|
2664
|
-
this.reconnectTimer = null;
|
|
2665
|
-
this.openSocket();
|
|
2666
|
-
}, delay);
|
|
2667
2479
|
}
|
|
2668
|
-
|
|
2480
|
+
async function tickCrons() {
|
|
2481
|
+
let crons;
|
|
2669
2482
|
try {
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2483
|
+
crons = await (await getConn()).command("HGETALL", cronsKey);
|
|
2484
|
+
} catch {
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const now = Date.now();
|
|
2488
|
+
const entries = crons;
|
|
2489
|
+
for (let i = 0; i + 1 < entries.length; i += 2) {
|
|
2490
|
+
const field = entries[i];
|
|
2491
|
+
const value = entries[i + 1];
|
|
2492
|
+
let def;
|
|
2493
|
+
try {
|
|
2494
|
+
def = JSON.parse(value);
|
|
2495
|
+
} catch {
|
|
2496
|
+
continue;
|
|
2682
2497
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2498
|
+
const parsed = parseCron(def.expr);
|
|
2499
|
+
let next = def.nextRunAt;
|
|
2500
|
+
while (next <= now) {
|
|
2501
|
+
const ts = next;
|
|
2502
|
+
const member = JSON.stringify({ id: `cron:${field}:${ts}`, name: def.name, data: def.data });
|
|
2503
|
+
try {
|
|
2504
|
+
await (await getConn()).command("ZADD", delayedKey, "NX", ts, member);
|
|
2505
|
+
} catch {
|
|
2506
|
+
break;
|
|
2507
|
+
}
|
|
2508
|
+
next = nextRun(parsed, new Date(ts)).getTime();
|
|
2686
2509
|
}
|
|
2687
|
-
if (
|
|
2688
|
-
|
|
2689
|
-
else this.clearSocketTimeout();
|
|
2510
|
+
if (next !== def.nextRunAt) {
|
|
2511
|
+
await (await getConn()).command("HSET", cronsKey, field, JSON.stringify({ ...def, nextRunAt: next }));
|
|
2690
2512
|
}
|
|
2691
|
-
} catch (e) {
|
|
2692
|
-
const queue2 = this.pending.slice(this.pendingHead);
|
|
2693
|
-
this.pending = [];
|
|
2694
|
-
this.pendingHead = 0;
|
|
2695
|
-
for (const q of queue2) q.reject(e);
|
|
2696
|
-
this.socket?.destroy();
|
|
2697
2513
|
}
|
|
2698
2514
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2515
|
+
const cron = async (expr, name, data) => {
|
|
2516
|
+
const parsed = parseCron(expr);
|
|
2517
|
+
const firstRun = nextRun(parsed, /* @__PURE__ */ new Date());
|
|
2518
|
+
const def = JSON.stringify({ expr, name, data, nextRunAt: firstRun.getTime() });
|
|
2519
|
+
await (await getConn()).command("HSET", cronsKey, name, def);
|
|
2520
|
+
};
|
|
2521
|
+
const cancelCron = async (name) => {
|
|
2522
|
+
const removed = await (await getConn()).command("HDEL", cronsKey, name);
|
|
2523
|
+
try {
|
|
2524
|
+
const pending = await (await getConn()).command("ZRANGE", delayedKey, 0, -1);
|
|
2525
|
+
for (const member of pending) {
|
|
2526
|
+
if (member.includes(`"id":"cron:${name}:`)) {
|
|
2527
|
+
await (await getConn()).command("ZREM", delayedKey, member);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
} catch {
|
|
2708
2531
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2532
|
+
return removed === 1;
|
|
2533
|
+
};
|
|
2534
|
+
async function start() {
|
|
2535
|
+
if (running) return;
|
|
2536
|
+
running = true;
|
|
2537
|
+
await getConn();
|
|
2538
|
+
await tick();
|
|
2539
|
+
tickTimer = setInterval(() => {
|
|
2540
|
+
tick().catch((e) => console.error("[scheduler] tick:", e instanceof Error ? e.message : e));
|
|
2541
|
+
}, tickMs);
|
|
2542
|
+
tickTimer.unref?.();
|
|
2543
|
+
}
|
|
2544
|
+
const schedule = async (name, data, opts = {}) => {
|
|
2545
|
+
const delayMs = opts.delayMs ?? 0;
|
|
2546
|
+
const runAt = opts.when ? opts.when.getTime() : Date.now() + delayMs;
|
|
2547
|
+
if (!Number.isFinite(runAt)) throw new Error("scheduler: invalid when/delayMs");
|
|
2548
|
+
const id = randomUUID2();
|
|
2549
|
+
const member = JSON.stringify({ id, name, data });
|
|
2550
|
+
await (await getConn()).command("ZADD", delayedKey, runAt, member);
|
|
2551
|
+
return { id };
|
|
2552
|
+
};
|
|
2553
|
+
const cancelSchedule = async (id) => {
|
|
2554
|
+
try {
|
|
2555
|
+
const pending = await (await getConn()).command("ZRANGE", delayedKey, 0, -1);
|
|
2556
|
+
for (const member of pending) {
|
|
2557
|
+
if (member.includes(`"id":"${id}"`)) {
|
|
2558
|
+
const removed = await (await getConn()).command("ZREM", delayedKey, member);
|
|
2559
|
+
if (removed === 1) return true;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
} catch {
|
|
2711
2563
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2564
|
+
return false;
|
|
2565
|
+
};
|
|
2566
|
+
const mw = (async (req, ctx, next) => {
|
|
2567
|
+
ctx.schedule = schedule;
|
|
2568
|
+
ctx.cron = cron;
|
|
2569
|
+
ctx.cancelCron = cancelCron;
|
|
2570
|
+
ctx.cancelSchedule = cancelSchedule;
|
|
2571
|
+
return next(req, ctx);
|
|
2572
|
+
});
|
|
2573
|
+
mw.__meta = { injects: ["schedule", "cron", "cancelCron", "cancelSchedule"], depends: ["queue"] };
|
|
2574
|
+
mw.schedule = schedule;
|
|
2575
|
+
mw.cron = cron;
|
|
2576
|
+
mw.cancelCron = cancelCron;
|
|
2577
|
+
mw.cancelSchedule = cancelSchedule;
|
|
2578
|
+
mw.close = async () => {
|
|
2579
|
+
running = false;
|
|
2580
|
+
if (tickTimer) {
|
|
2581
|
+
clearInterval(tickTimer);
|
|
2582
|
+
tickTimer = null;
|
|
2714
2583
|
}
|
|
2715
|
-
|
|
2716
|
-
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
2584
|
+
if (connPromise) await connPromise.then((c) => c.close()).catch(() => {
|
|
2717
2585
|
});
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2586
|
+
};
|
|
2587
|
+
start().catch((e) => console.error("[scheduler] start:", e instanceof Error ? e.message : e));
|
|
2588
|
+
return mw;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// src/ai/client.ts
|
|
2592
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2593
|
+
|
|
2594
|
+
// src/ai/sse.ts
|
|
2595
|
+
function sseResponse(run, options) {
|
|
2596
|
+
const encoder = new TextEncoder();
|
|
2597
|
+
const stream = new ReadableStream({
|
|
2598
|
+
async start(controller) {
|
|
2599
|
+
const emit = (name, data) => {
|
|
2600
|
+
controller.enqueue(encoder.encode(`event: ${name}
|
|
2601
|
+
data: ${JSON.stringify(data)}
|
|
2602
|
+
|
|
2603
|
+
`));
|
|
2727
2604
|
};
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2605
|
+
try {
|
|
2606
|
+
await run(emit);
|
|
2607
|
+
controller.close();
|
|
2608
|
+
} catch (err) {
|
|
2609
|
+
try {
|
|
2610
|
+
emit("wf:error", {
|
|
2611
|
+
code: "provider_error",
|
|
2612
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2613
|
+
});
|
|
2614
|
+
controller.close();
|
|
2615
|
+
} catch {
|
|
2616
|
+
controller.error(err);
|
|
2736
2617
|
}
|
|
2737
|
-
});
|
|
2738
|
-
}).then(
|
|
2739
|
-
(v) => {
|
|
2740
|
-
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
2741
|
-
return v;
|
|
2742
|
-
},
|
|
2743
|
-
(e) => {
|
|
2744
|
-
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
2745
|
-
throw e;
|
|
2746
2618
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
armTimeout(p) {
|
|
2751
|
-
const ms = this.opts.commandTimeoutMs;
|
|
2752
|
-
if (ms <= 0) return;
|
|
2753
|
-
p.timer = setTimeout(() => {
|
|
2754
|
-
p.timedOut = true;
|
|
2755
|
-
if (p.blocking) p.resolve(null);
|
|
2756
|
-
else p.reject(new TimeoutError("redis: command timeout", ms));
|
|
2757
|
-
}, ms);
|
|
2758
|
-
}
|
|
2759
|
-
/** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
|
|
2760
|
-
armSocketTimeout() {
|
|
2761
|
-
const ms = this.opts.socketTimeoutMs;
|
|
2762
|
-
if (ms <= 0) return;
|
|
2763
|
-
this.clearSocketTimeout();
|
|
2764
|
-
this.socketTimeoutTimer = setTimeout(() => {
|
|
2765
|
-
this.socketTimeoutTimer = null;
|
|
2766
|
-
const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
|
|
2767
|
-
const queue2 = this.pending.slice(this.pendingHead);
|
|
2768
|
-
this.pending = [];
|
|
2769
|
-
this.pendingHead = 0;
|
|
2770
|
-
for (const q of queue2) q.reject(err);
|
|
2771
|
-
this.clearSocketTimeout();
|
|
2772
|
-
this.socket?.destroy();
|
|
2773
|
-
}, ms);
|
|
2774
|
-
}
|
|
2775
|
-
clearSocketTimeout() {
|
|
2776
|
-
if (this.socketTimeoutTimer) {
|
|
2777
|
-
clearTimeout(this.socketTimeoutTimer);
|
|
2778
|
-
this.socketTimeoutTimer = null;
|
|
2619
|
+
},
|
|
2620
|
+
cancel() {
|
|
2621
|
+
options?.onAbort?.();
|
|
2779
2622
|
}
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2623
|
+
});
|
|
2624
|
+
return new Response(stream, {
|
|
2625
|
+
headers: {
|
|
2626
|
+
"Content-Type": "text/event-stream",
|
|
2627
|
+
"Cache-Control": "no-cache",
|
|
2628
|
+
Connection: "keep-alive"
|
|
2786
2629
|
}
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/ai/client.ts
|
|
2634
|
+
var AiError = class extends Error {
|
|
2635
|
+
code;
|
|
2636
|
+
constructor(code, message) {
|
|
2637
|
+
super(message);
|
|
2638
|
+
this.name = "AiError";
|
|
2639
|
+
this.code = code;
|
|
2787
2640
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2641
|
+
};
|
|
2642
|
+
async function* parseProviderSse(stream) {
|
|
2643
|
+
const reader = stream.getReader();
|
|
2644
|
+
const decoder = new TextDecoder();
|
|
2645
|
+
let buffer = "";
|
|
2646
|
+
try {
|
|
2647
|
+
while (true) {
|
|
2648
|
+
const { done, value } = await reader.read();
|
|
2649
|
+
if (done) break;
|
|
2650
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2651
|
+
const lines = buffer.split("\n");
|
|
2652
|
+
buffer = lines.pop() ?? "";
|
|
2653
|
+
for (const line of lines) {
|
|
2654
|
+
const trimmed = line.trim();
|
|
2655
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
2656
|
+
const data = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed;
|
|
2657
|
+
if (data === "[DONE]") return;
|
|
2658
|
+
try {
|
|
2659
|
+
yield JSON.parse(data);
|
|
2660
|
+
} catch {
|
|
2803
2661
|
}
|
|
2804
|
-
};
|
|
2805
|
-
for (let i = 0; i < count; i++) {
|
|
2806
|
-
const p = {
|
|
2807
|
-
// 正常响应与错误响应(RespError)都作为结果值收集——管道语义
|
|
2808
|
-
resolve: (v) => {
|
|
2809
|
-
results.push(v);
|
|
2810
|
-
maybeResolve();
|
|
2811
|
-
},
|
|
2812
|
-
reject: (e) => {
|
|
2813
|
-
results.push(e);
|
|
2814
|
-
maybeResolve();
|
|
2815
|
-
}
|
|
2816
|
-
};
|
|
2817
|
-
batchPending.push(p);
|
|
2818
|
-
this.pending.push(p);
|
|
2819
|
-
}
|
|
2820
|
-
let batchTimer;
|
|
2821
|
-
const ms = this.opts.commandTimeoutMs;
|
|
2822
|
-
if (ms > 0) {
|
|
2823
|
-
batchTimer = setTimeout(() => {
|
|
2824
|
-
settled = true;
|
|
2825
|
-
for (const p of batchPending) p.timedOut = true;
|
|
2826
|
-
reject(new TimeoutError("redis: batch timeout", ms));
|
|
2827
|
-
}, ms);
|
|
2828
2662
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
}
|
|
2833
|
-
/** 订阅频道:回调式(channel, message) */
|
|
2834
|
-
async subscribe(channel, fn) {
|
|
2835
|
-
this.subs.set(channel, fn);
|
|
2836
|
-
await this.command("SUBSCRIBE", channel);
|
|
2663
|
+
}
|
|
2664
|
+
} finally {
|
|
2665
|
+
reader.releaseLock();
|
|
2837
2666
|
}
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2667
|
+
}
|
|
2668
|
+
async function providerError(res) {
|
|
2669
|
+
let message = "";
|
|
2670
|
+
try {
|
|
2671
|
+
const body = await res.json();
|
|
2672
|
+
message = body?.error?.message ?? JSON.stringify(body);
|
|
2673
|
+
} catch {
|
|
2674
|
+
message = await res.text().catch(() => "");
|
|
2842
2675
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2676
|
+
const status = res.status;
|
|
2677
|
+
let code;
|
|
2678
|
+
if (status === 401 || status === 403) code = "auth_failed";
|
|
2679
|
+
else if (status === 429) code = "rate_limited";
|
|
2680
|
+
else if (status >= 500) code = "provider_error";
|
|
2681
|
+
else if (/context|token|length/i.test(message)) code = "context_length";
|
|
2682
|
+
else if (status === 400) code = "invalid_request";
|
|
2683
|
+
else code = "provider_error";
|
|
2684
|
+
return { code, message: message || `provider error (${status})` };
|
|
2685
|
+
}
|
|
2686
|
+
function createEmbeddingClient(ebd) {
|
|
2687
|
+
const apiKey = ebd?.apiKey ?? process.env.DASHSCOPE_API_KEY ?? "";
|
|
2688
|
+
const configured = !!(ebd?.apiKey || process.env.DASHSCOPE_API_KEY);
|
|
2689
|
+
const baseUrl = ebd?.baseUrl ?? process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
2690
|
+
const defaultModel = ebd?.defaultModel ?? process.env.DASHSCOPE_EMBEDDING_MODEL ?? "text-embedding-v4";
|
|
2691
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
|
|
2692
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
|
|
2693
|
+
async function embedMany(texts) {
|
|
2694
|
+
if (!configured) {
|
|
2695
|
+
throw new AiError("unsupported", "ai embedding: \u672A\u914D\u7F6E\u2014\u2014\u4F20 ai({ embedding }) \u6216\u8BBE DASHSCOPE_API_KEY\uFF08DeepSeek \u65E0 embedding API\uFF0C\u9700\u72EC\u7ACB provider\uFF09");
|
|
2852
2696
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
async close() {
|
|
2856
|
-
this.closed = true;
|
|
2857
|
-
this.status = "closed";
|
|
2858
|
-
if (this.reconnectTimer) {
|
|
2859
|
-
clearTimeout(this.reconnectTimer);
|
|
2860
|
-
this.reconnectTimer = null;
|
|
2697
|
+
if (!apiKey) {
|
|
2698
|
+
throw new AiError("auth_failed", "ai embedding: DASHSCOPE_API_KEY \u672A\u8BBE\u7F6E");
|
|
2861
2699
|
}
|
|
2862
|
-
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
};
|
|
2878
|
-
|
|
2879
|
-
// src/scheduler/cron.ts
|
|
2880
|
-
var RANGES = [
|
|
2881
|
-
[0, 59],
|
|
2882
|
-
// 分
|
|
2883
|
-
[0, 23],
|
|
2884
|
-
// 时
|
|
2885
|
-
[1, 31],
|
|
2886
|
-
// 日
|
|
2887
|
-
[1, 12],
|
|
2888
|
-
// 月
|
|
2889
|
-
[0, 6]
|
|
2890
|
-
// 周
|
|
2891
|
-
];
|
|
2892
|
-
function parseField(field, idx) {
|
|
2893
|
-
const [min, max] = RANGES[idx];
|
|
2894
|
-
const name = ["minute", "hour", "day", "month", "weekday"][idx];
|
|
2895
|
-
if (field === "*") return null;
|
|
2896
|
-
if (field.startsWith("*/")) {
|
|
2897
|
-
const n = Number(field.slice(2));
|
|
2898
|
-
if (!Number.isInteger(n) || n <= 0 || n > max) {
|
|
2899
|
-
throw new Error(`cron: invalid step '${field}' in ${name} field (must be 1..${max})`);
|
|
2700
|
+
const controller = new AbortController();
|
|
2701
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
2702
|
+
let res;
|
|
2703
|
+
try {
|
|
2704
|
+
res = await fetch(endpoint, {
|
|
2705
|
+
method: "POST",
|
|
2706
|
+
headers,
|
|
2707
|
+
body: JSON.stringify({ model: defaultModel, input: texts }),
|
|
2708
|
+
signal: controller.signal
|
|
2709
|
+
});
|
|
2710
|
+
} catch (err) {
|
|
2711
|
+
if (controller.signal.aborted) throw new AiError("provider_error", "embedding \u8BF7\u6C42\u8D85\u65F6\uFF083s\uFF09");
|
|
2712
|
+
throw new AiError("provider_error", err instanceof Error ? err.message : String(err));
|
|
2713
|
+
} finally {
|
|
2714
|
+
clearTimeout(timer);
|
|
2900
2715
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
}
|
|
2905
|
-
const set = /* @__PURE__ */ new Set();
|
|
2906
|
-
for (const part of field.split(",")) {
|
|
2907
|
-
if (part === "") throw new Error(`cron: empty item in '${field}' (${name})`);
|
|
2908
|
-
if (part.includes("-")) {
|
|
2909
|
-
const [rangePart, stepPart] = part.split("/");
|
|
2910
|
-
const step = stepPart !== void 0 ? Number(stepPart) : 1;
|
|
2911
|
-
const [a, b] = rangePart.split("-").map(Number);
|
|
2912
|
-
if (!Number.isInteger(a) || !Number.isInteger(b) || a < min || b > max || a > b) {
|
|
2913
|
-
throw new Error(`cron: invalid range '${part}' in ${name} field (${min}..${max})`);
|
|
2914
|
-
}
|
|
2915
|
-
if (!Number.isInteger(step) || step <= 0) throw new Error(`cron: invalid step in '${part}'`);
|
|
2916
|
-
for (let v = a; v <= b; v += step) set.add(v);
|
|
2917
|
-
} else {
|
|
2918
|
-
const v = Number(part);
|
|
2919
|
-
if (!Number.isInteger(v) || v < min || v > max) {
|
|
2920
|
-
throw new Error(`cron: value '${part}' out of range ${min}..${max} in ${name} field`);
|
|
2921
|
-
}
|
|
2922
|
-
set.add(v);
|
|
2716
|
+
if (!res.ok) {
|
|
2717
|
+
const { code, message } = await providerError(res);
|
|
2718
|
+
throw new AiError(code, message);
|
|
2923
2719
|
}
|
|
2720
|
+
const data = await res.json();
|
|
2721
|
+
data.data.sort((a, b) => a.index - b.index);
|
|
2722
|
+
return data.data.map((item) => item.embedding);
|
|
2924
2723
|
}
|
|
2925
|
-
return set;
|
|
2926
|
-
}
|
|
2927
|
-
function parseCron(expr) {
|
|
2928
|
-
const parts = expr.trim().split(/\s+/);
|
|
2929
|
-
if (parts.length !== 5) {
|
|
2930
|
-
throw new Error(`cron: expected 5 fields (min hour dom month dow), got ${parts.length}: '${expr}'`);
|
|
2931
|
-
}
|
|
2932
|
-
const fields = parts.map((p, i) => parseField(p, i));
|
|
2933
2724
|
return {
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2725
|
+
async embed(text) {
|
|
2726
|
+
const results = await embedMany([text]);
|
|
2727
|
+
return results[0];
|
|
2728
|
+
},
|
|
2729
|
+
embedMany
|
|
2937
2730
|
};
|
|
2938
2731
|
}
|
|
2939
|
-
function
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
}
|
|
2951
|
-
function
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2732
|
+
function createAiClient(opts) {
|
|
2733
|
+
const endpoint = `${opts.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
2734
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}` };
|
|
2735
|
+
const embedding = createEmbeddingClient(opts.embedding);
|
|
2736
|
+
const approvals = /* @__PURE__ */ new Map();
|
|
2737
|
+
function approve(response) {
|
|
2738
|
+
const resolve3 = approvals.get(response.id);
|
|
2739
|
+
if (!resolve3) return false;
|
|
2740
|
+
approvals.delete(response.id);
|
|
2741
|
+
resolve3(response);
|
|
2742
|
+
return true;
|
|
2743
|
+
}
|
|
2744
|
+
async function waitApproval(req, emit, timeoutMs = DEFAULT_APPROVAL_TIMEOUT) {
|
|
2745
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
2746
|
+
emit("wf:approval_request", { ...req, expiresAt });
|
|
2747
|
+
return new Promise((resolve3) => {
|
|
2748
|
+
const timer = setTimeout(() => {
|
|
2749
|
+
if (approvals.has(req.id)) {
|
|
2750
|
+
approvals.delete(req.id);
|
|
2751
|
+
resolve3({ id: req.id, decision: "rejected" });
|
|
2752
|
+
}
|
|
2753
|
+
}, timeoutMs);
|
|
2754
|
+
approvals.set(req.id, (resp) => {
|
|
2755
|
+
clearTimeout(timer);
|
|
2756
|
+
resolve3(resp);
|
|
2757
|
+
});
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
function aggregateToolCalls(chunks) {
|
|
2761
|
+
const calls = [];
|
|
2762
|
+
for (const chunk of chunks) {
|
|
2763
|
+
const delta = chunk.choices[0]?.delta;
|
|
2764
|
+
if (!delta?.tool_calls) continue;
|
|
2765
|
+
for (const tc of delta.tool_calls) {
|
|
2766
|
+
if (tc.id) {
|
|
2767
|
+
calls.push(tc);
|
|
2768
|
+
} else if (calls.length > 0) {
|
|
2769
|
+
const last = calls[calls.length - 1];
|
|
2770
|
+
if (tc.function?.arguments) last.function.arguments += tc.function.arguments;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2976
2773
|
}
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2774
|
+
return calls;
|
|
2775
|
+
}
|
|
2776
|
+
async function chat(params, options) {
|
|
2777
|
+
const res = await fetch(endpoint, {
|
|
2778
|
+
method: "POST",
|
|
2779
|
+
headers,
|
|
2780
|
+
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: false }),
|
|
2781
|
+
signal: options?.signal
|
|
2782
|
+
});
|
|
2783
|
+
if (!res.ok) {
|
|
2784
|
+
const { code, message } = await providerError(res);
|
|
2785
|
+
throw new AiError(code, message);
|
|
2983
2786
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2787
|
+
return res.json();
|
|
2788
|
+
}
|
|
2789
|
+
function stream(params, options) {
|
|
2790
|
+
const controller = new AbortController();
|
|
2791
|
+
const external = options?.signal;
|
|
2792
|
+
if (external) {
|
|
2793
|
+
if (external.aborted) controller.abort();
|
|
2794
|
+
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
2987
2795
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2796
|
+
return sseResponse(
|
|
2797
|
+
async (emit) => {
|
|
2798
|
+
emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
|
|
2799
|
+
await streamStep(params, {
|
|
2800
|
+
emit,
|
|
2801
|
+
signal: controller.signal,
|
|
2802
|
+
onFinish: (r) => emit("wf:done", { content: r.content, usage: r.usage })
|
|
2803
|
+
});
|
|
2804
|
+
},
|
|
2805
|
+
{ onAbort: () => controller.abort() }
|
|
2806
|
+
);
|
|
2999
2807
|
}
|
|
3000
|
-
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
// src/scheduler/index.ts
|
|
3004
|
-
function parseUrl(options) {
|
|
3005
|
-
const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
|
|
3006
|
-
const u = new URL(url);
|
|
3007
|
-
return { host: u.hostname, port: Number(u.port || 6379) };
|
|
3008
|
-
}
|
|
3009
|
-
function scheduler(options) {
|
|
3010
|
-
const prefix = options.prefix ?? "wf:sched:";
|
|
3011
|
-
const tickMs = options.tickMs ?? 1e3;
|
|
3012
|
-
const delayedKey = `${prefix}delayed`;
|
|
3013
|
-
const cronsKey = `${prefix}crons`;
|
|
3014
|
-
const connOpts = parseUrl(options);
|
|
3015
|
-
const queueModule = options.queue;
|
|
3016
|
-
const conn = new RedisConnection(connOpts);
|
|
3017
|
-
let running = false;
|
|
3018
|
-
let tickTimer = null;
|
|
3019
|
-
async function tick() {
|
|
3020
|
-
await tickCrons();
|
|
3021
|
-
let due;
|
|
2808
|
+
async function streamStep(params, stepOpts) {
|
|
2809
|
+
const { emit, signal } = stepOpts;
|
|
2810
|
+
let res;
|
|
3022
2811
|
try {
|
|
3023
|
-
|
|
3024
|
-
|
|
2812
|
+
res = await fetch(endpoint, {
|
|
2813
|
+
method: "POST",
|
|
2814
|
+
headers,
|
|
2815
|
+
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: true }),
|
|
2816
|
+
signal
|
|
2817
|
+
});
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
if (signal?.aborted) return;
|
|
2820
|
+
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
3025
2821
|
return;
|
|
3026
2822
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
removed = await conn.command("ZREM", delayedKey, member);
|
|
3031
|
-
} catch {
|
|
3032
|
-
return;
|
|
3033
|
-
}
|
|
3034
|
-
if (removed !== 1) continue;
|
|
3035
|
-
try {
|
|
3036
|
-
const task = JSON.parse(member);
|
|
3037
|
-
await queueModule.queue.add(task.name, task.data);
|
|
3038
|
-
} catch (e) {
|
|
3039
|
-
console.error("[scheduler] enqueue:", e instanceof Error ? e.message : e);
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
async function tickCrons() {
|
|
3044
|
-
let crons;
|
|
3045
|
-
try {
|
|
3046
|
-
crons = await conn.command("HGETALL", cronsKey);
|
|
3047
|
-
} catch {
|
|
2823
|
+
if (!res.ok) {
|
|
2824
|
+
const { code, message } = await providerError(res);
|
|
2825
|
+
emit("wf:error", { code, message });
|
|
3048
2826
|
return;
|
|
3049
2827
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
const value = entries[i + 1];
|
|
3055
|
-
let def;
|
|
3056
|
-
try {
|
|
3057
|
-
def = JSON.parse(value);
|
|
3058
|
-
} catch {
|
|
3059
|
-
continue;
|
|
3060
|
-
}
|
|
3061
|
-
const parsed = parseCron(def.expr);
|
|
3062
|
-
let next = def.nextRunAt;
|
|
3063
|
-
while (next <= now) {
|
|
3064
|
-
const ts = next;
|
|
3065
|
-
const member = JSON.stringify({ id: `cron:${field}:${ts}`, name: def.name, data: def.data });
|
|
3066
|
-
try {
|
|
3067
|
-
await conn.command("ZADD", delayedKey, "NX", ts, member);
|
|
3068
|
-
} catch {
|
|
3069
|
-
break;
|
|
3070
|
-
}
|
|
3071
|
-
next = nextRun(parsed, new Date(ts)).getTime();
|
|
3072
|
-
}
|
|
3073
|
-
if (next !== def.nextRunAt) {
|
|
3074
|
-
await conn.command("HSET", cronsKey, field, JSON.stringify({ ...def, nextRunAt: next }));
|
|
3075
|
-
}
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
const cron = async (expr, name, data) => {
|
|
3079
|
-
const parsed = parseCron(expr);
|
|
3080
|
-
const firstRun = nextRun(parsed, /* @__PURE__ */ new Date());
|
|
3081
|
-
const def = JSON.stringify({ expr, name, data, nextRunAt: firstRun.getTime() });
|
|
3082
|
-
await conn.command("HSET", cronsKey, name, def);
|
|
3083
|
-
};
|
|
3084
|
-
const cancelCron = async (name) => {
|
|
3085
|
-
const removed = await conn.command("HDEL", cronsKey, name);
|
|
2828
|
+
let content = "";
|
|
2829
|
+
let reasoning = "";
|
|
2830
|
+
const chunks = [];
|
|
2831
|
+
let usage;
|
|
3086
2832
|
try {
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
2833
|
+
for await (const chunk of parseProviderSse(res.body)) {
|
|
2834
|
+
if (signal?.aborted) return;
|
|
2835
|
+
const delta = chunk.choices[0]?.delta;
|
|
2836
|
+
if (delta?.content) {
|
|
2837
|
+
content += delta.content;
|
|
2838
|
+
emit("wf:token", { text: delta.content });
|
|
3091
2839
|
}
|
|
2840
|
+
if (delta?.reasoning_content) reasoning += delta.reasoning_content;
|
|
2841
|
+
if (delta?.tool_calls) chunks.push(chunk);
|
|
2842
|
+
if (chunk.usage) usage = chunk.usage;
|
|
3092
2843
|
}
|
|
3093
|
-
} catch {
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
if (signal?.aborted) return;
|
|
2846
|
+
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
2847
|
+
return;
|
|
3094
2848
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
tickTimer = setInterval(() => {
|
|
3103
|
-
tick().catch((e) => console.error("[scheduler] tick:", e instanceof Error ? e.message : e));
|
|
3104
|
-
}, tickMs);
|
|
3105
|
-
tickTimer.unref?.();
|
|
3106
|
-
}
|
|
3107
|
-
const schedule = async (name, data, opts = {}) => {
|
|
3108
|
-
const delayMs = opts.delayMs ?? 0;
|
|
3109
|
-
const runAt = opts.when ? opts.when.getTime() : Date.now() + delayMs;
|
|
3110
|
-
if (!Number.isFinite(runAt)) throw new Error("scheduler: invalid when/delayMs");
|
|
3111
|
-
const id = randomUUID2();
|
|
3112
|
-
const member = JSON.stringify({ id, name, data });
|
|
3113
|
-
await conn.command("ZADD", delayedKey, runAt, member);
|
|
3114
|
-
return { id };
|
|
3115
|
-
};
|
|
3116
|
-
const cancelSchedule = async (id) => {
|
|
3117
|
-
try {
|
|
3118
|
-
const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
|
|
3119
|
-
for (const member of pending) {
|
|
3120
|
-
if (member.includes(`"id":"${id}"`)) {
|
|
3121
|
-
const removed = await conn.command("ZREM", delayedKey, member);
|
|
3122
|
-
if (removed === 1) return true;
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
} catch {
|
|
2849
|
+
const toolCalls = aggregateToolCalls(chunks);
|
|
2850
|
+
for (const tc of toolCalls) {
|
|
2851
|
+
emit("wf:tool_call", {
|
|
2852
|
+
id: tc.id,
|
|
2853
|
+
name: tc.function?.name ?? "",
|
|
2854
|
+
args: safeParseArgs(tc.function?.arguments ?? "")
|
|
2855
|
+
});
|
|
3126
2856
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
mw.__meta = { injects: ["schedule", "cron", "cancelCron", "cancelSchedule"], depends: ["queue"] };
|
|
3137
|
-
mw.schedule = schedule;
|
|
3138
|
-
mw.cron = cron;
|
|
3139
|
-
mw.cancelCron = cancelCron;
|
|
3140
|
-
mw.cancelSchedule = cancelSchedule;
|
|
3141
|
-
mw.close = async () => {
|
|
3142
|
-
running = false;
|
|
3143
|
-
if (tickTimer) {
|
|
3144
|
-
clearInterval(tickTimer);
|
|
3145
|
-
tickTimer = null;
|
|
2857
|
+
if (usage && stepOpts.emitUsage !== false) emit("wf:usage", usage);
|
|
2858
|
+
stepOpts.onFinish?.({ content, reasoning_content: reasoning || void 0, toolCalls, usage });
|
|
2859
|
+
}
|
|
2860
|
+
function sse(run, options) {
|
|
2861
|
+
const controller = new AbortController();
|
|
2862
|
+
const external = options?.signal;
|
|
2863
|
+
if (external) {
|
|
2864
|
+
if (external.aborted) controller.abort();
|
|
2865
|
+
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3146
2866
|
}
|
|
3147
|
-
|
|
3148
|
-
|
|
2867
|
+
return sseResponse(run, { onAbort: () => controller.abort() });
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
chat,
|
|
2871
|
+
stream,
|
|
2872
|
+
sse,
|
|
2873
|
+
streamStep,
|
|
2874
|
+
waitApproval,
|
|
2875
|
+
approve,
|
|
2876
|
+
// embedding:未配置 provider 时明确抛 AiError(诚实裁剪:不静默降级)
|
|
2877
|
+
embed: (text) => embedding.embed(text),
|
|
2878
|
+
embedMany: (texts) => embedding.embedMany(texts)
|
|
3149
2879
|
};
|
|
3150
|
-
start().catch((e) => console.error("[scheduler] start:", e instanceof Error ? e.message : e));
|
|
3151
|
-
return mw;
|
|
3152
2880
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
async start(controller) {
|
|
3162
|
-
const emit = (name, data) => {
|
|
3163
|
-
controller.enqueue(encoder.encode(`event: ${name}
|
|
3164
|
-
data: ${JSON.stringify(data)}
|
|
3165
|
-
|
|
3166
|
-
`));
|
|
3167
|
-
};
|
|
3168
|
-
try {
|
|
3169
|
-
await run(emit);
|
|
3170
|
-
controller.close();
|
|
3171
|
-
} catch (err) {
|
|
3172
|
-
try {
|
|
3173
|
-
emit("wf:error", {
|
|
3174
|
-
code: "provider_error",
|
|
3175
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3176
|
-
});
|
|
3177
|
-
controller.close();
|
|
3178
|
-
} catch {
|
|
3179
|
-
controller.error(err);
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
},
|
|
3183
|
-
cancel() {
|
|
3184
|
-
options?.onAbort?.();
|
|
3185
|
-
}
|
|
3186
|
-
});
|
|
3187
|
-
return new Response(stream, {
|
|
3188
|
-
headers: {
|
|
3189
|
-
"Content-Type": "text/event-stream",
|
|
3190
|
-
"Cache-Control": "no-cache",
|
|
3191
|
-
Connection: "keep-alive"
|
|
3192
|
-
}
|
|
3193
|
-
});
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
// src/ai/client.ts
|
|
3197
|
-
var AiError = class extends Error {
|
|
3198
|
-
code;
|
|
3199
|
-
constructor(code, message) {
|
|
3200
|
-
super(message);
|
|
3201
|
-
this.name = "AiError";
|
|
3202
|
-
this.code = code;
|
|
3203
|
-
}
|
|
3204
|
-
};
|
|
3205
|
-
async function* parseProviderSse(stream) {
|
|
3206
|
-
const reader = stream.getReader();
|
|
3207
|
-
const decoder = new TextDecoder();
|
|
3208
|
-
let buffer = "";
|
|
3209
|
-
try {
|
|
3210
|
-
while (true) {
|
|
3211
|
-
const { done, value } = await reader.read();
|
|
3212
|
-
if (done) break;
|
|
3213
|
-
buffer += decoder.decode(value, { stream: true });
|
|
3214
|
-
const lines = buffer.split("\n");
|
|
3215
|
-
buffer = lines.pop() ?? "";
|
|
3216
|
-
for (const line of lines) {
|
|
3217
|
-
const trimmed = line.trim();
|
|
3218
|
-
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
3219
|
-
const data = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed;
|
|
3220
|
-
if (data === "[DONE]") return;
|
|
3221
|
-
try {
|
|
3222
|
-
yield JSON.parse(data);
|
|
3223
|
-
} catch {
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
} finally {
|
|
3228
|
-
reader.releaseLock();
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
async function providerError(res) {
|
|
3232
|
-
let message = "";
|
|
3233
|
-
try {
|
|
3234
|
-
const body = await res.json();
|
|
3235
|
-
message = body?.error?.message ?? JSON.stringify(body);
|
|
3236
|
-
} catch {
|
|
3237
|
-
message = await res.text().catch(() => "");
|
|
3238
|
-
}
|
|
3239
|
-
const status = res.status;
|
|
3240
|
-
let code;
|
|
3241
|
-
if (status === 401 || status === 403) code = "auth_failed";
|
|
3242
|
-
else if (status === 429) code = "rate_limited";
|
|
3243
|
-
else if (status >= 500) code = "provider_error";
|
|
3244
|
-
else if (/context|token|length/i.test(message)) code = "context_length";
|
|
3245
|
-
else if (status === 400) code = "invalid_request";
|
|
3246
|
-
else code = "provider_error";
|
|
3247
|
-
return { code, message: message || `provider error (${status})` };
|
|
3248
|
-
}
|
|
3249
|
-
function createEmbeddingClient(ebd) {
|
|
3250
|
-
const apiKey = ebd?.apiKey ?? process.env.DASHSCOPE_API_KEY ?? "";
|
|
3251
|
-
const configured = !!(ebd?.apiKey || process.env.DASHSCOPE_API_KEY);
|
|
3252
|
-
const baseUrl = ebd?.baseUrl ?? process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
3253
|
-
const defaultModel = ebd?.defaultModel ?? process.env.DASHSCOPE_EMBEDDING_MODEL ?? "text-embedding-v4";
|
|
3254
|
-
const endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
|
|
3255
|
-
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
|
|
3256
|
-
async function embedMany(texts) {
|
|
3257
|
-
if (!configured) {
|
|
3258
|
-
throw new AiError("unsupported", "ai embedding: \u672A\u914D\u7F6E\u2014\u2014\u4F20 ai({ embedding }) \u6216\u8BBE DASHSCOPE_API_KEY\uFF08DeepSeek \u65E0 embedding API\uFF0C\u9700\u72EC\u7ACB provider\uFF09");
|
|
3259
|
-
}
|
|
3260
|
-
if (!apiKey) {
|
|
3261
|
-
throw new AiError("auth_failed", "ai embedding: DASHSCOPE_API_KEY \u672A\u8BBE\u7F6E");
|
|
3262
|
-
}
|
|
3263
|
-
const controller = new AbortController();
|
|
3264
|
-
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
3265
|
-
let res;
|
|
3266
|
-
try {
|
|
3267
|
-
res = await fetch(endpoint, {
|
|
3268
|
-
method: "POST",
|
|
3269
|
-
headers,
|
|
3270
|
-
body: JSON.stringify({ model: defaultModel, input: texts }),
|
|
3271
|
-
signal: controller.signal
|
|
3272
|
-
});
|
|
3273
|
-
} catch (err) {
|
|
3274
|
-
if (controller.signal.aborted) throw new AiError("provider_error", "embedding \u8BF7\u6C42\u8D85\u65F6\uFF083s\uFF09");
|
|
3275
|
-
throw new AiError("provider_error", err instanceof Error ? err.message : String(err));
|
|
3276
|
-
} finally {
|
|
3277
|
-
clearTimeout(timer);
|
|
3278
|
-
}
|
|
3279
|
-
if (!res.ok) {
|
|
3280
|
-
const { code, message } = await providerError(res);
|
|
3281
|
-
throw new AiError(code, message);
|
|
3282
|
-
}
|
|
3283
|
-
const data = await res.json();
|
|
3284
|
-
data.data.sort((a, b) => a.index - b.index);
|
|
3285
|
-
return data.data.map((item) => item.embedding);
|
|
3286
|
-
}
|
|
3287
|
-
return {
|
|
3288
|
-
async embed(text) {
|
|
3289
|
-
const results = await embedMany([text]);
|
|
3290
|
-
return results[0];
|
|
3291
|
-
},
|
|
3292
|
-
embedMany
|
|
3293
|
-
};
|
|
3294
|
-
}
|
|
3295
|
-
function createAiClient(opts) {
|
|
3296
|
-
const endpoint = `${opts.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
3297
|
-
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}` };
|
|
3298
|
-
const embedding = createEmbeddingClient(opts.embedding);
|
|
3299
|
-
const approvals = /* @__PURE__ */ new Map();
|
|
3300
|
-
function approve(response) {
|
|
3301
|
-
const resolve3 = approvals.get(response.id);
|
|
3302
|
-
if (!resolve3) return false;
|
|
3303
|
-
approvals.delete(response.id);
|
|
3304
|
-
resolve3(response);
|
|
3305
|
-
return true;
|
|
3306
|
-
}
|
|
3307
|
-
async function waitApproval(req, emit, timeoutMs = DEFAULT_APPROVAL_TIMEOUT) {
|
|
3308
|
-
const expiresAt = Date.now() + timeoutMs;
|
|
3309
|
-
emit("wf:approval_request", { ...req, expiresAt });
|
|
3310
|
-
return new Promise((resolve3) => {
|
|
3311
|
-
const timer = setTimeout(() => {
|
|
3312
|
-
if (approvals.has(req.id)) {
|
|
3313
|
-
approvals.delete(req.id);
|
|
3314
|
-
resolve3({ id: req.id, decision: "rejected" });
|
|
3315
|
-
}
|
|
3316
|
-
}, timeoutMs);
|
|
3317
|
-
approvals.set(req.id, (resp) => {
|
|
3318
|
-
clearTimeout(timer);
|
|
3319
|
-
resolve3(resp);
|
|
3320
|
-
});
|
|
3321
|
-
});
|
|
3322
|
-
}
|
|
3323
|
-
function aggregateToolCalls(chunks) {
|
|
3324
|
-
const calls = [];
|
|
3325
|
-
for (const chunk of chunks) {
|
|
3326
|
-
const delta = chunk.choices[0]?.delta;
|
|
3327
|
-
if (!delta?.tool_calls) continue;
|
|
3328
|
-
for (const tc of delta.tool_calls) {
|
|
3329
|
-
if (tc.id) {
|
|
3330
|
-
calls.push(tc);
|
|
3331
|
-
} else if (calls.length > 0) {
|
|
3332
|
-
const last = calls[calls.length - 1];
|
|
3333
|
-
if (tc.function?.arguments) last.function.arguments += tc.function.arguments;
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
return calls;
|
|
3338
|
-
}
|
|
3339
|
-
async function chat(params, options) {
|
|
3340
|
-
const res = await fetch(endpoint, {
|
|
3341
|
-
method: "POST",
|
|
3342
|
-
headers,
|
|
3343
|
-
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: false }),
|
|
3344
|
-
signal: options?.signal
|
|
3345
|
-
});
|
|
3346
|
-
if (!res.ok) {
|
|
3347
|
-
const { code, message } = await providerError(res);
|
|
3348
|
-
throw new AiError(code, message);
|
|
3349
|
-
}
|
|
3350
|
-
return res.json();
|
|
3351
|
-
}
|
|
3352
|
-
function stream(params, options) {
|
|
3353
|
-
const controller = new AbortController();
|
|
3354
|
-
const external = options?.signal;
|
|
3355
|
-
if (external) {
|
|
3356
|
-
if (external.aborted) controller.abort();
|
|
3357
|
-
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3358
|
-
}
|
|
3359
|
-
return sseResponse(
|
|
3360
|
-
async (emit) => {
|
|
3361
|
-
emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
|
|
3362
|
-
await streamStep(params, {
|
|
3363
|
-
emit,
|
|
3364
|
-
signal: controller.signal,
|
|
3365
|
-
onFinish: (r) => emit("wf:done", { content: r.content, usage: r.usage })
|
|
3366
|
-
});
|
|
3367
|
-
},
|
|
3368
|
-
{ onAbort: () => controller.abort() }
|
|
3369
|
-
);
|
|
3370
|
-
}
|
|
3371
|
-
async function streamStep(params, stepOpts) {
|
|
3372
|
-
const { emit, signal } = stepOpts;
|
|
3373
|
-
let res;
|
|
3374
|
-
try {
|
|
3375
|
-
res = await fetch(endpoint, {
|
|
3376
|
-
method: "POST",
|
|
3377
|
-
headers,
|
|
3378
|
-
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: true }),
|
|
3379
|
-
signal
|
|
3380
|
-
});
|
|
3381
|
-
} catch (err) {
|
|
3382
|
-
if (signal?.aborted) return;
|
|
3383
|
-
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
3384
|
-
return;
|
|
3385
|
-
}
|
|
3386
|
-
if (!res.ok) {
|
|
3387
|
-
const { code, message } = await providerError(res);
|
|
3388
|
-
emit("wf:error", { code, message });
|
|
3389
|
-
return;
|
|
3390
|
-
}
|
|
3391
|
-
let content = "";
|
|
3392
|
-
let reasoning = "";
|
|
3393
|
-
const chunks = [];
|
|
3394
|
-
let usage;
|
|
3395
|
-
try {
|
|
3396
|
-
for await (const chunk of parseProviderSse(res.body)) {
|
|
3397
|
-
if (signal?.aborted) return;
|
|
3398
|
-
const delta = chunk.choices[0]?.delta;
|
|
3399
|
-
if (delta?.content) {
|
|
3400
|
-
content += delta.content;
|
|
3401
|
-
emit("wf:token", { text: delta.content });
|
|
3402
|
-
}
|
|
3403
|
-
if (delta?.reasoning_content) reasoning += delta.reasoning_content;
|
|
3404
|
-
if (delta?.tool_calls) chunks.push(chunk);
|
|
3405
|
-
if (chunk.usage) usage = chunk.usage;
|
|
3406
|
-
}
|
|
3407
|
-
} catch (err) {
|
|
3408
|
-
if (signal?.aborted) return;
|
|
3409
|
-
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
3410
|
-
return;
|
|
3411
|
-
}
|
|
3412
|
-
const toolCalls = aggregateToolCalls(chunks);
|
|
3413
|
-
for (const tc of toolCalls) {
|
|
3414
|
-
emit("wf:tool_call", {
|
|
3415
|
-
id: tc.id,
|
|
3416
|
-
name: tc.function?.name ?? "",
|
|
3417
|
-
args: safeParseArgs(tc.function?.arguments ?? "")
|
|
3418
|
-
});
|
|
3419
|
-
}
|
|
3420
|
-
if (usage && stepOpts.emitUsage !== false) emit("wf:usage", usage);
|
|
3421
|
-
stepOpts.onFinish?.({ content, reasoning_content: reasoning || void 0, toolCalls, usage });
|
|
3422
|
-
}
|
|
3423
|
-
function sse(run, options) {
|
|
3424
|
-
const controller = new AbortController();
|
|
3425
|
-
const external = options?.signal;
|
|
3426
|
-
if (external) {
|
|
3427
|
-
if (external.aborted) controller.abort();
|
|
3428
|
-
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3429
|
-
}
|
|
3430
|
-
return sseResponse(run, { onAbort: () => controller.abort() });
|
|
3431
|
-
}
|
|
3432
|
-
return {
|
|
3433
|
-
chat,
|
|
3434
|
-
stream,
|
|
3435
|
-
sse,
|
|
3436
|
-
streamStep,
|
|
3437
|
-
waitApproval,
|
|
3438
|
-
approve,
|
|
3439
|
-
// embedding:未配置 provider 时明确抛 AiError(诚实裁剪:不静默降级)
|
|
3440
|
-
embed: (text) => embedding.embed(text),
|
|
3441
|
-
embedMany: (texts) => embedding.embedMany(texts)
|
|
3442
|
-
};
|
|
3443
|
-
}
|
|
3444
|
-
var DEFAULT_APPROVAL_TIMEOUT = 5 * 6e4;
|
|
3445
|
-
function safeParseArgs(raw) {
|
|
3446
|
-
if (!raw) return {};
|
|
3447
|
-
try {
|
|
3448
|
-
return JSON.parse(raw);
|
|
3449
|
-
} catch {
|
|
3450
|
-
return {};
|
|
3451
|
-
}
|
|
2881
|
+
var DEFAULT_APPROVAL_TIMEOUT = 5 * 6e4;
|
|
2882
|
+
function safeParseArgs(raw) {
|
|
2883
|
+
if (!raw) return {};
|
|
2884
|
+
try {
|
|
2885
|
+
return JSON.parse(raw);
|
|
2886
|
+
} catch {
|
|
2887
|
+
return {};
|
|
2888
|
+
}
|
|
3452
2889
|
}
|
|
3453
2890
|
|
|
3454
2891
|
// src/ai/agent.ts
|
|
@@ -3641,10 +3078,42 @@ var HtmlSafe = class {
|
|
|
3641
3078
|
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
3642
3079
|
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
3643
3080
|
|
|
3644
|
-
// src/ui-dom/vdom/
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3081
|
+
// src/ui-dom/vdom/transform.ts
|
|
3082
|
+
function holeDetail(v) {
|
|
3083
|
+
if (v === false) return "false";
|
|
3084
|
+
if (v === null) return "null";
|
|
3085
|
+
if (v === void 0) return "undefined";
|
|
3086
|
+
if (v === true) return "true";
|
|
3087
|
+
if (typeof v === "object") {
|
|
3088
|
+
try {
|
|
3089
|
+
const s = JSON.stringify(v);
|
|
3090
|
+
const d = s != null && s.length > 80 ? s.slice(0, 80) + "\u2026" : s ?? "";
|
|
3091
|
+
return `object ${d}`;
|
|
3092
|
+
} catch {
|
|
3093
|
+
return `object ${Object.prototype.toString.call(v)}`;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
return `bad-vnode type=${typeof v}`;
|
|
3097
|
+
}
|
|
3098
|
+
function isInvalidVNodeType(t) {
|
|
3099
|
+
return typeof t !== "string" && typeof t !== "function" && t !== Fragment && t !== Portal;
|
|
3100
|
+
}
|
|
3101
|
+
function ensureArrayKeys(children) {
|
|
3102
|
+
for (let i = 0; i < children.length; i++) {
|
|
3103
|
+
const c = children[i];
|
|
3104
|
+
if (c != null && typeof c === "object" && !Array.isArray(c)) {
|
|
3105
|
+
const v = c;
|
|
3106
|
+
if (v.key === void 0) v.key = String(i);
|
|
3107
|
+
else v.key = String(v.key);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
var ENUMERATED_VALUE_BASED = /* @__PURE__ */ new Set(["draggable", "contenteditable", "spellcheck", "translate"]);
|
|
3112
|
+
|
|
3113
|
+
// src/ui-dom/vdom/ssr.ts
|
|
3114
|
+
var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
3115
|
+
var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g", "text", "defs", "use", "clipPath"]);
|
|
3116
|
+
function escape(s) {
|
|
3648
3117
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3649
3118
|
}
|
|
3650
3119
|
function classToString(v) {
|
|
@@ -3664,11 +3133,18 @@ async function renderSsr(input, ctx) {
|
|
|
3664
3133
|
if (input == null || typeof input === "boolean") return "";
|
|
3665
3134
|
if (typeof input === "string" || typeof input === "number") return escape(String(input));
|
|
3666
3135
|
if (Array.isArray(input)) {
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3136
|
+
ensureArrayKeys(input);
|
|
3137
|
+
const parts = await Promise.all(input.map((c) => {
|
|
3138
|
+
if (c == null || typeof c === "boolean") return Promise.resolve(`<!--wf-hole: ${holeDetail(c)}-->`);
|
|
3139
|
+
return renderSsr(c, ctx);
|
|
3140
|
+
}));
|
|
3141
|
+
return parts.join("");
|
|
3670
3142
|
}
|
|
3671
3143
|
const vnode = input;
|
|
3144
|
+
if (isInvalidVNodeType(vnode.type)) {
|
|
3145
|
+
console.warn(`[weifuwu] children \u9879\u975E\u6CD5\uFF1Atype=${String(vnode.type)}\uFF08${typeof vnode.type}\uFF09\u2014\u2014\u5DF2\u5360\u4F4D\uFF08wf-hole\uFF09`);
|
|
3146
|
+
return `<!--wf-hole: ${holeDetail(input)}-->`;
|
|
3147
|
+
}
|
|
3672
3148
|
if (vnode.type === Portal || vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
|
|
3673
3149
|
if (typeof vnode.type === "function") {
|
|
3674
3150
|
const childCtx = Object.create(ctx);
|
|
@@ -3678,12 +3154,24 @@ async function renderSsr(input, ctx) {
|
|
|
3678
3154
|
`Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
3679
3155
|
);
|
|
3680
3156
|
}
|
|
3681
|
-
|
|
3157
|
+
const out = await renderFn(vnode.props ?? {});
|
|
3158
|
+
if (vnode.key != null && out != null && typeof out === "object") {
|
|
3159
|
+
if (Array.isArray(out)) {
|
|
3160
|
+
for (const c of out) {
|
|
3161
|
+
if (c != null && typeof c === "object" && !Array.isArray(c)) c.key = vnode.key;
|
|
3162
|
+
}
|
|
3163
|
+
} else {
|
|
3164
|
+
;
|
|
3165
|
+
out.key = vnode.key;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
return renderSsr(out, childCtx);
|
|
3682
3169
|
}
|
|
3683
3170
|
const tag = vnode.type;
|
|
3684
3171
|
const props = vnode.props ?? {};
|
|
3685
3172
|
const attrs = [];
|
|
3686
3173
|
let innerHTML;
|
|
3174
|
+
if (vnode.key != null) attrs.push(` data-wf-key="${escape(String(vnode.key))}"`);
|
|
3687
3175
|
for (const [key, value] of Object.entries(props)) {
|
|
3688
3176
|
if (key === "children" || key === "key") continue;
|
|
3689
3177
|
if (key === "ref") continue;
|
|
@@ -3701,7 +3189,7 @@ async function renderSsr(input, ctx) {
|
|
|
3701
3189
|
attrs.push(` style="${escape(styleToString(value))}"`);
|
|
3702
3190
|
continue;
|
|
3703
3191
|
}
|
|
3704
|
-
if (key
|
|
3192
|
+
if (ENUMERATED_VALUE_BASED.has(key)) {
|
|
3705
3193
|
attrs.push(` ${key}="${value ? "true" : "false"}"`);
|
|
3706
3194
|
continue;
|
|
3707
3195
|
}
|
|
@@ -3728,9 +3216,6 @@ function createSsrUi() {
|
|
|
3728
3216
|
_selfId: "_wf_root",
|
|
3729
3217
|
render: () => {
|
|
3730
3218
|
},
|
|
3731
|
-
dirty: () => {
|
|
3732
|
-
},
|
|
3733
|
-
$: () => ({}),
|
|
3734
3219
|
selfId: () => {
|
|
3735
3220
|
},
|
|
3736
3221
|
bumpCtxVersion: () => {
|
|
@@ -3739,6 +3224,7 @@ function createSsrUi() {
|
|
|
3739
3224
|
},
|
|
3740
3225
|
endMounting: () => {
|
|
3741
3226
|
},
|
|
3227
|
+
onUnmount: () => void 0,
|
|
3742
3228
|
// hooks no-op(组件 SSR 安全——不注册监听/定时器)
|
|
3743
3229
|
useChat: () => ({ messages: [], input: "", streaming: false, error: null, usage: null, step: null, send: () => {
|
|
3744
3230
|
}, stop: () => {
|
|
@@ -3758,7 +3244,7 @@ function createSsrUi() {
|
|
|
3758
3244
|
},
|
|
3759
3245
|
useVisualViewport: () => ({ height: 0, offsetTop: 0, keyboardOpen: false }),
|
|
3760
3246
|
usePopup: () => ({ open: false, setOpen: () => {
|
|
3761
|
-
}, wrapProps: {}, portal: () =>
|
|
3247
|
+
}, phase: "closed", sync: (open2) => open2 ? "open" : "closed", wrapProps: {}, portal: (content) => content, refresh: () => {
|
|
3762
3248
|
} }),
|
|
3763
3249
|
useLongPress: noopReturn,
|
|
3764
3250
|
useInView: () => ({ isIn: false, ready: false, observe: () => {
|
|
@@ -3779,9 +3265,6 @@ function createSsrUi() {
|
|
|
3779
3265
|
}, triggerProps: {} }),
|
|
3780
3266
|
usePresence: () => ({ phase: "closed", ref: () => {
|
|
3781
3267
|
}, sync: () => "closed" }),
|
|
3782
|
-
useDialog: () => ({ phase: "closed", rootRef: () => {
|
|
3783
|
-
}, panelRef: () => {
|
|
3784
|
-
}, sync: (open2) => open2 ? "open" : "closed" }),
|
|
3785
3268
|
useGlobalKey: () => () => {
|
|
3786
3269
|
},
|
|
3787
3270
|
useDrag: noopReturn,
|
|
@@ -3995,9 +3478,53 @@ function ui() {
|
|
|
3995
3478
|
}
|
|
3996
3479
|
|
|
3997
3480
|
// src/db/postgres/connection.ts
|
|
3998
|
-
import
|
|
3481
|
+
import net2 from "node:net";
|
|
3999
3482
|
import crypto2 from "node:crypto";
|
|
4000
3483
|
|
|
3484
|
+
// src/db/errors.ts
|
|
3485
|
+
var DbError = class extends Error {
|
|
3486
|
+
kind;
|
|
3487
|
+
code;
|
|
3488
|
+
cause;
|
|
3489
|
+
constructor(kind, message, options) {
|
|
3490
|
+
super(message);
|
|
3491
|
+
this.name = "DbError";
|
|
3492
|
+
this.kind = kind;
|
|
3493
|
+
this.code = options?.code;
|
|
3494
|
+
this.cause = options?.cause;
|
|
3495
|
+
}
|
|
3496
|
+
};
|
|
3497
|
+
var ProtocolError = class extends DbError {
|
|
3498
|
+
constructor(feature, message) {
|
|
3499
|
+
super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
|
|
3500
|
+
this.name = "ProtocolError";
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
var ConnectionError = class extends DbError {
|
|
3504
|
+
attempts;
|
|
3505
|
+
constructor(message, attempts = 1, cause) {
|
|
3506
|
+
super("connection", message, { cause });
|
|
3507
|
+
this.name = "ConnectionError";
|
|
3508
|
+
this.attempts = attempts;
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
var TimeoutError = class extends DbError {
|
|
3512
|
+
operation;
|
|
3513
|
+
ms;
|
|
3514
|
+
constructor(operation, ms) {
|
|
3515
|
+
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
3516
|
+
this.name = "TimeoutError";
|
|
3517
|
+
this.operation = operation;
|
|
3518
|
+
this.ms = ms;
|
|
3519
|
+
}
|
|
3520
|
+
};
|
|
3521
|
+
var ValidationError = class extends DbError {
|
|
3522
|
+
constructor(message) {
|
|
3523
|
+
super("validation", message);
|
|
3524
|
+
this.name = "ValidationError";
|
|
3525
|
+
}
|
|
3526
|
+
};
|
|
3527
|
+
|
|
4001
3528
|
// src/db/postgres/protocol.ts
|
|
4002
3529
|
function encodeMessage(type, payload) {
|
|
4003
3530
|
const out = new Uint8Array(1 + 4 + payload.length);
|
|
@@ -4206,7 +3733,7 @@ function parseRowDescription(payload) {
|
|
|
4206
3733
|
for (let c = 0; c < count; c++) {
|
|
4207
3734
|
let j = i;
|
|
4208
3735
|
while (payload[j] !== 0) j++;
|
|
4209
|
-
const name =
|
|
3736
|
+
const name = _decoder.decode(payload.subarray(i, j));
|
|
4210
3737
|
i = j + 1;
|
|
4211
3738
|
i += 6;
|
|
4212
3739
|
const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
@@ -4228,7 +3755,7 @@ function parseDataRow(payload) {
|
|
|
4228
3755
|
if (len === -1) {
|
|
4229
3756
|
values.push(null);
|
|
4230
3757
|
} else {
|
|
4231
|
-
values.push(
|
|
3758
|
+
values.push(_decoder.decode(payload.subarray(i, i + len)));
|
|
4232
3759
|
i += len;
|
|
4233
3760
|
}
|
|
4234
3761
|
}
|
|
@@ -4241,7 +3768,7 @@ function parseErrorFields(payload) {
|
|
|
4241
3768
|
const type = String.fromCharCode(payload[i]);
|
|
4242
3769
|
let j = i + 1;
|
|
4243
3770
|
while (j < payload.length && payload[j] !== 0) j++;
|
|
4244
|
-
const value =
|
|
3771
|
+
const value = _decoder.decode(payload.subarray(i + 1, j));
|
|
4245
3772
|
switch (type) {
|
|
4246
3773
|
case "S":
|
|
4247
3774
|
out.severity = value;
|
|
@@ -4266,10 +3793,10 @@ function parseErrorFields(payload) {
|
|
|
4266
3793
|
}
|
|
4267
3794
|
return out;
|
|
4268
3795
|
}
|
|
4269
|
-
var
|
|
4270
|
-
var
|
|
3796
|
+
var _decoder = new TextDecoder();
|
|
3797
|
+
var _encoder = new TextEncoder();
|
|
4271
3798
|
function utf8(s) {
|
|
4272
|
-
return
|
|
3799
|
+
return _encoder.encode(s);
|
|
4273
3800
|
}
|
|
4274
3801
|
function concat(...parts) {
|
|
4275
3802
|
let total = 0;
|
|
@@ -4315,7 +3842,7 @@ var PgConnection = class _PgConnection {
|
|
|
4315
3842
|
connect() {
|
|
4316
3843
|
return new Promise((resolve3, reject) => {
|
|
4317
3844
|
this.status = "connecting";
|
|
4318
|
-
const sock =
|
|
3845
|
+
const sock = net2.connect(this.opts.port, this.opts.host);
|
|
4319
3846
|
this.socket = sock;
|
|
4320
3847
|
const timeout = setTimeout(() => {
|
|
4321
3848
|
sock.destroy();
|
|
@@ -5486,151 +5013,668 @@ function createQueryBuilder(sql, exec) {
|
|
|
5486
5013
|
const [t, a] = table2.split(/\s+/);
|
|
5487
5014
|
return mkSelect(t, a);
|
|
5488
5015
|
}
|
|
5489
|
-
return mkSelect(table2, alias);
|
|
5490
|
-
},
|
|
5491
|
-
insert: (table2) => mkInsert(table2),
|
|
5492
|
-
update: (table2) => mkUpdate(table2),
|
|
5493
|
-
delete: (table2) => mkDelete(table2)
|
|
5494
|
-
};
|
|
5495
|
-
}
|
|
5496
|
-
|
|
5497
|
-
// src/postgres/client.ts
|
|
5498
|
-
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
5499
|
-
var traceStore = new AsyncLocalStorage();
|
|
5500
|
-
function postgres(options) {
|
|
5501
|
-
const opts = typeof options === "string" ? { connection: options } : options ?? {};
|
|
5502
|
-
const connection = opts.connection ?? process.env.DATABASE_URL;
|
|
5503
|
-
if (!connection) {
|
|
5504
|
-
throw new Error(
|
|
5505
|
-
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
5016
|
+
return mkSelect(table2, alias);
|
|
5017
|
+
},
|
|
5018
|
+
insert: (table2) => mkInsert(table2),
|
|
5019
|
+
update: (table2) => mkUpdate(table2),
|
|
5020
|
+
delete: (table2) => mkDelete(table2)
|
|
5021
|
+
};
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
// src/postgres/client.ts
|
|
5025
|
+
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
5026
|
+
var traceStore = new AsyncLocalStorage();
|
|
5027
|
+
function postgres(options) {
|
|
5028
|
+
const opts = typeof options === "string" ? { connection: options } : options ?? {};
|
|
5029
|
+
const connection = opts.connection ?? process.env.DATABASE_URL;
|
|
5030
|
+
if (!connection) {
|
|
5031
|
+
throw new Error(
|
|
5032
|
+
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
5033
|
+
);
|
|
5034
|
+
}
|
|
5035
|
+
const u = new URL(connection);
|
|
5036
|
+
const pool = new PgPool({
|
|
5037
|
+
host: u.hostname,
|
|
5038
|
+
port: Number(u.port || 5432),
|
|
5039
|
+
user: decodeURIComponent(u.username),
|
|
5040
|
+
password: decodeURIComponent(u.password),
|
|
5041
|
+
database: u.pathname.replace(/^\//, ""),
|
|
5042
|
+
poolSize: opts.max ?? opts.poolSize ?? 10,
|
|
5043
|
+
acquireTimeoutMs: opts.acquireTimeoutMs,
|
|
5044
|
+
statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
|
|
5045
|
+
// onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
|
|
5046
|
+
onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
|
|
5047
|
+
const tid = traceStore.getStore();
|
|
5048
|
+
opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
|
|
5049
|
+
} : void 0
|
|
5050
|
+
});
|
|
5051
|
+
const sql = makeSql(pool);
|
|
5052
|
+
const mw = ((req, ctx, next) => {
|
|
5053
|
+
ctx.sql = sql;
|
|
5054
|
+
return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
|
|
5055
|
+
});
|
|
5056
|
+
mw.__meta = { injects: ["sql"], depends: [] };
|
|
5057
|
+
mw.sql = sql;
|
|
5058
|
+
mw.migrate = async () => {
|
|
5059
|
+
await sql.unsafe(`
|
|
5060
|
+
CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
|
|
5061
|
+
name TEXT PRIMARY KEY,
|
|
5062
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
5063
|
+
)
|
|
5064
|
+
`);
|
|
5065
|
+
};
|
|
5066
|
+
mw.markMigrated = async (moduleName) => {
|
|
5067
|
+
await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
|
|
5068
|
+
moduleName
|
|
5069
|
+
]);
|
|
5070
|
+
};
|
|
5071
|
+
mw.isMigrated = async (moduleName) => {
|
|
5072
|
+
const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
|
|
5073
|
+
return rows.length > 0;
|
|
5074
|
+
};
|
|
5075
|
+
mw.transaction = ((fn) => pool.begin(fn));
|
|
5076
|
+
mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
|
|
5077
|
+
mw.close = () => pool.close();
|
|
5078
|
+
return mw;
|
|
5079
|
+
}
|
|
5080
|
+
var TaggedQuery = class {
|
|
5081
|
+
text;
|
|
5082
|
+
params;
|
|
5083
|
+
executor;
|
|
5084
|
+
constructor(text, params, executor) {
|
|
5085
|
+
this.text = text;
|
|
5086
|
+
this.params = params;
|
|
5087
|
+
this.executor = executor;
|
|
5088
|
+
}
|
|
5089
|
+
then(resolve3, reject) {
|
|
5090
|
+
return this.executor(this.text, this.params).then(resolve3, reject);
|
|
5091
|
+
}
|
|
5092
|
+
catch(reject) {
|
|
5093
|
+
return this.executor(this.text, this.params).catch(reject);
|
|
5094
|
+
}
|
|
5095
|
+
finally(fn) {
|
|
5096
|
+
return this.executor(this.text, this.params).finally(fn);
|
|
5097
|
+
}
|
|
5098
|
+
/** 嵌套片段(agent-platform 条件过滤模式) */
|
|
5099
|
+
get __fragment() {
|
|
5100
|
+
return { sql: this.text, params: this.params };
|
|
5101
|
+
}
|
|
5102
|
+
};
|
|
5103
|
+
function makeSql(pool) {
|
|
5104
|
+
const sql = ((strings, ...values) => {
|
|
5105
|
+
const { sql: text, params } = parseTaggedFromPool(strings, values);
|
|
5106
|
+
return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
|
|
5107
|
+
});
|
|
5108
|
+
sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
|
|
5109
|
+
sql.query = createQueryBuilder(sql, async (q) => {
|
|
5110
|
+
const { sql: text, params: p } = compileQuery(q);
|
|
5111
|
+
return wrapError(pool.query(text, p));
|
|
5112
|
+
});
|
|
5113
|
+
sql.raw = (strings, ...values) => rawSql(
|
|
5114
|
+
strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), ""),
|
|
5115
|
+
values
|
|
5116
|
+
);
|
|
5117
|
+
sql.close = () => pool.close();
|
|
5118
|
+
return sql;
|
|
5119
|
+
}
|
|
5120
|
+
function parseTaggedFromPool(strings, values) {
|
|
5121
|
+
let sql = strings[0];
|
|
5122
|
+
const params = [];
|
|
5123
|
+
for (let i = 0; i < values.length; i++) {
|
|
5124
|
+
const frag = values[i]?.__fragment;
|
|
5125
|
+
if (frag) {
|
|
5126
|
+
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
5127
|
+
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
5128
|
+
return `$${params.length}`;
|
|
5129
|
+
});
|
|
5130
|
+
sql += renumbered + strings[i + 1];
|
|
5131
|
+
} else {
|
|
5132
|
+
params.push(values[i]);
|
|
5133
|
+
sql += `$${params.length}` + strings[i + 1];
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
return { sql, params };
|
|
5137
|
+
}
|
|
5138
|
+
var PG_ERROR_MAP = {
|
|
5139
|
+
"23505": 409,
|
|
5140
|
+
// unique_violation
|
|
5141
|
+
"23503": 400,
|
|
5142
|
+
// foreign_key_violation
|
|
5143
|
+
"23502": 400,
|
|
5144
|
+
// not_null_violation
|
|
5145
|
+
"23514": 400,
|
|
5146
|
+
// check_violation
|
|
5147
|
+
"22P02": 400,
|
|
5148
|
+
// invalid_text_representation
|
|
5149
|
+
"22003": 400
|
|
5150
|
+
// numeric_value_out_of_range
|
|
5151
|
+
};
|
|
5152
|
+
function wrapError(promise) {
|
|
5153
|
+
return promise.catch((err) => {
|
|
5154
|
+
const code = err?.code;
|
|
5155
|
+
if (code && PG_ERROR_MAP[code]) {
|
|
5156
|
+
throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
|
|
5157
|
+
}
|
|
5158
|
+
throw err;
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
// src/db/redis/connection.ts
|
|
5163
|
+
import net3 from "node:net";
|
|
5164
|
+
|
|
5165
|
+
// src/db/redis/resp.ts
|
|
5166
|
+
var RespError = class extends DbError {
|
|
5167
|
+
constructor(message) {
|
|
5168
|
+
super("protocol", message, { code: "RESP" });
|
|
5169
|
+
this.name = "RespError";
|
|
5170
|
+
}
|
|
5171
|
+
};
|
|
5172
|
+
var IncompleteError = class extends Error {
|
|
5173
|
+
constructor() {
|
|
5174
|
+
super("incomplete RESP message");
|
|
5175
|
+
this.name = "IncompleteError";
|
|
5176
|
+
}
|
|
5177
|
+
};
|
|
5178
|
+
function decodeValue(v, decodeBytes) {
|
|
5179
|
+
if (v instanceof Uint8Array) return decodeBytes ? _decoder2.decode(v) : v;
|
|
5180
|
+
if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
|
|
5181
|
+
return v;
|
|
5182
|
+
}
|
|
5183
|
+
function encodeCommand(args) {
|
|
5184
|
+
const lens = new Array(args.length);
|
|
5185
|
+
let total = headerLen(args.length);
|
|
5186
|
+
for (let i = 0; i < args.length; i++) {
|
|
5187
|
+
const arg = args[i];
|
|
5188
|
+
const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
|
|
5189
|
+
lens[i] = len;
|
|
5190
|
+
total += headerLen(len) + len + 2;
|
|
5191
|
+
}
|
|
5192
|
+
const out = new Uint8Array(total);
|
|
5193
|
+
let off = 0;
|
|
5194
|
+
const head = `*${args.length}\r
|
|
5195
|
+
`;
|
|
5196
|
+
out.set(_encoder2.encode(head), 0);
|
|
5197
|
+
off = head.length;
|
|
5198
|
+
for (let i = 0; i < args.length; i++) {
|
|
5199
|
+
const arg = args[i];
|
|
5200
|
+
const len = lens[i];
|
|
5201
|
+
const lh = `$${len}\r
|
|
5202
|
+
`;
|
|
5203
|
+
out.set(_encoder2.encode(lh), off);
|
|
5204
|
+
off += lh.length;
|
|
5205
|
+
if (typeof arg === "string") {
|
|
5206
|
+
out.set(_encoder2.encode(arg), off);
|
|
5207
|
+
} else if (arg instanceof Buffer) {
|
|
5208
|
+
out.set(arg, off);
|
|
5209
|
+
} else {
|
|
5210
|
+
out.set(_encoder2.encode(String(arg)), off);
|
|
5211
|
+
}
|
|
5212
|
+
off += len;
|
|
5213
|
+
out[off] = 13;
|
|
5214
|
+
out[off + 1] = 10;
|
|
5215
|
+
off += 2;
|
|
5216
|
+
}
|
|
5217
|
+
return out;
|
|
5218
|
+
}
|
|
5219
|
+
function headerLen(n) {
|
|
5220
|
+
if (n < 10) return 4;
|
|
5221
|
+
if (n < 100) return 5;
|
|
5222
|
+
if (n < 1e3) return 6;
|
|
5223
|
+
if (n < 1e4) return 7;
|
|
5224
|
+
return 8;
|
|
5225
|
+
}
|
|
5226
|
+
var _decoder2 = new TextDecoder();
|
|
5227
|
+
var _encoder2 = new TextEncoder();
|
|
5228
|
+
var RespParser = class {
|
|
5229
|
+
buf = new Uint8Array(0);
|
|
5230
|
+
off = 0;
|
|
5231
|
+
/** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
|
|
5232
|
+
push(chunk) {
|
|
5233
|
+
this.append(chunk);
|
|
5234
|
+
const saved = this.off;
|
|
5235
|
+
try {
|
|
5236
|
+
const value = this.parseValue();
|
|
5237
|
+
this.compact();
|
|
5238
|
+
return { value, incomplete: false };
|
|
5239
|
+
} catch (e) {
|
|
5240
|
+
this.off = saved;
|
|
5241
|
+
if (e instanceof IncompleteError) return { value: null, incomplete: true };
|
|
5242
|
+
throw e;
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
/** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
|
|
5246
|
+
pushAll(chunk) {
|
|
5247
|
+
this.append(chunk);
|
|
5248
|
+
const out = [];
|
|
5249
|
+
while (true) {
|
|
5250
|
+
const saved = this.off;
|
|
5251
|
+
try {
|
|
5252
|
+
out.push(this.parseValue());
|
|
5253
|
+
} catch (e) {
|
|
5254
|
+
this.off = saved;
|
|
5255
|
+
if (e instanceof IncompleteError) break;
|
|
5256
|
+
throw e;
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
if (out.length > 0) this.compact();
|
|
5260
|
+
return out;
|
|
5261
|
+
}
|
|
5262
|
+
/** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
|
|
5263
|
+
append(chunk) {
|
|
5264
|
+
if (chunk.length === 0) return;
|
|
5265
|
+
const rest = this.buf.length - this.off;
|
|
5266
|
+
if (rest === 0) {
|
|
5267
|
+
this.buf = chunk;
|
|
5268
|
+
this.off = 0;
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5271
|
+
const merged = new Uint8Array(rest + chunk.length);
|
|
5272
|
+
merged.set(this.buf.subarray(this.off), 0);
|
|
5273
|
+
merged.set(chunk, rest);
|
|
5274
|
+
this.buf = merged;
|
|
5275
|
+
this.off = 0;
|
|
5276
|
+
}
|
|
5277
|
+
/** 全部消费后压缩(释放底层) */
|
|
5278
|
+
compact() {
|
|
5279
|
+
if (this.off === this.buf.length) {
|
|
5280
|
+
this.buf = new Uint8Array(0);
|
|
5281
|
+
this.off = 0;
|
|
5282
|
+
} else if (this.off > 0) {
|
|
5283
|
+
this.buf = this.buf.subarray(this.off);
|
|
5284
|
+
this.off = 0;
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
parseValue() {
|
|
5288
|
+
if (this.buf.length - this.off < 3) throw new IncompleteError();
|
|
5289
|
+
const type = String.fromCharCode(this.buf[this.off]);
|
|
5290
|
+
this.off++;
|
|
5291
|
+
switch (type) {
|
|
5292
|
+
case "+": {
|
|
5293
|
+
const line = this.readLine();
|
|
5294
|
+
return line;
|
|
5295
|
+
}
|
|
5296
|
+
case "-": {
|
|
5297
|
+
const line = this.readLine();
|
|
5298
|
+
return new RespError(line);
|
|
5299
|
+
}
|
|
5300
|
+
case ":": {
|
|
5301
|
+
return this.readInt();
|
|
5302
|
+
}
|
|
5303
|
+
case "$": {
|
|
5304
|
+
const len = this.readInt();
|
|
5305
|
+
if (len === -1) return null;
|
|
5306
|
+
return this.readBulkBytes(len);
|
|
5307
|
+
}
|
|
5308
|
+
case "*": {
|
|
5309
|
+
const count = this.readInt();
|
|
5310
|
+
if (count === -1) return null;
|
|
5311
|
+
const items = [];
|
|
5312
|
+
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
5313
|
+
return items;
|
|
5314
|
+
}
|
|
5315
|
+
default:
|
|
5316
|
+
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
/**
|
|
5320
|
+
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
5321
|
+
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
5322
|
+
*/
|
|
5323
|
+
readInt() {
|
|
5324
|
+
const buf = this.buf;
|
|
5325
|
+
let i = this.off;
|
|
5326
|
+
let neg = false;
|
|
5327
|
+
if (i < buf.length && buf[i] === 45) {
|
|
5328
|
+
neg = true;
|
|
5329
|
+
i++;
|
|
5330
|
+
}
|
|
5331
|
+
let n = 0;
|
|
5332
|
+
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
|
|
5333
|
+
n = n * 10 + (buf[i] - 48);
|
|
5334
|
+
i++;
|
|
5335
|
+
}
|
|
5336
|
+
if (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
|
|
5337
|
+
this.off = i + 2;
|
|
5338
|
+
return neg ? -n : n;
|
|
5339
|
+
}
|
|
5340
|
+
throw new IncompleteError();
|
|
5341
|
+
}
|
|
5342
|
+
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
5343
|
+
readLine() {
|
|
5344
|
+
const idx = indexOfCRLF(this.buf, this.off);
|
|
5345
|
+
if (idx === -1) throw new IncompleteError();
|
|
5346
|
+
const line = _decoder2.decode(this.buf.subarray(this.off, idx));
|
|
5347
|
+
this.off = idx + 2;
|
|
5348
|
+
return line;
|
|
5349
|
+
}
|
|
5350
|
+
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
5351
|
+
readBulkBytes(len) {
|
|
5352
|
+
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
5353
|
+
const value = this.buf.subarray(this.off, this.off + len);
|
|
5354
|
+
this.off += len + 2;
|
|
5355
|
+
return value;
|
|
5356
|
+
}
|
|
5357
|
+
};
|
|
5358
|
+
function indexOfCRLF(buf, from = 0) {
|
|
5359
|
+
let i = buf.indexOf(13, from);
|
|
5360
|
+
while (i !== -1) {
|
|
5361
|
+
if (i + 1 < buf.length && buf[i + 1] === 10) return i;
|
|
5362
|
+
i = buf.indexOf(13, i + 1);
|
|
5363
|
+
}
|
|
5364
|
+
return -1;
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
// src/db/redis/connection.ts
|
|
5368
|
+
var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
|
|
5369
|
+
var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
|
|
5370
|
+
var RedisConnection = class {
|
|
5371
|
+
ready = false;
|
|
5372
|
+
opts;
|
|
5373
|
+
socket = null;
|
|
5374
|
+
parser = new RespParser();
|
|
5375
|
+
pending = [];
|
|
5376
|
+
/** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
|
|
5377
|
+
pendingHead = 0;
|
|
5378
|
+
offlineQueue = [];
|
|
5379
|
+
subs = /* @__PURE__ */ new Map();
|
|
5380
|
+
psubs = /* @__PURE__ */ new Map();
|
|
5381
|
+
status = "idle";
|
|
5382
|
+
retries = 0;
|
|
5383
|
+
reconnectTimer = null;
|
|
5384
|
+
socketTimeoutTimer = null;
|
|
5385
|
+
connectPromise = null;
|
|
5386
|
+
closed = false;
|
|
5387
|
+
connectedOnce = false;
|
|
5388
|
+
constructor(options = {}) {
|
|
5389
|
+
this.opts = {
|
|
5390
|
+
host: options.host ?? "127.0.0.1",
|
|
5391
|
+
port: options.port ?? 6379,
|
|
5392
|
+
retryDelayMs: options.retryDelayMs ?? 100,
|
|
5393
|
+
maxRetries: options.maxRetries ?? 10,
|
|
5394
|
+
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
5395
|
+
maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
|
|
5396
|
+
commandTimeoutMs: options.commandTimeoutMs ?? 0,
|
|
5397
|
+
socketTimeoutMs: options.socketTimeoutMs ?? 0,
|
|
5398
|
+
onCommand: options.onCommand
|
|
5399
|
+
};
|
|
5400
|
+
}
|
|
5401
|
+
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
5402
|
+
connect() {
|
|
5403
|
+
if (this.connectPromise) return this.connectPromise;
|
|
5404
|
+
this.closed = false;
|
|
5405
|
+
this.connectPromise = new Promise((resolve3, reject) => {
|
|
5406
|
+
this.openSocket();
|
|
5407
|
+
this.onceReady = () => resolve3();
|
|
5408
|
+
this.onceFailed = (err) => reject(err);
|
|
5409
|
+
}).finally(() => {
|
|
5410
|
+
this.connectPromise = null;
|
|
5411
|
+
this.onceReady = void 0;
|
|
5412
|
+
this.onceFailed = void 0;
|
|
5413
|
+
});
|
|
5414
|
+
return this.connectPromise;
|
|
5415
|
+
}
|
|
5416
|
+
onceReady;
|
|
5417
|
+
onceFailed;
|
|
5418
|
+
openSocket() {
|
|
5419
|
+
this.status = "connecting";
|
|
5420
|
+
const sock = net3.connect(this.opts.port, this.opts.host);
|
|
5421
|
+
this.socket = sock;
|
|
5422
|
+
sock.on("connect", () => {
|
|
5423
|
+
sock.setNoDelay(true);
|
|
5424
|
+
this.status = "ready";
|
|
5425
|
+
this.retries = 0;
|
|
5426
|
+
if (this.connectedOnce) {
|
|
5427
|
+
for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
|
|
5428
|
+
for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
|
|
5429
|
+
}
|
|
5430
|
+
this.connectedOnce = true;
|
|
5431
|
+
this.flushOffline();
|
|
5432
|
+
this.onceReady?.();
|
|
5433
|
+
});
|
|
5434
|
+
sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
|
|
5435
|
+
sock.on("error", (err) => {
|
|
5436
|
+
if (this.status === "connecting") {
|
|
5437
|
+
this.handleDisconnect(err);
|
|
5438
|
+
}
|
|
5439
|
+
});
|
|
5440
|
+
sock.on("close", () => {
|
|
5441
|
+
this.socket = null;
|
|
5442
|
+
if (this.status !== "closed" && this.status !== "idle") {
|
|
5443
|
+
this.handleDisconnect(new ConnectionError("redis: socket closed"));
|
|
5444
|
+
}
|
|
5445
|
+
});
|
|
5446
|
+
}
|
|
5447
|
+
handleDisconnect(err) {
|
|
5448
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5449
|
+
this.pending = [];
|
|
5450
|
+
this.pendingHead = 0;
|
|
5451
|
+
for (const p of queue2) p.reject(err);
|
|
5452
|
+
this.clearSocketTimeout();
|
|
5453
|
+
if (this.closed || this.status === "closed") return;
|
|
5454
|
+
this.retries++;
|
|
5455
|
+
this.status = "connecting";
|
|
5456
|
+
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
5457
|
+
this.status = "closed";
|
|
5458
|
+
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
5459
|
+
this.onceFailed?.(failErr);
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
|
|
5463
|
+
this.reconnectTimer = setTimeout(() => {
|
|
5464
|
+
this.reconnectTimer = null;
|
|
5465
|
+
this.openSocket();
|
|
5466
|
+
}, delay);
|
|
5467
|
+
}
|
|
5468
|
+
onData(chunk) {
|
|
5469
|
+
try {
|
|
5470
|
+
const values = this.parser.pushAll(chunk);
|
|
5471
|
+
for (const raw of values) {
|
|
5472
|
+
const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
|
|
5473
|
+
if (typeof first === "string" && PUSH_TYPES.has(first)) {
|
|
5474
|
+
this.dispatchSubscribe(decodeValue(raw, true));
|
|
5475
|
+
continue;
|
|
5476
|
+
}
|
|
5477
|
+
const p = this.pending[this.pendingHead++];
|
|
5478
|
+
if (!p) break;
|
|
5479
|
+
if (p.timedOut) continue;
|
|
5480
|
+
if (raw instanceof RespError) p.reject(raw);
|
|
5481
|
+
else p.resolve(decodeValue(raw, !p.asBuffer));
|
|
5482
|
+
}
|
|
5483
|
+
if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
|
|
5484
|
+
this.pending = this.pending.slice(this.pendingHead);
|
|
5485
|
+
this.pendingHead = 0;
|
|
5486
|
+
}
|
|
5487
|
+
if (this.opts.socketTimeoutMs > 0) {
|
|
5488
|
+
if (this.pendingHead < this.pending.length) this.armSocketTimeout();
|
|
5489
|
+
else this.clearSocketTimeout();
|
|
5490
|
+
}
|
|
5491
|
+
} catch (e) {
|
|
5492
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5493
|
+
this.pending = [];
|
|
5494
|
+
this.pendingHead = 0;
|
|
5495
|
+
for (const q of queue2) q.reject(e);
|
|
5496
|
+
this.socket?.destroy();
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
/**
|
|
5500
|
+
* 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
|
|
5501
|
+
* opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
|
|
5502
|
+
*/
|
|
5503
|
+
command(name, ...args) {
|
|
5504
|
+
const last = args[args.length - 1];
|
|
5505
|
+
const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
|
|
5506
|
+
if (this.status === "ready" && this.socket && !this.socket.destroyed) {
|
|
5507
|
+
return this.sendNow(name, args, opts?.asBuffer);
|
|
5508
|
+
}
|
|
5509
|
+
if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
|
|
5510
|
+
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
5511
|
+
}
|
|
5512
|
+
if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
|
|
5513
|
+
return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
|
|
5514
|
+
}
|
|
5515
|
+
return new Promise((resolve3, reject) => {
|
|
5516
|
+
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
5517
|
+
});
|
|
5518
|
+
}
|
|
5519
|
+
sendNow(name, args, asBuffer) {
|
|
5520
|
+
const start = performance.now();
|
|
5521
|
+
return new Promise((resolve3, reject) => {
|
|
5522
|
+
const p = {
|
|
5523
|
+
resolve: resolve3,
|
|
5524
|
+
reject,
|
|
5525
|
+
asBuffer,
|
|
5526
|
+
blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
|
|
5527
|
+
};
|
|
5528
|
+
this.armTimeout(p);
|
|
5529
|
+
this.pending.push(p);
|
|
5530
|
+
this.armSocketTimeout();
|
|
5531
|
+
this.socket.write(encodeCommand([name, ...args]), (err) => {
|
|
5532
|
+
if (err) {
|
|
5533
|
+
const idx = this.pending.indexOf(p);
|
|
5534
|
+
if (idx >= 0) this.pending.splice(idx, 1);
|
|
5535
|
+
p.reject(err instanceof Error ? err : new ConnectionError("redis: socket write failed"));
|
|
5536
|
+
}
|
|
5537
|
+
});
|
|
5538
|
+
}).then(
|
|
5539
|
+
(v) => {
|
|
5540
|
+
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
5541
|
+
return v;
|
|
5542
|
+
},
|
|
5543
|
+
(e) => {
|
|
5544
|
+
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
5545
|
+
throw e;
|
|
5546
|
+
}
|
|
5506
5547
|
);
|
|
5507
5548
|
}
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
|
|
5518
|
-
// onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
|
|
5519
|
-
onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
|
|
5520
|
-
const tid = traceStore.getStore();
|
|
5521
|
-
opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
|
|
5522
|
-
} : void 0
|
|
5523
|
-
});
|
|
5524
|
-
const sql = makeSql(pool);
|
|
5525
|
-
const mw = ((req, ctx, next) => {
|
|
5526
|
-
ctx.sql = sql;
|
|
5527
|
-
return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
|
|
5528
|
-
});
|
|
5529
|
-
mw.__meta = { injects: ["sql"], depends: [] };
|
|
5530
|
-
mw.sql = sql;
|
|
5531
|
-
mw.migrate = async () => {
|
|
5532
|
-
await sql.unsafe(`
|
|
5533
|
-
CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
|
|
5534
|
-
name TEXT PRIMARY KEY,
|
|
5535
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
5536
|
-
)
|
|
5537
|
-
`);
|
|
5538
|
-
};
|
|
5539
|
-
mw.markMigrated = async (moduleName) => {
|
|
5540
|
-
await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
|
|
5541
|
-
moduleName
|
|
5542
|
-
]);
|
|
5543
|
-
};
|
|
5544
|
-
mw.isMigrated = async (moduleName) => {
|
|
5545
|
-
const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
|
|
5546
|
-
return rows.length > 0;
|
|
5547
|
-
};
|
|
5548
|
-
mw.transaction = ((fn) => pool.begin(fn));
|
|
5549
|
-
mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
|
|
5550
|
-
mw.close = () => pool.close();
|
|
5551
|
-
return mw;
|
|
5552
|
-
}
|
|
5553
|
-
var TaggedQuery = class {
|
|
5554
|
-
text;
|
|
5555
|
-
params;
|
|
5556
|
-
executor;
|
|
5557
|
-
constructor(text, params, executor) {
|
|
5558
|
-
this.text = text;
|
|
5559
|
-
this.params = params;
|
|
5560
|
-
this.executor = executor;
|
|
5549
|
+
/** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
|
|
5550
|
+
armTimeout(p) {
|
|
5551
|
+
const ms = this.opts.commandTimeoutMs;
|
|
5552
|
+
if (ms <= 0) return;
|
|
5553
|
+
p.timer = setTimeout(() => {
|
|
5554
|
+
p.timedOut = true;
|
|
5555
|
+
if (p.blocking) p.resolve(null);
|
|
5556
|
+
else p.reject(new TimeoutError("redis: command timeout", ms));
|
|
5557
|
+
}, ms);
|
|
5561
5558
|
}
|
|
5562
|
-
|
|
5563
|
-
|
|
5559
|
+
/** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
|
|
5560
|
+
armSocketTimeout() {
|
|
5561
|
+
const ms = this.opts.socketTimeoutMs;
|
|
5562
|
+
if (ms <= 0) return;
|
|
5563
|
+
this.clearSocketTimeout();
|
|
5564
|
+
this.socketTimeoutTimer = setTimeout(() => {
|
|
5565
|
+
this.socketTimeoutTimer = null;
|
|
5566
|
+
const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
|
|
5567
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5568
|
+
this.pending = [];
|
|
5569
|
+
this.pendingHead = 0;
|
|
5570
|
+
for (const q of queue2) q.reject(err);
|
|
5571
|
+
this.clearSocketTimeout();
|
|
5572
|
+
this.socket?.destroy();
|
|
5573
|
+
}, ms);
|
|
5564
5574
|
}
|
|
5565
|
-
|
|
5566
|
-
|
|
5575
|
+
clearSocketTimeout() {
|
|
5576
|
+
if (this.socketTimeoutTimer) {
|
|
5577
|
+
clearTimeout(this.socketTimeoutTimer);
|
|
5578
|
+
this.socketTimeoutTimer = null;
|
|
5579
|
+
}
|
|
5567
5580
|
}
|
|
5568
|
-
|
|
5569
|
-
|
|
5581
|
+
flushOffline() {
|
|
5582
|
+
const queue2 = this.offlineQueue;
|
|
5583
|
+
this.offlineQueue = [];
|
|
5584
|
+
for (const q of queue2) {
|
|
5585
|
+
this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
|
|
5586
|
+
}
|
|
5570
5587
|
}
|
|
5571
|
-
/**
|
|
5572
|
-
|
|
5573
|
-
|
|
5588
|
+
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
|
5589
|
+
batch(payload, count) {
|
|
5590
|
+
if (this.status !== "ready" || !this.socket) {
|
|
5591
|
+
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
5592
|
+
}
|
|
5593
|
+
return new Promise((resolve3, reject) => {
|
|
5594
|
+
const results = [];
|
|
5595
|
+
const batchPending = [];
|
|
5596
|
+
let settled = false;
|
|
5597
|
+
const maybeResolve = () => {
|
|
5598
|
+
if (settled) return;
|
|
5599
|
+
if (results.length === count) {
|
|
5600
|
+
settled = true;
|
|
5601
|
+
if (batchTimer) clearTimeout(batchTimer);
|
|
5602
|
+
resolve3(results);
|
|
5603
|
+
}
|
|
5604
|
+
};
|
|
5605
|
+
for (let i = 0; i < count; i++) {
|
|
5606
|
+
const p = {
|
|
5607
|
+
// 正常响应与错误响应(RespError)都作为结果值收集——管道语义
|
|
5608
|
+
resolve: (v) => {
|
|
5609
|
+
results.push(v);
|
|
5610
|
+
maybeResolve();
|
|
5611
|
+
},
|
|
5612
|
+
reject: (e) => {
|
|
5613
|
+
results.push(e);
|
|
5614
|
+
maybeResolve();
|
|
5615
|
+
}
|
|
5616
|
+
};
|
|
5617
|
+
batchPending.push(p);
|
|
5618
|
+
this.pending.push(p);
|
|
5619
|
+
}
|
|
5620
|
+
let batchTimer;
|
|
5621
|
+
const ms = this.opts.commandTimeoutMs;
|
|
5622
|
+
if (ms > 0) {
|
|
5623
|
+
batchTimer = setTimeout(() => {
|
|
5624
|
+
settled = true;
|
|
5625
|
+
for (const p of batchPending) p.timedOut = true;
|
|
5626
|
+
reject(new TimeoutError("redis: batch timeout", ms));
|
|
5627
|
+
}, ms);
|
|
5628
|
+
}
|
|
5629
|
+
this.socket.write(payload);
|
|
5630
|
+
this.armSocketTimeout();
|
|
5631
|
+
});
|
|
5574
5632
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
let sql = strings[0];
|
|
5595
|
-
const params = [];
|
|
5596
|
-
for (let i = 0; i < values.length; i++) {
|
|
5597
|
-
const frag = values[i]?.__fragment;
|
|
5598
|
-
if (frag) {
|
|
5599
|
-
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
5600
|
-
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
5601
|
-
return `$${params.length}`;
|
|
5602
|
-
});
|
|
5603
|
-
sql += renumbered + strings[i + 1];
|
|
5604
|
-
} else {
|
|
5605
|
-
params.push(values[i]);
|
|
5606
|
-
sql += `$${params.length}` + strings[i + 1];
|
|
5633
|
+
/** 订阅频道:回调式(channel, message) */
|
|
5634
|
+
async subscribe(channel, fn) {
|
|
5635
|
+
this.subs.set(channel, fn);
|
|
5636
|
+
await this.command("SUBSCRIBE", channel);
|
|
5637
|
+
}
|
|
5638
|
+
/** 订阅模式:回调式(channel, message) */
|
|
5639
|
+
async psubscribe(pattern, fn) {
|
|
5640
|
+
this.psubs.set(pattern, fn);
|
|
5641
|
+
await this.command("PSUBSCRIBE", pattern);
|
|
5642
|
+
}
|
|
5643
|
+
/** 旁路分发订阅消息(RESP 数组路由到回调) */
|
|
5644
|
+
dispatchSubscribe(value) {
|
|
5645
|
+
const [type, a, b] = value;
|
|
5646
|
+
if (type === "message") {
|
|
5647
|
+
const fn = this.subs.get(a);
|
|
5648
|
+
fn?.(a, b);
|
|
5649
|
+
} else if (type === "pmessage") {
|
|
5650
|
+
const fn = this.psubs.get(a);
|
|
5651
|
+
fn?.(b, value[3] ?? "");
|
|
5607
5652
|
}
|
|
5608
5653
|
}
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
"23502": 400,
|
|
5617
|
-
// not_null_violation
|
|
5618
|
-
"23514": 400,
|
|
5619
|
-
// check_violation
|
|
5620
|
-
"22P02": 400,
|
|
5621
|
-
// invalid_text_representation
|
|
5622
|
-
"22003": 400
|
|
5623
|
-
// numeric_value_out_of_range
|
|
5624
|
-
};
|
|
5625
|
-
function wrapError(promise) {
|
|
5626
|
-
return promise.catch((err) => {
|
|
5627
|
-
const code = err?.code;
|
|
5628
|
-
if (code && PG_ERROR_MAP[code]) {
|
|
5629
|
-
throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
|
|
5654
|
+
/** 主动关闭——不再重连 */
|
|
5655
|
+
async close() {
|
|
5656
|
+
this.closed = true;
|
|
5657
|
+
this.status = "closed";
|
|
5658
|
+
if (this.reconnectTimer) {
|
|
5659
|
+
clearTimeout(this.reconnectTimer);
|
|
5660
|
+
this.reconnectTimer = null;
|
|
5630
5661
|
}
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5662
|
+
this.clearSocketTimeout();
|
|
5663
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5664
|
+
this.pending = [];
|
|
5665
|
+
this.pendingHead = 0;
|
|
5666
|
+
for (const p of queue2) p.reject(new ConnectionError("redis: connection closed"));
|
|
5667
|
+
const oq = this.offlineQueue;
|
|
5668
|
+
this.offlineQueue = [];
|
|
5669
|
+
for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
|
|
5670
|
+
const sock = this.socket;
|
|
5671
|
+
this.socket = null;
|
|
5672
|
+
sock?.destroy();
|
|
5673
|
+
}
|
|
5674
|
+
get connected() {
|
|
5675
|
+
return this.status === "ready";
|
|
5676
|
+
}
|
|
5677
|
+
};
|
|
5634
5678
|
|
|
5635
5679
|
// src/db/redis/pipeline.ts
|
|
5636
5680
|
var RedisPipeline = class {
|