weifuwu 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +38 -41
  2. package/dist/components/DatePicker/DatePicker.d.ts +1 -1
  3. package/dist/components/List/List.d.ts +3 -0
  4. package/dist/components/Tour/Tour.d.ts +1 -1
  5. package/dist/components/index.js +12 -12
  6. package/dist/components/style.css +17 -0
  7. package/dist/index.js +1272 -1228
  8. package/dist/scheduler/index.d.ts +7 -2
  9. package/dist/ui-dom/hooks/index.d.ts +0 -1
  10. package/dist/ui-dom/hooks/popup.d.ts +1 -10
  11. package/dist/ui-dom/hooks/stable.d.ts +1 -1
  12. package/dist/ui-dom/hooks/types.d.ts +0 -1
  13. package/dist/ui-dom/index.d.ts +1 -1
  14. package/dist/ui-dom/index.js +10 -10
  15. package/dist/ui-dom/jsx-runtime.js +1 -1
  16. package/dist/ui-dom/testing.js +1 -1
  17. package/dist/ui-dom/types.d.ts +31 -41
  18. package/dist/ui-dom/vdom/audit.d.ts +20 -0
  19. package/dist/ui-dom/vdom/build.d.ts +10 -3
  20. package/dist/ui-dom/vdom/diff.d.ts +7 -8
  21. package/dist/ui-dom/vdom/index.d.ts +1 -1
  22. package/dist/ui-dom/vdom/mount.d.ts +14 -5
  23. package/dist/ui-dom/vdom/render.d.ts +4 -0
  24. package/dist/ui-dom/vdom/serve.d.ts +1 -1
  25. package/dist/ui-dom/vdom/transform.d.ts +32 -0
  26. package/dist/ui-dom/vnode.d.ts +18 -10
  27. package/docs/components.md +5 -5
  28. package/docs/custom-components.md +86 -54
  29. package/docs/examples.md +35 -41
  30. package/docs/frontend-middleware.md +3 -4
  31. package/docs/frontend-ui-dom.md +22 -18
  32. package/docs/frontend.md +243 -168
  33. package/docs/mobile.md +2 -2
  34. package/docs/realtime.md +8 -3
  35. package/package.json +1 -1
  36. package/dist/ui-dom/focus-trap.d.ts +0 -4
  37. package/dist/ui-dom/scroll-lock.d.ts +0 -5
  38. package/dist/ui-dom/vdom/scheduler.d.ts +0 -13
