weifuwu 0.61.2 → 0.63.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.
Files changed (72) hide show
  1. package/README.md +168 -11
  2. package/dist/client/diff.d.ts +13 -1
  3. package/dist/client/index.js +3 -3
  4. package/dist/client/popup.d.ts +22 -0
  5. package/dist/client/types.d.ts +63 -0
  6. package/dist/client/ui.d.ts +6 -0
  7. package/dist/components/Accordion/Accordion.d.ts +9 -0
  8. package/dist/components/Affix/Affix.d.ts +16 -0
  9. package/dist/components/AspectRatio/AspectRatio.d.ts +10 -0
  10. package/dist/components/AvatarGroup/AvatarGroup.d.ts +21 -0
  11. package/dist/components/BackTop/BackTop.d.ts +16 -0
  12. package/dist/components/Calendar/Calendar.d.ts +21 -0
  13. package/dist/components/Carousel/Carousel.d.ts +17 -0
  14. package/dist/components/Cascader/Cascader.d.ts +21 -0
  15. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +23 -0
  16. package/dist/components/CodeBlock/CodeBlock.d.ts +14 -0
  17. package/dist/components/Collapse/Collapse.d.ts +28 -0
  18. package/dist/components/ColorPicker/ColorPicker.d.ts +16 -0
  19. package/dist/components/Command/Command.d.ts +22 -0
  20. package/dist/components/ContextMenu/ContextMenu.d.ts +17 -0
  21. package/dist/components/CopyButton/CopyButton.d.ts +18 -0
  22. package/dist/components/Descriptions/Descriptions.d.ts +21 -0
  23. package/dist/components/Highlight/Highlight.d.ts +13 -0
  24. package/dist/components/HoverCard/HoverCard.d.ts +16 -0
  25. package/dist/components/Icon/Icon.d.ts +1 -1
  26. package/dist/components/Img/Img.d.ts +4 -0
  27. package/dist/components/InView/InView.d.ts +1 -1
  28. package/dist/components/InfiniteScroll/InfiniteScroll.d.ts +15 -0
  29. package/dist/components/InputNumber/InputNumber.d.ts +25 -0
  30. package/dist/components/Label/Label.d.ts +11 -0
  31. package/dist/components/List/List.d.ts +18 -0
  32. package/dist/components/Markdown/Markdown.d.ts +14 -0
  33. package/dist/components/Markdown/parser.d.ts +32 -0
  34. package/dist/components/Mentions/Mentions.d.ts +19 -0
  35. package/dist/components/Menu/Menu.d.ts +25 -0
  36. package/dist/components/Menubar/Menubar.d.ts +22 -0
  37. package/dist/components/MessageBubble/MessageBubble.d.ts +20 -0
  38. package/dist/components/Notification/Notification.d.ts +70 -0
  39. package/dist/components/PasswordInput/PasswordInput.d.ts +21 -0
  40. package/dist/components/PinInput/PinInput.d.ts +18 -0
  41. package/dist/components/QRCode/QRCode.d.ts +20 -0
  42. package/dist/components/QRCode/qr.d.ts +15 -0
  43. package/dist/components/Rate/Rate.d.ts +16 -0
  44. package/dist/components/Resizable/Resizable.d.ts +18 -0
  45. package/dist/components/Result/Result.d.ts +17 -0
  46. package/dist/components/Select/Select.d.ts +4 -2
  47. package/dist/components/Table/Table.d.ts +10 -0
  48. package/dist/components/TagsInput/TagsInput.d.ts +21 -0
  49. package/dist/components/Timeline/Timeline.d.ts +25 -0
  50. package/dist/components/ToggleGroup/ToggleGroup.d.ts +33 -0
  51. package/dist/components/Transfer/Transfer.d.ts +18 -0
  52. package/dist/components/Tree/Tree.d.ts +27 -0
  53. package/dist/components/Typography/Typography.d.ts +39 -0
  54. package/dist/components/VirtualList/VirtualList.d.ts +21 -0
  55. package/dist/components/Watermark/Watermark.d.ts +19 -0
  56. package/dist/components/index.d.ts +85 -0
  57. package/dist/components/index.js +14 -1
  58. package/dist/components/style.css +4824 -2211
  59. package/dist/db/postgres/connection.d.ts +24 -3
  60. package/dist/db/postgres/pool.d.ts +30 -8
  61. package/dist/db/redis/client.d.ts +43 -0
  62. package/dist/db/redis/connection.d.ts +12 -0
  63. package/dist/db/redis/pipeline.d.ts +26 -0
  64. package/dist/db/redis/pool.d.ts +27 -2
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1055 -94
  67. package/dist/layout/weifuwu-layout.css +11 -1
  68. package/dist/postgres/types.d.ts +2 -1
  69. package/dist/redis/types.d.ts +6 -0
  70. package/dist/scheduler/cron.d.ts +26 -0
  71. package/dist/scheduler/index.d.ts +69 -0
  72. package/package.json +2 -4
