weifuwu 0.75.0 → 0.77.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 (75) hide show
  1. package/README.md +48 -53
  2. package/dist/ai/types.d.ts +2 -0
  3. package/dist/components/AiChat/AiChat.d.ts +5 -1
  4. package/dist/components/ApprovalCard/ApprovalCard.d.ts +5 -2
  5. package/dist/components/CitationCard/CitationCard.d.ts +38 -0
  6. package/dist/components/Command/Command.d.ts +1 -1
  7. package/dist/components/DatePicker/DatePicker.d.ts +1 -1
  8. package/dist/components/JsonSchemaForm/JsonSchemaForm.d.ts +48 -0
  9. package/dist/components/ReasoningBlock/ReasoningBlock.d.ts +25 -0
  10. package/dist/components/Select/Select.d.ts +11 -1
  11. package/dist/components/SessionList/SessionList.d.ts +42 -0
  12. package/dist/components/Tour/Tour.d.ts +1 -1
  13. package/dist/components/TreeSelect/TreeSelect.d.ts +3 -2
  14. package/dist/components/index.d.ts +10 -2
  15. package/dist/components/index.js +14 -14
  16. package/dist/components/style.css +625 -20
  17. package/dist/index.js +1349 -1529
  18. package/dist/layout/weifuwu-layout.css +9 -0
  19. package/dist/scheduler/index.d.ts +7 -2
  20. package/dist/test/client/setup.d.ts +12 -0
  21. package/dist/ui/index.d.ts +2 -2
  22. package/dist/ui-dom/Confirm.d.ts +5 -41
  23. package/dist/ui-dom/Notification.d.ts +8 -70
  24. package/dist/ui-dom/Toast.d.ts +8 -43
  25. package/dist/ui-dom/hooks/chat.d.ts +11 -0
  26. package/dist/ui-dom/hooks/events.d.ts +30 -0
  27. package/dist/ui-dom/hooks/external.d.ts +20 -0
  28. package/dist/ui-dom/hooks/index.d.ts +30 -0
  29. package/dist/ui-dom/hooks/input.d.ts +33 -0
  30. package/dist/ui-dom/hooks/media.d.ts +17 -0
  31. package/dist/ui-dom/hooks/popup.d.ts +28 -0
  32. package/dist/ui-dom/hooks/stable.d.ts +39 -0
  33. package/dist/ui-dom/hooks/test/hooks.test.d.ts +1 -0
  34. package/dist/ui-dom/hooks/types.d.ts +77 -0
  35. package/dist/ui-dom/index.d.ts +19 -17
  36. package/dist/ui-dom/index.js +11 -11
  37. package/dist/ui-dom/jsx-runtime.js +1 -1
  38. package/dist/ui-dom/store.d.ts +24 -0
  39. package/dist/ui-dom/testing.d.ts +74 -0
  40. package/dist/ui-dom/testing.js +1 -0
  41. package/dist/ui-dom/testing.test.d.ts +1 -0
  42. package/dist/ui-dom/types.d.ts +55 -50
  43. package/dist/ui-dom/use-chat.d.ts +19 -14
  44. package/dist/ui-dom/vdom/build.d.ts +41 -0
  45. package/dist/ui-dom/vdom/diff.d.ts +33 -0
  46. package/dist/ui-dom/vdom/hydration.d.ts +17 -0
  47. package/dist/ui-dom/vdom/index.d.ts +23 -0
  48. package/dist/ui-dom/vdom/mount.d.ts +58 -0
  49. package/dist/ui-dom/vdom/registry.d.ts +27 -0
  50. package/dist/ui-dom/vdom/render.d.ts +15 -0
  51. package/dist/ui-dom/vdom/serve.d.ts +30 -0
  52. package/dist/ui-dom/vdom/ssr.d.ts +44 -0
  53. package/dist/ui-dom/vnode.d.ts +19 -31
  54. package/docs/components-map.md +18 -1
  55. package/docs/components.md +12 -7
  56. package/docs/custom-components.md +90 -60
  57. package/docs/examples.md +35 -41
  58. package/docs/frontend-middleware.md +3 -4
  59. package/docs/frontend-ui-dom.md +21 -18
  60. package/docs/frontend.md +243 -168
  61. package/docs/layout.md +2 -2
  62. package/docs/mobile.md +2 -2
  63. package/docs/realtime.md +8 -3
  64. package/package.json +5 -1
  65. package/dist/ui/ssr.d.ts +0 -35
  66. package/dist/ui-dom/diff.d.ts +0 -28
  67. package/dist/ui-dom/focus-trap.d.ts +0 -4
  68. package/dist/ui-dom/hydration.d.ts +0 -13
  69. package/dist/ui-dom/reactive.d.ts +0 -6
  70. package/dist/ui-dom/registry.d.ts +0 -27
  71. package/dist/ui-dom/render.d.ts +0 -46
  72. package/dist/ui-dom/scroll-lock.d.ts +0 -5
  73. package/dist/ui-dom/serve.d.ts +0 -28
  74. package/dist/ui-dom/ssr.d.ts +0 -57
  75. package/dist/ui-dom/ui.d.ts +0 -78
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/db/redis/connection.ts
2319
- import net2 from "node:net";
2320
-
2321
- // src/db/errors.ts
2322
- var DbError = class extends Error {
2323
- kind;
2324
- code;
2325
- cause;
2326
- constructor(kind, message, options) {
2327
- super(message);
2328
- this.name = "DbError";
2329
- this.kind = kind;
2330
- this.code = options?.code;
2331
- this.cause = options?.cause;
2332
- }
2333
- };
2334
- var ProtocolError = class extends DbError {
2335
- constructor(feature, message) {
2336
- super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
2337
- this.name = "ProtocolError";
2338
- }
2339
- };
2340
- var ConnectionError = class extends DbError {
2341
- attempts;
2342
- constructor(message, attempts = 1, cause) {
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 out = new Uint8Array(total);
2393
- let off = 0;
2394
- const head = `*${args.length}\r
2395
- `;
2396
- out.set(_encoder.encode(head), 0);
2397
- off = head.length;
2398
- for (let i = 0; i < args.length; i++) {
2399
- const arg = args[i];
2400
- const len = lens[i];
2401
- const lh = `$${len}\r
2402
- `;
2403
- out.set(_encoder.encode(lh), off);
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
- out.set(_encoder.encode(String(arg)), off);
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 out;
2364
+ return set;
2418
2365
  }
2419
- function headerLen(n) {
2420
- if (n < 10) return 4;
2421
- if (n < 100) return 5;
2422
- if (n < 1e3) return 6;
2423
- if (n < 1e4) return 7;
2424
- return 8;
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
- var _decoder = new TextDecoder();
2427
- var _encoder = new TextEncoder();
2428
- var RespParser = class {
2429
- buf = new Uint8Array(0);
2430
- off = 0;
2431
- /** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
2432
- push(chunk) {
2433
- this.append(chunk);
2434
- const saved = this.off;
2435
- try {
2436
- const value = this.parseValue();
2437
- this.compact();
2438
- return { value, incomplete: false };
2439
- } catch (e) {
2440
- this.off = saved;
2441
- if (e instanceof IncompleteError) return { value: null, incomplete: true };
2442
- throw e;
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
- /** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
2446
- pushAll(chunk) {
2447
- this.append(chunk);
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
- if (out.length > 0) this.compact();
2460
- return out;
2461
- }
2462
- /** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
2463
- append(chunk) {
2464
- if (chunk.length === 0) return;
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
- const merged = new Uint8Array(rest + chunk.length);
2472
- merged.set(this.buf.subarray(this.off), 0);
2473
- merged.set(chunk, rest);
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
- parseValue() {
2488
- if (this.buf.length - this.off < 3) throw new IncompleteError();
2489
- const type = String.fromCharCode(this.buf[this.off]);
2490
- this.off++;
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 (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
2537
- this.off = i + 2;
2538
- return neg ? -n : n;
2433
+ if (nextMinute !== minute) {
2434
+ d.setMinutes(nextMinute, 0, 0);
2435
+ continue;
2539
2436
  }
2540
- throw new IncompleteError();
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
- return -1;
2439
+ throw new Error("cron: no next run within 5 years (invalid expression?)");
2565
2440
  }
2566
2441
 
2567
- // src/db/redis/connection.ts
2568
- var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
2569
- var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
2570
- var RedisConnection = class {
2571
- ready = false;
2572
- opts;
2573
- socket = null;
2574
- parser = new RespParser();
2575
- pending = [];
2576
- /** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
2577
- pendingHead = 0;
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
- onceReady;
2617
- onceFailed;
2618
- openSocket() {
2619
- this.status = "connecting";
2620
- const sock = net2.connect(this.opts.port, this.opts.host);
2621
- this.socket = sock;
2622
- sock.on("connect", () => {
2623
- sock.setNoDelay(true);
2624
- this.status = "ready";
2625
- this.retries = 0;
2626
- if (this.connectedOnce) {
2627
- for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
2628
- for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
2629
- }
2630
- this.connectedOnce = true;
2631
- this.flushOffline();
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
- sock.on("close", () => {
2641
- this.socket = null;
2642
- if (this.status !== "closed" && this.status !== "idle") {
2643
- this.handleDisconnect(new ConnectionError("redis: socket closed"));
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
- onData(chunk) {
2480
+ async function tickCrons() {
2481
+ let crons;
2669
2482
  try {
2670
- const values = this.parser.pushAll(chunk);
2671
- for (const raw of values) {
2672
- const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
2673
- if (typeof first === "string" && PUSH_TYPES.has(first)) {
2674
- this.dispatchSubscribe(decodeValue(raw, true));
2675
- continue;
2676
- }
2677
- const p = this.pending[this.pendingHead++];
2678
- if (!p) break;
2679
- if (p.timedOut) continue;
2680
- if (raw instanceof RespError) p.reject(raw);
2681
- else p.resolve(decodeValue(raw, !p.asBuffer));
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
- if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
2684
- this.pending = this.pending.slice(this.pendingHead);
2685
- this.pendingHead = 0;
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 (this.opts.socketTimeoutMs > 0) {
2688
- if (this.pendingHead < this.pending.length) this.armSocketTimeout();
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
- * 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
2701
- * opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
2702
- */
2703
- command(name, ...args) {
2704
- const last = args[args.length - 1];
2705
- const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
2706
- if (this.status === "ready" && this.socket && !this.socket.destroyed) {
2707
- return this.sendNow(name, args, opts?.asBuffer);
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
- if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
2710
- return Promise.reject(new ConnectionError("redis: not connected"));
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
- if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
2713
- return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
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
- return new Promise((resolve3, reject) => {
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
- sendNow(name, args, asBuffer) {
2720
- const start = performance.now();
2721
- return new Promise((resolve3, reject) => {
2722
- const p = {
2723
- resolve: resolve3,
2724
- reject,
2725
- asBuffer,
2726
- blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
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
- this.armTimeout(p);
2729
- this.pending.push(p);
2730
- this.armSocketTimeout();
2731
- this.socket.write(encodeCommand([name, ...args]), (err) => {
2732
- if (err) {
2733
- const idx = this.pending.indexOf(p);
2734
- if (idx >= 0) this.pending.splice(idx, 1);
2735
- p.reject(err instanceof Error ? err : new ConnectionError("redis: socket write failed"));
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
- /** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
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
- flushOffline() {
2782
- const queue2 = this.offlineQueue;
2783
- this.offlineQueue = [];
2784
- for (const q of queue2) {
2785
- this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
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
- /** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
2789
- batch(payload, count) {
2790
- if (this.status !== "ready" || !this.socket) {
2791
- return Promise.reject(new ConnectionError("redis: not connected"));
2792
- }
2793
- return new Promise((resolve3, reject) => {
2794
- const results = [];
2795
- const batchPending = [];
2796
- let settled = false;
2797
- const maybeResolve = () => {
2798
- if (settled) return;
2799
- if (results.length === count) {
2800
- settled = true;
2801
- if (batchTimer) clearTimeout(batchTimer);
2802
- resolve3(results);
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
- this.socket.write(payload);
2830
- this.armSocketTimeout();
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
- /** 订阅模式:回调式(channel, message) */
2839
- async psubscribe(pattern, fn) {
2840
- this.psubs.set(pattern, fn);
2841
- await this.command("PSUBSCRIBE", pattern);
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
- /** 旁路分发订阅消息(RESP 数组路由到回调) */
2844
- dispatchSubscribe(value) {
2845
- const [type, a, b] = value;
2846
- if (type === "message") {
2847
- const fn = this.subs.get(a);
2848
- fn?.(a, b);
2849
- } else if (type === "pmessage") {
2850
- const fn = this.psubs.get(a);
2851
- fn?.(b, value[3] ?? "");
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
- this.clearSocketTimeout();
2863
- const queue2 = this.pending.slice(this.pendingHead);
2864
- this.pending = [];
2865
- this.pendingHead = 0;
2866
- for (const p of queue2) p.reject(new ConnectionError("redis: connection closed"));
2867
- const oq = this.offlineQueue;
2868
- this.offlineQueue = [];
2869
- for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
2870
- const sock = this.socket;
2871
- this.socket = null;
2872
- sock?.destroy();
2873
- }
2874
- get connected() {
2875
- return this.status === "ready";
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
- const set2 = /* @__PURE__ */ new Set();
2902
- for (let v = min; v <= max; v += n) set2.add(v);
2903
- return set2;
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
- fields: fields.map((values) => ({ values })),
2935
- domExplicit: fields[2] !== null,
2936
- dowExplicit: fields[4] !== null
2725
+ async embed(text) {
2726
+ const results = await embedMany([text]);
2727
+ return results[0];
2728
+ },
2729
+ embedMany
2937
2730
  };
2938
2731
  }
2939
- function fieldMatches(values, v) {
2940
- if (values === null) return true;
2941
- return values.has(v);
2942
- }
2943
- function dayMatches(expr, dom, dow) {
2944
- const domOk = fieldMatches(expr.fields[2].values, dom);
2945
- const dowOk = fieldMatches(expr.fields[4].values, dow);
2946
- if (expr.domExplicit && expr.dowExplicit) return domOk || dowOk;
2947
- if (expr.domExplicit) return domOk;
2948
- if (expr.dowExplicit) return dowOk;
2949
- return true;
2950
- }
2951
- function nextMatch(values, v, min, max) {
2952
- if (values === null) return v;
2953
- for (let x = v; x <= max; x++) if (values.has(x)) return x;
2954
- return null;
2955
- }
2956
- function nextRun(expr, from) {
2957
- const d = new Date(from.getTime());
2958
- d.setSeconds(0, 0);
2959
- d.setMinutes(d.getMinutes() + 1);
2960
- const deadline = from.getTime() + 5 * 366 * 24 * 3600 * 1e3;
2961
- while (d.getTime() <= deadline) {
2962
- const year = d.getFullYear();
2963
- const month = d.getMonth() + 1;
2964
- const dom = d.getDate();
2965
- const dow = d.getDay();
2966
- if (!fieldMatches(expr.fields[3].values, month)) {
2967
- d.setMonth(month, 1);
2968
- d.setHours(0, 0, 0, 0);
2969
- d.setMonth(month);
2970
- continue;
2971
- }
2972
- if (!dayMatches(expr, dom, dow)) {
2973
- d.setDate(dom + 1);
2974
- d.setHours(0, 0, 0, 0);
2975
- continue;
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
- const hour = d.getHours();
2978
- const nextHour = nextMatch(expr.fields[1].values, hour, 0, 23);
2979
- if (nextHour === null) {
2980
- d.setDate(dom + 1);
2981
- d.setHours(0, 0, 0, 0);
2982
- continue;
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
- if (nextHour !== hour) {
2985
- d.setHours(nextHour, 0, 0, 0);
2986
- continue;
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
- const minute = d.getMinutes();
2989
- const nextMinute = nextMatch(expr.fields[0].values, minute, 0, 59);
2990
- if (nextMinute === null) {
2991
- d.setHours(hour + 1, 0, 0, 0);
2992
- continue;
2993
- }
2994
- if (nextMinute !== minute) {
2995
- d.setMinutes(nextMinute, 0, 0);
2996
- continue;
2997
- }
2998
- return d;
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
- throw new Error("cron: no next run within 5 years (invalid expression?)");
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
- due = await conn.command("ZRANGEBYSCORE", delayedKey, 0, Date.now());
3024
- } catch {
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
- for (const member of due) {
3028
- let removed;
3029
- try {
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
- const now = Date.now();
3051
- const entries = crons;
3052
- for (let i = 0; i + 1 < entries.length; i += 2) {
3053
- const field = entries[i];
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
- const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
3088
- for (const member of pending) {
3089
- if (member.includes(`"id":"cron:${name}:`)) {
3090
- await conn.command("ZREM", delayedKey, member);
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
- return removed === 1;
3096
- };
3097
- async function start() {
3098
- if (running) return;
3099
- running = true;
3100
- await conn.connect();
3101
- await tick();
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
- return false;
3128
- };
3129
- const mw = (async (req, ctx, next) => {
3130
- ctx.schedule = schedule;
3131
- ctx.cron = cron;
3132
- ctx.cancelCron = cancelCron;
3133
- ctx.cancelSchedule = cancelSchedule;
3134
- return next(req, ctx);
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
- await conn.close().catch(() => {
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
- // src/ai/client.ts
3155
- import { randomUUID as randomUUID3 } from "node:crypto";
3156
-
3157
- // src/ai/sse.ts
3158
- function sseResponse(run, options) {
3159
- const encoder = new TextEncoder();
3160
- const stream = new ReadableStream({
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
@@ -3540,7 +2977,7 @@ function createAgent(client, config) {
3540
2977
  if (signal?.aborted) return;
3541
2978
  if (!finish.toolCalls?.length) {
3542
2979
  emit("wf:usage", usage);
3543
- emit("wf:done", { content: finish.content, usage });
2980
+ emit("wf:done", { content: finish.content, usage, reasoning: finish.reasoning_content });
3544
2981
  return;
3545
2982
  }
3546
2983
  all.push({
@@ -3641,338 +3078,43 @@ var HtmlSafe = class {
3641
3078
  var Fragment = /* @__PURE__ */ Symbol("Fragment");
3642
3079
  var Portal = /* @__PURE__ */ Symbol("Portal");
3643
3080
 
3644
- // src/ui-dom/debug.ts
3645
- var _enabled = null;
3646
- function uiDebugEnabled() {
3647
- if (_enabled !== null) return _enabled;
3648
- _enabled = typeof process !== "undefined" && process.env?.UI_DEBUG === "1" || globalThis.__UI_DEBUG === true || typeof localStorage !== "undefined" && localStorage.getItem("__UI_DEBUG") === "1";
3649
- return _enabled;
3650
- }
3651
- var _depth = 0;
3652
- var counters = /* @__PURE__ */ new Map();
3653
- function uiLog(label, msg, opts) {
3654
- if (!uiDebugEnabled()) return;
3655
- const n = (counters.get(label) ?? 0) + 1;
3656
- counters.set(label, n);
3657
- const first = 10;
3658
- const every = opts?.throttle ?? 100;
3659
- if (n > first && n % every !== 0) return;
3660
- const d = opts?.depth !== void 0 ? opts.depth : _depth;
3661
- const indent = " ".repeat(Math.min(d, 30));
3662
- console.log(`[ui-debug] ${indent}${label}#${n} ${msg}`);
3081
+ // src/ui-dom/vdom/ssr.ts
3082
+ var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
3083
+ var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g", "text", "defs", "use", "clipPath"]);
3084
+ function escape(s) {
3085
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3663
3086
  }
3664
-
3665
- // src/ui-dom/reactive.ts
3666
- function createReactiveState(dirty) {
3667
- const _debugSets = /* @__PURE__ */ new Map();
3668
- const _dbg = (key, label) => {
3669
- const n = (_debugSets.get(key) ?? 0) + 1;
3670
- _debugSets.set(key, n);
3671
- if (n <= 5 || n % 100 === 0) uiLog(label, key + " count=" + n, { throttle: 100 });
3672
- };
3673
- const proxyCache = /* @__PURE__ */ new WeakMap();
3674
- const watchers = /* @__PURE__ */ new Set();
3675
- const reactive = (target) => {
3676
- if (target === null || typeof target !== "object") return target;
3677
- if (target instanceof Set || target instanceof Map) {
3678
- if (proxyCache.has(target)) return proxyCache.get(target);
3679
- const MUTATING = target instanceof Set ? /* @__PURE__ */ new Set(["add", "delete", "clear"]) : /* @__PURE__ */ new Set(["set", "delete", "clear"]);
3680
- const proxy2 = new Proxy(target, {
3681
- get(t, prop) {
3682
- const v = Reflect.get(t, prop);
3683
- if (typeof v === "function") {
3684
- if (!MUTATING.has(String(prop))) return v.bind(t);
3685
- return (...args) => {
3686
- const result = v.apply(t, args);
3687
- dirty();
3688
- for (const w of watchers) w();
3689
- return result;
3690
- };
3691
- }
3692
- return v;
3693
- },
3694
- set(t, key, value) {
3695
- Reflect.set(t, key, value);
3696
- _dbg(String(key), "set");
3697
- dirty();
3698
- for (const w of watchers) w();
3699
- return true;
3700
- }
3701
- });
3702
- proxyCache.set(target, proxy2);
3703
- return proxy2;
3704
- }
3705
- if (target instanceof Date || target instanceof RegExp) return target;
3706
- if (proxyCache.has(target)) return proxyCache.get(target);
3707
- const proxy = new Proxy(target, {
3708
- set(target2, key, value) {
3709
- const old = Reflect.get(target2, key);
3710
- if (old === value) return true;
3711
- Reflect.set(target2, key, value);
3712
- dirty();
3713
- for (const w of watchers) w();
3714
- return true;
3715
- },
3716
- get(target2, key) {
3717
- const value = Reflect.get(target2, key);
3718
- if (typeof value === "object" && value !== null) return reactive(value);
3719
- return value;
3720
- },
3721
- deleteProperty(target2, key) {
3722
- if (Reflect.has(target2, key)) {
3723
- Reflect.deleteProperty(target2, key);
3724
- dirty();
3725
- for (const w of watchers) w();
3726
- }
3727
- return true;
3728
- }
3729
- });
3730
- proxyCache.set(target, proxy);
3731
- return proxy;
3732
- };
3733
- const root = reactive({});
3734
- Object.defineProperty(root, "__watch", {
3735
- value: (cb) => {
3736
- watchers.add(cb);
3737
- return () => {
3738
- watchers.delete(cb);
3739
- };
3740
- },
3741
- writable: false,
3742
- enumerable: false
3743
- });
3744
- return root;
3745
- }
3746
-
3747
- // src/ui/ssr.ts
3748
- function escape(s) {
3749
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3750
- }
3751
- var VOID_TAGS = /* @__PURE__ */ new Set([
3752
- "area",
3753
- "base",
3754
- "br",
3755
- "col",
3756
- "embed",
3757
- "hr",
3758
- "img",
3759
- "input",
3760
- "link",
3761
- "meta",
3762
- "param",
3763
- "source",
3764
- "track",
3765
- "wbr"
3766
- ]);
3767
3087
  function classToString(v) {
3768
3088
  if (typeof v === "string") return v;
3769
3089
  if (Array.isArray(v)) return v.filter(Boolean).join(" ");
3770
- if (v && typeof v === "object") {
3771
- return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
3772
- }
3090
+ if (v && typeof v === "object") return Object.entries(v).filter(([, on]) => on).map(([k]) => k).join(" ");
3773
3091
  return "";
3774
3092
  }
3775
3093
  function styleToString(v) {
3776
- if (typeof v === "string") return v;
3777
- if (v && typeof v === "object") {
3778
- return Object.entries(v).filter(([, x]) => x != null).map(([k, x]) => `${k}:${typeof x === "number" ? x + "px" : String(x)}`).join(";");
3779
- }
3780
- return "";
3781
- }
3782
- function createSsrContext(serverCtx, dataStore) {
3783
- const usedIds = /* @__PURE__ */ new Set();
3784
- const ui2 = {
3785
- $: () => createReactiveState(() => {
3786
- }),
3787
- dirty: () => {
3788
- },
3789
- render: () => {
3790
- },
3791
- selfId: (name) => {
3792
- if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
3793
- usedIds.add(name);
3794
- },
3795
- useMedia: () => {
3796
- },
3797
- useBreakpoint: () => {
3798
- },
3799
- usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
3800
- } }),
3801
- // 新原语族:SSR 确定性 no-op(不启动监听/会话/取数;组件挂载不崩即契约)
3802
- useHoverCapable: () => false,
3803
- useVisualViewport: () => ({ height: 0, offsetTop: 0, keyboardOpen: false }),
3804
- useLongPress: () => ({}),
3805
- useInView: () => ({ isIn: false, ready: false, observe: () => {
3806
- }, refresh: () => {
3807
- }, disconnect: () => {
3808
- } }),
3809
- useScrollPosition: () => ({ y: 0, refresh: () => {
3810
- } }),
3811
- useStableRef: (init) => (el) => {
3812
- if (el) init?.(el);
3813
- },
3814
- useGlobalKey: () => () => {
3815
- },
3816
- useReducedMotion: () => false,
3817
- useAnimationEnd: () => () => {
3818
- },
3819
- useTween: (target) => {
3820
- const handle = { value: target, reset: (to) => {
3821
- handle.value = to;
3822
- } };
3823
- return handle;
3824
- },
3825
- useDrag: () => ({ onPointerDown: () => {
3826
- } }),
3827
- useDragDrop: () => ({ dropProps: {}, dragProps: {} }),
3828
- useControlled: (options) => ({
3829
- value: options.value,
3830
- setValue: (v) => {
3831
- options.onChange?.(v);
3832
- },
3833
- controlled: options.value !== void 0
3834
- }),
3835
- useControlledInput: (options) => ({
3836
- value: options.value,
3837
- setValue: (v) => {
3838
- options.onChange?.(v);
3839
- },
3840
- controlled: options.value !== void 0,
3841
- keyword: "",
3842
- setKeyword: () => {
3843
- },
3844
- selectedLabel: "",
3845
- setSelectedLabel: () => {
3846
- }
3847
- }),
3848
- useOpen: (options) => ({
3849
- get open() {
3850
- return !!options.open;
3851
- },
3852
- setOpen: (v) => {
3853
- options.onOpenChange?.(v);
3854
- },
3855
- triggerProps: {}
3856
- }),
3857
- useAsync: () => ({ data: void 0, loading: true, error: void 0, reload: () => {
3858
- } }),
3859
- usePopup: () => ({
3860
- open: false,
3861
- setOpen: () => {
3862
- },
3863
- wrapProps: {},
3864
- portal: () => null,
3865
- refresh: () => {
3866
- }
3867
- }),
3868
- usePresence: () => ({
3869
- phase: "closed",
3870
- ref: () => {
3871
- },
3872
- sync: (open2) => open2 ? "open" : "closed"
3873
- }),
3874
- useDialog: () => ({
3875
- phase: "closed",
3876
- rootRef: () => {
3877
- },
3878
- panelRef: () => {
3879
- },
3880
- sync: (open2) => open2 ? "open" : "closed"
3881
- }),
3882
- // SSR 确定性空态:会话不启动(无事件/无网络),仅保证挂载不崩
3883
- useChat: () => ({
3884
- messages: [],
3885
- input: "",
3886
- streaming: false,
3887
- error: null,
3888
- usage: null,
3889
- step: null,
3890
- send: () => {
3891
- },
3892
- stop: () => {
3893
- },
3894
- retry: () => {
3895
- },
3896
- clear: () => {
3897
- },
3898
- approve: async () => {
3899
- },
3900
- dispose: () => {
3901
- }
3902
- })
3903
- };
3904
- const browser = {
3905
- activeElement: () => null,
3906
- byId: () => null,
3907
- query: () => null,
3908
- createElement: () => null,
3909
- bodyAppend: () => {
3910
- },
3911
- bodyRemove: () => {
3912
- },
3913
- copyText: async () => false,
3914
- execCommand: () => false,
3915
- selectionText: () => null,
3916
- getSelection: () => null,
3917
- queryCommandState: () => false,
3918
- queryCommandValue: () => "",
3919
- viewportHeight: () => 0,
3920
- scrollTop: () => 0,
3921
- hash: () => "",
3922
- setHash: () => {
3923
- },
3924
- timeout: () => 0,
3925
- rootElement: () => null,
3926
- storageGet: () => null,
3927
- storageSet: () => {
3928
- }
3929
- };
3930
- const ctx = Object.create(serverCtx ?? {});
3931
- ctx.ui = ui2;
3932
- ctx.browser = browser;
3933
- ctx.data = {
3934
- async get(key, fetcher) {
3935
- if (dataStore.has(key)) return dataStore.get(key);
3936
- if (!fetcher) return void 0;
3937
- const val = await fetcher();
3938
- dataStore.set(key, val);
3939
- return val;
3940
- },
3941
- set(key, val) {
3942
- dataStore.set(key, val);
3943
- },
3944
- has(key) {
3945
- return dataStore.has(key);
3946
- }
3947
- };
3948
- return ctx;
3094
+ return Object.entries(v).filter(([, val]) => val != null).map(([k, val]) => {
3095
+ const key = k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
3096
+ const value = typeof val === "number" && val !== 0 ? `${val}px` : String(val);
3097
+ return `${key}:${value}`;
3098
+ }).join(";");
3949
3099
  }
3950
3100
  async function renderSsr(input, ctx) {
3951
3101
  if (input == null || typeof input === "boolean") return "";
3952
3102
  if (typeof input === "string" || typeof input === "number") return escape(String(input));
3953
3103
  if (Array.isArray(input)) {
3954
- let s = "";
3955
- for (const c of input) s += await renderSsr(c, ctx);
3956
- return s;
3104
+ const parts = await Promise.all(input.map((c) => renderSsr(c, ctx)));
3105
+ return parts.join("");
3957
3106
  }
3958
3107
  const vnode = input;
3959
- if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
3960
- if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
3108
+ if (vnode.type === Portal || vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
3961
3109
  if (typeof vnode.type === "function") {
3962
3110
  const childCtx = Object.create(ctx);
3963
- let renderFn;
3964
- try {
3965
- renderFn = vnode.type(vnode.props ?? {}, childCtx);
3966
- } catch (e) {
3967
- throw e;
3968
- }
3969
- if (renderFn instanceof Promise) renderFn = await renderFn;
3111
+ const renderFn = await vnode.type(vnode.props ?? {}, childCtx);
3970
3112
  if (typeof renderFn !== "function") {
3971
3113
  throw new Error(
3972
3114
  `Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
3973
3115
  );
3974
3116
  }
3975
- return renderSsr(renderFn(vnode.props ?? {}), childCtx);
3117
+ return renderSsr(await renderFn(vnode.props ?? {}), childCtx);
3976
3118
  }
3977
3119
  const tag = vnode.type;
3978
3120
  const props = vnode.props ?? {};
@@ -3995,6 +3137,10 @@ async function renderSsr(input, ctx) {
3995
3137
  attrs.push(` style="${escape(styleToString(value))}"`);
3996
3138
  continue;
3997
3139
  }
3140
+ if (key === "draggable" || key === "contenteditable" || key === "spellcheck") {
3141
+ attrs.push(` ${key}="${value ? "true" : "false"}"`);
3142
+ continue;
3143
+ }
3998
3144
  if (value === true) {
3999
3145
  attrs.push(` ${key}`);
4000
3146
  continue;
@@ -4003,21 +3149,134 @@ async function renderSsr(input, ctx) {
4003
3149
  attrs.push(` ${key}="${escape(String(value))}"`);
4004
3150
  }
4005
3151
  const attrStr = attrs.join("");
3152
+ const svgWrap = SVG_TAGS.has(tag);
4006
3153
  if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
4007
3154
  if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
4008
3155
  const children = await renderSsr(props.children, ctx);
3156
+ if (svgWrap) return `<${tag}${attrStr}>${children}</${tag}>`;
4009
3157
  return `<${tag}${attrStr}>${children}</${tag}>`;
4010
3158
  }
4011
- async function ssrToString(Comp, props, serverCtx, opts = {}) {
4012
- const dataStore = opts.data ?? /* @__PURE__ */ new Map();
4013
- const ctx = createSsrContext(serverCtx, dataStore);
4014
- const html = await renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
4015
- return new HtmlSafe(html);
3159
+ function createSsrUi() {
3160
+ const noop = () => () => {
3161
+ };
3162
+ const noopReturn = () => ({});
3163
+ return {
3164
+ _selfId: "_wf_root",
3165
+ render: () => {
3166
+ },
3167
+ selfId: () => {
3168
+ },
3169
+ bumpCtxVersion: () => {
3170
+ },
3171
+ setMounting: () => {
3172
+ },
3173
+ endMounting: () => {
3174
+ },
3175
+ // hooks no-op(组件 SSR 安全——不注册监听/定时器)
3176
+ useChat: () => ({ messages: [], input: "", streaming: false, error: null, usage: null, step: null, send: () => {
3177
+ }, stop: () => {
3178
+ }, retry: () => {
3179
+ }, clear: () => {
3180
+ }, approve: () => {
3181
+ }, subscribe: () => () => {
3182
+ } }),
3183
+ useExternal: (store) => store,
3184
+ useMedia: noop,
3185
+ useBreakpoint: noop,
3186
+ usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
3187
+ } }),
3188
+ useHoverCapable: () => false,
3189
+ useStableRef: (init) => (el) => {
3190
+ if (el) init?.(el);
3191
+ },
3192
+ useVisualViewport: () => ({ height: 0, offsetTop: 0, keyboardOpen: false }),
3193
+ usePopup: () => ({ open: false, setOpen: () => {
3194
+ }, phase: "closed", sync: (open2) => open2 ? "open" : "closed", wrapProps: {}, portal: (content) => content, refresh: () => {
3195
+ } }),
3196
+ useLongPress: noopReturn,
3197
+ useInView: () => ({ isIn: false, ready: false, observe: () => {
3198
+ }, refresh: () => {
3199
+ }, disconnect: () => {
3200
+ } }),
3201
+ useScrollPosition: () => ({ y: 0, refresh: () => {
3202
+ } }),
3203
+ useAsync: () => ({ data: void 0, loading: true, error: void 0, reload: () => {
3204
+ } }),
3205
+ useControlled: () => ({ value: void 0, setValue: () => {
3206
+ }, controlled: false }),
3207
+ useControlledInput: () => ({ value: void 0, setValue: () => {
3208
+ }, keyword: "", setKeyword: () => {
3209
+ }, selectedLabel: "", setSelectedLabel: () => {
3210
+ }, controlled: false }),
3211
+ useOpen: () => ({ open: false, setOpen: () => {
3212
+ }, triggerProps: {} }),
3213
+ usePresence: () => ({ phase: "closed", ref: () => {
3214
+ }, sync: () => "closed" }),
3215
+ useGlobalKey: () => () => {
3216
+ },
3217
+ useDrag: noopReturn,
3218
+ useDragDrop: () => ({ dropProps: {}, dragProps: {} }),
3219
+ useReducedMotion: () => true,
3220
+ // SSR 无动画——组件直落终值
3221
+ useAnimationEnd: () => () => {
3222
+ },
3223
+ useTween: (t) => {
3224
+ let v = t;
3225
+ return {
3226
+ get value() {
3227
+ return v;
3228
+ },
3229
+ set value(n) {
3230
+ v = n;
3231
+ },
3232
+ reset: (n) => {
3233
+ if (n !== void 0) v = n;
3234
+ return v;
3235
+ }
3236
+ };
3237
+ }
3238
+ };
3239
+ }
3240
+ function createSsrContext(serverCtx, dataStore) {
3241
+ const ctx = {
3242
+ ...serverCtx,
3243
+ ui: createSsrUi(),
3244
+ browser: void 0
3245
+ };
3246
+ const dataCache = /* @__PURE__ */ new Map();
3247
+ ctx.data = {
3248
+ async get(key, fetcher) {
3249
+ const entry = dataCache.get(key);
3250
+ if (entry && "value" in entry) return entry.value;
3251
+ if (entry?.promise) return entry.promise;
3252
+ if (!fetcher) return void 0;
3253
+ const promise = Promise.resolve().then(() => fetcher()).then((val) => {
3254
+ dataCache.set(key, { value: val });
3255
+ dataStore.set(key, val);
3256
+ return val;
3257
+ });
3258
+ dataCache.set(key, { promise });
3259
+ return promise;
3260
+ },
3261
+ set(key, value) {
3262
+ dataCache.set(key, { value });
3263
+ dataStore.set(key, value);
3264
+ },
3265
+ has(key) {
3266
+ return dataCache.has(key);
3267
+ }
3268
+ };
3269
+ return ctx;
4016
3270
  }
4017
3271
  function serializeData(data) {
4018
3272
  const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
4019
3273
  return `<script>window.__DATA__=${json};</script>`;
4020
3274
  }
3275
+ async function ssrToString(Comp, props, serverCtx, opts = {}) {
3276
+ const dataStore = opts.data ?? /* @__PURE__ */ new Map();
3277
+ const ctx = createSsrContext(serverCtx, dataStore);
3278
+ return renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
3279
+ }
4021
3280
 
4022
3281
  // src/ui/index.ts
4023
3282
  function escape2(s) {
@@ -4166,9 +3425,53 @@ function ui() {
4166
3425
  }
4167
3426
 
4168
3427
  // src/db/postgres/connection.ts
4169
- import net3 from "node:net";
3428
+ import net2 from "node:net";
4170
3429
  import crypto2 from "node:crypto";
4171
3430
 
3431
+ // src/db/errors.ts
3432
+ var DbError = class extends Error {
3433
+ kind;
3434
+ code;
3435
+ cause;
3436
+ constructor(kind, message, options) {
3437
+ super(message);
3438
+ this.name = "DbError";
3439
+ this.kind = kind;
3440
+ this.code = options?.code;
3441
+ this.cause = options?.cause;
3442
+ }
3443
+ };
3444
+ var ProtocolError = class extends DbError {
3445
+ constructor(feature, message) {
3446
+ super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
3447
+ this.name = "ProtocolError";
3448
+ }
3449
+ };
3450
+ var ConnectionError = class extends DbError {
3451
+ attempts;
3452
+ constructor(message, attempts = 1, cause) {
3453
+ super("connection", message, { cause });
3454
+ this.name = "ConnectionError";
3455
+ this.attempts = attempts;
3456
+ }
3457
+ };
3458
+ var TimeoutError = class extends DbError {
3459
+ operation;
3460
+ ms;
3461
+ constructor(operation, ms) {
3462
+ super("timeout", `${operation} timed out after ${ms}ms`);
3463
+ this.name = "TimeoutError";
3464
+ this.operation = operation;
3465
+ this.ms = ms;
3466
+ }
3467
+ };
3468
+ var ValidationError = class extends DbError {
3469
+ constructor(message) {
3470
+ super("validation", message);
3471
+ this.name = "ValidationError";
3472
+ }
3473
+ };
3474
+
4172
3475
  // src/db/postgres/protocol.ts
4173
3476
  function encodeMessage(type, payload) {
4174
3477
  const out = new Uint8Array(1 + 4 + payload.length);
@@ -4377,7 +3680,7 @@ function parseRowDescription(payload) {
4377
3680
  for (let c = 0; c < count; c++) {
4378
3681
  let j = i;
4379
3682
  while (payload[j] !== 0) j++;
4380
- const name = _decoder2.decode(payload.subarray(i, j));
3683
+ const name = _decoder.decode(payload.subarray(i, j));
4381
3684
  i = j + 1;
4382
3685
  i += 6;
4383
3686
  const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
@@ -4399,7 +3702,7 @@ function parseDataRow(payload) {
4399
3702
  if (len === -1) {
4400
3703
  values.push(null);
4401
3704
  } else {
4402
- values.push(_decoder2.decode(payload.subarray(i, i + len)));
3705
+ values.push(_decoder.decode(payload.subarray(i, i + len)));
4403
3706
  i += len;
4404
3707
  }
4405
3708
  }
@@ -4412,7 +3715,7 @@ function parseErrorFields(payload) {
4412
3715
  const type = String.fromCharCode(payload[i]);
4413
3716
  let j = i + 1;
4414
3717
  while (j < payload.length && payload[j] !== 0) j++;
4415
- const value = _decoder2.decode(payload.subarray(i + 1, j));
3718
+ const value = _decoder.decode(payload.subarray(i + 1, j));
4416
3719
  switch (type) {
4417
3720
  case "S":
4418
3721
  out.severity = value;
@@ -4437,10 +3740,10 @@ function parseErrorFields(payload) {
4437
3740
  }
4438
3741
  return out;
4439
3742
  }
4440
- var _decoder2 = new TextDecoder();
4441
- var _encoder2 = new TextEncoder();
3743
+ var _decoder = new TextDecoder();
3744
+ var _encoder = new TextEncoder();
4442
3745
  function utf8(s) {
4443
- return _encoder2.encode(s);
3746
+ return _encoder.encode(s);
4444
3747
  }
4445
3748
  function concat(...parts) {
4446
3749
  let total = 0;
@@ -4486,7 +3789,7 @@ var PgConnection = class _PgConnection {
4486
3789
  connect() {
4487
3790
  return new Promise((resolve3, reject) => {
4488
3791
  this.status = "connecting";
4489
- const sock = net3.connect(this.opts.port, this.opts.host);
3792
+ const sock = net2.connect(this.opts.port, this.opts.host);
4490
3793
  this.socket = sock;
4491
3794
  const timeout = setTimeout(() => {
4492
3795
  sock.destroy();
@@ -5657,151 +4960,668 @@ function createQueryBuilder(sql, exec) {
5657
4960
  const [t, a] = table2.split(/\s+/);
5658
4961
  return mkSelect(t, a);
5659
4962
  }
5660
- return mkSelect(table2, alias);
5661
- },
5662
- insert: (table2) => mkInsert(table2),
5663
- update: (table2) => mkUpdate(table2),
5664
- delete: (table2) => mkDelete(table2)
5665
- };
5666
- }
5667
-
5668
- // src/postgres/client.ts
5669
- var MIGRATIONS_TABLE = "_weifuwu_migrations";
5670
- var traceStore = new AsyncLocalStorage();
5671
- function postgres(options) {
5672
- const opts = typeof options === "string" ? { connection: options } : options ?? {};
5673
- const connection = opts.connection ?? process.env.DATABASE_URL;
5674
- if (!connection) {
5675
- throw new Error(
5676
- "postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
4963
+ return mkSelect(table2, alias);
4964
+ },
4965
+ insert: (table2) => mkInsert(table2),
4966
+ update: (table2) => mkUpdate(table2),
4967
+ delete: (table2) => mkDelete(table2)
4968
+ };
4969
+ }
4970
+
4971
+ // src/postgres/client.ts
4972
+ var MIGRATIONS_TABLE = "_weifuwu_migrations";
4973
+ var traceStore = new AsyncLocalStorage();
4974
+ function postgres(options) {
4975
+ const opts = typeof options === "string" ? { connection: options } : options ?? {};
4976
+ const connection = opts.connection ?? process.env.DATABASE_URL;
4977
+ if (!connection) {
4978
+ throw new Error(
4979
+ "postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
4980
+ );
4981
+ }
4982
+ const u = new URL(connection);
4983
+ const pool = new PgPool({
4984
+ host: u.hostname,
4985
+ port: Number(u.port || 5432),
4986
+ user: decodeURIComponent(u.username),
4987
+ password: decodeURIComponent(u.password),
4988
+ database: u.pathname.replace(/^\//, ""),
4989
+ poolSize: opts.max ?? opts.poolSize ?? 10,
4990
+ acquireTimeoutMs: opts.acquireTimeoutMs,
4991
+ statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
4992
+ // onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
4993
+ onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
4994
+ const tid = traceStore.getStore();
4995
+ opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
4996
+ } : void 0
4997
+ });
4998
+ const sql = makeSql(pool);
4999
+ const mw = ((req, ctx, next) => {
5000
+ ctx.sql = sql;
5001
+ return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
5002
+ });
5003
+ mw.__meta = { injects: ["sql"], depends: [] };
5004
+ mw.sql = sql;
5005
+ mw.migrate = async () => {
5006
+ await sql.unsafe(`
5007
+ CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
5008
+ name TEXT PRIMARY KEY,
5009
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
5010
+ )
5011
+ `);
5012
+ };
5013
+ mw.markMigrated = async (moduleName) => {
5014
+ await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
5015
+ moduleName
5016
+ ]);
5017
+ };
5018
+ mw.isMigrated = async (moduleName) => {
5019
+ const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
5020
+ return rows.length > 0;
5021
+ };
5022
+ mw.transaction = ((fn) => pool.begin(fn));
5023
+ mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
5024
+ mw.close = () => pool.close();
5025
+ return mw;
5026
+ }
5027
+ var TaggedQuery = class {
5028
+ text;
5029
+ params;
5030
+ executor;
5031
+ constructor(text, params, executor) {
5032
+ this.text = text;
5033
+ this.params = params;
5034
+ this.executor = executor;
5035
+ }
5036
+ then(resolve3, reject) {
5037
+ return this.executor(this.text, this.params).then(resolve3, reject);
5038
+ }
5039
+ catch(reject) {
5040
+ return this.executor(this.text, this.params).catch(reject);
5041
+ }
5042
+ finally(fn) {
5043
+ return this.executor(this.text, this.params).finally(fn);
5044
+ }
5045
+ /** 嵌套片段(agent-platform 条件过滤模式) */
5046
+ get __fragment() {
5047
+ return { sql: this.text, params: this.params };
5048
+ }
5049
+ };
5050
+ function makeSql(pool) {
5051
+ const sql = ((strings, ...values) => {
5052
+ const { sql: text, params } = parseTaggedFromPool(strings, values);
5053
+ return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
5054
+ });
5055
+ sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
5056
+ sql.query = createQueryBuilder(sql, async (q) => {
5057
+ const { sql: text, params: p } = compileQuery(q);
5058
+ return wrapError(pool.query(text, p));
5059
+ });
5060
+ sql.raw = (strings, ...values) => rawSql(
5061
+ strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), ""),
5062
+ values
5063
+ );
5064
+ sql.close = () => pool.close();
5065
+ return sql;
5066
+ }
5067
+ function parseTaggedFromPool(strings, values) {
5068
+ let sql = strings[0];
5069
+ const params = [];
5070
+ for (let i = 0; i < values.length; i++) {
5071
+ const frag = values[i]?.__fragment;
5072
+ if (frag) {
5073
+ const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
5074
+ params.push(frag.params[parseInt(idx, 10) - 1]);
5075
+ return `$${params.length}`;
5076
+ });
5077
+ sql += renumbered + strings[i + 1];
5078
+ } else {
5079
+ params.push(values[i]);
5080
+ sql += `$${params.length}` + strings[i + 1];
5081
+ }
5082
+ }
5083
+ return { sql, params };
5084
+ }
5085
+ var PG_ERROR_MAP = {
5086
+ "23505": 409,
5087
+ // unique_violation
5088
+ "23503": 400,
5089
+ // foreign_key_violation
5090
+ "23502": 400,
5091
+ // not_null_violation
5092
+ "23514": 400,
5093
+ // check_violation
5094
+ "22P02": 400,
5095
+ // invalid_text_representation
5096
+ "22003": 400
5097
+ // numeric_value_out_of_range
5098
+ };
5099
+ function wrapError(promise) {
5100
+ return promise.catch((err) => {
5101
+ const code = err?.code;
5102
+ if (code && PG_ERROR_MAP[code]) {
5103
+ throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
5104
+ }
5105
+ throw err;
5106
+ });
5107
+ }
5108
+
5109
+ // src/db/redis/connection.ts
5110
+ import net3 from "node:net";
5111
+
5112
+ // src/db/redis/resp.ts
5113
+ var RespError = class extends DbError {
5114
+ constructor(message) {
5115
+ super("protocol", message, { code: "RESP" });
5116
+ this.name = "RespError";
5117
+ }
5118
+ };
5119
+ var IncompleteError = class extends Error {
5120
+ constructor() {
5121
+ super("incomplete RESP message");
5122
+ this.name = "IncompleteError";
5123
+ }
5124
+ };
5125
+ function decodeValue(v, decodeBytes) {
5126
+ if (v instanceof Uint8Array) return decodeBytes ? _decoder2.decode(v) : v;
5127
+ if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
5128
+ return v;
5129
+ }
5130
+ function encodeCommand(args) {
5131
+ const lens = new Array(args.length);
5132
+ let total = headerLen(args.length);
5133
+ for (let i = 0; i < args.length; i++) {
5134
+ const arg = args[i];
5135
+ const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
5136
+ lens[i] = len;
5137
+ total += headerLen(len) + len + 2;
5138
+ }
5139
+ const out = new Uint8Array(total);
5140
+ let off = 0;
5141
+ const head = `*${args.length}\r
5142
+ `;
5143
+ out.set(_encoder2.encode(head), 0);
5144
+ off = head.length;
5145
+ for (let i = 0; i < args.length; i++) {
5146
+ const arg = args[i];
5147
+ const len = lens[i];
5148
+ const lh = `$${len}\r
5149
+ `;
5150
+ out.set(_encoder2.encode(lh), off);
5151
+ off += lh.length;
5152
+ if (typeof arg === "string") {
5153
+ out.set(_encoder2.encode(arg), off);
5154
+ } else if (arg instanceof Buffer) {
5155
+ out.set(arg, off);
5156
+ } else {
5157
+ out.set(_encoder2.encode(String(arg)), off);
5158
+ }
5159
+ off += len;
5160
+ out[off] = 13;
5161
+ out[off + 1] = 10;
5162
+ off += 2;
5163
+ }
5164
+ return out;
5165
+ }
5166
+ function headerLen(n) {
5167
+ if (n < 10) return 4;
5168
+ if (n < 100) return 5;
5169
+ if (n < 1e3) return 6;
5170
+ if (n < 1e4) return 7;
5171
+ return 8;
5172
+ }
5173
+ var _decoder2 = new TextDecoder();
5174
+ var _encoder2 = new TextEncoder();
5175
+ var RespParser = class {
5176
+ buf = new Uint8Array(0);
5177
+ off = 0;
5178
+ /** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
5179
+ push(chunk) {
5180
+ this.append(chunk);
5181
+ const saved = this.off;
5182
+ try {
5183
+ const value = this.parseValue();
5184
+ this.compact();
5185
+ return { value, incomplete: false };
5186
+ } catch (e) {
5187
+ this.off = saved;
5188
+ if (e instanceof IncompleteError) return { value: null, incomplete: true };
5189
+ throw e;
5190
+ }
5191
+ }
5192
+ /** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
5193
+ pushAll(chunk) {
5194
+ this.append(chunk);
5195
+ const out = [];
5196
+ while (true) {
5197
+ const saved = this.off;
5198
+ try {
5199
+ out.push(this.parseValue());
5200
+ } catch (e) {
5201
+ this.off = saved;
5202
+ if (e instanceof IncompleteError) break;
5203
+ throw e;
5204
+ }
5205
+ }
5206
+ if (out.length > 0) this.compact();
5207
+ return out;
5208
+ }
5209
+ /** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
5210
+ append(chunk) {
5211
+ if (chunk.length === 0) return;
5212
+ const rest = this.buf.length - this.off;
5213
+ if (rest === 0) {
5214
+ this.buf = chunk;
5215
+ this.off = 0;
5216
+ return;
5217
+ }
5218
+ const merged = new Uint8Array(rest + chunk.length);
5219
+ merged.set(this.buf.subarray(this.off), 0);
5220
+ merged.set(chunk, rest);
5221
+ this.buf = merged;
5222
+ this.off = 0;
5223
+ }
5224
+ /** 全部消费后压缩(释放底层) */
5225
+ compact() {
5226
+ if (this.off === this.buf.length) {
5227
+ this.buf = new Uint8Array(0);
5228
+ this.off = 0;
5229
+ } else if (this.off > 0) {
5230
+ this.buf = this.buf.subarray(this.off);
5231
+ this.off = 0;
5232
+ }
5233
+ }
5234
+ parseValue() {
5235
+ if (this.buf.length - this.off < 3) throw new IncompleteError();
5236
+ const type = String.fromCharCode(this.buf[this.off]);
5237
+ this.off++;
5238
+ switch (type) {
5239
+ case "+": {
5240
+ const line = this.readLine();
5241
+ return line;
5242
+ }
5243
+ case "-": {
5244
+ const line = this.readLine();
5245
+ return new RespError(line);
5246
+ }
5247
+ case ":": {
5248
+ return this.readInt();
5249
+ }
5250
+ case "$": {
5251
+ const len = this.readInt();
5252
+ if (len === -1) return null;
5253
+ return this.readBulkBytes(len);
5254
+ }
5255
+ case "*": {
5256
+ const count = this.readInt();
5257
+ if (count === -1) return null;
5258
+ const items = [];
5259
+ for (let i = 0; i < count; i++) items.push(this.parseValue());
5260
+ return items;
5261
+ }
5262
+ default:
5263
+ throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
5264
+ }
5265
+ }
5266
+ /**
5267
+ * 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
5268
+ * 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
5269
+ */
5270
+ readInt() {
5271
+ const buf = this.buf;
5272
+ let i = this.off;
5273
+ let neg = false;
5274
+ if (i < buf.length && buf[i] === 45) {
5275
+ neg = true;
5276
+ i++;
5277
+ }
5278
+ let n = 0;
5279
+ while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
5280
+ n = n * 10 + (buf[i] - 48);
5281
+ i++;
5282
+ }
5283
+ if (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
5284
+ this.off = i + 2;
5285
+ return neg ? -n : n;
5286
+ }
5287
+ throw new IncompleteError();
5288
+ }
5289
+ /** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
5290
+ readLine() {
5291
+ const idx = indexOfCRLF(this.buf, this.off);
5292
+ if (idx === -1) throw new IncompleteError();
5293
+ const line = _decoder2.decode(this.buf.subarray(this.off, idx));
5294
+ this.off = idx + 2;
5295
+ return line;
5296
+ }
5297
+ /** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
5298
+ readBulkBytes(len) {
5299
+ if (this.buf.length - this.off < len + 2) throw new IncompleteError();
5300
+ const value = this.buf.subarray(this.off, this.off + len);
5301
+ this.off += len + 2;
5302
+ return value;
5303
+ }
5304
+ };
5305
+ function indexOfCRLF(buf, from = 0) {
5306
+ let i = buf.indexOf(13, from);
5307
+ while (i !== -1) {
5308
+ if (i + 1 < buf.length && buf[i + 1] === 10) return i;
5309
+ i = buf.indexOf(13, i + 1);
5310
+ }
5311
+ return -1;
5312
+ }
5313
+
5314
+ // src/db/redis/connection.ts
5315
+ var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
5316
+ var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
5317
+ var RedisConnection = class {
5318
+ ready = false;
5319
+ opts;
5320
+ socket = null;
5321
+ parser = new RespParser();
5322
+ pending = [];
5323
+ /** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
5324
+ pendingHead = 0;
5325
+ offlineQueue = [];
5326
+ subs = /* @__PURE__ */ new Map();
5327
+ psubs = /* @__PURE__ */ new Map();
5328
+ status = "idle";
5329
+ retries = 0;
5330
+ reconnectTimer = null;
5331
+ socketTimeoutTimer = null;
5332
+ connectPromise = null;
5333
+ closed = false;
5334
+ connectedOnce = false;
5335
+ constructor(options = {}) {
5336
+ this.opts = {
5337
+ host: options.host ?? "127.0.0.1",
5338
+ port: options.port ?? 6379,
5339
+ retryDelayMs: options.retryDelayMs ?? 100,
5340
+ maxRetries: options.maxRetries ?? 10,
5341
+ enableOfflineQueue: options.enableOfflineQueue ?? true,
5342
+ maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
5343
+ commandTimeoutMs: options.commandTimeoutMs ?? 0,
5344
+ socketTimeoutMs: options.socketTimeoutMs ?? 0,
5345
+ onCommand: options.onCommand
5346
+ };
5347
+ }
5348
+ /** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
5349
+ connect() {
5350
+ if (this.connectPromise) return this.connectPromise;
5351
+ this.closed = false;
5352
+ this.connectPromise = new Promise((resolve3, reject) => {
5353
+ this.openSocket();
5354
+ this.onceReady = () => resolve3();
5355
+ this.onceFailed = (err) => reject(err);
5356
+ }).finally(() => {
5357
+ this.connectPromise = null;
5358
+ this.onceReady = void 0;
5359
+ this.onceFailed = void 0;
5360
+ });
5361
+ return this.connectPromise;
5362
+ }
5363
+ onceReady;
5364
+ onceFailed;
5365
+ openSocket() {
5366
+ this.status = "connecting";
5367
+ const sock = net3.connect(this.opts.port, this.opts.host);
5368
+ this.socket = sock;
5369
+ sock.on("connect", () => {
5370
+ sock.setNoDelay(true);
5371
+ this.status = "ready";
5372
+ this.retries = 0;
5373
+ if (this.connectedOnce) {
5374
+ for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
5375
+ for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
5376
+ }
5377
+ this.connectedOnce = true;
5378
+ this.flushOffline();
5379
+ this.onceReady?.();
5380
+ });
5381
+ sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
5382
+ sock.on("error", (err) => {
5383
+ if (this.status === "connecting") {
5384
+ this.handleDisconnect(err);
5385
+ }
5386
+ });
5387
+ sock.on("close", () => {
5388
+ this.socket = null;
5389
+ if (this.status !== "closed" && this.status !== "idle") {
5390
+ this.handleDisconnect(new ConnectionError("redis: socket closed"));
5391
+ }
5392
+ });
5393
+ }
5394
+ handleDisconnect(err) {
5395
+ const queue2 = this.pending.slice(this.pendingHead);
5396
+ this.pending = [];
5397
+ this.pendingHead = 0;
5398
+ for (const p of queue2) p.reject(err);
5399
+ this.clearSocketTimeout();
5400
+ if (this.closed || this.status === "closed") return;
5401
+ this.retries++;
5402
+ this.status = "connecting";
5403
+ if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
5404
+ this.status = "closed";
5405
+ const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
5406
+ this.onceFailed?.(failErr);
5407
+ return;
5408
+ }
5409
+ const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
5410
+ this.reconnectTimer = setTimeout(() => {
5411
+ this.reconnectTimer = null;
5412
+ this.openSocket();
5413
+ }, delay);
5414
+ }
5415
+ onData(chunk) {
5416
+ try {
5417
+ const values = this.parser.pushAll(chunk);
5418
+ for (const raw of values) {
5419
+ const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
5420
+ if (typeof first === "string" && PUSH_TYPES.has(first)) {
5421
+ this.dispatchSubscribe(decodeValue(raw, true));
5422
+ continue;
5423
+ }
5424
+ const p = this.pending[this.pendingHead++];
5425
+ if (!p) break;
5426
+ if (p.timedOut) continue;
5427
+ if (raw instanceof RespError) p.reject(raw);
5428
+ else p.resolve(decodeValue(raw, !p.asBuffer));
5429
+ }
5430
+ if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
5431
+ this.pending = this.pending.slice(this.pendingHead);
5432
+ this.pendingHead = 0;
5433
+ }
5434
+ if (this.opts.socketTimeoutMs > 0) {
5435
+ if (this.pendingHead < this.pending.length) this.armSocketTimeout();
5436
+ else this.clearSocketTimeout();
5437
+ }
5438
+ } catch (e) {
5439
+ const queue2 = this.pending.slice(this.pendingHead);
5440
+ this.pending = [];
5441
+ this.pendingHead = 0;
5442
+ for (const q of queue2) q.reject(e);
5443
+ this.socket?.destroy();
5444
+ }
5445
+ }
5446
+ /**
5447
+ * 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
5448
+ * opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
5449
+ */
5450
+ command(name, ...args) {
5451
+ const last = args[args.length - 1];
5452
+ const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
5453
+ if (this.status === "ready" && this.socket && !this.socket.destroyed) {
5454
+ return this.sendNow(name, args, opts?.asBuffer);
5455
+ }
5456
+ if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
5457
+ return Promise.reject(new ConnectionError("redis: not connected"));
5458
+ }
5459
+ if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
5460
+ return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
5461
+ }
5462
+ return new Promise((resolve3, reject) => {
5463
+ this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
5464
+ });
5465
+ }
5466
+ sendNow(name, args, asBuffer) {
5467
+ const start = performance.now();
5468
+ return new Promise((resolve3, reject) => {
5469
+ const p = {
5470
+ resolve: resolve3,
5471
+ reject,
5472
+ asBuffer,
5473
+ blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
5474
+ };
5475
+ this.armTimeout(p);
5476
+ this.pending.push(p);
5477
+ this.armSocketTimeout();
5478
+ this.socket.write(encodeCommand([name, ...args]), (err) => {
5479
+ if (err) {
5480
+ const idx = this.pending.indexOf(p);
5481
+ if (idx >= 0) this.pending.splice(idx, 1);
5482
+ p.reject(err instanceof Error ? err : new ConnectionError("redis: socket write failed"));
5483
+ }
5484
+ });
5485
+ }).then(
5486
+ (v) => {
5487
+ if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
5488
+ return v;
5489
+ },
5490
+ (e) => {
5491
+ if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
5492
+ throw e;
5493
+ }
5677
5494
  );
5678
5495
  }
5679
- const u = new URL(connection);
5680
- const pool = new PgPool({
5681
- host: u.hostname,
5682
- port: Number(u.port || 5432),
5683
- user: decodeURIComponent(u.username),
5684
- password: decodeURIComponent(u.password),
5685
- database: u.pathname.replace(/^\//, ""),
5686
- poolSize: opts.max ?? opts.poolSize ?? 10,
5687
- acquireTimeoutMs: opts.acquireTimeoutMs,
5688
- statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
5689
- // onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
5690
- onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
5691
- const tid = traceStore.getStore();
5692
- opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
5693
- } : void 0
5694
- });
5695
- const sql = makeSql(pool);
5696
- const mw = ((req, ctx, next) => {
5697
- ctx.sql = sql;
5698
- return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
5699
- });
5700
- mw.__meta = { injects: ["sql"], depends: [] };
5701
- mw.sql = sql;
5702
- mw.migrate = async () => {
5703
- await sql.unsafe(`
5704
- CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
5705
- name TEXT PRIMARY KEY,
5706
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
5707
- )
5708
- `);
5709
- };
5710
- mw.markMigrated = async (moduleName) => {
5711
- await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
5712
- moduleName
5713
- ]);
5714
- };
5715
- mw.isMigrated = async (moduleName) => {
5716
- const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
5717
- return rows.length > 0;
5718
- };
5719
- mw.transaction = ((fn) => pool.begin(fn));
5720
- mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
5721
- mw.close = () => pool.close();
5722
- return mw;
5723
- }
5724
- var TaggedQuery = class {
5725
- text;
5726
- params;
5727
- executor;
5728
- constructor(text, params, executor) {
5729
- this.text = text;
5730
- this.params = params;
5731
- this.executor = executor;
5496
+ /** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
5497
+ armTimeout(p) {
5498
+ const ms = this.opts.commandTimeoutMs;
5499
+ if (ms <= 0) return;
5500
+ p.timer = setTimeout(() => {
5501
+ p.timedOut = true;
5502
+ if (p.blocking) p.resolve(null);
5503
+ else p.reject(new TimeoutError("redis: command timeout", ms));
5504
+ }, ms);
5732
5505
  }
5733
- then(resolve3, reject) {
5734
- return this.executor(this.text, this.params).then(resolve3, reject);
5506
+ /** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
5507
+ armSocketTimeout() {
5508
+ const ms = this.opts.socketTimeoutMs;
5509
+ if (ms <= 0) return;
5510
+ this.clearSocketTimeout();
5511
+ this.socketTimeoutTimer = setTimeout(() => {
5512
+ this.socketTimeoutTimer = null;
5513
+ const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
5514
+ const queue2 = this.pending.slice(this.pendingHead);
5515
+ this.pending = [];
5516
+ this.pendingHead = 0;
5517
+ for (const q of queue2) q.reject(err);
5518
+ this.clearSocketTimeout();
5519
+ this.socket?.destroy();
5520
+ }, ms);
5735
5521
  }
5736
- catch(reject) {
5737
- return this.executor(this.text, this.params).catch(reject);
5522
+ clearSocketTimeout() {
5523
+ if (this.socketTimeoutTimer) {
5524
+ clearTimeout(this.socketTimeoutTimer);
5525
+ this.socketTimeoutTimer = null;
5526
+ }
5738
5527
  }
5739
- finally(fn) {
5740
- return this.executor(this.text, this.params).finally(fn);
5528
+ flushOffline() {
5529
+ const queue2 = this.offlineQueue;
5530
+ this.offlineQueue = [];
5531
+ for (const q of queue2) {
5532
+ this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
5533
+ }
5741
5534
  }
5742
- /** 嵌套片段(agent-platform 条件过滤模式) */
5743
- get __fragment() {
5744
- return { sql: this.text, params: this.params };
5535
+ /** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
5536
+ batch(payload, count) {
5537
+ if (this.status !== "ready" || !this.socket) {
5538
+ return Promise.reject(new ConnectionError("redis: not connected"));
5539
+ }
5540
+ return new Promise((resolve3, reject) => {
5541
+ const results = [];
5542
+ const batchPending = [];
5543
+ let settled = false;
5544
+ const maybeResolve = () => {
5545
+ if (settled) return;
5546
+ if (results.length === count) {
5547
+ settled = true;
5548
+ if (batchTimer) clearTimeout(batchTimer);
5549
+ resolve3(results);
5550
+ }
5551
+ };
5552
+ for (let i = 0; i < count; i++) {
5553
+ const p = {
5554
+ // 正常响应与错误响应(RespError)都作为结果值收集——管道语义
5555
+ resolve: (v) => {
5556
+ results.push(v);
5557
+ maybeResolve();
5558
+ },
5559
+ reject: (e) => {
5560
+ results.push(e);
5561
+ maybeResolve();
5562
+ }
5563
+ };
5564
+ batchPending.push(p);
5565
+ this.pending.push(p);
5566
+ }
5567
+ let batchTimer;
5568
+ const ms = this.opts.commandTimeoutMs;
5569
+ if (ms > 0) {
5570
+ batchTimer = setTimeout(() => {
5571
+ settled = true;
5572
+ for (const p of batchPending) p.timedOut = true;
5573
+ reject(new TimeoutError("redis: batch timeout", ms));
5574
+ }, ms);
5575
+ }
5576
+ this.socket.write(payload);
5577
+ this.armSocketTimeout();
5578
+ });
5745
5579
  }
5746
- };
5747
- function makeSql(pool) {
5748
- const sql = ((strings, ...values) => {
5749
- const { sql: text, params } = parseTaggedFromPool(strings, values);
5750
- return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
5751
- });
5752
- sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
5753
- sql.query = createQueryBuilder(sql, async (q) => {
5754
- const { sql: text, params: p } = compileQuery(q);
5755
- return wrapError(pool.query(text, p));
5756
- });
5757
- sql.raw = (strings, ...values) => rawSql(
5758
- strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), ""),
5759
- values
5760
- );
5761
- sql.close = () => pool.close();
5762
- return sql;
5763
- }
5764
- function parseTaggedFromPool(strings, values) {
5765
- let sql = strings[0];
5766
- const params = [];
5767
- for (let i = 0; i < values.length; i++) {
5768
- const frag = values[i]?.__fragment;
5769
- if (frag) {
5770
- const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
5771
- params.push(frag.params[parseInt(idx, 10) - 1]);
5772
- return `$${params.length}`;
5773
- });
5774
- sql += renumbered + strings[i + 1];
5775
- } else {
5776
- params.push(values[i]);
5777
- sql += `$${params.length}` + strings[i + 1];
5580
+ /** 订阅频道:回调式(channel, message) */
5581
+ async subscribe(channel, fn) {
5582
+ this.subs.set(channel, fn);
5583
+ await this.command("SUBSCRIBE", channel);
5584
+ }
5585
+ /** 订阅模式:回调式(channel, message) */
5586
+ async psubscribe(pattern, fn) {
5587
+ this.psubs.set(pattern, fn);
5588
+ await this.command("PSUBSCRIBE", pattern);
5589
+ }
5590
+ /** 旁路分发订阅消息(RESP 数组路由到回调) */
5591
+ dispatchSubscribe(value) {
5592
+ const [type, a, b] = value;
5593
+ if (type === "message") {
5594
+ const fn = this.subs.get(a);
5595
+ fn?.(a, b);
5596
+ } else if (type === "pmessage") {
5597
+ const fn = this.psubs.get(a);
5598
+ fn?.(b, value[3] ?? "");
5778
5599
  }
5779
5600
  }
5780
- return { sql, params };
5781
- }
5782
- var PG_ERROR_MAP = {
5783
- "23505": 409,
5784
- // unique_violation
5785
- "23503": 400,
5786
- // foreign_key_violation
5787
- "23502": 400,
5788
- // not_null_violation
5789
- "23514": 400,
5790
- // check_violation
5791
- "22P02": 400,
5792
- // invalid_text_representation
5793
- "22003": 400
5794
- // numeric_value_out_of_range
5795
- };
5796
- function wrapError(promise) {
5797
- return promise.catch((err) => {
5798
- const code = err?.code;
5799
- if (code && PG_ERROR_MAP[code]) {
5800
- throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
5601
+ /** 主动关闭——不再重连 */
5602
+ async close() {
5603
+ this.closed = true;
5604
+ this.status = "closed";
5605
+ if (this.reconnectTimer) {
5606
+ clearTimeout(this.reconnectTimer);
5607
+ this.reconnectTimer = null;
5801
5608
  }
5802
- throw err;
5803
- });
5804
- }
5609
+ this.clearSocketTimeout();
5610
+ const queue2 = this.pending.slice(this.pendingHead);
5611
+ this.pending = [];
5612
+ this.pendingHead = 0;
5613
+ for (const p of queue2) p.reject(new ConnectionError("redis: connection closed"));
5614
+ const oq = this.offlineQueue;
5615
+ this.offlineQueue = [];
5616
+ for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
5617
+ const sock = this.socket;
5618
+ this.socket = null;
5619
+ sock?.destroy();
5620
+ }
5621
+ get connected() {
5622
+ return this.status === "ready";
5623
+ }
5624
+ };
5805
5625
 
5806
5626
  // src/db/redis/pipeline.ts
5807
5627
  var RedisPipeline = class {