package/dist/index.js CHANGED
@@ -2315,1140 +2315,577 @@ function isConnClosed(e) {
2315
2315
  // src/scheduler/index.ts
2316
2316
  import { randomUUID as randomUUID2 } from "node:crypto";
2317
2317
 
2318
- // src/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
@@ -3641,10 +3078,42 @@ var HtmlSafe = class {
3641
3078
  var Fragment = /* @__PURE__ */ Symbol("Fragment");
3642
3079
  var Portal = /* @__PURE__ */ Symbol("Portal");
3643
3080
 
3644
- // src/ui-dom/vdom/ssr.ts
3645
- var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
3646
- var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g", "text", "defs", "use", "clipPath"]);
3647
- function escape(s) {
3081
+ // src/ui-dom/vdom/transform.ts
3082
+ function holeDetail(v) {
3083
+ if (v === false) return "false";
3084
+ if (v === null) return "null";
3085
+ if (v === void 0) return "undefined";
3086
+ if (v === true) return "true";
3087
+ if (typeof v === "object") {
3088
+ try {
3089
+ const s = JSON.stringify(v);
3090
+ const d = s != null && s.length > 80 ? s.slice(0, 80) + "\u2026" : s ?? "";
3091
+ return `object ${d}`;
3092
+ } catch {
3093
+ return `object ${Object.prototype.toString.call(v)}`;
3094
+ }
3095
+ }
3096
+ return `bad-vnode type=${typeof v}`;
3097
+ }
3098
+ function isInvalidVNodeType(t) {
3099
+ return typeof t !== "string" && typeof t !== "function" && t !== Fragment && t !== Portal;
3100
+ }
3101
+ function ensureArrayKeys(children) {
3102
+ for (let i = 0; i < children.length; i++) {
3103
+ const c = children[i];
3104
+ if (c != null && typeof c === "object" && !Array.isArray(c)) {
3105
+ const v = c;
3106
+ if (v.key === void 0) v.key = String(i);
3107
+ else v.key = String(v.key);
3108
+ }
3109
+ }
3110
+ }
3111
+ var ENUMERATED_VALUE_BASED = /* @__PURE__ */ new Set(["draggable", "contenteditable", "spellcheck", "translate"]);
3112
+
3113
+ // src/ui-dom/vdom/ssr.ts
3114
+ var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
3115
+ var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g", "text", "defs", "use", "clipPath"]);
3116
+ function escape(s) {
3648
3117
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3649
3118
  }
3650
3119
  function classToString(v) {
@@ -3664,11 +3133,18 @@ async function renderSsr(input, ctx) {
3664
3133
  if (input == null || typeof input === "boolean") return "";
3665
3134
  if (typeof input === "string" || typeof input === "number") return escape(String(input));
3666
3135
  if (Array.isArray(input)) {
3667
- let s = "";
3668
- for (const c of input) s += await renderSsr(c, ctx);
3669
- return s;
3136
+ ensureArrayKeys(input);
3137
+ const parts = await Promise.all(input.map((c) => {
3138
+ if (c == null || typeof c === "boolean") return Promise.resolve(`<!--wf-hole: ${holeDetail(c)}-->`);
3139
+ return renderSsr(c, ctx);
3140
+ }));
3141
+ return parts.join("");
3670
3142
  }
3671
3143
  const vnode = input;
3144
+ if (isInvalidVNodeType(vnode.type)) {
3145
+ console.warn(`[weifuwu] children \u9879\u975E\u6CD5\uFF1Atype=${String(vnode.type)}\uFF08${typeof vnode.type}\uFF09\u2014\u2014\u5DF2\u5360\u4F4D\uFF08wf-hole\uFF09`);
3146
+ return `<!--wf-hole: ${holeDetail(input)}-->`;
3147
+ }
3672
3148
  if (vnode.type === Portal || vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
3673
3149
  if (typeof vnode.type === "function") {
3674
3150
  const childCtx = Object.create(ctx);
@@ -3678,12 +3154,24 @@ async function renderSsr(input, ctx) {
3678
3154
  `Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
3679
3155
  );
3680
3156
  }
3681
- return renderSsr(renderFn(vnode.props ?? {}), childCtx);
3157
+ const out = await renderFn(vnode.props ?? {});
3158
+ if (vnode.key != null && out != null && typeof out === "object") {
3159
+ if (Array.isArray(out)) {
3160
+ for (const c of out) {
3161
+ if (c != null && typeof c === "object" && !Array.isArray(c)) c.key = vnode.key;
3162
+ }
3163
+ } else {
3164
+ ;
3165
+ out.key = vnode.key;
3166
+ }
3167
+ }
3168
+ return renderSsr(out, childCtx);
3682
3169
  }
3683
3170
  const tag = vnode.type;
3684
3171
  const props = vnode.props ?? {};
3685
3172
  const attrs = [];
3686
3173
  let innerHTML;
3174
+ if (vnode.key != null) attrs.push(` data-wf-key="${escape(String(vnode.key))}"`);
3687
3175
  for (const [key, value] of Object.entries(props)) {
3688
3176
  if (key === "children" || key === "key") continue;
3689
3177
  if (key === "ref") continue;
@@ -3701,7 +3189,7 @@ async function renderSsr(input, ctx) {
3701
3189
  attrs.push(` style="${escape(styleToString(value))}"`);
3702
3190
  continue;
3703
3191
  }
3704
- if (key === "draggable" || key === "contenteditable" || key === "spellcheck") {
3192
+ if (ENUMERATED_VALUE_BASED.has(key)) {
3705
3193
  attrs.push(` ${key}="${value ? "true" : "false"}"`);
3706
3194
  continue;
3707
3195
  }
@@ -3728,9 +3216,6 @@ function createSsrUi() {
3728
3216
  _selfId: "_wf_root",
3729
3217
  render: () => {
3730
3218
  },
3731
- dirty: () => {
3732
- },
3733
- $: () => ({}),
3734
3219
  selfId: () => {
3735
3220
  },
3736
3221
  bumpCtxVersion: () => {
@@ -3739,6 +3224,7 @@ function createSsrUi() {
3739
3224
  },
3740
3225
  endMounting: () => {
3741
3226
  },
3227
+ onUnmount: () => void 0,
3742
3228
  // hooks no-op(组件 SSR 安全——不注册监听/定时器)
3743
3229
  useChat: () => ({ messages: [], input: "", streaming: false, error: null, usage: null, step: null, send: () => {
3744
3230
  }, stop: () => {
@@ -3758,7 +3244,7 @@ function createSsrUi() {
3758
3244
  },
3759
3245
  useVisualViewport: () => ({ height: 0, offsetTop: 0, keyboardOpen: false }),
3760
3246
  usePopup: () => ({ open: false, setOpen: () => {
3761
- }, wrapProps: {}, portal: () => null, refresh: () => {
3247
+ }, phase: "closed", sync: (open2) => open2 ? "open" : "closed", wrapProps: {}, portal: (content) => content, refresh: () => {
3762
3248
  } }),
3763
3249
  useLongPress: noopReturn,
3764
3250
  useInView: () => ({ isIn: false, ready: false, observe: () => {
@@ -3779,9 +3265,6 @@ function createSsrUi() {
3779
3265
  }, triggerProps: {} }),
3780
3266
  usePresence: () => ({ phase: "closed", ref: () => {
3781
3267
  }, sync: () => "closed" }),
3782
- useDialog: () => ({ phase: "closed", rootRef: () => {
3783
- }, panelRef: () => {
3784
- }, sync: (open2) => open2 ? "open" : "closed" }),
3785
3268
  useGlobalKey: () => () => {
3786
3269
  },
3787
3270
  useDrag: noopReturn,
@@ -3995,9 +3478,53 @@ function ui() {
3995
3478
  }
3996
3479
 
3997
3480
  // src/db/postgres/connection.ts
3998
- import net3 from "node:net";
3481
+ import net2 from "node:net";
3999
3482
  import crypto2 from "node:crypto";
4000
3483
 
3484
+ // src/db/errors.ts
3485
+ var DbError = class extends Error {
3486
+ kind;
3487
+ code;
3488
+ cause;
3489
+ constructor(kind, message, options) {
3490
+ super(message);
3491
+ this.name = "DbError";
3492
+ this.kind = kind;
3493
+ this.code = options?.code;
3494
+ this.cause = options?.cause;
3495
+ }
3496
+ };
3497
+ var ProtocolError = class extends DbError {
3498
+ constructor(feature, message) {
3499
+ super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
3500
+ this.name = "ProtocolError";
3501
+ }
3502
+ };
3503
+ var ConnectionError = class extends DbError {
3504
+ attempts;
3505
+ constructor(message, attempts = 1, cause) {
3506
+ super("connection", message, { cause });
3507
+ this.name = "ConnectionError";
3508
+ this.attempts = attempts;
3509
+ }
3510
+ };
3511
+ var TimeoutError = class extends DbError {
3512
+ operation;
3513
+ ms;
3514
+ constructor(operation, ms) {
3515
+ super("timeout", `${operation} timed out after ${ms}ms`);
3516
+ this.name = "TimeoutError";
3517
+ this.operation = operation;
3518
+ this.ms = ms;
3519
+ }
3520
+ };
3521
+ var ValidationError = class extends DbError {
3522
+ constructor(message) {
3523
+ super("validation", message);
3524
+ this.name = "ValidationError";
3525
+ }
3526
+ };
3527
+
4001
3528
  // src/db/postgres/protocol.ts
4002
3529
  function encodeMessage(type, payload) {
4003
3530
  const out = new Uint8Array(1 + 4 + payload.length);
@@ -4206,7 +3733,7 @@ function parseRowDescription(payload) {
4206
3733
  for (let c = 0; c < count; c++) {
4207
3734
  let j = i;
4208
3735
  while (payload[j] !== 0) j++;
4209
- const name = _decoder2.decode(payload.subarray(i, j));
3736
+ const name = _decoder.decode(payload.subarray(i, j));
4210
3737
  i = j + 1;
4211
3738
  i += 6;
4212
3739
  const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
@@ -4228,7 +3755,7 @@ function parseDataRow(payload) {
4228
3755
  if (len === -1) {
4229
3756
  values.push(null);
4230
3757
  } else {
4231
- values.push(_decoder2.decode(payload.subarray(i, i + len)));
3758
+ values.push(_decoder.decode(payload.subarray(i, i + len)));
4232
3759
  i += len;
4233
3760
  }
4234
3761
  }
@@ -4241,7 +3768,7 @@ function parseErrorFields(payload) {
4241
3768
  const type = String.fromCharCode(payload[i]);
4242
3769
  let j = i + 1;
4243
3770
  while (j < payload.length && payload[j] !== 0) j++;
4244
- const value = _decoder2.decode(payload.subarray(i + 1, j));
3771
+ const value = _decoder.decode(payload.subarray(i + 1, j));
4245
3772
  switch (type) {
4246
3773
  case "S":
4247
3774
  out.severity = value;
@@ -4266,10 +3793,10 @@ function parseErrorFields(payload) {
4266
3793
  }
4267
3794
  return out;
4268
3795
  }
4269
- var _decoder2 = new TextDecoder();
4270
- var _encoder2 = new TextEncoder();
3796
+ var _decoder = new TextDecoder();
3797
+ var _encoder = new TextEncoder();
4271
3798
  function utf8(s) {
4272
- return _encoder2.encode(s);
3799
+ return _encoder.encode(s);
4273
3800
  }
4274
3801
  function concat(...parts) {
4275
3802
  let total = 0;
@@ -4315,7 +3842,7 @@ var PgConnection = class _PgConnection {
4315
3842
  connect() {
4316
3843
  return new Promise((resolve3, reject) => {
4317
3844
  this.status = "connecting";
4318
- const sock = net3.connect(this.opts.port, this.opts.host);
3845
+ const sock = net2.connect(this.opts.port, this.opts.host);
4319
3846
  this.socket = sock;
4320
3847
  const timeout = setTimeout(() => {
4321
3848
  sock.destroy();
@@ -5486,151 +5013,668 @@ function createQueryBuilder(sql, exec) {
5486
5013
  const [t, a] = table2.split(/\s+/);
5487
5014
  return mkSelect(t, a);
5488
5015
  }
5489
- return mkSelect(table2, alias);
5490
- },
5491
- insert: (table2) => mkInsert(table2),
5492
- update: (table2) => mkUpdate(table2),
5493
- delete: (table2) => mkDelete(table2)
5494
- };
5495
- }
5496
-
5497
- // src/postgres/client.ts
5498
- var MIGRATIONS_TABLE = "_weifuwu_migrations";
5499
- var traceStore = new AsyncLocalStorage();
5500
- function postgres(options) {
5501
- const opts = typeof options === "string" ? { connection: options } : options ?? {};
5502
- const connection = opts.connection ?? process.env.DATABASE_URL;
5503
- if (!connection) {
5504
- throw new Error(
5505
- "postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
5016
+ return mkSelect(table2, alias);
5017
+ },
5018
+ insert: (table2) => mkInsert(table2),
5019
+ update: (table2) => mkUpdate(table2),
5020
+ delete: (table2) => mkDelete(table2)
5021
+ };
5022
+ }
5023
+
5024
+ // src/postgres/client.ts
5025
+ var MIGRATIONS_TABLE = "_weifuwu_migrations";
5026
+ var traceStore = new AsyncLocalStorage();
5027
+ function postgres(options) {
5028
+ const opts = typeof options === "string" ? { connection: options } : options ?? {};
5029
+ const connection = opts.connection ?? process.env.DATABASE_URL;
5030
+ if (!connection) {
5031
+ throw new Error(
5032
+ "postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
5033
+ );
5034
+ }
5035
+ const u = new URL(connection);
5036
+ const pool = new PgPool({
5037
+ host: u.hostname,
5038
+ port: Number(u.port || 5432),
5039
+ user: decodeURIComponent(u.username),
5040
+ password: decodeURIComponent(u.password),
5041
+ database: u.pathname.replace(/^\//, ""),
5042
+ poolSize: opts.max ?? opts.poolSize ?? 10,
5043
+ acquireTimeoutMs: opts.acquireTimeoutMs,
5044
+ statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
5045
+ // onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
5046
+ onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
5047
+ const tid = traceStore.getStore();
5048
+ opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
5049
+ } : void 0
5050
+ });
5051
+ const sql = makeSql(pool);
5052
+ const mw = ((req, ctx, next) => {
5053
+ ctx.sql = sql;
5054
+ return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
5055
+ });
5056
+ mw.__meta = { injects: ["sql"], depends: [] };
5057
+ mw.sql = sql;
5058
+ mw.migrate = async () => {
5059
+ await sql.unsafe(`
5060
+ CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
5061
+ name TEXT PRIMARY KEY,
5062
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
5063
+ )
5064
+ `);
5065
+ };
5066
+ mw.markMigrated = async (moduleName) => {
5067
+ await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
5068
+ moduleName
5069
+ ]);
5070
+ };
5071
+ mw.isMigrated = async (moduleName) => {
5072
+ const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
5073
+ return rows.length > 0;
5074
+ };
5075
+ mw.transaction = ((fn) => pool.begin(fn));
5076
+ mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
5077
+ mw.close = () => pool.close();
5078
+ return mw;
5079
+ }
5080
+ var TaggedQuery = class {
5081
+ text;
5082
+ params;
5083
+ executor;
5084
+ constructor(text, params, executor) {
5085
+ this.text = text;
5086
+ this.params = params;
5087
+ this.executor = executor;
5088
+ }
5089
+ then(resolve3, reject) {
5090
+ return this.executor(this.text, this.params).then(resolve3, reject);
5091
+ }
5092
+ catch(reject) {
5093
+ return this.executor(this.text, this.params).catch(reject);
5094
+ }
5095
+ finally(fn) {
5096
+ return this.executor(this.text, this.params).finally(fn);
5097
+ }
5098
+ /** 嵌套片段(agent-platform 条件过滤模式) */
5099
+ get __fragment() {
5100
+ return { sql: this.text, params: this.params };
5101
+ }
5102
+ };
5103
+ function makeSql(pool) {
5104
+ const sql = ((strings, ...values) => {
5105
+ const { sql: text, params } = parseTaggedFromPool(strings, values);
5106
+ return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
5107
+ });
5108
+ sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
5109
+ sql.query = createQueryBuilder(sql, async (q) => {
5110
+ const { sql: text, params: p } = compileQuery(q);
5111
+ return wrapError(pool.query(text, p));
5112
+ });
5113
+ sql.raw = (strings, ...values) => rawSql(
5114
+ strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), ""),
5115
+ values
5116
+ );
5117
+ sql.close = () => pool.close();
5118
+ return sql;
5119
+ }
5120
+ function parseTaggedFromPool(strings, values) {
5121
+ let sql = strings[0];
5122
+ const params = [];
5123
+ for (let i = 0; i < values.length; i++) {
5124
+ const frag = values[i]?.__fragment;
5125
+ if (frag) {
5126
+ const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
5127
+ params.push(frag.params[parseInt(idx, 10) - 1]);
5128
+ return `$${params.length}`;
5129
+ });
5130
+ sql += renumbered + strings[i + 1];
5131
+ } else {
5132
+ params.push(values[i]);
5133
+ sql += `$${params.length}` + strings[i + 1];
5134
+ }
5135
+ }
5136
+ return { sql, params };
5137
+ }
5138
+ var PG_ERROR_MAP = {
5139
+ "23505": 409,
5140
+ // unique_violation
5141
+ "23503": 400,
5142
+ // foreign_key_violation
5143
+ "23502": 400,
5144
+ // not_null_violation
5145
+ "23514": 400,
5146
+ // check_violation
5147
+ "22P02": 400,
5148
+ // invalid_text_representation
5149
+ "22003": 400
5150
+ // numeric_value_out_of_range
5151
+ };
5152
+ function wrapError(promise) {
5153
+ return promise.catch((err) => {
5154
+ const code = err?.code;
5155
+ if (code && PG_ERROR_MAP[code]) {
5156
+ throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
5157
+ }
5158
+ throw err;
5159
+ });
5160
+ }
5161
+
5162
+ // src/db/redis/connection.ts
5163
+ import net3 from "node:net";
5164
+
5165
+ // src/db/redis/resp.ts
5166
+ var RespError = class extends DbError {
5167
+ constructor(message) {
5168
+ super("protocol", message, { code: "RESP" });
5169
+ this.name = "RespError";
5170
+ }
5171
+ };
5172
+ var IncompleteError = class extends Error {
5173
+ constructor() {
5174
+ super("incomplete RESP message");
5175
+ this.name = "IncompleteError";
5176
+ }
5177
+ };
5178
+ function decodeValue(v, decodeBytes) {
5179
+ if (v instanceof Uint8Array) return decodeBytes ? _decoder2.decode(v) : v;
5180
+ if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
5181
+ return v;
5182
+ }
5183
+ function encodeCommand(args) {
5184
+ const lens = new Array(args.length);
5185
+ let total = headerLen(args.length);
5186
+ for (let i = 0; i < args.length; i++) {
5187
+ const arg = args[i];
5188
+ const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
5189
+ lens[i] = len;
5190
+ total += headerLen(len) + len + 2;
5191
+ }
5192
+ const out = new Uint8Array(total);
5193
+ let off = 0;
5194
+ const head = `*${args.length}\r
5195
+ `;
5196
+ out.set(_encoder2.encode(head), 0);
5197
+ off = head.length;
5198
+ for (let i = 0; i < args.length; i++) {
5199
+ const arg = args[i];
5200
+ const len = lens[i];
5201
+ const lh = `$${len}\r
5202
+ `;
5203
+ out.set(_encoder2.encode(lh), off);
5204
+ off += lh.length;
5205
+ if (typeof arg === "string") {
5206
+ out.set(_encoder2.encode(arg), off);
5207
+ } else if (arg instanceof Buffer) {
5208
+ out.set(arg, off);
5209
+ } else {
5210
+ out.set(_encoder2.encode(String(arg)), off);
5211
+ }
5212
+ off += len;
5213
+ out[off] = 13;
5214
+ out[off + 1] = 10;
5215
+ off += 2;
5216
+ }
5217
+ return out;
5218
+ }
5219
+ function headerLen(n) {
5220
+ if (n < 10) return 4;
5221
+ if (n < 100) return 5;
5222
+ if (n < 1e3) return 6;
5223
+ if (n < 1e4) return 7;
5224
+ return 8;
5225
+ }
5226
+ var _decoder2 = new TextDecoder();
5227
+ var _encoder2 = new TextEncoder();
5228
+ var RespParser = class {
5229
+ buf = new Uint8Array(0);
5230
+ off = 0;
5231
+ /** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
5232
+ push(chunk) {
5233
+ this.append(chunk);
5234
+ const saved = this.off;
5235
+ try {
5236
+ const value = this.parseValue();
5237
+ this.compact();
5238
+ return { value, incomplete: false };
5239
+ } catch (e) {
5240
+ this.off = saved;
5241
+ if (e instanceof IncompleteError) return { value: null, incomplete: true };
5242
+ throw e;
5243
+ }
5244
+ }
5245
+ /** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
5246
+ pushAll(chunk) {
5247
+ this.append(chunk);
5248
+ const out = [];
5249
+ while (true) {
5250
+ const saved = this.off;
5251
+ try {
5252
+ out.push(this.parseValue());
5253
+ } catch (e) {
5254
+ this.off = saved;
5255
+ if (e instanceof IncompleteError) break;
5256
+ throw e;
5257
+ }
5258
+ }
5259
+ if (out.length > 0) this.compact();
5260
+ return out;
5261
+ }
5262
+ /** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
5263
+ append(chunk) {
5264
+ if (chunk.length === 0) return;
5265
+ const rest = this.buf.length - this.off;
5266
+ if (rest === 0) {
5267
+ this.buf = chunk;
5268
+ this.off = 0;
5269
+ return;
5270
+ }
5271
+ const merged = new Uint8Array(rest + chunk.length);
5272
+ merged.set(this.buf.subarray(this.off), 0);
5273
+ merged.set(chunk, rest);
5274
+ this.buf = merged;
5275
+ this.off = 0;
5276
+ }
5277
+ /** 全部消费后压缩(释放底层) */
5278
+ compact() {
5279
+ if (this.off === this.buf.length) {
5280
+ this.buf = new Uint8Array(0);
5281
+ this.off = 0;
5282
+ } else if (this.off > 0) {
5283
+ this.buf = this.buf.subarray(this.off);
5284
+ this.off = 0;
5285
+ }
5286
+ }
5287
+ parseValue() {
5288
+ if (this.buf.length - this.off < 3) throw new IncompleteError();
5289
+ const type = String.fromCharCode(this.buf[this.off]);
5290
+ this.off++;
5291
+ switch (type) {
5292
+ case "+": {
5293
+ const line = this.readLine();
5294
+ return line;
5295
+ }
5296
+ case "-": {
5297
+ const line = this.readLine();
5298
+ return new RespError(line);
5299
+ }
5300
+ case ":": {
5301
+ return this.readInt();
5302
+ }
5303
+ case "$": {
5304
+ const len = this.readInt();
5305
+ if (len === -1) return null;
5306
+ return this.readBulkBytes(len);
5307
+ }
5308
+ case "*": {
5309
+ const count = this.readInt();
5310
+ if (count === -1) return null;
5311
+ const items = [];
5312
+ for (let i = 0; i < count; i++) items.push(this.parseValue());
5313
+ return items;
5314
+ }
5315
+ default:
5316
+ throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
5317
+ }
5318
+ }
5319
+ /**
5320
+ * 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
5321
+ * 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
5322
+ */
5323
+ readInt() {
5324
+ const buf = this.buf;
5325
+ let i = this.off;
5326
+ let neg = false;
5327
+ if (i < buf.length && buf[i] === 45) {
5328
+ neg = true;
5329
+ i++;
5330
+ }
5331
+ let n = 0;
5332
+ while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
5333
+ n = n * 10 + (buf[i] - 48);
5334
+ i++;
5335
+ }
5336
+ if (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
5337
+ this.off = i + 2;
5338
+ return neg ? -n : n;
5339
+ }
5340
+ throw new IncompleteError();
5341
+ }
5342
+ /** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
5343
+ readLine() {
5344
+ const idx = indexOfCRLF(this.buf, this.off);
5345
+ if (idx === -1) throw new IncompleteError();
5346
+ const line = _decoder2.decode(this.buf.subarray(this.off, idx));
5347
+ this.off = idx + 2;
5348
+ return line;
5349
+ }
5350
+ /** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
5351
+ readBulkBytes(len) {
5352
+ if (this.buf.length - this.off < len + 2) throw new IncompleteError();
5353
+ const value = this.buf.subarray(this.off, this.off + len);
5354
+ this.off += len + 2;
5355
+ return value;
5356
+ }
5357
+ };
5358
+ function indexOfCRLF(buf, from = 0) {
5359
+ let i = buf.indexOf(13, from);
5360
+ while (i !== -1) {
5361
+ if (i + 1 < buf.length && buf[i + 1] === 10) return i;
5362
+ i = buf.indexOf(13, i + 1);
5363
+ }
5364
+ return -1;
5365
+ }
5366
+
5367
+ // src/db/redis/connection.ts
5368
+ var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
5369
+ var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
5370
+ var RedisConnection = class {
5371
+ ready = false;
5372
+ opts;
5373
+ socket = null;
5374
+ parser = new RespParser();
5375
+ pending = [];
5376
+ /** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
5377
+ pendingHead = 0;
5378
+ offlineQueue = [];
5379
+ subs = /* @__PURE__ */ new Map();
5380
+ psubs = /* @__PURE__ */ new Map();
5381
+ status = "idle";
5382
+ retries = 0;
5383
+ reconnectTimer = null;
5384
+ socketTimeoutTimer = null;
5385
+ connectPromise = null;
5386
+ closed = false;
5387
+ connectedOnce = false;
5388
+ constructor(options = {}) {
5389
+ this.opts = {
5390
+ host: options.host ?? "127.0.0.1",
5391
+ port: options.port ?? 6379,
5392
+ retryDelayMs: options.retryDelayMs ?? 100,
5393
+ maxRetries: options.maxRetries ?? 10,
5394
+ enableOfflineQueue: options.enableOfflineQueue ?? true,
5395
+ maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
5396
+ commandTimeoutMs: options.commandTimeoutMs ?? 0,
5397
+ socketTimeoutMs: options.socketTimeoutMs ?? 0,
5398
+ onCommand: options.onCommand
5399
+ };
5400
+ }
5401
+ /** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
5402
+ connect() {
5403
+ if (this.connectPromise) return this.connectPromise;
5404
+ this.closed = false;
5405
+ this.connectPromise = new Promise((resolve3, reject) => {
5406
+ this.openSocket();
5407
+ this.onceReady = () => resolve3();
5408
+ this.onceFailed = (err) => reject(err);
5409
+ }).finally(() => {
5410
+ this.connectPromise = null;
5411
+ this.onceReady = void 0;
5412
+ this.onceFailed = void 0;
5413
+ });
5414
+ return this.connectPromise;
5415
+ }
5416
+ onceReady;
5417
+ onceFailed;
5418
+ openSocket() {
5419
+ this.status = "connecting";
5420
+ const sock = net3.connect(this.opts.port, this.opts.host);
5421
+ this.socket = sock;
5422
+ sock.on("connect", () => {
5423
+ sock.setNoDelay(true);
5424
+ this.status = "ready";
5425
+ this.retries = 0;
5426
+ if (this.connectedOnce) {
5427
+ for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
5428
+ for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
5429
+ }
5430
+ this.connectedOnce = true;
5431
+ this.flushOffline();
5432
+ this.onceReady?.();
5433
+ });
5434
+ sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
5435
+ sock.on("error", (err) => {
5436
+ if (this.status === "connecting") {
5437
+ this.handleDisconnect(err);
5438
+ }
5439
+ });
5440
+ sock.on("close", () => {
5441
+ this.socket = null;
5442
+ if (this.status !== "closed" && this.status !== "idle") {
5443
+ this.handleDisconnect(new ConnectionError("redis: socket closed"));
5444
+ }
5445
+ });
5446
+ }
5447
+ handleDisconnect(err) {
5448
+ const queue2 = this.pending.slice(this.pendingHead);
5449
+ this.pending = [];
5450
+ this.pendingHead = 0;
5451
+ for (const p of queue2) p.reject(err);
5452
+ this.clearSocketTimeout();
5453
+ if (this.closed || this.status === "closed") return;
5454
+ this.retries++;
5455
+ this.status = "connecting";
5456
+ if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
5457
+ this.status = "closed";
5458
+ const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
5459
+ this.onceFailed?.(failErr);
5460
+ return;
5461
+ }
5462
+ const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
5463
+ this.reconnectTimer = setTimeout(() => {
5464
+ this.reconnectTimer = null;
5465
+ this.openSocket();
5466
+ }, delay);
5467
+ }
5468
+ onData(chunk) {
5469
+ try {
5470
+ const values = this.parser.pushAll(chunk);
5471
+ for (const raw of values) {
5472
+ const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
5473
+ if (typeof first === "string" && PUSH_TYPES.has(first)) {
5474
+ this.dispatchSubscribe(decodeValue(raw, true));
5475
+ continue;
5476
+ }
5477
+ const p = this.pending[this.pendingHead++];
5478
+ if (!p) break;
5479
+ if (p.timedOut) continue;
5480
+ if (raw instanceof RespError) p.reject(raw);
5481
+ else p.resolve(decodeValue(raw, !p.asBuffer));
5482
+ }
5483
+ if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
5484
+ this.pending = this.pending.slice(this.pendingHead);
5485
+ this.pendingHead = 0;
5486
+ }
5487
+ if (this.opts.socketTimeoutMs > 0) {
5488
+ if (this.pendingHead < this.pending.length) this.armSocketTimeout();
5489
+ else this.clearSocketTimeout();
5490
+ }
5491
+ } catch (e) {
5492
+ const queue2 = this.pending.slice(this.pendingHead);
5493
+ this.pending = [];
5494
+ this.pendingHead = 0;
5495
+ for (const q of queue2) q.reject(e);
5496
+ this.socket?.destroy();
5497
+ }
5498
+ }
5499
+ /**
5500
+ * 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
5501
+ * opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
5502
+ */
5503
+ command(name, ...args) {
5504
+ const last = args[args.length - 1];
5505
+ const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
5506
+ if (this.status === "ready" && this.socket && !this.socket.destroyed) {
5507
+ return this.sendNow(name, args, opts?.asBuffer);
5508
+ }
5509
+ if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
5510
+ return Promise.reject(new ConnectionError("redis: not connected"));
5511
+ }
5512
+ if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
5513
+ return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
5514
+ }
5515
+ return new Promise((resolve3, reject) => {
5516
+ this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
5517
+ });
5518
+ }
5519
+ sendNow(name, args, asBuffer) {
5520
+ const start = performance.now();
5521
+ return new Promise((resolve3, reject) => {
5522
+ const p = {
5523
+ resolve: resolve3,
5524
+ reject,
5525
+ asBuffer,
5526
+ blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
5527
+ };
5528
+ this.armTimeout(p);
5529
+ this.pending.push(p);
5530
+ this.armSocketTimeout();
5531
+ this.socket.write(encodeCommand([name, ...args]), (err) => {
5532
+ if (err) {
5533
+ const idx = this.pending.indexOf(p);
5534
+ if (idx >= 0) this.pending.splice(idx, 1);
5535
+ p.reject(err instanceof Error ? err : new ConnectionError("redis: socket write failed"));
5536
+ }
5537
+ });
5538
+ }).then(
5539
+ (v) => {
5540
+ if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
5541
+ return v;
5542
+ },
5543
+ (e) => {
5544
+ if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
5545
+ throw e;
5546
+ }
5506
5547
  );
5507
5548
  }
5508
- const u = new URL(connection);
5509
- const pool = new PgPool({
5510
- host: u.hostname,
5511
- port: Number(u.port || 5432),
5512
- user: decodeURIComponent(u.username),
5513
- password: decodeURIComponent(u.password),
5514
- database: u.pathname.replace(/^\//, ""),
5515
- poolSize: opts.max ?? opts.poolSize ?? 10,
5516
- acquireTimeoutMs: opts.acquireTimeoutMs,
5517
- statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
5518
- // onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
5519
- onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
5520
- const tid = traceStore.getStore();
5521
- opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
5522
- } : void 0
5523
- });
5524
- const sql = makeSql(pool);
5525
- const mw = ((req, ctx, next) => {
5526
- ctx.sql = sql;
5527
- return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
5528
- });
5529
- mw.__meta = { injects: ["sql"], depends: [] };
5530
- mw.sql = sql;
5531
- mw.migrate = async () => {
5532
- await sql.unsafe(`
5533
- CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
5534
- name TEXT PRIMARY KEY,
5535
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
5536
- )
5537
- `);
5538
- };
5539
- mw.markMigrated = async (moduleName) => {
5540
- await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
5541
- moduleName
5542
- ]);
5543
- };
5544
- mw.isMigrated = async (moduleName) => {
5545
- const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
5546
- return rows.length > 0;
5547
- };
5548
- mw.transaction = ((fn) => pool.begin(fn));
5549
- mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
5550
- mw.close = () => pool.close();
5551
- return mw;
5552
- }
5553
- var TaggedQuery = class {
5554
- text;
5555
- params;
5556
- executor;
5557
- constructor(text, params, executor) {
5558
- this.text = text;
5559
- this.params = params;
5560
- this.executor = executor;
5549
+ /** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
5550
+ armTimeout(p) {
5551
+ const ms = this.opts.commandTimeoutMs;
5552
+ if (ms <= 0) return;
5553
+ p.timer = setTimeout(() => {
5554
+ p.timedOut = true;
5555
+ if (p.blocking) p.resolve(null);
5556
+ else p.reject(new TimeoutError("redis: command timeout", ms));
5557
+ }, ms);
5561
5558
  }
5562
- then(resolve3, reject) {
5563
- return this.executor(this.text, this.params).then(resolve3, reject);
5559
+ /** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
5560
+ armSocketTimeout() {
5561
+ const ms = this.opts.socketTimeoutMs;
5562
+ if (ms <= 0) return;
5563
+ this.clearSocketTimeout();
5564
+ this.socketTimeoutTimer = setTimeout(() => {
5565
+ this.socketTimeoutTimer = null;
5566
+ const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
5567
+ const queue2 = this.pending.slice(this.pendingHead);
5568
+ this.pending = [];
5569
+ this.pendingHead = 0;
5570
+ for (const q of queue2) q.reject(err);
5571
+ this.clearSocketTimeout();
5572
+ this.socket?.destroy();
5573
+ }, ms);
5564
5574
  }
5565
- catch(reject) {
5566
- return this.executor(this.text, this.params).catch(reject);
5575
+ clearSocketTimeout() {
5576
+ if (this.socketTimeoutTimer) {
5577
+ clearTimeout(this.socketTimeoutTimer);
5578
+ this.socketTimeoutTimer = null;
5579
+ }
5567
5580
  }
5568
- finally(fn) {
5569
- return this.executor(this.text, this.params).finally(fn);
5581
+ flushOffline() {
5582
+ const queue2 = this.offlineQueue;
5583
+ this.offlineQueue = [];
5584
+ for (const q of queue2) {
5585
+ this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
5586
+ }
5570
5587
  }
5571
- /** 嵌套片段(agent-platform 条件过滤模式) */
5572
- get __fragment() {
5573
- return { sql: this.text, params: this.params };
5588
+ /** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
5589
+ batch(payload, count) {
5590
+ if (this.status !== "ready" || !this.socket) {
5591
+ return Promise.reject(new ConnectionError("redis: not connected"));
5592
+ }
5593
+ return new Promise((resolve3, reject) => {
5594
+ const results = [];
5595
+ const batchPending = [];
5596
+ let settled = false;
5597
+ const maybeResolve = () => {
5598
+ if (settled) return;
5599
+ if (results.length === count) {
5600
+ settled = true;
5601
+ if (batchTimer) clearTimeout(batchTimer);
5602
+ resolve3(results);
5603
+ }
5604
+ };
5605
+ for (let i = 0; i < count; i++) {
5606
+ const p = {
5607
+ // 正常响应与错误响应(RespError)都作为结果值收集——管道语义
5608
+ resolve: (v) => {
5609
+ results.push(v);
5610
+ maybeResolve();
5611
+ },
5612
+ reject: (e) => {
5613
+ results.push(e);
5614
+ maybeResolve();
5615
+ }
5616
+ };
5617
+ batchPending.push(p);
5618
+ this.pending.push(p);
5619
+ }
5620
+ let batchTimer;
5621
+ const ms = this.opts.commandTimeoutMs;
5622
+ if (ms > 0) {
5623
+ batchTimer = setTimeout(() => {
5624
+ settled = true;
5625
+ for (const p of batchPending) p.timedOut = true;
5626
+ reject(new TimeoutError("redis: batch timeout", ms));
5627
+ }, ms);
5628
+ }
5629
+ this.socket.write(payload);
5630
+ this.armSocketTimeout();
5631
+ });
5574
5632
  }
5575
- };
5576
- function makeSql(pool) {
5577
- const sql = ((strings, ...values) => {
5578
- const { sql: text, params } = parseTaggedFromPool(strings, values);
5579
- return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
5580
- });
5581
- sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
5582
- sql.query = createQueryBuilder(sql, async (q) => {
5583
- const { sql: text, params: p } = compileQuery(q);
5584
- return wrapError(pool.query(text, p));
5585
- });
5586
- sql.raw = (strings, ...values) => rawSql(
5587
- strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), ""),
5588
- values
5589
- );
5590
- sql.close = () => pool.close();
5591
- return sql;
5592
- }
5593
- function parseTaggedFromPool(strings, values) {
5594
- let sql = strings[0];
5595
- const params = [];
5596
- for (let i = 0; i < values.length; i++) {
5597
- const frag = values[i]?.__fragment;
5598
- if (frag) {
5599
- const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
5600
- params.push(frag.params[parseInt(idx, 10) - 1]);
5601
- return `$${params.length}`;
5602
- });
5603
- sql += renumbered + strings[i + 1];
5604
- } else {
5605
- params.push(values[i]);
5606
- sql += `$${params.length}` + strings[i + 1];
5633
+ /** 订阅频道:回调式(channel, message) */
5634
+ async subscribe(channel, fn) {
5635
+ this.subs.set(channel, fn);
5636
+ await this.command("SUBSCRIBE", channel);
5637
+ }
5638
+ /** 订阅模式:回调式(channel, message) */
5639
+ async psubscribe(pattern, fn) {
5640
+ this.psubs.set(pattern, fn);
5641
+ await this.command("PSUBSCRIBE", pattern);
5642
+ }
5643
+ /** 旁路分发订阅消息(RESP 数组路由到回调) */
5644
+ dispatchSubscribe(value) {
5645
+ const [type, a, b] = value;
5646
+ if (type === "message") {
5647
+ const fn = this.subs.get(a);
5648
+ fn?.(a, b);
5649
+ } else if (type === "pmessage") {
5650
+ const fn = this.psubs.get(a);
5651
+ fn?.(b, value[3] ?? "");
5607
5652
  }
5608
5653
  }
5609
- return { sql, params };
5610
- }
5611
- var PG_ERROR_MAP = {
5612
- "23505": 409,
5613
- // unique_violation
5614
- "23503": 400,
5615
- // foreign_key_violation
5616
- "23502": 400,
5617
- // not_null_violation
5618
- "23514": 400,
5619
- // check_violation
5620
- "22P02": 400,
5621
- // invalid_text_representation
5622
- "22003": 400
5623
- // numeric_value_out_of_range
5624
- };
5625
- function wrapError(promise) {
5626
- return promise.catch((err) => {
5627
- const code = err?.code;
5628
- if (code && PG_ERROR_MAP[code]) {
5629
- throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
5654
+ /** 主动关闭——不再重连 */
5655
+ async close() {
5656
+ this.closed = true;
5657
+ this.status = "closed";
5658
+ if (this.reconnectTimer) {
5659
+ clearTimeout(this.reconnectTimer);
5660
+ this.reconnectTimer = null;
5630
5661
  }
5631
- throw err;
5632
- });
5633
- }
5662
+ this.clearSocketTimeout();
5663
+ const queue2 = this.pending.slice(this.pendingHead);
5664
+ this.pending = [];
5665
+ this.pendingHead = 0;
5666
+ for (const p of queue2) p.reject(new ConnectionError("redis: connection closed"));
5667
+ const oq = this.offlineQueue;
5668
+ this.offlineQueue = [];
5669
+ for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
5670
+ const sock = this.socket;
5671
+ this.socket = null;
5672
+ sock?.destroy();
5673
+ }
5674
+ get connected() {
5675
+ return this.status === "ready";
5676
+ }
5677
+ };
5634
5678
 
5635
5679
  // src/db/redis/pipeline.ts
5636
5680
  var RedisPipeline = class {