package/dist/index.js CHANGED
@@ -1087,6 +1087,9 @@ var MIME_TYPES = {
1087
1087
 
1088
1088
  // src/middleware/rate-limit.ts
1089
1089
  var PREFIX = "rl:";
1090
+ function requestIp(req) {
1091
+ return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
1092
+ }
1090
1093
  function rateLimit(options) {
1091
1094
  const opts = {
1092
1095
  windowMs: options?.windowMs ?? 6e4,
@@ -1171,7 +1174,8 @@ function rateLimit(options) {
1171
1174
  const mw = (async (req, ctx, next) => {
1172
1175
  if (opts.store === "redis") requireRedis(ctx);
1173
1176
  ctx.limit = async (name, manual) => {
1174
- const key = `${PREFIX}${name}`;
1177
+ const dim = (manual?.scope ?? "ip") === "global" ? "" : `:${requestIp(req)}`;
1178
+ const key = `${PREFIX}${name}${dim}`;
1175
1179
  const max = manual?.max ?? opts.max;
1176
1180
  const windowMs = manual?.windowMs ?? opts.windowMs;
1177
1181
  const r2 = await check(key, max, windowMs);
@@ -1681,6 +1685,7 @@ function userSystem(options) {
1681
1685
  }
1682
1686
 
1683
1687
  // src/messager/index.ts
1688
+ var messagerSeq = 0;
1684
1689
  var CONVERSATIONS = "_weifuwu_conversations";
1685
1690
  var MEMBERS = "_weifuwu_conversation_members";
1686
1691
  var MESSAGES = "_weifuwu_messages";
@@ -1835,16 +1840,19 @@ function messager(options) {
1835
1840
  redisSub.psubscribe(`${REDIS_PREFIX}*`, (channel, message) => {
1836
1841
  const room = channel.slice(REDIS_PREFIX.length);
1837
1842
  const event = JSON.parse(message);
1843
+ if (event._pid === SELF_PID) return;
1838
1844
  broadcastLocal(room, event);
1839
1845
  });
1840
1846
  }).catch((err) => {
1841
1847
  console.error("[messager] redis subscriber init error:", err);
1842
1848
  });
1843
1849
  }
1850
+ const SELF_PID = `wf:${process.pid}:${++messagerSeq}`;
1844
1851
  function broadcastLocal(room, event) {
1845
1852
  const members = rooms.get(room);
1846
1853
  if (!members) return;
1847
- const payload = JSON.stringify(event);
1854
+ const { _pid, ...clean } = event;
1855
+ const payload = JSON.stringify(clean);
1848
1856
  for (const ws of [...members]) {
1849
1857
  if (ws.readyState === ws.OPEN) {
1850
1858
  try {
@@ -1858,7 +1866,7 @@ function messager(options) {
1858
1866
  broadcastLocal(room, event);
1859
1867
  const pool = options.redis?.redis;
1860
1868
  if (pool) {
1861
- pool.publish(`${REDIS_PREFIX}${room}`, JSON.stringify(event)).catch((err) => {
1869
+ pool.publish(`${REDIS_PREFIX}${room}`, JSON.stringify({ ...event, _pid: SELF_PID })).catch((err) => {
1862
1870
  console.error("[messager] redis publish error:", err);
1863
1871
  });
1864
1872
  }
@@ -2326,6 +2334,7 @@ function indexOfCRLF(buf, from = 0) {
2326
2334
 
2327
2335
  // src/db/redis/connection.ts
2328
2336
  var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
2337
+ var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
2329
2338
  var RedisConnection = class {
2330
2339
  ready = false;
2331
2340
  opts;
@@ -2340,6 +2349,7 @@ var RedisConnection = class {
2340
2349
  status = "idle";
2341
2350
  retries = 0;
2342
2351
  reconnectTimer = null;
2352
+ socketTimeoutTimer = null;
2343
2353
  connectPromise = null;
2344
2354
  closed = false;
2345
2355
  connectedOnce = false;
@@ -2350,7 +2360,10 @@ var RedisConnection = class {
2350
2360
  retryDelayMs: options.retryDelayMs ?? 100,
2351
2361
  maxRetries: options.maxRetries ?? 10,
2352
2362
  enableOfflineQueue: options.enableOfflineQueue ?? true,
2353
- maxOfflineQueue: options.maxOfflineQueue ?? 5e3
2363
+ maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
2364
+ commandTimeoutMs: options.commandTimeoutMs ?? 0,
2365
+ socketTimeoutMs: options.socketTimeoutMs ?? 0,
2366
+ onCommand: options.onCommand
2354
2367
  };
2355
2368
  }
2356
2369
  /** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
@@ -2404,8 +2417,10 @@ var RedisConnection = class {
2404
2417
  this.pending = [];
2405
2418
  this.pendingHead = 0;
2406
2419
  for (const p of queue2) p.reject(err);
2420
+ this.clearSocketTimeout();
2407
2421
  if (this.closed || this.status === "closed") return;
2408
2422
  this.retries++;
2423
+ this.status = "connecting";
2409
2424
  if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
2410
2425
  this.status = "closed";
2411
2426
  const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
@@ -2429,6 +2444,7 @@ var RedisConnection = class {
2429
2444
  }
2430
2445
  const p = this.pending[this.pendingHead++];
2431
2446
  if (!p) break;
2447
+ if (p.timedOut) continue;
2432
2448
  if (raw instanceof RespError) p.reject(raw);
2433
2449
  else p.resolve(decodeValue(raw, !p.asBuffer));
2434
2450
  }
@@ -2436,6 +2452,10 @@ var RedisConnection = class {
2436
2452
  this.pending = this.pending.slice(this.pendingHead);
2437
2453
  this.pendingHead = 0;
2438
2454
  }
2455
+ if (this.opts.socketTimeoutMs > 0) {
2456
+ if (this.pendingHead < this.pending.length) this.armSocketTimeout();
2457
+ else this.clearSocketTimeout();
2458
+ }
2439
2459
  } catch (e) {
2440
2460
  const queue2 = this.pending.slice(this.pendingHead);
2441
2461
  this.pending = [];
@@ -2465,10 +2485,60 @@ var RedisConnection = class {
2465
2485
  });
2466
2486
  }
2467
2487
  sendNow(name, args, asBuffer) {
2488
+ const start = performance.now();
2468
2489
  return new Promise((resolve3, reject) => {
2469
- this.pending.push({ resolve: resolve3, reject, asBuffer });
2490
+ const p = {
2491
+ resolve: resolve3,
2492
+ reject,
2493
+ asBuffer,
2494
+ blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
2495
+ };
2496
+ this.armTimeout(p);
2497
+ this.pending.push(p);
2498
+ this.armSocketTimeout();
2470
2499
  this.socket.write(encodeCommand([name, ...args]));
2471
- });
2500
+ }).then(
2501
+ (v) => {
2502
+ if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
2503
+ return v;
2504
+ },
2505
+ (e) => {
2506
+ if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
2507
+ throw e;
2508
+ }
2509
+ );
2510
+ }
2511
+ /** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
2512
+ armTimeout(p) {
2513
+ const ms = this.opts.commandTimeoutMs;
2514
+ if (ms <= 0) return;
2515
+ p.timer = setTimeout(() => {
2516
+ p.timedOut = true;
2517
+ if (p.blocking) p.resolve(null);
2518
+ else p.reject(new TimeoutError("redis: command timeout", ms));
2519
+ }, ms);
2520
+ }
2521
+ /** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
2522
+ armSocketTimeout() {
2523
+ const ms = this.opts.socketTimeoutMs;
2524
+ if (ms <= 0) return;
2525
+ this.clearSocketTimeout();
2526
+ this.socketTimeoutTimer = setTimeout(() => {
2527
+ this.socketTimeoutTimer = null;
2528
+ const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
2529
+ const queue2 = this.pending.slice(this.pendingHead);
2530
+ this.pending = [];
2531
+ this.pendingHead = 0;
2532
+ for (const q of queue2) q.reject(err);
2533
+ this.clearSocketTimeout();
2534
+ this.socket?.destroy();
2535
+ }, ms);
2536
+ }
2537
+ clearSocketTimeout() {
2538
+ if (this.socketTimeoutTimer) {
2539
+ clearTimeout(this.socketTimeoutTimer);
2540
+ this.socketTimeoutTimer = null;
2541
+ }
2472
2542
  }
2473
2543
  flushOffline() {
2474
2544
  const queue2 = this.offlineQueue;
@@ -2484,11 +2554,18 @@ var RedisConnection = class {
2484
2554
  }
2485
2555
  return new Promise((resolve3, reject) => {
2486
2556
  const results = [];
2557
+ const batchPending = [];
2558
+ let settled = false;
2487
2559
  const maybeResolve = () => {
2488
- if (results.length === count) resolve3(results);
2560
+ if (settled) return;
2561
+ if (results.length === count) {
2562
+ settled = true;
2563
+ if (batchTimer) clearTimeout(batchTimer);
2564
+ resolve3(results);
2565
+ }
2489
2566
  };
2490
2567
  for (let i = 0; i < count; i++) {
2491
- this.pending.push({
2568
+ const p = {
2492
2569
  // 正常响应与错误响应(RespError)都作为结果值收集——管道语义
2493
2570
  resolve: (v) => {
2494
2571
  results.push(v);
@@ -2498,9 +2575,21 @@ var RedisConnection = class {
2498
2575
  results.push(e);
2499
2576
  maybeResolve();
2500
2577
  }
2501
- });
2578
+ };
2579
+ batchPending.push(p);
2580
+ this.pending.push(p);
2581
+ }
2582
+ let batchTimer;
2583
+ const ms = this.opts.commandTimeoutMs;
2584
+ if (ms > 0) {
2585
+ batchTimer = setTimeout(() => {
2586
+ settled = true;
2587
+ for (const p of batchPending) p.timedOut = true;
2588
+ reject(new TimeoutError("redis: batch timeout", ms));
2589
+ }, ms);
2502
2590
  }
2503
2591
  this.socket.write(payload);
2592
+ this.armSocketTimeout();
2504
2593
  });
2505
2594
  }
2506
2595
  /** 订阅频道:回调式(channel, message) */
@@ -2532,6 +2621,7 @@ var RedisConnection = class {
2532
2621
  clearTimeout(this.reconnectTimer);
2533
2622
  this.reconnectTimer = null;
2534
2623
  }
2624
+ this.clearSocketTimeout();
2535
2625
  const queue2 = this.pending.slice(this.pendingHead);
2536
2626
  this.pending = [];
2537
2627
  this.pendingHead = 0;
@@ -2548,6 +2638,55 @@ var RedisConnection = class {
2548
2638
  }
2549
2639
  };
2550
2640
 
2641
+ // src/db/redis/pipeline.ts
2642
+ var RedisPipeline = class {
2643
+ cmds = [];
2644
+ conn;
2645
+ constructor(conn) {
2646
+ this.conn = conn;
2647
+ }
2648
+ set(key, value, ttl) {
2649
+ this.cmds.push(ttl !== void 0 ? { name: "SET", args: [key, value, "EX", ttl] } : { name: "SET", args: [key, value] });
2650
+ return this;
2651
+ }
2652
+ get(key) {
2653
+ this.cmds.push({ name: "GET", args: [key] });
2654
+ return this;
2655
+ }
2656
+ del(...keys) {
2657
+ this.cmds.push({ name: "DEL", args: keys });
2658
+ return this;
2659
+ }
2660
+ incr(key) {
2661
+ this.cmds.push({ name: "INCR", args: [key] });
2662
+ return this;
2663
+ }
2664
+ expire(key, seconds) {
2665
+ this.cmds.push({ name: "EXPIRE", args: [key, seconds] });
2666
+ return this;
2667
+ }
2668
+ ttl(key) {
2669
+ this.cmds.push({ name: "TTL", args: [key] });
2670
+ return this;
2671
+ }
2672
+ /** 任意命令透传 */
2673
+ raw(name, ...args) {
2674
+ this.cmds.push({ name, args });
2675
+ return this;
2676
+ }
2677
+ /**
2678
+ * 一次往返执行所有命令。结果按命令顺序;错误命令对应位置为 Error 实例,
2679
+ * 其余命令不受影响(Redis 管道语义)。
2680
+ */
2681
+ async exec() {
2682
+ const cmds = this.cmds;
2683
+ this.cmds = [];
2684
+ if (cmds.length === 0) return [];
2685
+ const payload = Buffer.concat(cmds.map((c) => Buffer.from(encodeCommand([c.name, ...c.args]))));
2686
+ return this.conn.batch(payload, cmds.length);
2687
+ }
2688
+ };
2689
+
2551
2690
  // src/db/redis/client.ts
2552
2691
  var RedisClient = class _RedisClient {
2553
2692
  conn;
@@ -2564,6 +2703,10 @@ var RedisClient = class _RedisClient {
2564
2703
  command(name, ...args) {
2565
2704
  return this.conn.command(name, ...args);
2566
2705
  }
2706
+ /** 连接是否就绪(池坏连接剔除用) */
2707
+ get connected() {
2708
+ return this.conn.connected;
2709
+ }
2567
2710
  // ── 基础命令 ──────────────────────────────
2568
2711
  async get(key) {
2569
2712
  const v = await this.conn.command("GET", key);
@@ -2602,6 +2745,104 @@ var RedisClient = class _RedisClient {
2602
2745
  async ttl(key) {
2603
2746
  return Number(await this.conn.command("TTL", key));
2604
2747
  }
2748
+ // ── 批量 / 存在 / 计数 ────────────────────────
2749
+ /** 批量读:返回与 key 一一对应的值(缺失为 null) */
2750
+ async mget(...keys) {
2751
+ const v = await this.conn.command("MGET", ...keys);
2752
+ return v.map((x) => x === null ? null : String(x));
2753
+ }
2754
+ /** 批量写:mset(k1, v1, k2, v2, ...)(偶数参数) */
2755
+ async mset(...kv) {
2756
+ const reply = await this.conn.command("MSET", ...kv);
2757
+ return String(reply);
2758
+ }
2759
+ /** 存在性:返回存在的 key 数 */
2760
+ async exists(...keys) {
2761
+ return Number(await this.conn.command("EXISTS", ...keys));
2762
+ }
2763
+ /** 原子设置(仅 key 不存在时):1=设置成功 0=已存在(分布式锁基础) */
2764
+ async setnx(key, value) {
2765
+ return Number(await this.conn.command("SETNX", key, value));
2766
+ }
2767
+ /** 原子加增量,返回新值 */
2768
+ async incrby(key, delta) {
2769
+ return Number(await this.conn.command("INCRBY", key, delta));
2770
+ }
2771
+ // ── hash ─────────────────────────────────────
2772
+ /** 设置字段,返回新增字段数 */
2773
+ async hset(key, field, value) {
2774
+ return Number(await this.conn.command("HSET", key, field, value));
2775
+ }
2776
+ /** 读字段(缺失 null) */
2777
+ async hget(key, field) {
2778
+ const v = await this.conn.command("HGET", key, field);
2779
+ return v === null ? null : String(v);
2780
+ }
2781
+ /** 读整个 hash → Record(key 不存在 → {}) */
2782
+ async hgetall(key) {
2783
+ const v = await this.conn.command("HGETALL", key);
2784
+ const arr = v;
2785
+ const out = {};
2786
+ for (let i = 0; i + 1 < arr.length; i += 2) out[String(arr[i])] = String(arr[i + 1]);
2787
+ return out;
2788
+ }
2789
+ /** 删除字段,返回删除数 */
2790
+ async hdel(key, ...fields) {
2791
+ return Number(await this.conn.command("HDEL", key, ...fields));
2792
+ }
2793
+ // ── list ─────────────────────────────────────
2794
+ /** 头插,返回长度 */
2795
+ async lpush(key, ...values) {
2796
+ return Number(await this.conn.command("LPUSH", key, ...values));
2797
+ }
2798
+ /** 尾插,返回长度 */
2799
+ async rpush(key, ...values) {
2800
+ return Number(await this.conn.command("RPUSH", key, ...values));
2801
+ }
2802
+ /** 头弹(缺失 null) */
2803
+ async lpop(key) {
2804
+ const v = await this.conn.command("LPOP", key);
2805
+ return v === null ? null : String(v);
2806
+ }
2807
+ /** 尾弹(缺失 null) */
2808
+ async rpop(key) {
2809
+ const v = await this.conn.command("RPOP", key);
2810
+ return v === null ? null : String(v);
2811
+ }
2812
+ /** 范围读(start/stop 支持负数,如 0 -1 全量) */
2813
+ async lrange(key, start, stop) {
2814
+ const v = await this.conn.command("LRANGE", key, start, stop);
2815
+ return v.map(String);
2816
+ }
2817
+ // ── set ──────────────────────────────────────
2818
+ /** 加成员,返回新增数(重复不加) */
2819
+ async sadd(key, ...members) {
2820
+ return Number(await this.conn.command("SADD", key, ...members));
2821
+ }
2822
+ /** 删成员,返回删除数 */
2823
+ async srem(key, ...members) {
2824
+ return Number(await this.conn.command("SREM", key, ...members));
2825
+ }
2826
+ /** 全部成员(无序) */
2827
+ async smembers(key) {
2828
+ const v = await this.conn.command("SMEMBERS", key);
2829
+ return v.map(String);
2830
+ }
2831
+ // ── zset ─────────────────────────────────────
2832
+ /** 加成员(score 排序),返回新增数 */
2833
+ async zadd(key, score, member) {
2834
+ return Number(await this.conn.command("ZADD", key, score, member));
2835
+ }
2836
+ /** 范围读(按 score 升序;start/stop 为排名,支持负数) */
2837
+ async zrange(key, start, stop) {
2838
+ const v = await this.conn.command("ZRANGE", key, start, stop);
2839
+ return v.map(String);
2840
+ }
2841
+ // ── pipeline ─────────────────────────────────
2842
+ /** 创建管道(批量命令一次往返;结果按序) */
2843
+ pipeline() {
2844
+ return new RedisPipeline(this.conn);
2845
+ }
2605
2846
  // ── JSON 存取(自动序列化) ────────────────
2606
2847
  /** 读取并 JSON.parse;key 不存在返回 null */
2607
2848
  async jsonGet(key) {
@@ -2661,6 +2902,7 @@ var RedisSubscriber = class {
2661
2902
  var RedisPool = class _RedisPool {
2662
2903
  clients = [];
2663
2904
  rr = 0;
2905
+ replenishWaiters = [];
2664
2906
  closed = false;
2665
2907
  keyPrefix;
2666
2908
  opts;
@@ -2696,65 +2938,218 @@ var RedisPool = class _RedisPool {
2696
2938
  k(key) {
2697
2939
  return this.keyPrefix + key;
2698
2940
  }
2699
- /** round-robin 选择连接 */
2700
- next() {
2701
- if (this.closed || this.clients.length === 0) {
2702
- throw new ConnectionError("redis: pool is closed");
2941
+ /** round-robin 选择健康连接;跳过不可用连接(断线/重连中/closed)——剔除并异步重建,防分发到死连接。
2942
+ * 全部不可用时等待 replenish 补位(Redis 真宕机 1s 后明确报错,不无限挂起)。 */
2943
+ async acquireHealthy() {
2944
+ if (this.closed) throw new ConnectionError("redis: pool is closed");
2945
+ while (this.clients.length > 0) {
2946
+ const idx = this.rr % this.clients.length;
2947
+ this.rr++;
2948
+ const c = this.clients[idx];
2949
+ if (c.connected) return c;
2950
+ this.clients.splice(idx, 1);
2951
+ c.close().catch(() => {
2952
+ });
2953
+ this.replenish();
2703
2954
  }
2704
- const client = this.clients[this.rr % this.clients.length];
2705
- this.rr++;
2706
- return client;
2955
+ return new Promise((resolve3, reject) => {
2956
+ const waiter = () => {
2957
+ if (this.clients.length > 0 && this.clients[0].connected) resolve3(this.clients[0]);
2958
+ else reject(new ConnectionError("redis: pool exhausted after reconnect wait"));
2959
+ };
2960
+ this.replenishWaiters.push(waiter);
2961
+ setTimeout(() => {
2962
+ const i = this.replenishWaiters.indexOf(waiter);
2963
+ if (i >= 0) this.replenishWaiters.splice(i, 1);
2964
+ reject(new ConnectionError("redis: no healthy connection available"));
2965
+ }, 1e3);
2966
+ });
2967
+ }
2968
+ /** 剔除后补位:异步重建连接,就绪后回池并唤醒等待者 */
2969
+ replenish() {
2970
+ if (this.closed) return;
2971
+ RedisClient.connect(this.opts).then((c) => {
2972
+ if (this.closed) {
2973
+ c.close();
2974
+ return;
2975
+ }
2976
+ this.clients.push(c);
2977
+ const ws = this.replenishWaiters;
2978
+ this.replenishWaiters = [];
2979
+ for (const w of ws) w();
2980
+ }).catch(() => {
2981
+ setTimeout(() => this.replenish(), 500);
2982
+ });
2707
2983
  }
2708
2984
  // ── 与 RedisClient 相同的方法面(代理到轮询连接) ──
2709
2985
  async command(name, ...args) {
2710
2986
  await this.ensure();
2711
- return this.next().command(name, ...args);
2987
+ const c = await this.acquireHealthy();
2988
+ return c.command(name, ...args);
2712
2989
  }
2713
2990
  async get(key) {
2714
2991
  await this.ensure();
2715
- return this.next().get(this.k(key));
2992
+ const c = await this.acquireHealthy();
2993
+ return c.get(this.k(key));
2716
2994
  }
2717
2995
  /** 二进制安全读取(原始字节,不解码) */
2718
2996
  async getBuffer(key) {
2719
2997
  await this.ensure();
2720
- return this.next().getBuffer(this.k(key));
2998
+ const c = await this.acquireHealthy();
2999
+ return c.getBuffer(this.k(key));
2721
3000
  }
2722
3001
  async set(key, value, ttl) {
2723
3002
  await this.ensure();
2724
- return this.next().set(this.k(key), value, ttl);
3003
+ const c = await this.acquireHealthy();
3004
+ return c.set(this.k(key), value, ttl);
2725
3005
  }
2726
3006
  async del(...keys) {
2727
3007
  await this.ensure();
2728
- return this.next().del(...keys.map((k) => this.k(k)));
3008
+ const c = await this.acquireHealthy();
3009
+ return c.del(...keys.map((k) => this.k(k)));
2729
3010
  }
2730
3011
  async incr(key) {
2731
3012
  await this.ensure();
2732
- return this.next().incr(this.k(key));
3013
+ const c = await this.acquireHealthy();
3014
+ return c.incr(this.k(key));
2733
3015
  }
2734
3016
  async expire(key, seconds) {
2735
3017
  await this.ensure();
2736
- return this.next().expire(this.k(key), seconds);
3018
+ const c = await this.acquireHealthy();
3019
+ return c.expire(this.k(key), seconds);
2737
3020
  }
2738
3021
  async ttl(key) {
2739
3022
  await this.ensure();
2740
- return this.next().ttl(this.k(key));
3023
+ const c = await this.acquireHealthy();
3024
+ return c.ttl(this.k(key));
2741
3025
  }
2742
3026
  async jsonGet(key) {
2743
3027
  await this.ensure();
2744
- return this.next().jsonGet(this.k(key));
3028
+ const c = await this.acquireHealthy();
3029
+ return c.jsonGet(this.k(key));
2745
3030
  }
2746
3031
  async jsonSet(key, value, ttl) {
2747
3032
  await this.ensure();
2748
- return this.next().jsonSet(this.k(key), value, ttl);
3033
+ const c = await this.acquireHealthy();
3034
+ return c.jsonSet(this.k(key), value, ttl);
2749
3035
  }
2750
3036
  async cache(key, fn, ttl) {
2751
3037
  await this.ensure();
2752
- return this.next().cache(this.k(key), fn, ttl);
3038
+ const c = await this.acquireHealthy();
3039
+ return c.cache(this.k(key), fn, ttl);
3040
+ }
3041
+ // ── 批量 / 存在 / 计数 ────────────────────────
3042
+ async mget(...keys) {
3043
+ await this.ensure();
3044
+ const c = await this.acquireHealthy();
3045
+ return c.mget(...keys.map((k) => this.k(k)));
3046
+ }
3047
+ async mset(...kv) {
3048
+ await this.ensure();
3049
+ const c = await this.acquireHealthy();
3050
+ const prefixed = kv.map((x, i) => i % 2 === 0 ? this.k(String(x)) : x);
3051
+ return c.mset(...prefixed);
3052
+ }
3053
+ async exists(...keys) {
3054
+ await this.ensure();
3055
+ const c = await this.acquireHealthy();
3056
+ return c.exists(...keys.map((k) => this.k(k)));
3057
+ }
3058
+ async setnx(key, value) {
3059
+ await this.ensure();
3060
+ const c = await this.acquireHealthy();
3061
+ return c.setnx(this.k(key), value);
3062
+ }
3063
+ async incrby(key, delta) {
3064
+ await this.ensure();
3065
+ const c = await this.acquireHealthy();
3066
+ return c.incrby(this.k(key), delta);
3067
+ }
3068
+ // ── hash ─────────────────────────────────────
3069
+ async hset(key, field, value) {
3070
+ await this.ensure();
3071
+ const c = await this.acquireHealthy();
3072
+ return c.hset(this.k(key), field, value);
3073
+ }
3074
+ async hget(key, field) {
3075
+ await this.ensure();
3076
+ const c = await this.acquireHealthy();
3077
+ return c.hget(this.k(key), field);
3078
+ }
3079
+ async hgetall(key) {
3080
+ await this.ensure();
3081
+ const c = await this.acquireHealthy();
3082
+ return c.hgetall(this.k(key));
3083
+ }
3084
+ async hdel(key, ...fields) {
3085
+ await this.ensure();
3086
+ const c = await this.acquireHealthy();
3087
+ return c.hdel(this.k(key), ...fields);
3088
+ }
3089
+ // ── list ─────────────────────────────────────
3090
+ async lpush(key, ...values) {
3091
+ await this.ensure();
3092
+ const c = await this.acquireHealthy();
3093
+ return c.lpush(this.k(key), ...values);
3094
+ }
3095
+ async rpush(key, ...values) {
3096
+ await this.ensure();
3097
+ const c = await this.acquireHealthy();
3098
+ return c.rpush(this.k(key), ...values);
3099
+ }
3100
+ async lpop(key) {
3101
+ await this.ensure();
3102
+ const c = await this.acquireHealthy();
3103
+ return c.lpop(this.k(key));
3104
+ }
3105
+ async rpop(key) {
3106
+ await this.ensure();
3107
+ const c = await this.acquireHealthy();
3108
+ return c.rpop(this.k(key));
3109
+ }
3110
+ async lrange(key, start, stop) {
3111
+ await this.ensure();
3112
+ const c = await this.acquireHealthy();
3113
+ return c.lrange(this.k(key), start, stop);
3114
+ }
3115
+ // ── set ──────────────────────────────────────
3116
+ async sadd(key, ...members) {
3117
+ await this.ensure();
3118
+ const c = await this.acquireHealthy();
3119
+ return c.sadd(this.k(key), ...members);
3120
+ }
3121
+ async srem(key, ...members) {
3122
+ await this.ensure();
3123
+ const c = await this.acquireHealthy();
3124
+ return c.srem(this.k(key), ...members);
3125
+ }
3126
+ async smembers(key) {
3127
+ await this.ensure();
3128
+ const c = await this.acquireHealthy();
3129
+ return c.smembers(this.k(key));
3130
+ }
3131
+ // ── zset ─────────────────────────────────────
3132
+ async zadd(key, score, member) {
3133
+ await this.ensure();
3134
+ const c = await this.acquireHealthy();
3135
+ return c.zadd(this.k(key), score, member);
3136
+ }
3137
+ async zrange(key, start, stop) {
3138
+ await this.ensure();
3139
+ const c = await this.acquireHealthy();
3140
+ return c.zrange(this.k(key), start, stop);
3141
+ }
3142
+ // ── pipeline(池级:选一健康连接;key 自动加前缀) ──
3143
+ async pipeline() {
3144
+ await this.ensure();
3145
+ const c = await this.acquireHealthy();
3146
+ return c.pipeline();
2753
3147
  }
2754
3148
  /** PUBLISH 消息到频道,返回收到消息的订阅者数 */
2755
3149
  async publish(channel, message) {
2756
3150
  await this.ensure();
2757
- return Number(await this.next().command("PUBLISH", channel, message));
3151
+ const c = await this.acquireHealthy();
3152
+ return Number(await c.command("PUBLISH", channel, message));
2758
3153
  }
2759
3154
  /** 创建独立订阅者连接(Pub/Sub 场景) */
2760
3155
  createSubscriber() {
@@ -2763,7 +3158,8 @@ var RedisPool = class _RedisPool {
2763
3158
  /** 清空当前库(测试/重置场景) */
2764
3159
  async flushdb() {
2765
3160
  await this.ensure();
2766
- const v = await this.next().command("FLUSHDB");
3161
+ const c = await this.acquireHealthy();
3162
+ const v = await c.command("FLUSHDB");
2767
3163
  return String(v);
2768
3164
  }
2769
3165
  /** 关闭所有池内连接 */
@@ -2780,18 +3176,18 @@ var RedisPool = class _RedisPool {
2780
3176
 
2781
3177
  // src/queue/index.ts
2782
3178
  var GROUP = "workers";
2783
- function makePool(options) {
3179
+ function parseUrl(options) {
2784
3180
  const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
2785
3181
  const u = new URL(url);
2786
- return new RedisPool({
3182
+ const opts = {
2787
3183
  host: u.hostname,
2788
- port: Number(u.port || 6379),
2789
- poolSize: options?.poolSize
2790
- });
3184
+ port: Number(u.port || 6379)
3185
+ };
3186
+ return { conn: opts, pool: new RedisPool({ ...opts, poolSize: options?.poolSize }) };
2791
3187
  }
2792
3188
  function queue(options) {
2793
3189
  const prefix = options?.prefix ?? "q:";
2794
- const pool = makePool(options);
3190
+ const { conn: connOpts, pool } = parseUrl(options);
2795
3191
  const stream = (name) => `${prefix}${name}`;
2796
3192
  const deadStream = (name) => `${prefix}${name}:dead`;
2797
3193
  async function pushJob(name, payload) {
@@ -2820,21 +3216,48 @@ function queue(options) {
2820
3216
  const dead = deadStream(name);
2821
3217
  const delayed = `${prefix}${name}:delayed`;
2822
3218
  let running = false;
3219
+ let lastErrAt = 0;
3220
+ let epoch = 0;
3221
+ let conn = null;
3222
+ let connEpoch = -1;
3223
+ const loops = /* @__PURE__ */ new Map();
2823
3224
  const inflight = /* @__PURE__ */ new Set();
3225
+ async function getConn(myEpoch) {
3226
+ if (conn && connEpoch === myEpoch) return conn;
3227
+ if (conn) {
3228
+ const c2 = conn;
3229
+ conn = null;
3230
+ connEpoch = -1;
3231
+ await c2.close().catch(() => {
3232
+ });
3233
+ }
3234
+ const c = new RedisConnection(connOpts);
3235
+ await c.connect();
3236
+ conn = c;
3237
+ connEpoch = myEpoch;
3238
+ return c;
3239
+ }
2824
3240
  async function ensureGroup() {
2825
- try {
2826
- await pool.command("XGROUP", "CREATE", s, GROUP, "0", "MKSTREAM");
2827
- } catch (e) {
2828
- if (!String(e.message).includes("BUSYGROUP")) throw e;
3241
+ let lastErr;
3242
+ for (let attempt = 0; attempt < 3; attempt++) {
3243
+ try {
3244
+ await conn.command("XGROUP", "CREATE", s, GROUP, "0", "MKSTREAM");
3245
+ return;
3246
+ } catch (e) {
3247
+ if (String(e.message).includes("BUSYGROUP")) return;
3248
+ lastErr = e;
3249
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 50));
3250
+ }
2829
3251
  }
3252
+ throw lastErr;
2830
3253
  }
2831
3254
  async function processEntry(entryId, fields) {
2832
3255
  let payload;
2833
3256
  try {
2834
3257
  payload = JSON.parse(fields.payload ?? "{}");
2835
3258
  } catch {
2836
- await pool.command("XACK", s, GROUP, entryId);
2837
- await pool.command("XADD", dead, "*", "payload", JSON.stringify({
3259
+ await conn.command("XACK", s, GROUP, entryId);
3260
+ await conn.command("XADD", dead, "*", "payload", JSON.stringify({
2838
3261
  originalId: entryId,
2839
3262
  name,
2840
3263
  error: "unparseable payload",
@@ -2851,12 +3274,12 @@ function queue(options) {
2851
3274
  data: payload.data,
2852
3275
  attempts
2853
3276
  });
2854
- await pool.command("XACK", s, GROUP, entryId);
3277
+ await conn.command("XACK", s, GROUP, entryId);
2855
3278
  } catch (err) {
2856
3279
  const nextAttempt = attempts + 1;
2857
- await pool.command("XACK", s, GROUP, entryId);
3280
+ await conn.command("XACK", s, GROUP, entryId);
2858
3281
  if (nextAttempt >= maxAttempts) {
2859
- await pool.command("XADD", dead, "*", "payload", JSON.stringify({
3282
+ await conn.command("XADD", dead, "*", "payload", JSON.stringify({
2860
3283
  originalId: entryId,
2861
3284
  name,
2862
3285
  data: payload.data,
@@ -2865,7 +3288,7 @@ function queue(options) {
2865
3288
  }));
2866
3289
  console.error(`[queue] ${name} job failed permanently (attempt ${nextAttempt}/${maxAttempts}):`, err);
2867
3290
  } else {
2868
- await pool.command("ZADD", delayed, String(Date.now() + visibilityTimeout), JSON.stringify({
3291
+ await conn.command("ZADD", delayed, String(Date.now() + visibilityTimeout), JSON.stringify({
2869
3292
  ...payload,
2870
3293
  attempts: nextAttempt
2871
3294
  }));
@@ -2880,25 +3303,27 @@ function queue(options) {
2880
3303
  const now = Date.now();
2881
3304
  let due;
2882
3305
  try {
2883
- due = await pool.command("ZRANGEBYSCORE", delayed, 0, now);
3306
+ due = await conn.command("ZRANGEBYSCORE", delayed, 0, now);
2884
3307
  } catch (e) {
2885
- if (isPoolClosed(e)) {
3308
+ if (isConnClosed(e)) {
2886
3309
  running = false;
2887
3310
  return;
2888
3311
  }
2889
- if (running) console.error("[queue] ZRANGEBYSCORE:", e instanceof Error ? e.message : e);
3312
+ if (running && shouldLogError()) {
3313
+ console.error("[queue] ZRANGEBYSCORE:", e instanceof Error ? e.message : e);
3314
+ }
2890
3315
  return;
2891
3316
  }
2892
3317
  for (const member of due) {
2893
- const removed = await pool.command("ZREM", delayed, member);
3318
+ const removed = await conn.command("ZREM", delayed, member);
2894
3319
  if (removed) {
2895
- await pool.command("XADD", s, "*", "payload", member);
3320
+ await conn.command("XADD", s, "*", "payload", member);
2896
3321
  }
2897
3322
  }
2898
3323
  }
2899
3324
  async function claimStale() {
2900
3325
  try {
2901
- const result = await pool.command(
3326
+ const result = await conn.command(
2902
3327
  "XAUTOCLAIM",
2903
3328
  s,
2904
3329
  GROUP,
@@ -2913,21 +3338,41 @@ function queue(options) {
2913
3338
  await processEntry(entryId, flatFieldsToRecord(fields));
2914
3339
  }
2915
3340
  } catch (e) {
2916
- if (isPoolClosed(e)) {
3341
+ if (isConnClosed(e)) {
2917
3342
  running = false;
2918
3343
  return;
2919
3344
  }
2920
- if (running) console.error("[queue] XAUTOCLAIM:", e instanceof Error ? e.message : e);
3345
+ if (await recoverGroupIfMissing(e)) return;
3346
+ if (running && shouldLogError()) {
3347
+ console.error("[queue] XAUTOCLAIM:", e instanceof Error ? e.message : e);
3348
+ }
3349
+ }
3350
+ }
3351
+ async function recoverGroupIfMissing(e) {
3352
+ const msg = e instanceof Error ? e.message : String(e);
3353
+ if (!/NOGROUP/.test(msg)) return false;
3354
+ try {
3355
+ await ensureGroup();
3356
+ return true;
3357
+ } catch {
3358
+ return false;
2921
3359
  }
2922
3360
  }
2923
- async function loop() {
2924
- await ensureGroup();
2925
- while (running) {
3361
+ function shouldLogError() {
3362
+ const now = Date.now();
3363
+ if (now - lastErrAt > 5e3) {
3364
+ lastErrAt = now;
3365
+ return true;
3366
+ }
3367
+ return false;
3368
+ }
3369
+ async function loop(myEpoch) {
3370
+ while (running && myEpoch === epoch) {
2926
3371
  await claimStale();
2927
3372
  await requeueDelayed();
2928
3373
  let result;
2929
3374
  try {
2930
- result = await pool.command(
3375
+ result = await conn.command(
2931
3376
  "XREADGROUP",
2932
3377
  "GROUP",
2933
3378
  GROUP,
@@ -2941,11 +3386,14 @@ function queue(options) {
2941
3386
  ">"
2942
3387
  );
2943
3388
  } catch (e) {
2944
- if (isPoolClosed(e)) {
3389
+ if (isConnClosed(e)) {
2945
3390
  running = false;
2946
3391
  return;
2947
3392
  }
2948
- if (running) console.error("[queue] XREADGROUP:", e instanceof Error ? e.message : e);
3393
+ if (await recoverGroupIfMissing(e)) continue;
3394
+ if (running && shouldLogError()) {
3395
+ console.error("[queue] XREADGROUP:", e instanceof Error ? e.message : e);
3396
+ }
2949
3397
  continue;
2950
3398
  }
2951
3399
  if (!result) continue;
@@ -2963,11 +3411,42 @@ function queue(options) {
2963
3411
  start: async () => {
2964
3412
  if (running) return;
2965
3413
  running = true;
2966
- loop().catch((e) => console.error("[queue] worker loop crashed:", e));
3414
+ const myEpoch = epoch;
3415
+ try {
3416
+ await getConn(myEpoch);
3417
+ await ensureGroup();
3418
+ } catch (e) {
3419
+ running = false;
3420
+ if (conn) {
3421
+ const c = conn;
3422
+ conn = null;
3423
+ connEpoch = -1;
3424
+ await c.close().catch(() => {
3425
+ });
3426
+ }
3427
+ throw e;
3428
+ }
3429
+ const lp = loop(myEpoch);
3430
+ loops.set(myEpoch, lp);
3431
+ lp.finally(() => loops.delete(myEpoch)).catch(
3432
+ (e) => console.error("[queue] worker loop crashed:", e)
3433
+ );
2967
3434
  },
2968
3435
  stop: async () => {
3436
+ if (!running) {
3437
+ await Promise.allSettled([...loops.values()]);
3438
+ return;
3439
+ }
3440
+ epoch++;
2969
3441
  running = false;
3442
+ const oldConn = conn;
3443
+ conn = null;
3444
+ connEpoch = -1;
3445
+ const oldLoops = [...loops.entries()].filter(([e]) => e < epoch).map(([, p]) => p);
2970
3446
  await Promise.allSettled([...inflight]);
3447
+ await Promise.allSettled(oldLoops);
3448
+ await oldConn?.close().catch(() => {
3449
+ });
2971
3450
  }
2972
3451
  };
2973
3452
  }
@@ -2988,14 +3467,292 @@ function flatFieldsToRecord(fields) {
2988
3467
  }
2989
3468
  return rec;
2990
3469
  }
2991
- function isPoolClosed(e) {
3470
+ function isConnClosed(e) {
2992
3471
  const msg = e instanceof Error ? e.message : String(e);
2993
3472
  return /pool is closed|connection closed/.test(msg);
2994
3473
  }
2995
3474
 
2996
- // src/ai/client.ts
3475
+ // src/scheduler/index.ts
2997
3476
  import { randomUUID as randomUUID2 } from "node:crypto";
2998
3477
 
3478
+ // src/scheduler/cron.ts
3479
+ var RANGES = [
3480
+ [0, 59],
3481
+ // 分
3482
+ [0, 23],
3483
+ // 时
3484
+ [1, 31],
3485
+ // 日
3486
+ [1, 12],
3487
+ // 月
3488
+ [0, 6]
3489
+ // 周
3490
+ ];
3491
+ function parseField(field, idx) {
3492
+ const [min, max] = RANGES[idx];
3493
+ const name = ["minute", "hour", "day", "month", "weekday"][idx];
3494
+ if (field === "*") return null;
3495
+ if (field.startsWith("*/")) {
3496
+ const n = Number(field.slice(2));
3497
+ if (!Number.isInteger(n) || n <= 0 || n > max) {
3498
+ throw new Error(`cron: invalid step '${field}' in ${name} field (must be 1..${max})`);
3499
+ }
3500
+ const set2 = /* @__PURE__ */ new Set();
3501
+ for (let v = min; v <= max; v += n) set2.add(v);
3502
+ return set2;
3503
+ }
3504
+ const set = /* @__PURE__ */ new Set();
3505
+ for (const part of field.split(",")) {
3506
+ if (part === "") throw new Error(`cron: empty item in '${field}' (${name})`);
3507
+ if (part.includes("-")) {
3508
+ const [rangePart, stepPart] = part.split("/");
3509
+ const step = stepPart !== void 0 ? Number(stepPart) : 1;
3510
+ const [a, b] = rangePart.split("-").map(Number);
3511
+ if (!Number.isInteger(a) || !Number.isInteger(b) || a < min || b > max || a > b) {
3512
+ throw new Error(`cron: invalid range '${part}' in ${name} field (${min}..${max})`);
3513
+ }
3514
+ if (!Number.isInteger(step) || step <= 0) throw new Error(`cron: invalid step in '${part}'`);
3515
+ for (let v = a; v <= b; v += step) set.add(v);
3516
+ } else {
3517
+ const v = Number(part);
3518
+ if (!Number.isInteger(v) || v < min || v > max) {
3519
+ throw new Error(`cron: value '${part}' out of range ${min}..${max} in ${name} field`);
3520
+ }
3521
+ set.add(v);
3522
+ }
3523
+ }
3524
+ return set;
3525
+ }
3526
+ function parseCron(expr) {
3527
+ const parts = expr.trim().split(/\s+/);
3528
+ if (parts.length !== 5) {
3529
+ throw new Error(`cron: expected 5 fields (min hour dom month dow), got ${parts.length}: '${expr}'`);
3530
+ }
3531
+ const fields = parts.map((p, i) => parseField(p, i));
3532
+ return {
3533
+ fields: fields.map((values) => ({ values })),
3534
+ domExplicit: fields[2] !== null,
3535
+ dowExplicit: fields[4] !== null
3536
+ };
3537
+ }
3538
+ function fieldMatches(values, v) {
3539
+ if (values === null) return true;
3540
+ return values.has(v);
3541
+ }
3542
+ function dayMatches(expr, dom, dow) {
3543
+ const domOk = fieldMatches(expr.fields[2].values, dom);
3544
+ const dowOk = fieldMatches(expr.fields[4].values, dow);
3545
+ if (expr.domExplicit && expr.dowExplicit) return domOk || dowOk;
3546
+ if (expr.domExplicit) return domOk;
3547
+ if (expr.dowExplicit) return dowOk;
3548
+ return true;
3549
+ }
3550
+ function nextMatch(values, v, min, max) {
3551
+ if (values === null) return v;
3552
+ for (let x = v; x <= max; x++) if (values.has(x)) return x;
3553
+ return null;
3554
+ }
3555
+ function nextRun(expr, from) {
3556
+ const d = new Date(from.getTime());
3557
+ d.setSeconds(0, 0);
3558
+ d.setMinutes(d.getMinutes() + 1);
3559
+ const deadline = from.getTime() + 5 * 366 * 24 * 3600 * 1e3;
3560
+ while (d.getTime() <= deadline) {
3561
+ const year = d.getFullYear();
3562
+ const month = d.getMonth() + 1;
3563
+ const dom = d.getDate();
3564
+ const dow = d.getDay();
3565
+ if (!fieldMatches(expr.fields[3].values, month)) {
3566
+ d.setMonth(month, 1);
3567
+ d.setHours(0, 0, 0, 0);
3568
+ d.setMonth(month);
3569
+ continue;
3570
+ }
3571
+ if (!dayMatches(expr, dom, dow)) {
3572
+ d.setDate(dom + 1);
3573
+ d.setHours(0, 0, 0, 0);
3574
+ continue;
3575
+ }
3576
+ const hour = d.getHours();
3577
+ const nextHour = nextMatch(expr.fields[1].values, hour, 0, 23);
3578
+ if (nextHour === null) {
3579
+ d.setDate(dom + 1);
3580
+ d.setHours(0, 0, 0, 0);
3581
+ continue;
3582
+ }
3583
+ if (nextHour !== hour) {
3584
+ d.setHours(nextHour, 0, 0, 0);
3585
+ continue;
3586
+ }
3587
+ const minute = d.getMinutes();
3588
+ const nextMinute = nextMatch(expr.fields[0].values, minute, 0, 59);
3589
+ if (nextMinute === null) {
3590
+ d.setHours(hour + 1, 0, 0, 0);
3591
+ continue;
3592
+ }
3593
+ if (nextMinute !== minute) {
3594
+ d.setMinutes(nextMinute, 0, 0);
3595
+ continue;
3596
+ }
3597
+ return d;
3598
+ }
3599
+ throw new Error("cron: no next run within 5 years (invalid expression?)");
3600
+ }
3601
+
3602
+ // src/scheduler/index.ts
3603
+ function parseUrl2(options) {
3604
+ const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
3605
+ const u = new URL(url);
3606
+ return { host: u.hostname, port: Number(u.port || 6379) };
3607
+ }
3608
+ function scheduler(options) {
3609
+ const prefix = options.prefix ?? "wf:sched:";
3610
+ const tickMs = options.tickMs ?? 1e3;
3611
+ const delayedKey = `${prefix}delayed`;
3612
+ const cronsKey = `${prefix}crons`;
3613
+ const connOpts = parseUrl2(options);
3614
+ const queueModule = options.queue;
3615
+ const conn = new RedisConnection(connOpts);
3616
+ let running = false;
3617
+ let tickTimer = null;
3618
+ async function tick() {
3619
+ await tickCrons();
3620
+ let due;
3621
+ try {
3622
+ due = await conn.command("ZRANGEBYSCORE", delayedKey, 0, Date.now());
3623
+ } catch {
3624
+ return;
3625
+ }
3626
+ for (const member of due) {
3627
+ let removed;
3628
+ try {
3629
+ removed = await conn.command("ZREM", delayedKey, member);
3630
+ } catch {
3631
+ return;
3632
+ }
3633
+ if (removed !== 1) continue;
3634
+ try {
3635
+ const task = JSON.parse(member);
3636
+ await queueModule.queue.add(task.name, task.data);
3637
+ } catch (e) {
3638
+ console.error("[scheduler] enqueue:", e instanceof Error ? e.message : e);
3639
+ }
3640
+ }
3641
+ }
3642
+ async function tickCrons() {
3643
+ let crons;
3644
+ try {
3645
+ crons = await conn.command("HGETALL", cronsKey);
3646
+ } catch {
3647
+ return;
3648
+ }
3649
+ const now = Date.now();
3650
+ const entries = crons;
3651
+ for (let i = 0; i + 1 < entries.length; i += 2) {
3652
+ const field = entries[i];
3653
+ const value = entries[i + 1];
3654
+ let def;
3655
+ try {
3656
+ def = JSON.parse(value);
3657
+ } catch {
3658
+ continue;
3659
+ }
3660
+ const parsed = parseCron(def.expr);
3661
+ let next = def.nextRunAt;
3662
+ while (next <= now) {
3663
+ const ts = next;
3664
+ const member = JSON.stringify({ id: `cron:${field}:${ts}`, name: def.name, data: def.data });
3665
+ try {
3666
+ await conn.command("ZADD", delayedKey, "NX", ts, member);
3667
+ } catch {
3668
+ break;
3669
+ }
3670
+ next = nextRun(parsed, new Date(ts)).getTime();
3671
+ }
3672
+ if (next !== def.nextRunAt) {
3673
+ await conn.command("HSET", cronsKey, field, JSON.stringify({ ...def, nextRunAt: next }));
3674
+ }
3675
+ }
3676
+ }
3677
+ const cron = async (expr, name, data) => {
3678
+ const parsed = parseCron(expr);
3679
+ const firstRun = nextRun(parsed, /* @__PURE__ */ new Date());
3680
+ const def = JSON.stringify({ expr, name, data, nextRunAt: firstRun.getTime() });
3681
+ await conn.command("HSET", cronsKey, name, def);
3682
+ };
3683
+ const cancelCron = async (name) => {
3684
+ const removed = await conn.command("HDEL", cronsKey, name);
3685
+ try {
3686
+ const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
3687
+ for (const member of pending) {
3688
+ if (member.includes(`"id":"cron:${name}:`)) {
3689
+ await conn.command("ZREM", delayedKey, member);
3690
+ }
3691
+ }
3692
+ } catch {
3693
+ }
3694
+ return removed === 1;
3695
+ };
3696
+ async function start() {
3697
+ if (running) return;
3698
+ running = true;
3699
+ await conn.connect();
3700
+ await tick();
3701
+ tickTimer = setInterval(() => {
3702
+ tick().catch((e) => console.error("[scheduler] tick:", e instanceof Error ? e.message : e));
3703
+ }, tickMs);
3704
+ tickTimer.unref?.();
3705
+ }
3706
+ const schedule = async (name, data, opts = {}) => {
3707
+ const delayMs = opts.delayMs ?? 0;
3708
+ const runAt = opts.when ? opts.when.getTime() : Date.now() + delayMs;
3709
+ if (!Number.isFinite(runAt)) throw new Error("scheduler: invalid when/delayMs");
3710
+ const id = randomUUID2();
3711
+ const member = JSON.stringify({ id, name, data });
3712
+ await conn.command("ZADD", delayedKey, runAt, member);
3713
+ return { id };
3714
+ };
3715
+ const cancelSchedule = async (id) => {
3716
+ try {
3717
+ const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
3718
+ for (const member of pending) {
3719
+ if (member.includes(`"id":"${id}"`)) {
3720
+ const removed = await conn.command("ZREM", delayedKey, member);
3721
+ if (removed === 1) return true;
3722
+ }
3723
+ }
3724
+ } catch {
3725
+ }
3726
+ return false;
3727
+ };
3728
+ const mw = (async (req, ctx, next) => {
3729
+ ctx.schedule = schedule;
3730
+ ctx.cron = cron;
3731
+ ctx.cancelCron = cancelCron;
3732
+ ctx.cancelSchedule = cancelSchedule;
3733
+ return next(req, ctx);
3734
+ });
3735
+ mw.__meta = { injects: ["schedule", "cron", "cancelCron", "cancelSchedule"], depends: ["queue"] };
3736
+ mw.schedule = schedule;
3737
+ mw.cron = cron;
3738
+ mw.cancelCron = cancelCron;
3739
+ mw.cancelSchedule = cancelSchedule;
3740
+ mw.close = async () => {
3741
+ running = false;
3742
+ if (tickTimer) {
3743
+ clearInterval(tickTimer);
3744
+ tickTimer = null;
3745
+ }
3746
+ await conn.close().catch(() => {
3747
+ });
3748
+ };
3749
+ start().catch((e) => console.error("[scheduler] start:", e instanceof Error ? e.message : e));
3750
+ return mw;
3751
+ }
3752
+
3753
+ // src/ai/client.ts
3754
+ import { randomUUID as randomUUID3 } from "node:crypto";
3755
+
2999
3756
  // src/ai/sse.ts
3000
3757
  function sseResponse(run, options) {
3001
3758
  const encoder = new TextEncoder();
@@ -3200,7 +3957,7 @@ function createAiClient(opts) {
3200
3957
  }
3201
3958
  return sseResponse(
3202
3959
  async (emit) => {
3203
- emit("wf:message_start", { id: options?.traceId ?? randomUUID2() });
3960
+ emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
3204
3961
  await streamStep(params, {
3205
3962
  emit,
3206
3963
  signal: controller.signal,
@@ -3294,7 +4051,7 @@ function safeParseArgs(raw) {
3294
4051
  }
3295
4052
 
3296
4053
  // src/ai/agent.ts
3297
- import { randomUUID as randomUUID3 } from "node:crypto";
4054
+ import { randomUUID as randomUUID4 } from "node:crypto";
3298
4055
  function createAgent(client, config) {
3299
4056
  const maxSteps = config.maxSteps ?? 10;
3300
4057
  const toolDefs = config.tools.map((t) => ({
@@ -3313,14 +4070,14 @@ function createAgent(client, config) {
3313
4070
  const controller = createController(options?.signal);
3314
4071
  const emit = options?.emit ?? (() => {
3315
4072
  });
3316
- emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
4073
+ emit("wf:message_start", { id: options?.traceId ?? randomUUID4() });
3317
4074
  await loop(messages, emit, controller.signal);
3318
4075
  }
3319
4076
  function run(messages, options) {
3320
4077
  const controller = createController(options?.signal);
3321
4078
  return sseResponse(
3322
4079
  async (emit) => {
3323
- emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
4080
+ emit("wf:message_start", { id: options?.traceId ?? randomUUID4() });
3324
4081
  await loop(messages, emit, controller.signal);
3325
4082
  },
3326
4083
  { onAbort: () => controller.abort() }
@@ -3399,7 +4156,7 @@ function createAgent(client, config) {
3399
4156
  let execArgs = args;
3400
4157
  if (config.humanInTheLoop) {
3401
4158
  const decision = await client.waitApproval(
3402
- { id: randomUUID3(), toolCallId: tc.id, name, args },
4159
+ { id: randomUUID4(), toolCallId: tc.id, name, args },
3403
4160
  emit,
3404
4161
  config.approvalTimeoutMs
3405
4162
  );
@@ -4210,6 +4967,9 @@ function concat(...parts) {
4210
4967
  }
4211
4968
 
4212
4969
  // src/db/postgres/connection.ts
4970
+ function isCacheInvalidationError(message) {
4971
+ return message.includes("cached plan must not change result type") || message.includes("cache lookup failed for type");
4972
+ }
4213
4973
  var PgConnection = class _PgConnection {
4214
4974
  opts;
4215
4975
  timeoutSet = false;
@@ -4233,6 +4993,8 @@ var PgConnection = class _PgConnection {
4233
4993
  get connected() {
4234
4994
  return this.status === "ready";
4235
4995
  }
4996
+ /** 池空闲回收用:最近一次查询完成时间戳(pool release 时更新) */
4997
+ lastUsed = 0;
4236
4998
  connect() {
4237
4999
  return new Promise((resolve3, reject) => {
4238
5000
  this.status = "connecting";
@@ -4282,7 +5044,13 @@ var PgConnection = class _PgConnection {
4282
5044
  // sig → stmt + 列缓存(LRU)
4283
5045
  static PREPARED_MAX = 128;
4284
5046
  stmtSeq = 0;
5047
+ /** LRU 淘汰的 statement 名——连接空闲时批量 DEALLOCATE(服务端同步释放,防 plan 缓存膨胀) */
5048
+ deallocQueue = [];
5049
+ /** DEALLOCATE 批量的响应 Z——仅消费(不 resolve 查询) */
5050
+ awaitingDeallocZ = false;
4285
5051
  currentQuery = null;
5052
+ /** DDL 失效重试:缓存语句被 DROP 后服务器拒绝执行,Sync 复位后以新语句名重 Parse */
5053
+ pendingRetry = null;
4286
5054
  onData(chunk) {
4287
5055
  const msgs = this.stream.push(new Uint8Array(chunk));
4288
5056
  for (const msg of msgs) {
@@ -4378,9 +5146,15 @@ var PgConnection = class _PgConnection {
4378
5146
  }
4379
5147
  break;
4380
5148
  }
4381
- case "C":
5149
+ case "C": {
5150
+ const q = this.currentQuery;
5151
+ if (q) {
5152
+ const tag = new TextDecoder().decode(msg.payload).replace(/\0+$/, "");
5153
+ const m = tag.match(/^(?:INSERT|UPDATE|DELETE|MERGE)\s+(?:\d+\s+)?(\d+)$/);
5154
+ if (m) q.affectedRows = parseInt(m[1], 10);
5155
+ }
4382
5156
  break;
4383
- // CommandComplete——忽略 tag
5157
+ }
4384
5158
  case "t": {
4385
5159
  if (this.currentQuery?.awaitingDescribe && this.currentQuery.sql !== void 0) {
4386
5160
  this.currentQuery.awaitingDescribe = false;
@@ -4399,11 +5173,32 @@ var PgConnection = class _PgConnection {
4399
5173
  case "Z": {
4400
5174
  if (this.pendingErrorZ) {
4401
5175
  this.pendingErrorZ = false;
5176
+ if (this.pendingRetry) {
5177
+ const r = this.pendingRetry;
5178
+ this.pendingRetry = null;
5179
+ this.issueExtendedQuery(r.sql, r.params ?? [], r.resolve, r.reject, r.retried);
5180
+ }
5181
+ break;
5182
+ }
5183
+ if (this.awaitingDeallocZ) {
5184
+ this.awaitingDeallocZ = false;
4402
5185
  break;
4403
5186
  }
4404
5187
  const q = this.currentQuery;
4405
5188
  this.currentQuery = null;
4406
- if (q) q.resolve(q.rows);
5189
+ if (q) {
5190
+ const rows = q.rows;
5191
+ if (q.affectedRows !== void 0) {
5192
+ Object.defineProperty(rows, "affectedRows", {
5193
+ value: q.affectedRows,
5194
+ enumerable: false,
5195
+ writable: true,
5196
+ configurable: true
5197
+ });
5198
+ }
5199
+ q.resolve(rows);
5200
+ }
5201
+ this.flushDealloc();
4407
5202
  if (this.awaitingReady) {
4408
5203
  this.awaitingReady = false;
4409
5204
  this.onReady?.();
@@ -4417,9 +5212,24 @@ var PgConnection = class _PgConnection {
4417
5212
  this.currentQuery = null;
4418
5213
  if (q) {
4419
5214
  this.pendingErrorZ = true;
4420
- if (q.awaitingDescribe) this.send(syncMessage());
4421
5215
  const err = new Error(fields.message ?? "postgres query error");
4422
5216
  err.code = fields.code;
5217
+ if (q.prepKey && isCacheInvalidationError(fields.message ?? "")) {
5218
+ this.prepared.clear();
5219
+ if (!q.retried) {
5220
+ this.pendingRetry = {
5221
+ sql: q.sql ?? "",
5222
+ params: q.params,
5223
+ columns: q.columns,
5224
+ resolve: q.resolve,
5225
+ reject: q.reject,
5226
+ retried: true
5227
+ };
5228
+ if (q.awaitingDescribe) this.send(syncMessage());
5229
+ break;
5230
+ }
5231
+ }
5232
+ if (q.awaitingDescribe) this.send(syncMessage());
4423
5233
  q.reject(err);
4424
5234
  }
4425
5235
  break;
@@ -4478,15 +5288,28 @@ var PgConnection = class _PgConnection {
4478
5288
  }
4479
5289
  return entry;
4480
5290
  }
4481
- /** LRU 写入:刷新位置;超上限淘汰最旧(长运行服务防无限累积) */
5291
+ /** LRU 写入:刷新位置;超上限淘汰最旧(长运行服务防无限累积)——淘汰的 statement 推入 deallocQueue,连接空闲时服务端同步释放 */
4482
5292
  setPrepared(sig, entry) {
4483
5293
  this.prepared.delete(sig);
4484
5294
  this.prepared.set(sig, entry);
4485
5295
  if (this.prepared.size > _PgConnection.PREPARED_MAX) {
4486
5296
  const oldest = this.prepared.keys().next().value;
4487
- if (oldest !== void 0) this.prepared.delete(oldest);
5297
+ if (oldest !== void 0) {
5298
+ const evicted = this.prepared.get(oldest);
5299
+ this.prepared.delete(oldest);
5300
+ if (evicted) this.deallocQueue.push(evicted.name);
5301
+ }
4488
5302
  }
4489
5303
  }
5304
+ /** 连接空闲时批量 DEALLOCATE:LRU 淘汰的 statement 不残留服务端(plan 缓存膨胀防线)。
5305
+ * 同步 write 先于后续查询(调用点在 Z handler 的同步段内,resolve 的微任务后至)。
5306
+ * 其响应 C+Z 在无 currentQuery 时到达——awaitingDeallocZ 消费首个 Z,后续查询 Z 正常。 */
5307
+ flushDealloc() {
5308
+ if (this.deallocQueue.length === 0 || !this.socket) return;
5309
+ const names = this.deallocQueue.splice(0);
5310
+ this.awaitingDeallocZ = true;
5311
+ this.socket.write(queryMessage(names.map((n) => `DEALLOCATE "${n}"`).join("; ")));
5312
+ }
4490
5313
  /** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
4491
5314
  async transaction(fn) {
4492
5315
  await this.query("BEGIN");
@@ -4524,21 +5347,7 @@ var PgConnection = class _PgConnection {
4524
5347
  let stmtEntry = this.getPrepared(sig);
4525
5348
  const encoded = encodeParams(params);
4526
5349
  if (!stmtEntry) {
4527
- const name = `wf_s${++this.stmtSeq}`;
4528
- this.currentQuery = {
4529
- columns: [],
4530
- rows: [],
4531
- resolve: resolve3,
4532
- reject,
4533
- sql,
4534
- params: encoded,
4535
- awaitingDescribe: true,
4536
- prepKey: sig,
4537
- prepName: name
4538
- };
4539
- this.socket.write(parseMessage(name, sql, params.map(() => 0)));
4540
- this.socket.write(describeMessage("S", name));
4541
- this.socket.write(flushMessage());
5350
+ this.issueExtendedQuery(sql, encoded, resolve3, reject);
4542
5351
  } else {
4543
5352
  this.currentQuery = {
4544
5353
  columns: [...stmtEntry.columns],
@@ -4547,7 +5356,9 @@ var PgConnection = class _PgConnection {
4547
5356
  reject,
4548
5357
  sql,
4549
5358
  params: encoded,
4550
- awaitingDescribe: false
5359
+ awaitingDescribe: false,
5360
+ prepKey: sig,
5361
+ prepName: stmtEntry.name
4551
5362
  };
4552
5363
  this.socket.write(bindMessage(stmtEntry.name, encoded));
4553
5364
  this.socket.write(executeMessage());
@@ -4556,6 +5367,28 @@ var PgConnection = class _PgConnection {
4556
5367
  }
4557
5368
  });
4558
5369
  }
5370
+ /**
5371
+ * 扩展查询首次准备:Parse + Describe + Flush(无 Sync——错误时由调用方发 Sync 复位)。
5372
+ * retry 路径复用:新语句名强制服务器按当前 schema 重新解析(DDL 失效恢复)。
5373
+ */
5374
+ issueExtendedQuery(sql, params, resolve3, reject, retried) {
5375
+ const name = `wf_s${++this.stmtSeq}`;
5376
+ this.currentQuery = {
5377
+ columns: [],
5378
+ rows: [],
5379
+ resolve: resolve3,
5380
+ reject,
5381
+ sql,
5382
+ params,
5383
+ awaitingDescribe: true,
5384
+ prepKey: `${sql}|${params.length}`,
5385
+ prepName: name,
5386
+ retried
5387
+ };
5388
+ this.socket?.write(parseMessage(name, sql, params.map(() => 0)));
5389
+ this.socket?.write(describeMessage("S", name));
5390
+ this.socket?.write(flushMessage());
5391
+ }
4559
5392
  send(data) {
4560
5393
  this.socket?.write(data);
4561
5394
  }
@@ -4631,6 +5464,8 @@ function convertValue(oid, value) {
4631
5464
  return parseFloat(value);
4632
5465
  case 16:
4633
5466
  return value === "t";
5467
+ case 1184:
5468
+ return new Date(value);
4634
5469
  default:
4635
5470
  return value;
4636
5471
  }
@@ -4691,6 +5526,7 @@ var PgPool = class _PgPool {
4691
5526
  opts;
4692
5527
  initPromise = null;
4693
5528
  schemas = /* @__PURE__ */ new Map();
5529
+ idleTimer = null;
4694
5530
  /** 懒连接:构造不连接,ensure() 首次初始化(中间件注入场景) */
4695
5531
  constructor(options = {}) {
4696
5532
  this.opts = options;
@@ -4720,13 +5556,54 @@ var PgPool = class _PgPool {
4720
5556
  this.all = conns;
4721
5557
  this.available = [...conns];
4722
5558
  this.readyPromise = Promise.resolve();
5559
+ this.startIdleReaper();
5560
+ }
5561
+ /** 空闲回收定时器(idleTimeoutMs > 0 时启动;unref 不阻塞进程退出) */
5562
+ startIdleReaper() {
5563
+ const ms = this.opts.idleTimeoutMs ?? 0;
5564
+ if (ms <= 0 || this.idleTimer) return;
5565
+ this.idleTimer = setInterval(() => this.reapIdle(), ms);
5566
+ this.idleTimer.unref?.();
5567
+ }
5568
+ /** 扫描空闲连接:超时未使用 → close + 从池移除(容量收缩;acquire 时自动重建) */
5569
+ reapIdle() {
5570
+ if (this.closed) return;
5571
+ const now = Date.now();
5572
+ const ms = this.opts.idleTimeoutMs ?? 0;
5573
+ for (let i = this.available.length - 1; i >= 0; i--) {
5574
+ const conn = this.available[i];
5575
+ if (conn.lastUsed > 0 && now - conn.lastUsed >= ms) {
5576
+ this.available.splice(i, 1);
5577
+ const idx = this.all.indexOf(conn);
5578
+ if (idx >= 0) this.all.splice(idx, 1);
5579
+ conn.close();
5580
+ }
5581
+ }
4723
5582
  }
4724
- /** 获取一个空闲连接(全忙则排队等待) */
5583
+ /** 当前打开的连接数(含借出;测试/观测用) */
5584
+ get open() {
5585
+ return this.all.length;
5586
+ }
5587
+ /** 获取一个空闲连接(全忙则排队等待;池被空闲回收收缩时自动新建恢复容量) */
4725
5588
  acquire() {
4726
5589
  if (this.closed) return Promise.reject(new ConnectionError("postgres: pool is closed"));
4727
5590
  if (this.available.length > 0) {
4728
5591
  return Promise.resolve(this.available.pop());
4729
5592
  }
5593
+ const poolSize = this.opts.poolSize ?? 5;
5594
+ if (this.all.length < poolSize) {
5595
+ const c = new PgConnection(this.opts);
5596
+ return c.connect().then(
5597
+ () => {
5598
+ this.all.push(c);
5599
+ return c;
5600
+ },
5601
+ () => {
5602
+ setTimeout(() => this.replenish(), 500);
5603
+ throw new ConnectionError("postgres: failed to establish connection");
5604
+ }
5605
+ );
5606
+ }
4730
5607
  const timeoutMs = this.opts.acquireTimeoutMs ?? 3e4;
4731
5608
  return new Promise((resolve3, reject) => {
4732
5609
  const waiter = {
@@ -4753,6 +5630,7 @@ var PgPool = class _PgPool {
4753
5630
  this.replenish();
4754
5631
  return;
4755
5632
  }
5633
+ conn.lastUsed = Date.now();
4756
5634
  this.dispatchAvailable(conn);
4757
5635
  }
4758
5636
  /** 连接可用事件统一入口:优先唤醒等待者,否则回空闲池(release 与 replenish 共用) */
@@ -4796,6 +5674,69 @@ var PgPool = class _PgPool {
4796
5674
  register(table, schema) {
4797
5675
  this.schemas.set(table, schema);
4798
5676
  }
5677
+ /**
5678
+ * 批量插入:多行 VALUES 单次往返(种子/批量写场景 N 往返 → 1)。
5679
+ * 诚实裁剪:所有行必须键集合一致(否则抛 ValidationError);batchSize 分批(默认 500)。
5680
+ */
5681
+ async insertMany(table, rows, opts = {}) {
5682
+ const empty = [];
5683
+ if (rows.length === 0) {
5684
+ Object.defineProperty(empty, "affectedRows", { value: 0, enumerable: false, writable: true });
5685
+ return empty;
5686
+ }
5687
+ const schema = this.schemas.get(table);
5688
+ const keys = Object.keys(rows[0]).sort();
5689
+ const batchSize = Math.max(1, opts.batchSize ?? 500);
5690
+ let total = 0;
5691
+ let last = empty;
5692
+ for (let i = 0; i < rows.length; i += batchSize) {
5693
+ const batch = rows.slice(i, i + batchSize);
5694
+ for (let r2 = 0; r2 < batch.length; r2++) {
5695
+ const row = batch[r2];
5696
+ const rowKeys = Object.keys(row).sort();
5697
+ if (rowKeys.length !== keys.length || rowKeys.some((k, j) => k !== keys[j])) {
5698
+ throw new ValidationError(
5699
+ `schema: insertMany requires identical keys across rows (row ${i + r2} has [${rowKeys}], expected [${keys}])`
5700
+ );
5701
+ }
5702
+ if (schema) validateRow(schema, row);
5703
+ }
5704
+ const vals = batch.map((_, r2) => `(${keys.map((_2, c) => `$${r2 * keys.length + c + 1}`).join(", ")})`).join(", ");
5705
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${vals}`;
5706
+ const params = batch.flatMap((row) => keys.map((k) => row[k]));
5707
+ const r = await this.query(sql, params);
5708
+ total += r.affectedRows ?? batch.length;
5709
+ if (r.length > 0) last = r;
5710
+ }
5711
+ Object.defineProperty(last, "affectedRows", { value: total, enumerable: false, writable: true });
5712
+ return last;
5713
+ }
5714
+ /** 参数化 UPDATE:SET + WHERE 全部参数化(防注入);返回 affectedRows;returning 可回读修改行 */
5715
+ async update(table, set, where, opts = {}) {
5716
+ const schema = this.schemas.get(table);
5717
+ if (schema) validateRow(schema, { ...set, ...where });
5718
+ const setCols = Object.keys(set);
5719
+ if (setCols.length === 0) throw new ValidationError("schema: update requires at least one SET column");
5720
+ const whereCols = Object.keys(where);
5721
+ if (whereCols.length === 0) {
5722
+ throw new ValidationError("schema: update requires a WHERE clause (full-table update via unsafe)");
5723
+ }
5724
+ const setSql = setCols.map((c, i) => `${c} = $${i + 1}`).join(", ");
5725
+ const whereSql = whereCols.map((c, i) => `${c} = $${setCols.length + i + 1}`).join(" AND ");
5726
+ const returning = opts.returning?.length ? ` RETURNING ${opts.returning.join(", ")}` : "";
5727
+ const sql = `UPDATE ${table} SET ${setSql} WHERE ${whereSql}${returning}`;
5728
+ const params = [...setCols.map((c) => set[c]), ...whereCols.map((c) => where[c])];
5729
+ return this.query(sql, params);
5730
+ }
5731
+ /** 参数化 DELETE:WHERE 参数化;返回 affectedRows。WHERE 必填(防全表误删) */
5732
+ async delete(table, where) {
5733
+ const schema = this.schemas.get(table);
5734
+ if (schema) validateRow(schema, where);
5735
+ const whereCols = Object.keys(where);
5736
+ if (whereCols.length === 0) throw new ValidationError("schema: delete requires a WHERE clause");
5737
+ const sql = `DELETE FROM ${table} WHERE ${whereCols.map((c, i) => `${c} = $${i + 1}`).join(" AND ")}`;
5738
+ return this.query(sql, whereCols.map((c) => where[c]));
5739
+ }
4799
5740
  /** 写前校验 + 参数化插入(schema 驱动,脏数据源头拦截) */
4800
5741
  async insert(table, row) {
4801
5742
  const schema = this.schemas.get(table);
@@ -4865,6 +5806,10 @@ var PgPool = class _PgPool {
4865
5806
  }
4866
5807
  async close() {
4867
5808
  this.closed = true;
5809
+ if (this.idleTimer) {
5810
+ clearInterval(this.idleTimer);
5811
+ this.idleTimer = null;
5812
+ }
4868
5813
  this.readyPromise = null;
4869
5814
  this.available = [];
4870
5815
  await Promise.all(this.all.map((c) => c.close()));
@@ -4900,7 +5845,9 @@ function parseTagged(strings, values) {
4900
5845
  }
4901
5846
 
4902
5847
  // src/postgres/client.ts
5848
+ import { AsyncLocalStorage } from "node:async_hooks";
4903
5849
  var MIGRATIONS_TABLE = "_weifuwu_migrations";
5850
+ var traceStore = new AsyncLocalStorage();
4904
5851
  function postgres(options) {
4905
5852
  const opts = typeof options === "string" ? { connection: options } : options ?? {};
4906
5853
  const connection = opts.connection ?? process.env.DATABASE_URL;
@@ -4919,12 +5866,16 @@ function postgres(options) {
4919
5866
  poolSize: opts.max ?? opts.poolSize ?? 10,
4920
5867
  acquireTimeoutMs: opts.acquireTimeoutMs,
4921
5868
  statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
4922
- onQuery: opts.onQuery
5869
+ // onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
5870
+ onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
5871
+ const tid = traceStore.getStore();
5872
+ opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
5873
+ } : void 0
4923
5874
  });
4924
5875
  const sql = makeSql(pool);
4925
5876
  const mw = ((req, ctx, next) => {
4926
5877
  ctx.sql = sql;
4927
- return next(req, ctx);
5878
+ return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
4928
5879
  });
4929
5880
  mw.__meta = { injects: ["sql"], depends: [] };
4930
5881
  mw.sql = sql;
@@ -5030,6 +5981,8 @@ function wrapError(promise) {
5030
5981
  }
5031
5982
 
5032
5983
  // src/redis/client.ts
5984
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
5985
+ var traceStore2 = new AsyncLocalStorage2();
5033
5986
  function redis(options) {
5034
5987
  const opts = typeof options === "string" ? { url: options } : options ?? {};
5035
5988
  const url = opts.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
@@ -5039,11 +5992,18 @@ function redis(options) {
5039
5992
  port: Number(u.port || 6379),
5040
5993
  poolSize: opts.poolSize,
5041
5994
  keyPrefix: opts.keyPrefix,
5042
- enableOfflineQueue: opts.enableOfflineQueue
5995
+ enableOfflineQueue: opts.enableOfflineQueue,
5996
+ commandTimeoutMs: opts.commandTimeoutMs,
5997
+ socketTimeoutMs: opts.socketTimeoutMs,
5998
+ // onCommand 包装:从 ALS 读请求级 traceId 追加到第 4 参数(无头不注入)
5999
+ onCommand: opts.onCommand ? (cmd, args, dur) => {
6000
+ const tid = traceStore2.getStore();
6001
+ opts.onCommand?.(cmd, args, dur, tid || void 0);
6002
+ } : void 0
5043
6003
  });
5044
6004
  const mw = (async (req, ctx, next) => {
5045
6005
  ctx.redis = pool;
5046
- return next(req, ctx);
6006
+ return traceStore2.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
5047
6007
  });
5048
6008
  mw.__meta = { injects: ["redis"], depends: [] };
5049
6009
  mw.redis = pool;
@@ -5085,6 +6045,7 @@ export {
5085
6045
  rateLimit,
5086
6046
  redirect,
5087
6047
  redis,
6048
+ scheduler,
5088
6049
  sendSmtp,
5089
6050
  serve,
5090
6051
  serveStatic,