weifuwu 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -53
- package/dist/ai/types.d.ts +2 -0
- package/dist/components/AiChat/AiChat.d.ts +5 -1
- package/dist/components/ApprovalCard/ApprovalCard.d.ts +5 -2
- package/dist/components/CitationCard/CitationCard.d.ts +38 -0
- package/dist/components/Command/Command.d.ts +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/components/JsonSchemaForm/JsonSchemaForm.d.ts +48 -0
- package/dist/components/ReasoningBlock/ReasoningBlock.d.ts +25 -0
- package/dist/components/Select/Select.d.ts +11 -1
- package/dist/components/SessionList/SessionList.d.ts +42 -0
- package/dist/components/Tour/Tour.d.ts +1 -1
- package/dist/components/TreeSelect/TreeSelect.d.ts +3 -2
- package/dist/components/index.d.ts +10 -2
- package/dist/components/index.js +14 -14
- package/dist/components/style.css +625 -20
- package/dist/index.js +1349 -1529
- package/dist/layout/weifuwu-layout.css +9 -0
- package/dist/scheduler/index.d.ts +7 -2
- package/dist/test/client/setup.d.ts +12 -0
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui-dom/Confirm.d.ts +5 -41
- package/dist/ui-dom/Notification.d.ts +8 -70
- package/dist/ui-dom/Toast.d.ts +8 -43
- package/dist/ui-dom/hooks/chat.d.ts +11 -0
- package/dist/ui-dom/hooks/events.d.ts +30 -0
- package/dist/ui-dom/hooks/external.d.ts +20 -0
- package/dist/ui-dom/hooks/index.d.ts +30 -0
- package/dist/ui-dom/hooks/input.d.ts +33 -0
- package/dist/ui-dom/hooks/media.d.ts +17 -0
- package/dist/ui-dom/hooks/popup.d.ts +28 -0
- package/dist/ui-dom/hooks/stable.d.ts +39 -0
- package/dist/ui-dom/hooks/test/hooks.test.d.ts +1 -0
- package/dist/ui-dom/hooks/types.d.ts +77 -0
- package/dist/ui-dom/index.d.ts +19 -17
- package/dist/ui-dom/index.js +11 -11
- package/dist/ui-dom/jsx-runtime.js +1 -1
- package/dist/ui-dom/store.d.ts +24 -0
- package/dist/ui-dom/testing.d.ts +74 -0
- package/dist/ui-dom/testing.js +1 -0
- package/dist/ui-dom/testing.test.d.ts +1 -0
- package/dist/ui-dom/types.d.ts +55 -50
- package/dist/ui-dom/use-chat.d.ts +19 -14
- package/dist/ui-dom/vdom/build.d.ts +41 -0
- package/dist/ui-dom/vdom/diff.d.ts +33 -0
- package/dist/ui-dom/vdom/hydration.d.ts +17 -0
- package/dist/ui-dom/vdom/index.d.ts +23 -0
- package/dist/ui-dom/vdom/mount.d.ts +58 -0
- package/dist/ui-dom/vdom/registry.d.ts +27 -0
- package/dist/ui-dom/vdom/render.d.ts +15 -0
- package/dist/ui-dom/vdom/serve.d.ts +30 -0
- package/dist/ui-dom/vdom/ssr.d.ts +44 -0
- package/dist/ui-dom/vnode.d.ts +19 -31
- package/docs/components-map.md +18 -1
- package/docs/components.md +12 -7
- package/docs/custom-components.md +90 -60
- package/docs/examples.md +35 -41
- package/docs/frontend-middleware.md +3 -4
- package/docs/frontend-ui-dom.md +21 -18
- package/docs/frontend.md +243 -168
- package/docs/layout.md +2 -2
- package/docs/mobile.md +2 -2
- package/docs/realtime.md +8 -3
- package/package.json +5 -1
- package/dist/ui/ssr.d.ts +0 -35
- package/dist/ui-dom/diff.d.ts +0 -28
- package/dist/ui-dom/focus-trap.d.ts +0 -4
- package/dist/ui-dom/hydration.d.ts +0 -13
- package/dist/ui-dom/reactive.d.ts +0 -6
- package/dist/ui-dom/registry.d.ts +0 -27
- package/dist/ui-dom/render.d.ts +0 -46
- package/dist/ui-dom/scroll-lock.d.ts +0 -5
- package/dist/ui-dom/serve.d.ts +0 -28
- package/dist/ui-dom/ssr.d.ts +0 -57
- package/dist/ui-dom/ui.d.ts +0 -78
package/dist/index.js
CHANGED
|
@@ -2315,1140 +2315,577 @@ function isConnClosed(e) {
|
|
|
2315
2315
|
// src/scheduler/index.ts
|
|
2316
2316
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2317
2317
|
|
|
2318
|
-
// src/
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
//
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
super("connection", message, { cause });
|
|
2344
|
-
this.name = "ConnectionError";
|
|
2345
|
-
this.attempts = attempts;
|
|
2346
|
-
}
|
|
2347
|
-
};
|
|
2348
|
-
var TimeoutError = class extends DbError {
|
|
2349
|
-
operation;
|
|
2350
|
-
ms;
|
|
2351
|
-
constructor(operation, ms) {
|
|
2352
|
-
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
2353
|
-
this.name = "TimeoutError";
|
|
2354
|
-
this.operation = operation;
|
|
2355
|
-
this.ms = ms;
|
|
2356
|
-
}
|
|
2357
|
-
};
|
|
2358
|
-
var ValidationError = class extends DbError {
|
|
2359
|
-
constructor(message) {
|
|
2360
|
-
super("validation", message);
|
|
2361
|
-
this.name = "ValidationError";
|
|
2362
|
-
}
|
|
2363
|
-
};
|
|
2364
|
-
|
|
2365
|
-
// src/db/redis/resp.ts
|
|
2366
|
-
var RespError = class extends DbError {
|
|
2367
|
-
constructor(message) {
|
|
2368
|
-
super("protocol", message, { code: "RESP" });
|
|
2369
|
-
this.name = "RespError";
|
|
2370
|
-
}
|
|
2371
|
-
};
|
|
2372
|
-
var IncompleteError = class extends Error {
|
|
2373
|
-
constructor() {
|
|
2374
|
-
super("incomplete RESP message");
|
|
2375
|
-
this.name = "IncompleteError";
|
|
2376
|
-
}
|
|
2377
|
-
};
|
|
2378
|
-
function decodeValue(v, decodeBytes) {
|
|
2379
|
-
if (v instanceof Uint8Array) return decodeBytes ? _decoder.decode(v) : v;
|
|
2380
|
-
if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
|
|
2381
|
-
return v;
|
|
2382
|
-
}
|
|
2383
|
-
function encodeCommand(args) {
|
|
2384
|
-
const lens = new Array(args.length);
|
|
2385
|
-
let total = headerLen(args.length);
|
|
2386
|
-
for (let i = 0; i < args.length; i++) {
|
|
2387
|
-
const arg = args[i];
|
|
2388
|
-
const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
|
|
2389
|
-
lens[i] = len;
|
|
2390
|
-
total += headerLen(len) + len + 2;
|
|
2318
|
+
// src/scheduler/cron.ts
|
|
2319
|
+
var RANGES = [
|
|
2320
|
+
[0, 59],
|
|
2321
|
+
// 分
|
|
2322
|
+
[0, 23],
|
|
2323
|
+
// 时
|
|
2324
|
+
[1, 31],
|
|
2325
|
+
// 日
|
|
2326
|
+
[1, 12],
|
|
2327
|
+
// 月
|
|
2328
|
+
[0, 6]
|
|
2329
|
+
// 周
|
|
2330
|
+
];
|
|
2331
|
+
function parseField(field, idx) {
|
|
2332
|
+
const [min, max] = RANGES[idx];
|
|
2333
|
+
const name = ["minute", "hour", "day", "month", "weekday"][idx];
|
|
2334
|
+
if (field === "*") return null;
|
|
2335
|
+
if (field.startsWith("*/")) {
|
|
2336
|
+
const n = Number(field.slice(2));
|
|
2337
|
+
if (!Number.isInteger(n) || n <= 0 || n > max) {
|
|
2338
|
+
throw new Error(`cron: invalid step '${field}' in ${name} field (must be 1..${max})`);
|
|
2339
|
+
}
|
|
2340
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
2341
|
+
for (let v = min; v <= max; v += n) set2.add(v);
|
|
2342
|
+
return set2;
|
|
2391
2343
|
}
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
off += lh.length;
|
|
2405
|
-
if (typeof arg === "string") {
|
|
2406
|
-
out.set(_encoder.encode(arg), off);
|
|
2407
|
-
} else if (arg instanceof Buffer) {
|
|
2408
|
-
out.set(arg, off);
|
|
2344
|
+
const set = /* @__PURE__ */ new Set();
|
|
2345
|
+
for (const part of field.split(",")) {
|
|
2346
|
+
if (part === "") throw new Error(`cron: empty item in '${field}' (${name})`);
|
|
2347
|
+
if (part.includes("-")) {
|
|
2348
|
+
const [rangePart, stepPart] = part.split("/");
|
|
2349
|
+
const step = stepPart !== void 0 ? Number(stepPart) : 1;
|
|
2350
|
+
const [a, b] = rangePart.split("-").map(Number);
|
|
2351
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a < min || b > max || a > b) {
|
|
2352
|
+
throw new Error(`cron: invalid range '${part}' in ${name} field (${min}..${max})`);
|
|
2353
|
+
}
|
|
2354
|
+
if (!Number.isInteger(step) || step <= 0) throw new Error(`cron: invalid step in '${part}'`);
|
|
2355
|
+
for (let v = a; v <= b; v += step) set.add(v);
|
|
2409
2356
|
} else {
|
|
2410
|
-
|
|
2357
|
+
const v = Number(part);
|
|
2358
|
+
if (!Number.isInteger(v) || v < min || v > max) {
|
|
2359
|
+
throw new Error(`cron: value '${part}' out of range ${min}..${max} in ${name} field`);
|
|
2360
|
+
}
|
|
2361
|
+
set.add(v);
|
|
2411
2362
|
}
|
|
2412
|
-
off += len;
|
|
2413
|
-
out[off] = 13;
|
|
2414
|
-
out[off + 1] = 10;
|
|
2415
|
-
off += 2;
|
|
2416
2363
|
}
|
|
2417
|
-
return
|
|
2364
|
+
return set;
|
|
2418
2365
|
}
|
|
2419
|
-
function
|
|
2420
|
-
|
|
2421
|
-
if (
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2366
|
+
function parseCron(expr) {
|
|
2367
|
+
const parts = expr.trim().split(/\s+/);
|
|
2368
|
+
if (parts.length !== 5) {
|
|
2369
|
+
throw new Error(`cron: expected 5 fields (min hour dom month dow), got ${parts.length}: '${expr}'`);
|
|
2370
|
+
}
|
|
2371
|
+
const fields = parts.map((p, i) => parseField(p, i));
|
|
2372
|
+
return {
|
|
2373
|
+
fields: fields.map((values) => ({ values })),
|
|
2374
|
+
domExplicit: fields[2] !== null,
|
|
2375
|
+
dowExplicit: fields[4] !== null
|
|
2376
|
+
};
|
|
2425
2377
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2378
|
+
function fieldMatches(values, v) {
|
|
2379
|
+
if (values === null) return true;
|
|
2380
|
+
return values.has(v);
|
|
2381
|
+
}
|
|
2382
|
+
function dayMatches(expr, dom, dow) {
|
|
2383
|
+
const domOk = fieldMatches(expr.fields[2].values, dom);
|
|
2384
|
+
const dowOk = fieldMatches(expr.fields[4].values, dow);
|
|
2385
|
+
if (expr.domExplicit && expr.dowExplicit) return domOk || dowOk;
|
|
2386
|
+
if (expr.domExplicit) return domOk;
|
|
2387
|
+
if (expr.dowExplicit) return dowOk;
|
|
2388
|
+
return true;
|
|
2389
|
+
}
|
|
2390
|
+
function nextMatch(values, v, min, max) {
|
|
2391
|
+
if (values === null) return v;
|
|
2392
|
+
for (let x = v; x <= max; x++) if (values.has(x)) return x;
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
function nextRun(expr, from) {
|
|
2396
|
+
const d = new Date(from.getTime());
|
|
2397
|
+
d.setSeconds(0, 0);
|
|
2398
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
2399
|
+
const deadline = from.getTime() + 5 * 366 * 24 * 3600 * 1e3;
|
|
2400
|
+
while (d.getTime() <= deadline) {
|
|
2401
|
+
const year = d.getFullYear();
|
|
2402
|
+
const month = d.getMonth() + 1;
|
|
2403
|
+
const dom = d.getDate();
|
|
2404
|
+
const dow = d.getDay();
|
|
2405
|
+
if (!fieldMatches(expr.fields[3].values, month)) {
|
|
2406
|
+
d.setMonth(month, 1);
|
|
2407
|
+
d.setHours(0, 0, 0, 0);
|
|
2408
|
+
d.setMonth(month);
|
|
2409
|
+
continue;
|
|
2443
2410
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
const out = [];
|
|
2449
|
-
while (true) {
|
|
2450
|
-
const saved = this.off;
|
|
2451
|
-
try {
|
|
2452
|
-
out.push(this.parseValue());
|
|
2453
|
-
} catch (e) {
|
|
2454
|
-
this.off = saved;
|
|
2455
|
-
if (e instanceof IncompleteError) break;
|
|
2456
|
-
throw e;
|
|
2457
|
-
}
|
|
2411
|
+
if (!dayMatches(expr, dom, dow)) {
|
|
2412
|
+
d.setDate(dom + 1);
|
|
2413
|
+
d.setHours(0, 0, 0, 0);
|
|
2414
|
+
continue;
|
|
2458
2415
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
const rest = this.buf.length - this.off;
|
|
2466
|
-
if (rest === 0) {
|
|
2467
|
-
this.buf = chunk;
|
|
2468
|
-
this.off = 0;
|
|
2469
|
-
return;
|
|
2416
|
+
const hour = d.getHours();
|
|
2417
|
+
const nextHour = nextMatch(expr.fields[1].values, hour, 0, 23);
|
|
2418
|
+
if (nextHour === null) {
|
|
2419
|
+
d.setDate(dom + 1);
|
|
2420
|
+
d.setHours(0, 0, 0, 0);
|
|
2421
|
+
continue;
|
|
2470
2422
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
this.buf = merged;
|
|
2475
|
-
this.off = 0;
|
|
2476
|
-
}
|
|
2477
|
-
/** 全部消费后压缩(释放底层) */
|
|
2478
|
-
compact() {
|
|
2479
|
-
if (this.off === this.buf.length) {
|
|
2480
|
-
this.buf = new Uint8Array(0);
|
|
2481
|
-
this.off = 0;
|
|
2482
|
-
} else if (this.off > 0) {
|
|
2483
|
-
this.buf = this.buf.subarray(this.off);
|
|
2484
|
-
this.off = 0;
|
|
2423
|
+
if (nextHour !== hour) {
|
|
2424
|
+
d.setHours(nextHour, 0, 0, 0);
|
|
2425
|
+
continue;
|
|
2485
2426
|
}
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
if (
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
switch (type) {
|
|
2492
|
-
case "+": {
|
|
2493
|
-
const line = this.readLine();
|
|
2494
|
-
return line;
|
|
2495
|
-
}
|
|
2496
|
-
case "-": {
|
|
2497
|
-
const line = this.readLine();
|
|
2498
|
-
return new RespError(line);
|
|
2499
|
-
}
|
|
2500
|
-
case ":": {
|
|
2501
|
-
return this.readInt();
|
|
2502
|
-
}
|
|
2503
|
-
case "$": {
|
|
2504
|
-
const len = this.readInt();
|
|
2505
|
-
if (len === -1) return null;
|
|
2506
|
-
return this.readBulkBytes(len);
|
|
2507
|
-
}
|
|
2508
|
-
case "*": {
|
|
2509
|
-
const count = this.readInt();
|
|
2510
|
-
if (count === -1) return null;
|
|
2511
|
-
const items = [];
|
|
2512
|
-
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
2513
|
-
return items;
|
|
2514
|
-
}
|
|
2515
|
-
default:
|
|
2516
|
-
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
/**
|
|
2520
|
-
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
2521
|
-
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
2522
|
-
*/
|
|
2523
|
-
readInt() {
|
|
2524
|
-
const buf = this.buf;
|
|
2525
|
-
let i = this.off;
|
|
2526
|
-
let neg = false;
|
|
2527
|
-
if (i < buf.length && buf[i] === 45) {
|
|
2528
|
-
neg = true;
|
|
2529
|
-
i++;
|
|
2530
|
-
}
|
|
2531
|
-
let n = 0;
|
|
2532
|
-
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
|
|
2533
|
-
n = n * 10 + (buf[i] - 48);
|
|
2534
|
-
i++;
|
|
2427
|
+
const minute = d.getMinutes();
|
|
2428
|
+
const nextMinute = nextMatch(expr.fields[0].values, minute, 0, 59);
|
|
2429
|
+
if (nextMinute === null) {
|
|
2430
|
+
d.setHours(hour + 1, 0, 0, 0);
|
|
2431
|
+
continue;
|
|
2535
2432
|
}
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2538
|
-
|
|
2433
|
+
if (nextMinute !== minute) {
|
|
2434
|
+
d.setMinutes(nextMinute, 0, 0);
|
|
2435
|
+
continue;
|
|
2539
2436
|
}
|
|
2540
|
-
|
|
2541
|
-
}
|
|
2542
|
-
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
2543
|
-
readLine() {
|
|
2544
|
-
const idx = indexOfCRLF(this.buf, this.off);
|
|
2545
|
-
if (idx === -1) throw new IncompleteError();
|
|
2546
|
-
const line = _decoder.decode(this.buf.subarray(this.off, idx));
|
|
2547
|
-
this.off = idx + 2;
|
|
2548
|
-
return line;
|
|
2549
|
-
}
|
|
2550
|
-
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
2551
|
-
readBulkBytes(len) {
|
|
2552
|
-
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
2553
|
-
const value = this.buf.subarray(this.off, this.off + len);
|
|
2554
|
-
this.off += len + 2;
|
|
2555
|
-
return value;
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
function indexOfCRLF(buf, from = 0) {
|
|
2559
|
-
let i = buf.indexOf(13, from);
|
|
2560
|
-
while (i !== -1) {
|
|
2561
|
-
if (i + 1 < buf.length && buf[i + 1] === 10) return i;
|
|
2562
|
-
i = buf.indexOf(13, i + 1);
|
|
2437
|
+
return d;
|
|
2563
2438
|
}
|
|
2564
|
-
|
|
2439
|
+
throw new Error("cron: no next run within 5 years (invalid expression?)");
|
|
2565
2440
|
}
|
|
2566
2441
|
|
|
2567
|
-
// src/
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
offlineQueue = [];
|
|
2579
|
-
subs = /* @__PURE__ */ new Map();
|
|
2580
|
-
psubs = /* @__PURE__ */ new Map();
|
|
2581
|
-
status = "idle";
|
|
2582
|
-
retries = 0;
|
|
2583
|
-
reconnectTimer = null;
|
|
2584
|
-
socketTimeoutTimer = null;
|
|
2585
|
-
connectPromise = null;
|
|
2586
|
-
closed = false;
|
|
2587
|
-
connectedOnce = false;
|
|
2588
|
-
constructor(options = {}) {
|
|
2589
|
-
this.opts = {
|
|
2590
|
-
host: options.host ?? "127.0.0.1",
|
|
2591
|
-
port: options.port ?? 6379,
|
|
2592
|
-
retryDelayMs: options.retryDelayMs ?? 100,
|
|
2593
|
-
maxRetries: options.maxRetries ?? 10,
|
|
2594
|
-
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
2595
|
-
maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
|
|
2596
|
-
commandTimeoutMs: options.commandTimeoutMs ?? 0,
|
|
2597
|
-
socketTimeoutMs: options.socketTimeoutMs ?? 0,
|
|
2598
|
-
onCommand: options.onCommand
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
2602
|
-
connect() {
|
|
2603
|
-
if (this.connectPromise) return this.connectPromise;
|
|
2604
|
-
this.closed = false;
|
|
2605
|
-
this.connectPromise = new Promise((resolve3, reject) => {
|
|
2606
|
-
this.openSocket();
|
|
2607
|
-
this.onceReady = () => resolve3();
|
|
2608
|
-
this.onceFailed = (err) => reject(err);
|
|
2609
|
-
}).finally(() => {
|
|
2610
|
-
this.connectPromise = null;
|
|
2611
|
-
this.onceReady = void 0;
|
|
2612
|
-
this.onceFailed = void 0;
|
|
2613
|
-
});
|
|
2614
|
-
return this.connectPromise;
|
|
2442
|
+
// src/scheduler/index.ts
|
|
2443
|
+
function scheduler(options) {
|
|
2444
|
+
const prefix = options.prefix ?? "wf:sched:";
|
|
2445
|
+
const tickMs = options.tickMs ?? 1e3;
|
|
2446
|
+
const delayedKey = `${prefix}delayed`;
|
|
2447
|
+
const cronsKey = `${prefix}crons`;
|
|
2448
|
+
const queueModule = options.queue;
|
|
2449
|
+
let connPromise = null;
|
|
2450
|
+
function getConn() {
|
|
2451
|
+
if (!connPromise) connPromise = options.redis.createConnection();
|
|
2452
|
+
return connPromise;
|
|
2615
2453
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
this.onceReady?.();
|
|
2633
|
-
});
|
|
2634
|
-
sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
|
|
2635
|
-
sock.on("error", (err) => {
|
|
2636
|
-
if (this.status === "connecting") {
|
|
2637
|
-
this.handleDisconnect(err);
|
|
2454
|
+
let running = false;
|
|
2455
|
+
let tickTimer = null;
|
|
2456
|
+
async function tick() {
|
|
2457
|
+
await tickCrons();
|
|
2458
|
+
let due;
|
|
2459
|
+
try {
|
|
2460
|
+
due = await (await getConn()).command("ZRANGEBYSCORE", delayedKey, 0, Date.now());
|
|
2461
|
+
} catch {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
for (const member of due) {
|
|
2465
|
+
let removed;
|
|
2466
|
+
try {
|
|
2467
|
+
removed = await (await getConn()).command("ZREM", delayedKey, member);
|
|
2468
|
+
} catch {
|
|
2469
|
+
return;
|
|
2638
2470
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2471
|
+
if (removed !== 1) continue;
|
|
2472
|
+
try {
|
|
2473
|
+
const task = JSON.parse(member);
|
|
2474
|
+
await queueModule.queue.add(task.name, task.data);
|
|
2475
|
+
} catch (e) {
|
|
2476
|
+
console.error("[scheduler] enqueue:", e instanceof Error ? e.message : e);
|
|
2644
2477
|
}
|
|
2645
|
-
});
|
|
2646
|
-
}
|
|
2647
|
-
handleDisconnect(err) {
|
|
2648
|
-
const queue2 = this.pending.slice(this.pendingHead);
|
|
2649
|
-
this.pending = [];
|
|
2650
|
-
this.pendingHead = 0;
|
|
2651
|
-
for (const p of queue2) p.reject(err);
|
|
2652
|
-
this.clearSocketTimeout();
|
|
2653
|
-
if (this.closed || this.status === "closed") return;
|
|
2654
|
-
this.retries++;
|
|
2655
|
-
this.status = "connecting";
|
|
2656
|
-
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
2657
|
-
this.status = "closed";
|
|
2658
|
-
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
2659
|
-
this.onceFailed?.(failErr);
|
|
2660
|
-
return;
|
|
2661
2478
|
}
|
|
2662
|
-
const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
|
|
2663
|
-
this.reconnectTimer = setTimeout(() => {
|
|
2664
|
-
this.reconnectTimer = null;
|
|
2665
|
-
this.openSocket();
|
|
2666
|
-
}, delay);
|
|
2667
2479
|
}
|
|
2668
|
-
|
|
2480
|
+
async function tickCrons() {
|
|
2481
|
+
let crons;
|
|
2669
2482
|
try {
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2483
|
+
crons = await (await getConn()).command("HGETALL", cronsKey);
|
|
2484
|
+
} catch {
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const now = Date.now();
|
|
2488
|
+
const entries = crons;
|
|
2489
|
+
for (let i = 0; i + 1 < entries.length; i += 2) {
|
|
2490
|
+
const field = entries[i];
|
|
2491
|
+
const value = entries[i + 1];
|
|
2492
|
+
let def;
|
|
2493
|
+
try {
|
|
2494
|
+
def = JSON.parse(value);
|
|
2495
|
+
} catch {
|
|
2496
|
+
continue;
|
|
2682
2497
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2498
|
+
const parsed = parseCron(def.expr);
|
|
2499
|
+
let next = def.nextRunAt;
|
|
2500
|
+
while (next <= now) {
|
|
2501
|
+
const ts = next;
|
|
2502
|
+
const member = JSON.stringify({ id: `cron:${field}:${ts}`, name: def.name, data: def.data });
|
|
2503
|
+
try {
|
|
2504
|
+
await (await getConn()).command("ZADD", delayedKey, "NX", ts, member);
|
|
2505
|
+
} catch {
|
|
2506
|
+
break;
|
|
2507
|
+
}
|
|
2508
|
+
next = nextRun(parsed, new Date(ts)).getTime();
|
|
2686
2509
|
}
|
|
2687
|
-
if (
|
|
2688
|
-
|
|
2689
|
-
else this.clearSocketTimeout();
|
|
2510
|
+
if (next !== def.nextRunAt) {
|
|
2511
|
+
await (await getConn()).command("HSET", cronsKey, field, JSON.stringify({ ...def, nextRunAt: next }));
|
|
2690
2512
|
}
|
|
2691
|
-
} catch (e) {
|
|
2692
|
-
const queue2 = this.pending.slice(this.pendingHead);
|
|
2693
|
-
this.pending = [];
|
|
2694
|
-
this.pendingHead = 0;
|
|
2695
|
-
for (const q of queue2) q.reject(e);
|
|
2696
|
-
this.socket?.destroy();
|
|
2697
2513
|
}
|
|
2698
2514
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2515
|
+
const cron = async (expr, name, data) => {
|
|
2516
|
+
const parsed = parseCron(expr);
|
|
2517
|
+
const firstRun = nextRun(parsed, /* @__PURE__ */ new Date());
|
|
2518
|
+
const def = JSON.stringify({ expr, name, data, nextRunAt: firstRun.getTime() });
|
|
2519
|
+
await (await getConn()).command("HSET", cronsKey, name, def);
|
|
2520
|
+
};
|
|
2521
|
+
const cancelCron = async (name) => {
|
|
2522
|
+
const removed = await (await getConn()).command("HDEL", cronsKey, name);
|
|
2523
|
+
try {
|
|
2524
|
+
const pending = await (await getConn()).command("ZRANGE", delayedKey, 0, -1);
|
|
2525
|
+
for (const member of pending) {
|
|
2526
|
+
if (member.includes(`"id":"cron:${name}:`)) {
|
|
2527
|
+
await (await getConn()).command("ZREM", delayedKey, member);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
} catch {
|
|
2708
2531
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2532
|
+
return removed === 1;
|
|
2533
|
+
};
|
|
2534
|
+
async function start() {
|
|
2535
|
+
if (running) return;
|
|
2536
|
+
running = true;
|
|
2537
|
+
await getConn();
|
|
2538
|
+
await tick();
|
|
2539
|
+
tickTimer = setInterval(() => {
|
|
2540
|
+
tick().catch((e) => console.error("[scheduler] tick:", e instanceof Error ? e.message : e));
|
|
2541
|
+
}, tickMs);
|
|
2542
|
+
tickTimer.unref?.();
|
|
2543
|
+
}
|
|
2544
|
+
const schedule = async (name, data, opts = {}) => {
|
|
2545
|
+
const delayMs = opts.delayMs ?? 0;
|
|
2546
|
+
const runAt = opts.when ? opts.when.getTime() : Date.now() + delayMs;
|
|
2547
|
+
if (!Number.isFinite(runAt)) throw new Error("scheduler: invalid when/delayMs");
|
|
2548
|
+
const id = randomUUID2();
|
|
2549
|
+
const member = JSON.stringify({ id, name, data });
|
|
2550
|
+
await (await getConn()).command("ZADD", delayedKey, runAt, member);
|
|
2551
|
+
return { id };
|
|
2552
|
+
};
|
|
2553
|
+
const cancelSchedule = async (id) => {
|
|
2554
|
+
try {
|
|
2555
|
+
const pending = await (await getConn()).command("ZRANGE", delayedKey, 0, -1);
|
|
2556
|
+
for (const member of pending) {
|
|
2557
|
+
if (member.includes(`"id":"${id}"`)) {
|
|
2558
|
+
const removed = await (await getConn()).command("ZREM", delayedKey, member);
|
|
2559
|
+
if (removed === 1) return true;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
} catch {
|
|
2711
2563
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2564
|
+
return false;
|
|
2565
|
+
};
|
|
2566
|
+
const mw = (async (req, ctx, next) => {
|
|
2567
|
+
ctx.schedule = schedule;
|
|
2568
|
+
ctx.cron = cron;
|
|
2569
|
+
ctx.cancelCron = cancelCron;
|
|
2570
|
+
ctx.cancelSchedule = cancelSchedule;
|
|
2571
|
+
return next(req, ctx);
|
|
2572
|
+
});
|
|
2573
|
+
mw.__meta = { injects: ["schedule", "cron", "cancelCron", "cancelSchedule"], depends: ["queue"] };
|
|
2574
|
+
mw.schedule = schedule;
|
|
2575
|
+
mw.cron = cron;
|
|
2576
|
+
mw.cancelCron = cancelCron;
|
|
2577
|
+
mw.cancelSchedule = cancelSchedule;
|
|
2578
|
+
mw.close = async () => {
|
|
2579
|
+
running = false;
|
|
2580
|
+
if (tickTimer) {
|
|
2581
|
+
clearInterval(tickTimer);
|
|
2582
|
+
tickTimer = null;
|
|
2714
2583
|
}
|
|
2715
|
-
|
|
2716
|
-
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
2584
|
+
if (connPromise) await connPromise.then((c) => c.close()).catch(() => {
|
|
2717
2585
|
});
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2586
|
+
};
|
|
2587
|
+
start().catch((e) => console.error("[scheduler] start:", e instanceof Error ? e.message : e));
|
|
2588
|
+
return mw;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// src/ai/client.ts
|
|
2592
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2593
|
+
|
|
2594
|
+
// src/ai/sse.ts
|
|
2595
|
+
function sseResponse(run, options) {
|
|
2596
|
+
const encoder = new TextEncoder();
|
|
2597
|
+
const stream = new ReadableStream({
|
|
2598
|
+
async start(controller) {
|
|
2599
|
+
const emit = (name, data) => {
|
|
2600
|
+
controller.enqueue(encoder.encode(`event: ${name}
|
|
2601
|
+
data: ${JSON.stringify(data)}
|
|
2602
|
+
|
|
2603
|
+
`));
|
|
2727
2604
|
};
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2605
|
+
try {
|
|
2606
|
+
await run(emit);
|
|
2607
|
+
controller.close();
|
|
2608
|
+
} catch (err) {
|
|
2609
|
+
try {
|
|
2610
|
+
emit("wf:error", {
|
|
2611
|
+
code: "provider_error",
|
|
2612
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2613
|
+
});
|
|
2614
|
+
controller.close();
|
|
2615
|
+
} catch {
|
|
2616
|
+
controller.error(err);
|
|
2736
2617
|
}
|
|
2737
|
-
});
|
|
2738
|
-
}).then(
|
|
2739
|
-
(v) => {
|
|
2740
|
-
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
2741
|
-
return v;
|
|
2742
|
-
},
|
|
2743
|
-
(e) => {
|
|
2744
|
-
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
2745
|
-
throw e;
|
|
2746
2618
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
armTimeout(p) {
|
|
2751
|
-
const ms = this.opts.commandTimeoutMs;
|
|
2752
|
-
if (ms <= 0) return;
|
|
2753
|
-
p.timer = setTimeout(() => {
|
|
2754
|
-
p.timedOut = true;
|
|
2755
|
-
if (p.blocking) p.resolve(null);
|
|
2756
|
-
else p.reject(new TimeoutError("redis: command timeout", ms));
|
|
2757
|
-
}, ms);
|
|
2758
|
-
}
|
|
2759
|
-
/** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
|
|
2760
|
-
armSocketTimeout() {
|
|
2761
|
-
const ms = this.opts.socketTimeoutMs;
|
|
2762
|
-
if (ms <= 0) return;
|
|
2763
|
-
this.clearSocketTimeout();
|
|
2764
|
-
this.socketTimeoutTimer = setTimeout(() => {
|
|
2765
|
-
this.socketTimeoutTimer = null;
|
|
2766
|
-
const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
|
|
2767
|
-
const queue2 = this.pending.slice(this.pendingHead);
|
|
2768
|
-
this.pending = [];
|
|
2769
|
-
this.pendingHead = 0;
|
|
2770
|
-
for (const q of queue2) q.reject(err);
|
|
2771
|
-
this.clearSocketTimeout();
|
|
2772
|
-
this.socket?.destroy();
|
|
2773
|
-
}, ms);
|
|
2774
|
-
}
|
|
2775
|
-
clearSocketTimeout() {
|
|
2776
|
-
if (this.socketTimeoutTimer) {
|
|
2777
|
-
clearTimeout(this.socketTimeoutTimer);
|
|
2778
|
-
this.socketTimeoutTimer = null;
|
|
2619
|
+
},
|
|
2620
|
+
cancel() {
|
|
2621
|
+
options?.onAbort?.();
|
|
2779
2622
|
}
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2623
|
+
});
|
|
2624
|
+
return new Response(stream, {
|
|
2625
|
+
headers: {
|
|
2626
|
+
"Content-Type": "text/event-stream",
|
|
2627
|
+
"Cache-Control": "no-cache",
|
|
2628
|
+
Connection: "keep-alive"
|
|
2786
2629
|
}
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/ai/client.ts
|
|
2634
|
+
var AiError = class extends Error {
|
|
2635
|
+
code;
|
|
2636
|
+
constructor(code, message) {
|
|
2637
|
+
super(message);
|
|
2638
|
+
this.name = "AiError";
|
|
2639
|
+
this.code = code;
|
|
2787
2640
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2641
|
+
};
|
|
2642
|
+
async function* parseProviderSse(stream) {
|
|
2643
|
+
const reader = stream.getReader();
|
|
2644
|
+
const decoder = new TextDecoder();
|
|
2645
|
+
let buffer = "";
|
|
2646
|
+
try {
|
|
2647
|
+
while (true) {
|
|
2648
|
+
const { done, value } = await reader.read();
|
|
2649
|
+
if (done) break;
|
|
2650
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2651
|
+
const lines = buffer.split("\n");
|
|
2652
|
+
buffer = lines.pop() ?? "";
|
|
2653
|
+
for (const line of lines) {
|
|
2654
|
+
const trimmed = line.trim();
|
|
2655
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
2656
|
+
const data = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed;
|
|
2657
|
+
if (data === "[DONE]") return;
|
|
2658
|
+
try {
|
|
2659
|
+
yield JSON.parse(data);
|
|
2660
|
+
} catch {
|
|
2803
2661
|
}
|
|
2804
|
-
};
|
|
2805
|
-
for (let i = 0; i < count; i++) {
|
|
2806
|
-
const p = {
|
|
2807
|
-
// 正常响应与错误响应(RespError)都作为结果值收集——管道语义
|
|
2808
|
-
resolve: (v) => {
|
|
2809
|
-
results.push(v);
|
|
2810
|
-
maybeResolve();
|
|
2811
|
-
},
|
|
2812
|
-
reject: (e) => {
|
|
2813
|
-
results.push(e);
|
|
2814
|
-
maybeResolve();
|
|
2815
|
-
}
|
|
2816
|
-
};
|
|
2817
|
-
batchPending.push(p);
|
|
2818
|
-
this.pending.push(p);
|
|
2819
|
-
}
|
|
2820
|
-
let batchTimer;
|
|
2821
|
-
const ms = this.opts.commandTimeoutMs;
|
|
2822
|
-
if (ms > 0) {
|
|
2823
|
-
batchTimer = setTimeout(() => {
|
|
2824
|
-
settled = true;
|
|
2825
|
-
for (const p of batchPending) p.timedOut = true;
|
|
2826
|
-
reject(new TimeoutError("redis: batch timeout", ms));
|
|
2827
|
-
}, ms);
|
|
2828
2662
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
}
|
|
2833
|
-
/** 订阅频道:回调式(channel, message) */
|
|
2834
|
-
async subscribe(channel, fn) {
|
|
2835
|
-
this.subs.set(channel, fn);
|
|
2836
|
-
await this.command("SUBSCRIBE", channel);
|
|
2663
|
+
}
|
|
2664
|
+
} finally {
|
|
2665
|
+
reader.releaseLock();
|
|
2837
2666
|
}
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2667
|
+
}
|
|
2668
|
+
async function providerError(res) {
|
|
2669
|
+
let message = "";
|
|
2670
|
+
try {
|
|
2671
|
+
const body = await res.json();
|
|
2672
|
+
message = body?.error?.message ?? JSON.stringify(body);
|
|
2673
|
+
} catch {
|
|
2674
|
+
message = await res.text().catch(() => "");
|
|
2842
2675
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2676
|
+
const status = res.status;
|
|
2677
|
+
let code;
|
|
2678
|
+
if (status === 401 || status === 403) code = "auth_failed";
|
|
2679
|
+
else if (status === 429) code = "rate_limited";
|
|
2680
|
+
else if (status >= 500) code = "provider_error";
|
|
2681
|
+
else if (/context|token|length/i.test(message)) code = "context_length";
|
|
2682
|
+
else if (status === 400) code = "invalid_request";
|
|
2683
|
+
else code = "provider_error";
|
|
2684
|
+
return { code, message: message || `provider error (${status})` };
|
|
2685
|
+
}
|
|
2686
|
+
function createEmbeddingClient(ebd) {
|
|
2687
|
+
const apiKey = ebd?.apiKey ?? process.env.DASHSCOPE_API_KEY ?? "";
|
|
2688
|
+
const configured = !!(ebd?.apiKey || process.env.DASHSCOPE_API_KEY);
|
|
2689
|
+
const baseUrl = ebd?.baseUrl ?? process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
2690
|
+
const defaultModel = ebd?.defaultModel ?? process.env.DASHSCOPE_EMBEDDING_MODEL ?? "text-embedding-v4";
|
|
2691
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
|
|
2692
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
|
|
2693
|
+
async function embedMany(texts) {
|
|
2694
|
+
if (!configured) {
|
|
2695
|
+
throw new AiError("unsupported", "ai embedding: \u672A\u914D\u7F6E\u2014\u2014\u4F20 ai({ embedding }) \u6216\u8BBE DASHSCOPE_API_KEY\uFF08DeepSeek \u65E0 embedding API\uFF0C\u9700\u72EC\u7ACB provider\uFF09");
|
|
2852
2696
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
async close() {
|
|
2856
|
-
this.closed = true;
|
|
2857
|
-
this.status = "closed";
|
|
2858
|
-
if (this.reconnectTimer) {
|
|
2859
|
-
clearTimeout(this.reconnectTimer);
|
|
2860
|
-
this.reconnectTimer = null;
|
|
2697
|
+
if (!apiKey) {
|
|
2698
|
+
throw new AiError("auth_failed", "ai embedding: DASHSCOPE_API_KEY \u672A\u8BBE\u7F6E");
|
|
2861
2699
|
}
|
|
2862
|
-
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
};
|
|
2878
|
-
|
|
2879
|
-
// src/scheduler/cron.ts
|
|
2880
|
-
var RANGES = [
|
|
2881
|
-
[0, 59],
|
|
2882
|
-
// 分
|
|
2883
|
-
[0, 23],
|
|
2884
|
-
// 时
|
|
2885
|
-
[1, 31],
|
|
2886
|
-
// 日
|
|
2887
|
-
[1, 12],
|
|
2888
|
-
// 月
|
|
2889
|
-
[0, 6]
|
|
2890
|
-
// 周
|
|
2891
|
-
];
|
|
2892
|
-
function parseField(field, idx) {
|
|
2893
|
-
const [min, max] = RANGES[idx];
|
|
2894
|
-
const name = ["minute", "hour", "day", "month", "weekday"][idx];
|
|
2895
|
-
if (field === "*") return null;
|
|
2896
|
-
if (field.startsWith("*/")) {
|
|
2897
|
-
const n = Number(field.slice(2));
|
|
2898
|
-
if (!Number.isInteger(n) || n <= 0 || n > max) {
|
|
2899
|
-
throw new Error(`cron: invalid step '${field}' in ${name} field (must be 1..${max})`);
|
|
2700
|
+
const controller = new AbortController();
|
|
2701
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
2702
|
+
let res;
|
|
2703
|
+
try {
|
|
2704
|
+
res = await fetch(endpoint, {
|
|
2705
|
+
method: "POST",
|
|
2706
|
+
headers,
|
|
2707
|
+
body: JSON.stringify({ model: defaultModel, input: texts }),
|
|
2708
|
+
signal: controller.signal
|
|
2709
|
+
});
|
|
2710
|
+
} catch (err) {
|
|
2711
|
+
if (controller.signal.aborted) throw new AiError("provider_error", "embedding \u8BF7\u6C42\u8D85\u65F6\uFF083s\uFF09");
|
|
2712
|
+
throw new AiError("provider_error", err instanceof Error ? err.message : String(err));
|
|
2713
|
+
} finally {
|
|
2714
|
+
clearTimeout(timer);
|
|
2900
2715
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
}
|
|
2905
|
-
const set = /* @__PURE__ */ new Set();
|
|
2906
|
-
for (const part of field.split(",")) {
|
|
2907
|
-
if (part === "") throw new Error(`cron: empty item in '${field}' (${name})`);
|
|
2908
|
-
if (part.includes("-")) {
|
|
2909
|
-
const [rangePart, stepPart] = part.split("/");
|
|
2910
|
-
const step = stepPart !== void 0 ? Number(stepPart) : 1;
|
|
2911
|
-
const [a, b] = rangePart.split("-").map(Number);
|
|
2912
|
-
if (!Number.isInteger(a) || !Number.isInteger(b) || a < min || b > max || a > b) {
|
|
2913
|
-
throw new Error(`cron: invalid range '${part}' in ${name} field (${min}..${max})`);
|
|
2914
|
-
}
|
|
2915
|
-
if (!Number.isInteger(step) || step <= 0) throw new Error(`cron: invalid step in '${part}'`);
|
|
2916
|
-
for (let v = a; v <= b; v += step) set.add(v);
|
|
2917
|
-
} else {
|
|
2918
|
-
const v = Number(part);
|
|
2919
|
-
if (!Number.isInteger(v) || v < min || v > max) {
|
|
2920
|
-
throw new Error(`cron: value '${part}' out of range ${min}..${max} in ${name} field`);
|
|
2921
|
-
}
|
|
2922
|
-
set.add(v);
|
|
2716
|
+
if (!res.ok) {
|
|
2717
|
+
const { code, message } = await providerError(res);
|
|
2718
|
+
throw new AiError(code, message);
|
|
2923
2719
|
}
|
|
2720
|
+
const data = await res.json();
|
|
2721
|
+
data.data.sort((a, b) => a.index - b.index);
|
|
2722
|
+
return data.data.map((item) => item.embedding);
|
|
2924
2723
|
}
|
|
2925
|
-
return set;
|
|
2926
|
-
}
|
|
2927
|
-
function parseCron(expr) {
|
|
2928
|
-
const parts = expr.trim().split(/\s+/);
|
|
2929
|
-
if (parts.length !== 5) {
|
|
2930
|
-
throw new Error(`cron: expected 5 fields (min hour dom month dow), got ${parts.length}: '${expr}'`);
|
|
2931
|
-
}
|
|
2932
|
-
const fields = parts.map((p, i) => parseField(p, i));
|
|
2933
2724
|
return {
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2725
|
+
async embed(text) {
|
|
2726
|
+
const results = await embedMany([text]);
|
|
2727
|
+
return results[0];
|
|
2728
|
+
},
|
|
2729
|
+
embedMany
|
|
2937
2730
|
};
|
|
2938
2731
|
}
|
|
2939
|
-
function
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
}
|
|
2951
|
-
function
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2732
|
+
function createAiClient(opts) {
|
|
2733
|
+
const endpoint = `${opts.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
2734
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}` };
|
|
2735
|
+
const embedding = createEmbeddingClient(opts.embedding);
|
|
2736
|
+
const approvals = /* @__PURE__ */ new Map();
|
|
2737
|
+
function approve(response) {
|
|
2738
|
+
const resolve3 = approvals.get(response.id);
|
|
2739
|
+
if (!resolve3) return false;
|
|
2740
|
+
approvals.delete(response.id);
|
|
2741
|
+
resolve3(response);
|
|
2742
|
+
return true;
|
|
2743
|
+
}
|
|
2744
|
+
async function waitApproval(req, emit, timeoutMs = DEFAULT_APPROVAL_TIMEOUT) {
|
|
2745
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
2746
|
+
emit("wf:approval_request", { ...req, expiresAt });
|
|
2747
|
+
return new Promise((resolve3) => {
|
|
2748
|
+
const timer = setTimeout(() => {
|
|
2749
|
+
if (approvals.has(req.id)) {
|
|
2750
|
+
approvals.delete(req.id);
|
|
2751
|
+
resolve3({ id: req.id, decision: "rejected" });
|
|
2752
|
+
}
|
|
2753
|
+
}, timeoutMs);
|
|
2754
|
+
approvals.set(req.id, (resp) => {
|
|
2755
|
+
clearTimeout(timer);
|
|
2756
|
+
resolve3(resp);
|
|
2757
|
+
});
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
function aggregateToolCalls(chunks) {
|
|
2761
|
+
const calls = [];
|
|
2762
|
+
for (const chunk of chunks) {
|
|
2763
|
+
const delta = chunk.choices[0]?.delta;
|
|
2764
|
+
if (!delta?.tool_calls) continue;
|
|
2765
|
+
for (const tc of delta.tool_calls) {
|
|
2766
|
+
if (tc.id) {
|
|
2767
|
+
calls.push(tc);
|
|
2768
|
+
} else if (calls.length > 0) {
|
|
2769
|
+
const last = calls[calls.length - 1];
|
|
2770
|
+
if (tc.function?.arguments) last.function.arguments += tc.function.arguments;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2976
2773
|
}
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2774
|
+
return calls;
|
|
2775
|
+
}
|
|
2776
|
+
async function chat(params, options) {
|
|
2777
|
+
const res = await fetch(endpoint, {
|
|
2778
|
+
method: "POST",
|
|
2779
|
+
headers,
|
|
2780
|
+
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: false }),
|
|
2781
|
+
signal: options?.signal
|
|
2782
|
+
});
|
|
2783
|
+
if (!res.ok) {
|
|
2784
|
+
const { code, message } = await providerError(res);
|
|
2785
|
+
throw new AiError(code, message);
|
|
2983
2786
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2787
|
+
return res.json();
|
|
2788
|
+
}
|
|
2789
|
+
function stream(params, options) {
|
|
2790
|
+
const controller = new AbortController();
|
|
2791
|
+
const external = options?.signal;
|
|
2792
|
+
if (external) {
|
|
2793
|
+
if (external.aborted) controller.abort();
|
|
2794
|
+
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
2987
2795
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2796
|
+
return sseResponse(
|
|
2797
|
+
async (emit) => {
|
|
2798
|
+
emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
|
|
2799
|
+
await streamStep(params, {
|
|
2800
|
+
emit,
|
|
2801
|
+
signal: controller.signal,
|
|
2802
|
+
onFinish: (r) => emit("wf:done", { content: r.content, usage: r.usage })
|
|
2803
|
+
});
|
|
2804
|
+
},
|
|
2805
|
+
{ onAbort: () => controller.abort() }
|
|
2806
|
+
);
|
|
2999
2807
|
}
|
|
3000
|
-
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
// src/scheduler/index.ts
|
|
3004
|
-
function parseUrl(options) {
|
|
3005
|
-
const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
|
|
3006
|
-
const u = new URL(url);
|
|
3007
|
-
return { host: u.hostname, port: Number(u.port || 6379) };
|
|
3008
|
-
}
|
|
3009
|
-
function scheduler(options) {
|
|
3010
|
-
const prefix = options.prefix ?? "wf:sched:";
|
|
3011
|
-
const tickMs = options.tickMs ?? 1e3;
|
|
3012
|
-
const delayedKey = `${prefix}delayed`;
|
|
3013
|
-
const cronsKey = `${prefix}crons`;
|
|
3014
|
-
const connOpts = parseUrl(options);
|
|
3015
|
-
const queueModule = options.queue;
|
|
3016
|
-
const conn = new RedisConnection(connOpts);
|
|
3017
|
-
let running = false;
|
|
3018
|
-
let tickTimer = null;
|
|
3019
|
-
async function tick() {
|
|
3020
|
-
await tickCrons();
|
|
3021
|
-
let due;
|
|
2808
|
+
async function streamStep(params, stepOpts) {
|
|
2809
|
+
const { emit, signal } = stepOpts;
|
|
2810
|
+
let res;
|
|
3022
2811
|
try {
|
|
3023
|
-
|
|
3024
|
-
|
|
2812
|
+
res = await fetch(endpoint, {
|
|
2813
|
+
method: "POST",
|
|
2814
|
+
headers,
|
|
2815
|
+
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: true }),
|
|
2816
|
+
signal
|
|
2817
|
+
});
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
if (signal?.aborted) return;
|
|
2820
|
+
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
3025
2821
|
return;
|
|
3026
2822
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
removed = await conn.command("ZREM", delayedKey, member);
|
|
3031
|
-
} catch {
|
|
3032
|
-
return;
|
|
3033
|
-
}
|
|
3034
|
-
if (removed !== 1) continue;
|
|
3035
|
-
try {
|
|
3036
|
-
const task = JSON.parse(member);
|
|
3037
|
-
await queueModule.queue.add(task.name, task.data);
|
|
3038
|
-
} catch (e) {
|
|
3039
|
-
console.error("[scheduler] enqueue:", e instanceof Error ? e.message : e);
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
async function tickCrons() {
|
|
3044
|
-
let crons;
|
|
3045
|
-
try {
|
|
3046
|
-
crons = await conn.command("HGETALL", cronsKey);
|
|
3047
|
-
} catch {
|
|
2823
|
+
if (!res.ok) {
|
|
2824
|
+
const { code, message } = await providerError(res);
|
|
2825
|
+
emit("wf:error", { code, message });
|
|
3048
2826
|
return;
|
|
3049
2827
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
const value = entries[i + 1];
|
|
3055
|
-
let def;
|
|
3056
|
-
try {
|
|
3057
|
-
def = JSON.parse(value);
|
|
3058
|
-
} catch {
|
|
3059
|
-
continue;
|
|
3060
|
-
}
|
|
3061
|
-
const parsed = parseCron(def.expr);
|
|
3062
|
-
let next = def.nextRunAt;
|
|
3063
|
-
while (next <= now) {
|
|
3064
|
-
const ts = next;
|
|
3065
|
-
const member = JSON.stringify({ id: `cron:${field}:${ts}`, name: def.name, data: def.data });
|
|
3066
|
-
try {
|
|
3067
|
-
await conn.command("ZADD", delayedKey, "NX", ts, member);
|
|
3068
|
-
} catch {
|
|
3069
|
-
break;
|
|
3070
|
-
}
|
|
3071
|
-
next = nextRun(parsed, new Date(ts)).getTime();
|
|
3072
|
-
}
|
|
3073
|
-
if (next !== def.nextRunAt) {
|
|
3074
|
-
await conn.command("HSET", cronsKey, field, JSON.stringify({ ...def, nextRunAt: next }));
|
|
3075
|
-
}
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
const cron = async (expr, name, data) => {
|
|
3079
|
-
const parsed = parseCron(expr);
|
|
3080
|
-
const firstRun = nextRun(parsed, /* @__PURE__ */ new Date());
|
|
3081
|
-
const def = JSON.stringify({ expr, name, data, nextRunAt: firstRun.getTime() });
|
|
3082
|
-
await conn.command("HSET", cronsKey, name, def);
|
|
3083
|
-
};
|
|
3084
|
-
const cancelCron = async (name) => {
|
|
3085
|
-
const removed = await conn.command("HDEL", cronsKey, name);
|
|
2828
|
+
let content = "";
|
|
2829
|
+
let reasoning = "";
|
|
2830
|
+
const chunks = [];
|
|
2831
|
+
let usage;
|
|
3086
2832
|
try {
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
2833
|
+
for await (const chunk of parseProviderSse(res.body)) {
|
|
2834
|
+
if (signal?.aborted) return;
|
|
2835
|
+
const delta = chunk.choices[0]?.delta;
|
|
2836
|
+
if (delta?.content) {
|
|
2837
|
+
content += delta.content;
|
|
2838
|
+
emit("wf:token", { text: delta.content });
|
|
3091
2839
|
}
|
|
2840
|
+
if (delta?.reasoning_content) reasoning += delta.reasoning_content;
|
|
2841
|
+
if (delta?.tool_calls) chunks.push(chunk);
|
|
2842
|
+
if (chunk.usage) usage = chunk.usage;
|
|
3092
2843
|
}
|
|
3093
|
-
} catch {
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
if (signal?.aborted) return;
|
|
2846
|
+
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
2847
|
+
return;
|
|
3094
2848
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
tickTimer = setInterval(() => {
|
|
3103
|
-
tick().catch((e) => console.error("[scheduler] tick:", e instanceof Error ? e.message : e));
|
|
3104
|
-
}, tickMs);
|
|
3105
|
-
tickTimer.unref?.();
|
|
3106
|
-
}
|
|
3107
|
-
const schedule = async (name, data, opts = {}) => {
|
|
3108
|
-
const delayMs = opts.delayMs ?? 0;
|
|
3109
|
-
const runAt = opts.when ? opts.when.getTime() : Date.now() + delayMs;
|
|
3110
|
-
if (!Number.isFinite(runAt)) throw new Error("scheduler: invalid when/delayMs");
|
|
3111
|
-
const id = randomUUID2();
|
|
3112
|
-
const member = JSON.stringify({ id, name, data });
|
|
3113
|
-
await conn.command("ZADD", delayedKey, runAt, member);
|
|
3114
|
-
return { id };
|
|
3115
|
-
};
|
|
3116
|
-
const cancelSchedule = async (id) => {
|
|
3117
|
-
try {
|
|
3118
|
-
const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
|
|
3119
|
-
for (const member of pending) {
|
|
3120
|
-
if (member.includes(`"id":"${id}"`)) {
|
|
3121
|
-
const removed = await conn.command("ZREM", delayedKey, member);
|
|
3122
|
-
if (removed === 1) return true;
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
} catch {
|
|
2849
|
+
const toolCalls = aggregateToolCalls(chunks);
|
|
2850
|
+
for (const tc of toolCalls) {
|
|
2851
|
+
emit("wf:tool_call", {
|
|
2852
|
+
id: tc.id,
|
|
2853
|
+
name: tc.function?.name ?? "",
|
|
2854
|
+
args: safeParseArgs(tc.function?.arguments ?? "")
|
|
2855
|
+
});
|
|
3126
2856
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
mw.__meta = { injects: ["schedule", "cron", "cancelCron", "cancelSchedule"], depends: ["queue"] };
|
|
3137
|
-
mw.schedule = schedule;
|
|
3138
|
-
mw.cron = cron;
|
|
3139
|
-
mw.cancelCron = cancelCron;
|
|
3140
|
-
mw.cancelSchedule = cancelSchedule;
|
|
3141
|
-
mw.close = async () => {
|
|
3142
|
-
running = false;
|
|
3143
|
-
if (tickTimer) {
|
|
3144
|
-
clearInterval(tickTimer);
|
|
3145
|
-
tickTimer = null;
|
|
2857
|
+
if (usage && stepOpts.emitUsage !== false) emit("wf:usage", usage);
|
|
2858
|
+
stepOpts.onFinish?.({ content, reasoning_content: reasoning || void 0, toolCalls, usage });
|
|
2859
|
+
}
|
|
2860
|
+
function sse(run, options) {
|
|
2861
|
+
const controller = new AbortController();
|
|
2862
|
+
const external = options?.signal;
|
|
2863
|
+
if (external) {
|
|
2864
|
+
if (external.aborted) controller.abort();
|
|
2865
|
+
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3146
2866
|
}
|
|
3147
|
-
|
|
3148
|
-
|
|
2867
|
+
return sseResponse(run, { onAbort: () => controller.abort() });
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
chat,
|
|
2871
|
+
stream,
|
|
2872
|
+
sse,
|
|
2873
|
+
streamStep,
|
|
2874
|
+
waitApproval,
|
|
2875
|
+
approve,
|
|
2876
|
+
// embedding:未配置 provider 时明确抛 AiError(诚实裁剪:不静默降级)
|
|
2877
|
+
embed: (text) => embedding.embed(text),
|
|
2878
|
+
embedMany: (texts) => embedding.embedMany(texts)
|
|
3149
2879
|
};
|
|
3150
|
-
start().catch((e) => console.error("[scheduler] start:", e instanceof Error ? e.message : e));
|
|
3151
|
-
return mw;
|
|
3152
2880
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
async start(controller) {
|
|
3162
|
-
const emit = (name, data) => {
|
|
3163
|
-
controller.enqueue(encoder.encode(`event: ${name}
|
|
3164
|
-
data: ${JSON.stringify(data)}
|
|
3165
|
-
|
|
3166
|
-
`));
|
|
3167
|
-
};
|
|
3168
|
-
try {
|
|
3169
|
-
await run(emit);
|
|
3170
|
-
controller.close();
|
|
3171
|
-
} catch (err) {
|
|
3172
|
-
try {
|
|
3173
|
-
emit("wf:error", {
|
|
3174
|
-
code: "provider_error",
|
|
3175
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3176
|
-
});
|
|
3177
|
-
controller.close();
|
|
3178
|
-
} catch {
|
|
3179
|
-
controller.error(err);
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
},
|
|
3183
|
-
cancel() {
|
|
3184
|
-
options?.onAbort?.();
|
|
3185
|
-
}
|
|
3186
|
-
});
|
|
3187
|
-
return new Response(stream, {
|
|
3188
|
-
headers: {
|
|
3189
|
-
"Content-Type": "text/event-stream",
|
|
3190
|
-
"Cache-Control": "no-cache",
|
|
3191
|
-
Connection: "keep-alive"
|
|
3192
|
-
}
|
|
3193
|
-
});
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
// src/ai/client.ts
|
|
3197
|
-
var AiError = class extends Error {
|
|
3198
|
-
code;
|
|
3199
|
-
constructor(code, message) {
|
|
3200
|
-
super(message);
|
|
3201
|
-
this.name = "AiError";
|
|
3202
|
-
this.code = code;
|
|
3203
|
-
}
|
|
3204
|
-
};
|
|
3205
|
-
async function* parseProviderSse(stream) {
|
|
3206
|
-
const reader = stream.getReader();
|
|
3207
|
-
const decoder = new TextDecoder();
|
|
3208
|
-
let buffer = "";
|
|
3209
|
-
try {
|
|
3210
|
-
while (true) {
|
|
3211
|
-
const { done, value } = await reader.read();
|
|
3212
|
-
if (done) break;
|
|
3213
|
-
buffer += decoder.decode(value, { stream: true });
|
|
3214
|
-
const lines = buffer.split("\n");
|
|
3215
|
-
buffer = lines.pop() ?? "";
|
|
3216
|
-
for (const line of lines) {
|
|
3217
|
-
const trimmed = line.trim();
|
|
3218
|
-
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
3219
|
-
const data = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed;
|
|
3220
|
-
if (data === "[DONE]") return;
|
|
3221
|
-
try {
|
|
3222
|
-
yield JSON.parse(data);
|
|
3223
|
-
} catch {
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
} finally {
|
|
3228
|
-
reader.releaseLock();
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
async function providerError(res) {
|
|
3232
|
-
let message = "";
|
|
3233
|
-
try {
|
|
3234
|
-
const body = await res.json();
|
|
3235
|
-
message = body?.error?.message ?? JSON.stringify(body);
|
|
3236
|
-
} catch {
|
|
3237
|
-
message = await res.text().catch(() => "");
|
|
3238
|
-
}
|
|
3239
|
-
const status = res.status;
|
|
3240
|
-
let code;
|
|
3241
|
-
if (status === 401 || status === 403) code = "auth_failed";
|
|
3242
|
-
else if (status === 429) code = "rate_limited";
|
|
3243
|
-
else if (status >= 500) code = "provider_error";
|
|
3244
|
-
else if (/context|token|length/i.test(message)) code = "context_length";
|
|
3245
|
-
else if (status === 400) code = "invalid_request";
|
|
3246
|
-
else code = "provider_error";
|
|
3247
|
-
return { code, message: message || `provider error (${status})` };
|
|
3248
|
-
}
|
|
3249
|
-
function createEmbeddingClient(ebd) {
|
|
3250
|
-
const apiKey = ebd?.apiKey ?? process.env.DASHSCOPE_API_KEY ?? "";
|
|
3251
|
-
const configured = !!(ebd?.apiKey || process.env.DASHSCOPE_API_KEY);
|
|
3252
|
-
const baseUrl = ebd?.baseUrl ?? process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
3253
|
-
const defaultModel = ebd?.defaultModel ?? process.env.DASHSCOPE_EMBEDDING_MODEL ?? "text-embedding-v4";
|
|
3254
|
-
const endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
|
|
3255
|
-
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
|
|
3256
|
-
async function embedMany(texts) {
|
|
3257
|
-
if (!configured) {
|
|
3258
|
-
throw new AiError("unsupported", "ai embedding: \u672A\u914D\u7F6E\u2014\u2014\u4F20 ai({ embedding }) \u6216\u8BBE DASHSCOPE_API_KEY\uFF08DeepSeek \u65E0 embedding API\uFF0C\u9700\u72EC\u7ACB provider\uFF09");
|
|
3259
|
-
}
|
|
3260
|
-
if (!apiKey) {
|
|
3261
|
-
throw new AiError("auth_failed", "ai embedding: DASHSCOPE_API_KEY \u672A\u8BBE\u7F6E");
|
|
3262
|
-
}
|
|
3263
|
-
const controller = new AbortController();
|
|
3264
|
-
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
3265
|
-
let res;
|
|
3266
|
-
try {
|
|
3267
|
-
res = await fetch(endpoint, {
|
|
3268
|
-
method: "POST",
|
|
3269
|
-
headers,
|
|
3270
|
-
body: JSON.stringify({ model: defaultModel, input: texts }),
|
|
3271
|
-
signal: controller.signal
|
|
3272
|
-
});
|
|
3273
|
-
} catch (err) {
|
|
3274
|
-
if (controller.signal.aborted) throw new AiError("provider_error", "embedding \u8BF7\u6C42\u8D85\u65F6\uFF083s\uFF09");
|
|
3275
|
-
throw new AiError("provider_error", err instanceof Error ? err.message : String(err));
|
|
3276
|
-
} finally {
|
|
3277
|
-
clearTimeout(timer);
|
|
3278
|
-
}
|
|
3279
|
-
if (!res.ok) {
|
|
3280
|
-
const { code, message } = await providerError(res);
|
|
3281
|
-
throw new AiError(code, message);
|
|
3282
|
-
}
|
|
3283
|
-
const data = await res.json();
|
|
3284
|
-
data.data.sort((a, b) => a.index - b.index);
|
|
3285
|
-
return data.data.map((item) => item.embedding);
|
|
3286
|
-
}
|
|
3287
|
-
return {
|
|
3288
|
-
async embed(text) {
|
|
3289
|
-
const results = await embedMany([text]);
|
|
3290
|
-
return results[0];
|
|
3291
|
-
},
|
|
3292
|
-
embedMany
|
|
3293
|
-
};
|
|
3294
|
-
}
|
|
3295
|
-
function createAiClient(opts) {
|
|
3296
|
-
const endpoint = `${opts.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
3297
|
-
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}` };
|
|
3298
|
-
const embedding = createEmbeddingClient(opts.embedding);
|
|
3299
|
-
const approvals = /* @__PURE__ */ new Map();
|
|
3300
|
-
function approve(response) {
|
|
3301
|
-
const resolve3 = approvals.get(response.id);
|
|
3302
|
-
if (!resolve3) return false;
|
|
3303
|
-
approvals.delete(response.id);
|
|
3304
|
-
resolve3(response);
|
|
3305
|
-
return true;
|
|
3306
|
-
}
|
|
3307
|
-
async function waitApproval(req, emit, timeoutMs = DEFAULT_APPROVAL_TIMEOUT) {
|
|
3308
|
-
const expiresAt = Date.now() + timeoutMs;
|
|
3309
|
-
emit("wf:approval_request", { ...req, expiresAt });
|
|
3310
|
-
return new Promise((resolve3) => {
|
|
3311
|
-
const timer = setTimeout(() => {
|
|
3312
|
-
if (approvals.has(req.id)) {
|
|
3313
|
-
approvals.delete(req.id);
|
|
3314
|
-
resolve3({ id: req.id, decision: "rejected" });
|
|
3315
|
-
}
|
|
3316
|
-
}, timeoutMs);
|
|
3317
|
-
approvals.set(req.id, (resp) => {
|
|
3318
|
-
clearTimeout(timer);
|
|
3319
|
-
resolve3(resp);
|
|
3320
|
-
});
|
|
3321
|
-
});
|
|
3322
|
-
}
|
|
3323
|
-
function aggregateToolCalls(chunks) {
|
|
3324
|
-
const calls = [];
|
|
3325
|
-
for (const chunk of chunks) {
|
|
3326
|
-
const delta = chunk.choices[0]?.delta;
|
|
3327
|
-
if (!delta?.tool_calls) continue;
|
|
3328
|
-
for (const tc of delta.tool_calls) {
|
|
3329
|
-
if (tc.id) {
|
|
3330
|
-
calls.push(tc);
|
|
3331
|
-
} else if (calls.length > 0) {
|
|
3332
|
-
const last = calls[calls.length - 1];
|
|
3333
|
-
if (tc.function?.arguments) last.function.arguments += tc.function.arguments;
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
return calls;
|
|
3338
|
-
}
|
|
3339
|
-
async function chat(params, options) {
|
|
3340
|
-
const res = await fetch(endpoint, {
|
|
3341
|
-
method: "POST",
|
|
3342
|
-
headers,
|
|
3343
|
-
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: false }),
|
|
3344
|
-
signal: options?.signal
|
|
3345
|
-
});
|
|
3346
|
-
if (!res.ok) {
|
|
3347
|
-
const { code, message } = await providerError(res);
|
|
3348
|
-
throw new AiError(code, message);
|
|
3349
|
-
}
|
|
3350
|
-
return res.json();
|
|
3351
|
-
}
|
|
3352
|
-
function stream(params, options) {
|
|
3353
|
-
const controller = new AbortController();
|
|
3354
|
-
const external = options?.signal;
|
|
3355
|
-
if (external) {
|
|
3356
|
-
if (external.aborted) controller.abort();
|
|
3357
|
-
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3358
|
-
}
|
|
3359
|
-
return sseResponse(
|
|
3360
|
-
async (emit) => {
|
|
3361
|
-
emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
|
|
3362
|
-
await streamStep(params, {
|
|
3363
|
-
emit,
|
|
3364
|
-
signal: controller.signal,
|
|
3365
|
-
onFinish: (r) => emit("wf:done", { content: r.content, usage: r.usage })
|
|
3366
|
-
});
|
|
3367
|
-
},
|
|
3368
|
-
{ onAbort: () => controller.abort() }
|
|
3369
|
-
);
|
|
3370
|
-
}
|
|
3371
|
-
async function streamStep(params, stepOpts) {
|
|
3372
|
-
const { emit, signal } = stepOpts;
|
|
3373
|
-
let res;
|
|
3374
|
-
try {
|
|
3375
|
-
res = await fetch(endpoint, {
|
|
3376
|
-
method: "POST",
|
|
3377
|
-
headers,
|
|
3378
|
-
body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: true }),
|
|
3379
|
-
signal
|
|
3380
|
-
});
|
|
3381
|
-
} catch (err) {
|
|
3382
|
-
if (signal?.aborted) return;
|
|
3383
|
-
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
3384
|
-
return;
|
|
3385
|
-
}
|
|
3386
|
-
if (!res.ok) {
|
|
3387
|
-
const { code, message } = await providerError(res);
|
|
3388
|
-
emit("wf:error", { code, message });
|
|
3389
|
-
return;
|
|
3390
|
-
}
|
|
3391
|
-
let content = "";
|
|
3392
|
-
let reasoning = "";
|
|
3393
|
-
const chunks = [];
|
|
3394
|
-
let usage;
|
|
3395
|
-
try {
|
|
3396
|
-
for await (const chunk of parseProviderSse(res.body)) {
|
|
3397
|
-
if (signal?.aborted) return;
|
|
3398
|
-
const delta = chunk.choices[0]?.delta;
|
|
3399
|
-
if (delta?.content) {
|
|
3400
|
-
content += delta.content;
|
|
3401
|
-
emit("wf:token", { text: delta.content });
|
|
3402
|
-
}
|
|
3403
|
-
if (delta?.reasoning_content) reasoning += delta.reasoning_content;
|
|
3404
|
-
if (delta?.tool_calls) chunks.push(chunk);
|
|
3405
|
-
if (chunk.usage) usage = chunk.usage;
|
|
3406
|
-
}
|
|
3407
|
-
} catch (err) {
|
|
3408
|
-
if (signal?.aborted) return;
|
|
3409
|
-
emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
3410
|
-
return;
|
|
3411
|
-
}
|
|
3412
|
-
const toolCalls = aggregateToolCalls(chunks);
|
|
3413
|
-
for (const tc of toolCalls) {
|
|
3414
|
-
emit("wf:tool_call", {
|
|
3415
|
-
id: tc.id,
|
|
3416
|
-
name: tc.function?.name ?? "",
|
|
3417
|
-
args: safeParseArgs(tc.function?.arguments ?? "")
|
|
3418
|
-
});
|
|
3419
|
-
}
|
|
3420
|
-
if (usage && stepOpts.emitUsage !== false) emit("wf:usage", usage);
|
|
3421
|
-
stepOpts.onFinish?.({ content, reasoning_content: reasoning || void 0, toolCalls, usage });
|
|
3422
|
-
}
|
|
3423
|
-
function sse(run, options) {
|
|
3424
|
-
const controller = new AbortController();
|
|
3425
|
-
const external = options?.signal;
|
|
3426
|
-
if (external) {
|
|
3427
|
-
if (external.aborted) controller.abort();
|
|
3428
|
-
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3429
|
-
}
|
|
3430
|
-
return sseResponse(run, { onAbort: () => controller.abort() });
|
|
3431
|
-
}
|
|
3432
|
-
return {
|
|
3433
|
-
chat,
|
|
3434
|
-
stream,
|
|
3435
|
-
sse,
|
|
3436
|
-
streamStep,
|
|
3437
|
-
waitApproval,
|
|
3438
|
-
approve,
|
|
3439
|
-
// embedding:未配置 provider 时明确抛 AiError(诚实裁剪:不静默降级)
|
|
3440
|
-
embed: (text) => embedding.embed(text),
|
|
3441
|
-
embedMany: (texts) => embedding.embedMany(texts)
|
|
3442
|
-
};
|
|
3443
|
-
}
|
|
3444
|
-
var DEFAULT_APPROVAL_TIMEOUT = 5 * 6e4;
|
|
3445
|
-
function safeParseArgs(raw) {
|
|
3446
|
-
if (!raw) return {};
|
|
3447
|
-
try {
|
|
3448
|
-
return JSON.parse(raw);
|
|
3449
|
-
} catch {
|
|
3450
|
-
return {};
|
|
3451
|
-
}
|
|
2881
|
+
var DEFAULT_APPROVAL_TIMEOUT = 5 * 6e4;
|
|
2882
|
+
function safeParseArgs(raw) {
|
|
2883
|
+
if (!raw) return {};
|
|
2884
|
+
try {
|
|
2885
|
+
return JSON.parse(raw);
|
|
2886
|
+
} catch {
|
|
2887
|
+
return {};
|
|
2888
|
+
}
|
|
3452
2889
|
}
|
|
3453
2890
|
|
|
3454
2891
|
// src/ai/agent.ts
|
|
@@ -3540,7 +2977,7 @@ function createAgent(client, config) {
|
|
|
3540
2977
|
if (signal?.aborted) return;
|
|
3541
2978
|
if (!finish.toolCalls?.length) {
|
|
3542
2979
|
emit("wf:usage", usage);
|
|
3543
|
-
emit("wf:done", { content: finish.content, usage });
|
|
2980
|
+
emit("wf:done", { content: finish.content, usage, reasoning: finish.reasoning_content });
|
|
3544
2981
|
return;
|
|
3545
2982
|
}
|
|
3546
2983
|
all.push({
|
|
@@ -3641,338 +3078,43 @@ var HtmlSafe = class {
|
|
|
3641
3078
|
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
3642
3079
|
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
3643
3080
|
|
|
3644
|
-
// src/ui-dom/
|
|
3645
|
-
var
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
return _enabled;
|
|
3650
|
-
}
|
|
3651
|
-
var _depth = 0;
|
|
3652
|
-
var counters = /* @__PURE__ */ new Map();
|
|
3653
|
-
function uiLog(label, msg, opts) {
|
|
3654
|
-
if (!uiDebugEnabled()) return;
|
|
3655
|
-
const n = (counters.get(label) ?? 0) + 1;
|
|
3656
|
-
counters.set(label, n);
|
|
3657
|
-
const first = 10;
|
|
3658
|
-
const every = opts?.throttle ?? 100;
|
|
3659
|
-
if (n > first && n % every !== 0) return;
|
|
3660
|
-
const d = opts?.depth !== void 0 ? opts.depth : _depth;
|
|
3661
|
-
const indent = " ".repeat(Math.min(d, 30));
|
|
3662
|
-
console.log(`[ui-debug] ${indent}${label}#${n} ${msg}`);
|
|
3081
|
+
// src/ui-dom/vdom/ssr.ts
|
|
3082
|
+
var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
3083
|
+
var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g", "text", "defs", "use", "clipPath"]);
|
|
3084
|
+
function escape(s) {
|
|
3085
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3663
3086
|
}
|
|
3664
|
-
|
|
3665
|
-
// src/ui-dom/reactive.ts
|
|
3666
|
-
function createReactiveState(dirty) {
|
|
3667
|
-
const _debugSets = /* @__PURE__ */ new Map();
|
|
3668
|
-
const _dbg = (key, label) => {
|
|
3669
|
-
const n = (_debugSets.get(key) ?? 0) + 1;
|
|
3670
|
-
_debugSets.set(key, n);
|
|
3671
|
-
if (n <= 5 || n % 100 === 0) uiLog(label, key + " count=" + n, { throttle: 100 });
|
|
3672
|
-
};
|
|
3673
|
-
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
3674
|
-
const watchers = /* @__PURE__ */ new Set();
|
|
3675
|
-
const reactive = (target) => {
|
|
3676
|
-
if (target === null || typeof target !== "object") return target;
|
|
3677
|
-
if (target instanceof Set || target instanceof Map) {
|
|
3678
|
-
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
3679
|
-
const MUTATING = target instanceof Set ? /* @__PURE__ */ new Set(["add", "delete", "clear"]) : /* @__PURE__ */ new Set(["set", "delete", "clear"]);
|
|
3680
|
-
const proxy2 = new Proxy(target, {
|
|
3681
|
-
get(t, prop) {
|
|
3682
|
-
const v = Reflect.get(t, prop);
|
|
3683
|
-
if (typeof v === "function") {
|
|
3684
|
-
if (!MUTATING.has(String(prop))) return v.bind(t);
|
|
3685
|
-
return (...args) => {
|
|
3686
|
-
const result = v.apply(t, args);
|
|
3687
|
-
dirty();
|
|
3688
|
-
for (const w of watchers) w();
|
|
3689
|
-
return result;
|
|
3690
|
-
};
|
|
3691
|
-
}
|
|
3692
|
-
return v;
|
|
3693
|
-
},
|
|
3694
|
-
set(t, key, value) {
|
|
3695
|
-
Reflect.set(t, key, value);
|
|
3696
|
-
_dbg(String(key), "set");
|
|
3697
|
-
dirty();
|
|
3698
|
-
for (const w of watchers) w();
|
|
3699
|
-
return true;
|
|
3700
|
-
}
|
|
3701
|
-
});
|
|
3702
|
-
proxyCache.set(target, proxy2);
|
|
3703
|
-
return proxy2;
|
|
3704
|
-
}
|
|
3705
|
-
if (target instanceof Date || target instanceof RegExp) return target;
|
|
3706
|
-
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
3707
|
-
const proxy = new Proxy(target, {
|
|
3708
|
-
set(target2, key, value) {
|
|
3709
|
-
const old = Reflect.get(target2, key);
|
|
3710
|
-
if (old === value) return true;
|
|
3711
|
-
Reflect.set(target2, key, value);
|
|
3712
|
-
dirty();
|
|
3713
|
-
for (const w of watchers) w();
|
|
3714
|
-
return true;
|
|
3715
|
-
},
|
|
3716
|
-
get(target2, key) {
|
|
3717
|
-
const value = Reflect.get(target2, key);
|
|
3718
|
-
if (typeof value === "object" && value !== null) return reactive(value);
|
|
3719
|
-
return value;
|
|
3720
|
-
},
|
|
3721
|
-
deleteProperty(target2, key) {
|
|
3722
|
-
if (Reflect.has(target2, key)) {
|
|
3723
|
-
Reflect.deleteProperty(target2, key);
|
|
3724
|
-
dirty();
|
|
3725
|
-
for (const w of watchers) w();
|
|
3726
|
-
}
|
|
3727
|
-
return true;
|
|
3728
|
-
}
|
|
3729
|
-
});
|
|
3730
|
-
proxyCache.set(target, proxy);
|
|
3731
|
-
return proxy;
|
|
3732
|
-
};
|
|
3733
|
-
const root = reactive({});
|
|
3734
|
-
Object.defineProperty(root, "__watch", {
|
|
3735
|
-
value: (cb) => {
|
|
3736
|
-
watchers.add(cb);
|
|
3737
|
-
return () => {
|
|
3738
|
-
watchers.delete(cb);
|
|
3739
|
-
};
|
|
3740
|
-
},
|
|
3741
|
-
writable: false,
|
|
3742
|
-
enumerable: false
|
|
3743
|
-
});
|
|
3744
|
-
return root;
|
|
3745
|
-
}
|
|
3746
|
-
|
|
3747
|
-
// src/ui/ssr.ts
|
|
3748
|
-
function escape(s) {
|
|
3749
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3750
|
-
}
|
|
3751
|
-
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
3752
|
-
"area",
|
|
3753
|
-
"base",
|
|
3754
|
-
"br",
|
|
3755
|
-
"col",
|
|
3756
|
-
"embed",
|
|
3757
|
-
"hr",
|
|
3758
|
-
"img",
|
|
3759
|
-
"input",
|
|
3760
|
-
"link",
|
|
3761
|
-
"meta",
|
|
3762
|
-
"param",
|
|
3763
|
-
"source",
|
|
3764
|
-
"track",
|
|
3765
|
-
"wbr"
|
|
3766
|
-
]);
|
|
3767
3087
|
function classToString(v) {
|
|
3768
3088
|
if (typeof v === "string") return v;
|
|
3769
3089
|
if (Array.isArray(v)) return v.filter(Boolean).join(" ");
|
|
3770
|
-
if (v && typeof v === "object")
|
|
3771
|
-
return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
|
|
3772
|
-
}
|
|
3090
|
+
if (v && typeof v === "object") return Object.entries(v).filter(([, on]) => on).map(([k]) => k).join(" ");
|
|
3773
3091
|
return "";
|
|
3774
3092
|
}
|
|
3775
3093
|
function styleToString(v) {
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
}
|
|
3782
|
-
function createSsrContext(serverCtx, dataStore) {
|
|
3783
|
-
const usedIds = /* @__PURE__ */ new Set();
|
|
3784
|
-
const ui2 = {
|
|
3785
|
-
$: () => createReactiveState(() => {
|
|
3786
|
-
}),
|
|
3787
|
-
dirty: () => {
|
|
3788
|
-
},
|
|
3789
|
-
render: () => {
|
|
3790
|
-
},
|
|
3791
|
-
selfId: (name) => {
|
|
3792
|
-
if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
|
|
3793
|
-
usedIds.add(name);
|
|
3794
|
-
},
|
|
3795
|
-
useMedia: () => {
|
|
3796
|
-
},
|
|
3797
|
-
useBreakpoint: () => {
|
|
3798
|
-
},
|
|
3799
|
-
usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
|
|
3800
|
-
} }),
|
|
3801
|
-
// 新原语族:SSR 确定性 no-op(不启动监听/会话/取数;组件挂载不崩即契约)
|
|
3802
|
-
useHoverCapable: () => false,
|
|
3803
|
-
useVisualViewport: () => ({ height: 0, offsetTop: 0, keyboardOpen: false }),
|
|
3804
|
-
useLongPress: () => ({}),
|
|
3805
|
-
useInView: () => ({ isIn: false, ready: false, observe: () => {
|
|
3806
|
-
}, refresh: () => {
|
|
3807
|
-
}, disconnect: () => {
|
|
3808
|
-
} }),
|
|
3809
|
-
useScrollPosition: () => ({ y: 0, refresh: () => {
|
|
3810
|
-
} }),
|
|
3811
|
-
useStableRef: (init) => (el) => {
|
|
3812
|
-
if (el) init?.(el);
|
|
3813
|
-
},
|
|
3814
|
-
useGlobalKey: () => () => {
|
|
3815
|
-
},
|
|
3816
|
-
useReducedMotion: () => false,
|
|
3817
|
-
useAnimationEnd: () => () => {
|
|
3818
|
-
},
|
|
3819
|
-
useTween: (target) => {
|
|
3820
|
-
const handle = { value: target, reset: (to) => {
|
|
3821
|
-
handle.value = to;
|
|
3822
|
-
} };
|
|
3823
|
-
return handle;
|
|
3824
|
-
},
|
|
3825
|
-
useDrag: () => ({ onPointerDown: () => {
|
|
3826
|
-
} }),
|
|
3827
|
-
useDragDrop: () => ({ dropProps: {}, dragProps: {} }),
|
|
3828
|
-
useControlled: (options) => ({
|
|
3829
|
-
value: options.value,
|
|
3830
|
-
setValue: (v) => {
|
|
3831
|
-
options.onChange?.(v);
|
|
3832
|
-
},
|
|
3833
|
-
controlled: options.value !== void 0
|
|
3834
|
-
}),
|
|
3835
|
-
useControlledInput: (options) => ({
|
|
3836
|
-
value: options.value,
|
|
3837
|
-
setValue: (v) => {
|
|
3838
|
-
options.onChange?.(v);
|
|
3839
|
-
},
|
|
3840
|
-
controlled: options.value !== void 0,
|
|
3841
|
-
keyword: "",
|
|
3842
|
-
setKeyword: () => {
|
|
3843
|
-
},
|
|
3844
|
-
selectedLabel: "",
|
|
3845
|
-
setSelectedLabel: () => {
|
|
3846
|
-
}
|
|
3847
|
-
}),
|
|
3848
|
-
useOpen: (options) => ({
|
|
3849
|
-
get open() {
|
|
3850
|
-
return !!options.open;
|
|
3851
|
-
},
|
|
3852
|
-
setOpen: (v) => {
|
|
3853
|
-
options.onOpenChange?.(v);
|
|
3854
|
-
},
|
|
3855
|
-
triggerProps: {}
|
|
3856
|
-
}),
|
|
3857
|
-
useAsync: () => ({ data: void 0, loading: true, error: void 0, reload: () => {
|
|
3858
|
-
} }),
|
|
3859
|
-
usePopup: () => ({
|
|
3860
|
-
open: false,
|
|
3861
|
-
setOpen: () => {
|
|
3862
|
-
},
|
|
3863
|
-
wrapProps: {},
|
|
3864
|
-
portal: () => null,
|
|
3865
|
-
refresh: () => {
|
|
3866
|
-
}
|
|
3867
|
-
}),
|
|
3868
|
-
usePresence: () => ({
|
|
3869
|
-
phase: "closed",
|
|
3870
|
-
ref: () => {
|
|
3871
|
-
},
|
|
3872
|
-
sync: (open2) => open2 ? "open" : "closed"
|
|
3873
|
-
}),
|
|
3874
|
-
useDialog: () => ({
|
|
3875
|
-
phase: "closed",
|
|
3876
|
-
rootRef: () => {
|
|
3877
|
-
},
|
|
3878
|
-
panelRef: () => {
|
|
3879
|
-
},
|
|
3880
|
-
sync: (open2) => open2 ? "open" : "closed"
|
|
3881
|
-
}),
|
|
3882
|
-
// SSR 确定性空态:会话不启动(无事件/无网络),仅保证挂载不崩
|
|
3883
|
-
useChat: () => ({
|
|
3884
|
-
messages: [],
|
|
3885
|
-
input: "",
|
|
3886
|
-
streaming: false,
|
|
3887
|
-
error: null,
|
|
3888
|
-
usage: null,
|
|
3889
|
-
step: null,
|
|
3890
|
-
send: () => {
|
|
3891
|
-
},
|
|
3892
|
-
stop: () => {
|
|
3893
|
-
},
|
|
3894
|
-
retry: () => {
|
|
3895
|
-
},
|
|
3896
|
-
clear: () => {
|
|
3897
|
-
},
|
|
3898
|
-
approve: async () => {
|
|
3899
|
-
},
|
|
3900
|
-
dispose: () => {
|
|
3901
|
-
}
|
|
3902
|
-
})
|
|
3903
|
-
};
|
|
3904
|
-
const browser = {
|
|
3905
|
-
activeElement: () => null,
|
|
3906
|
-
byId: () => null,
|
|
3907
|
-
query: () => null,
|
|
3908
|
-
createElement: () => null,
|
|
3909
|
-
bodyAppend: () => {
|
|
3910
|
-
},
|
|
3911
|
-
bodyRemove: () => {
|
|
3912
|
-
},
|
|
3913
|
-
copyText: async () => false,
|
|
3914
|
-
execCommand: () => false,
|
|
3915
|
-
selectionText: () => null,
|
|
3916
|
-
getSelection: () => null,
|
|
3917
|
-
queryCommandState: () => false,
|
|
3918
|
-
queryCommandValue: () => "",
|
|
3919
|
-
viewportHeight: () => 0,
|
|
3920
|
-
scrollTop: () => 0,
|
|
3921
|
-
hash: () => "",
|
|
3922
|
-
setHash: () => {
|
|
3923
|
-
},
|
|
3924
|
-
timeout: () => 0,
|
|
3925
|
-
rootElement: () => null,
|
|
3926
|
-
storageGet: () => null,
|
|
3927
|
-
storageSet: () => {
|
|
3928
|
-
}
|
|
3929
|
-
};
|
|
3930
|
-
const ctx = Object.create(serverCtx ?? {});
|
|
3931
|
-
ctx.ui = ui2;
|
|
3932
|
-
ctx.browser = browser;
|
|
3933
|
-
ctx.data = {
|
|
3934
|
-
async get(key, fetcher) {
|
|
3935
|
-
if (dataStore.has(key)) return dataStore.get(key);
|
|
3936
|
-
if (!fetcher) return void 0;
|
|
3937
|
-
const val = await fetcher();
|
|
3938
|
-
dataStore.set(key, val);
|
|
3939
|
-
return val;
|
|
3940
|
-
},
|
|
3941
|
-
set(key, val) {
|
|
3942
|
-
dataStore.set(key, val);
|
|
3943
|
-
},
|
|
3944
|
-
has(key) {
|
|
3945
|
-
return dataStore.has(key);
|
|
3946
|
-
}
|
|
3947
|
-
};
|
|
3948
|
-
return ctx;
|
|
3094
|
+
return Object.entries(v).filter(([, val]) => val != null).map(([k, val]) => {
|
|
3095
|
+
const key = k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
3096
|
+
const value = typeof val === "number" && val !== 0 ? `${val}px` : String(val);
|
|
3097
|
+
return `${key}:${value}`;
|
|
3098
|
+
}).join(";");
|
|
3949
3099
|
}
|
|
3950
3100
|
async function renderSsr(input, ctx) {
|
|
3951
3101
|
if (input == null || typeof input === "boolean") return "";
|
|
3952
3102
|
if (typeof input === "string" || typeof input === "number") return escape(String(input));
|
|
3953
3103
|
if (Array.isArray(input)) {
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
return s;
|
|
3104
|
+
const parts = await Promise.all(input.map((c) => renderSsr(c, ctx)));
|
|
3105
|
+
return parts.join("");
|
|
3957
3106
|
}
|
|
3958
3107
|
const vnode = input;
|
|
3959
|
-
if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
|
|
3960
|
-
if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
|
|
3108
|
+
if (vnode.type === Portal || vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
|
|
3961
3109
|
if (typeof vnode.type === "function") {
|
|
3962
3110
|
const childCtx = Object.create(ctx);
|
|
3963
|
-
|
|
3964
|
-
try {
|
|
3965
|
-
renderFn = vnode.type(vnode.props ?? {}, childCtx);
|
|
3966
|
-
} catch (e) {
|
|
3967
|
-
throw e;
|
|
3968
|
-
}
|
|
3969
|
-
if (renderFn instanceof Promise) renderFn = await renderFn;
|
|
3111
|
+
const renderFn = await vnode.type(vnode.props ?? {}, childCtx);
|
|
3970
3112
|
if (typeof renderFn !== "function") {
|
|
3971
3113
|
throw new Error(
|
|
3972
3114
|
`Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
3973
3115
|
);
|
|
3974
3116
|
}
|
|
3975
|
-
return renderSsr(renderFn(vnode.props ?? {}), childCtx);
|
|
3117
|
+
return renderSsr(await renderFn(vnode.props ?? {}), childCtx);
|
|
3976
3118
|
}
|
|
3977
3119
|
const tag = vnode.type;
|
|
3978
3120
|
const props = vnode.props ?? {};
|
|
@@ -3995,6 +3137,10 @@ async function renderSsr(input, ctx) {
|
|
|
3995
3137
|
attrs.push(` style="${escape(styleToString(value))}"`);
|
|
3996
3138
|
continue;
|
|
3997
3139
|
}
|
|
3140
|
+
if (key === "draggable" || key === "contenteditable" || key === "spellcheck") {
|
|
3141
|
+
attrs.push(` ${key}="${value ? "true" : "false"}"`);
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3998
3144
|
if (value === true) {
|
|
3999
3145
|
attrs.push(` ${key}`);
|
|
4000
3146
|
continue;
|
|
@@ -4003,21 +3149,134 @@ async function renderSsr(input, ctx) {
|
|
|
4003
3149
|
attrs.push(` ${key}="${escape(String(value))}"`);
|
|
4004
3150
|
}
|
|
4005
3151
|
const attrStr = attrs.join("");
|
|
3152
|
+
const svgWrap = SVG_TAGS.has(tag);
|
|
4006
3153
|
if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
|
|
4007
3154
|
if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
|
|
4008
3155
|
const children = await renderSsr(props.children, ctx);
|
|
3156
|
+
if (svgWrap) return `<${tag}${attrStr}>${children}</${tag}>`;
|
|
4009
3157
|
return `<${tag}${attrStr}>${children}</${tag}>`;
|
|
4010
3158
|
}
|
|
4011
|
-
|
|
4012
|
-
const
|
|
4013
|
-
|
|
4014
|
-
const
|
|
4015
|
-
return
|
|
3159
|
+
function createSsrUi() {
|
|
3160
|
+
const noop = () => () => {
|
|
3161
|
+
};
|
|
3162
|
+
const noopReturn = () => ({});
|
|
3163
|
+
return {
|
|
3164
|
+
_selfId: "_wf_root",
|
|
3165
|
+
render: () => {
|
|
3166
|
+
},
|
|
3167
|
+
selfId: () => {
|
|
3168
|
+
},
|
|
3169
|
+
bumpCtxVersion: () => {
|
|
3170
|
+
},
|
|
3171
|
+
setMounting: () => {
|
|
3172
|
+
},
|
|
3173
|
+
endMounting: () => {
|
|
3174
|
+
},
|
|
3175
|
+
// hooks no-op(组件 SSR 安全——不注册监听/定时器)
|
|
3176
|
+
useChat: () => ({ messages: [], input: "", streaming: false, error: null, usage: null, step: null, send: () => {
|
|
3177
|
+
}, stop: () => {
|
|
3178
|
+
}, retry: () => {
|
|
3179
|
+
}, clear: () => {
|
|
3180
|
+
}, approve: () => {
|
|
3181
|
+
}, subscribe: () => () => {
|
|
3182
|
+
} }),
|
|
3183
|
+
useExternal: (store) => store,
|
|
3184
|
+
useMedia: noop,
|
|
3185
|
+
useBreakpoint: noop,
|
|
3186
|
+
usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
|
|
3187
|
+
} }),
|
|
3188
|
+
useHoverCapable: () => false,
|
|
3189
|
+
useStableRef: (init) => (el) => {
|
|
3190
|
+
if (el) init?.(el);
|
|
3191
|
+
},
|
|
3192
|
+
useVisualViewport: () => ({ height: 0, offsetTop: 0, keyboardOpen: false }),
|
|
3193
|
+
usePopup: () => ({ open: false, setOpen: () => {
|
|
3194
|
+
}, phase: "closed", sync: (open2) => open2 ? "open" : "closed", wrapProps: {}, portal: (content) => content, refresh: () => {
|
|
3195
|
+
} }),
|
|
3196
|
+
useLongPress: noopReturn,
|
|
3197
|
+
useInView: () => ({ isIn: false, ready: false, observe: () => {
|
|
3198
|
+
}, refresh: () => {
|
|
3199
|
+
}, disconnect: () => {
|
|
3200
|
+
} }),
|
|
3201
|
+
useScrollPosition: () => ({ y: 0, refresh: () => {
|
|
3202
|
+
} }),
|
|
3203
|
+
useAsync: () => ({ data: void 0, loading: true, error: void 0, reload: () => {
|
|
3204
|
+
} }),
|
|
3205
|
+
useControlled: () => ({ value: void 0, setValue: () => {
|
|
3206
|
+
}, controlled: false }),
|
|
3207
|
+
useControlledInput: () => ({ value: void 0, setValue: () => {
|
|
3208
|
+
}, keyword: "", setKeyword: () => {
|
|
3209
|
+
}, selectedLabel: "", setSelectedLabel: () => {
|
|
3210
|
+
}, controlled: false }),
|
|
3211
|
+
useOpen: () => ({ open: false, setOpen: () => {
|
|
3212
|
+
}, triggerProps: {} }),
|
|
3213
|
+
usePresence: () => ({ phase: "closed", ref: () => {
|
|
3214
|
+
}, sync: () => "closed" }),
|
|
3215
|
+
useGlobalKey: () => () => {
|
|
3216
|
+
},
|
|
3217
|
+
useDrag: noopReturn,
|
|
3218
|
+
useDragDrop: () => ({ dropProps: {}, dragProps: {} }),
|
|
3219
|
+
useReducedMotion: () => true,
|
|
3220
|
+
// SSR 无动画——组件直落终值
|
|
3221
|
+
useAnimationEnd: () => () => {
|
|
3222
|
+
},
|
|
3223
|
+
useTween: (t) => {
|
|
3224
|
+
let v = t;
|
|
3225
|
+
return {
|
|
3226
|
+
get value() {
|
|
3227
|
+
return v;
|
|
3228
|
+
},
|
|
3229
|
+
set value(n) {
|
|
3230
|
+
v = n;
|
|
3231
|
+
},
|
|
3232
|
+
reset: (n) => {
|
|
3233
|
+
if (n !== void 0) v = n;
|
|
3234
|
+
return v;
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
function createSsrContext(serverCtx, dataStore) {
|
|
3241
|
+
const ctx = {
|
|
3242
|
+
...serverCtx,
|
|
3243
|
+
ui: createSsrUi(),
|
|
3244
|
+
browser: void 0
|
|
3245
|
+
};
|
|
3246
|
+
const dataCache = /* @__PURE__ */ new Map();
|
|
3247
|
+
ctx.data = {
|
|
3248
|
+
async get(key, fetcher) {
|
|
3249
|
+
const entry = dataCache.get(key);
|
|
3250
|
+
if (entry && "value" in entry) return entry.value;
|
|
3251
|
+
if (entry?.promise) return entry.promise;
|
|
3252
|
+
if (!fetcher) return void 0;
|
|
3253
|
+
const promise = Promise.resolve().then(() => fetcher()).then((val) => {
|
|
3254
|
+
dataCache.set(key, { value: val });
|
|
3255
|
+
dataStore.set(key, val);
|
|
3256
|
+
return val;
|
|
3257
|
+
});
|
|
3258
|
+
dataCache.set(key, { promise });
|
|
3259
|
+
return promise;
|
|
3260
|
+
},
|
|
3261
|
+
set(key, value) {
|
|
3262
|
+
dataCache.set(key, { value });
|
|
3263
|
+
dataStore.set(key, value);
|
|
3264
|
+
},
|
|
3265
|
+
has(key) {
|
|
3266
|
+
return dataCache.has(key);
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
return ctx;
|
|
4016
3270
|
}
|
|
4017
3271
|
function serializeData(data) {
|
|
4018
3272
|
const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
|
|
4019
3273
|
return `<script>window.__DATA__=${json};</script>`;
|
|
4020
3274
|
}
|
|
3275
|
+
async function ssrToString(Comp, props, serverCtx, opts = {}) {
|
|
3276
|
+
const dataStore = opts.data ?? /* @__PURE__ */ new Map();
|
|
3277
|
+
const ctx = createSsrContext(serverCtx, dataStore);
|
|
3278
|
+
return renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
|
|
3279
|
+
}
|
|
4021
3280
|
|
|
4022
3281
|
// src/ui/index.ts
|
|
4023
3282
|
function escape2(s) {
|
|
@@ -4166,9 +3425,53 @@ function ui() {
|
|
|
4166
3425
|
}
|
|
4167
3426
|
|
|
4168
3427
|
// src/db/postgres/connection.ts
|
|
4169
|
-
import
|
|
3428
|
+
import net2 from "node:net";
|
|
4170
3429
|
import crypto2 from "node:crypto";
|
|
4171
3430
|
|
|
3431
|
+
// src/db/errors.ts
|
|
3432
|
+
var DbError = class extends Error {
|
|
3433
|
+
kind;
|
|
3434
|
+
code;
|
|
3435
|
+
cause;
|
|
3436
|
+
constructor(kind, message, options) {
|
|
3437
|
+
super(message);
|
|
3438
|
+
this.name = "DbError";
|
|
3439
|
+
this.kind = kind;
|
|
3440
|
+
this.code = options?.code;
|
|
3441
|
+
this.cause = options?.cause;
|
|
3442
|
+
}
|
|
3443
|
+
};
|
|
3444
|
+
var ProtocolError = class extends DbError {
|
|
3445
|
+
constructor(feature, message) {
|
|
3446
|
+
super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
|
|
3447
|
+
this.name = "ProtocolError";
|
|
3448
|
+
}
|
|
3449
|
+
};
|
|
3450
|
+
var ConnectionError = class extends DbError {
|
|
3451
|
+
attempts;
|
|
3452
|
+
constructor(message, attempts = 1, cause) {
|
|
3453
|
+
super("connection", message, { cause });
|
|
3454
|
+
this.name = "ConnectionError";
|
|
3455
|
+
this.attempts = attempts;
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
var TimeoutError = class extends DbError {
|
|
3459
|
+
operation;
|
|
3460
|
+
ms;
|
|
3461
|
+
constructor(operation, ms) {
|
|
3462
|
+
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
3463
|
+
this.name = "TimeoutError";
|
|
3464
|
+
this.operation = operation;
|
|
3465
|
+
this.ms = ms;
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
var ValidationError = class extends DbError {
|
|
3469
|
+
constructor(message) {
|
|
3470
|
+
super("validation", message);
|
|
3471
|
+
this.name = "ValidationError";
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
|
|
4172
3475
|
// src/db/postgres/protocol.ts
|
|
4173
3476
|
function encodeMessage(type, payload) {
|
|
4174
3477
|
const out = new Uint8Array(1 + 4 + payload.length);
|
|
@@ -4377,7 +3680,7 @@ function parseRowDescription(payload) {
|
|
|
4377
3680
|
for (let c = 0; c < count; c++) {
|
|
4378
3681
|
let j = i;
|
|
4379
3682
|
while (payload[j] !== 0) j++;
|
|
4380
|
-
const name =
|
|
3683
|
+
const name = _decoder.decode(payload.subarray(i, j));
|
|
4381
3684
|
i = j + 1;
|
|
4382
3685
|
i += 6;
|
|
4383
3686
|
const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
@@ -4399,7 +3702,7 @@ function parseDataRow(payload) {
|
|
|
4399
3702
|
if (len === -1) {
|
|
4400
3703
|
values.push(null);
|
|
4401
3704
|
} else {
|
|
4402
|
-
values.push(
|
|
3705
|
+
values.push(_decoder.decode(payload.subarray(i, i + len)));
|
|
4403
3706
|
i += len;
|
|
4404
3707
|
}
|
|
4405
3708
|
}
|
|
@@ -4412,7 +3715,7 @@ function parseErrorFields(payload) {
|
|
|
4412
3715
|
const type = String.fromCharCode(payload[i]);
|
|
4413
3716
|
let j = i + 1;
|
|
4414
3717
|
while (j < payload.length && payload[j] !== 0) j++;
|
|
4415
|
-
const value =
|
|
3718
|
+
const value = _decoder.decode(payload.subarray(i + 1, j));
|
|
4416
3719
|
switch (type) {
|
|
4417
3720
|
case "S":
|
|
4418
3721
|
out.severity = value;
|
|
@@ -4437,10 +3740,10 @@ function parseErrorFields(payload) {
|
|
|
4437
3740
|
}
|
|
4438
3741
|
return out;
|
|
4439
3742
|
}
|
|
4440
|
-
var
|
|
4441
|
-
var
|
|
3743
|
+
var _decoder = new TextDecoder();
|
|
3744
|
+
var _encoder = new TextEncoder();
|
|
4442
3745
|
function utf8(s) {
|
|
4443
|
-
return
|
|
3746
|
+
return _encoder.encode(s);
|
|
4444
3747
|
}
|
|
4445
3748
|
function concat(...parts) {
|
|
4446
3749
|
let total = 0;
|
|
@@ -4486,7 +3789,7 @@ var PgConnection = class _PgConnection {
|
|
|
4486
3789
|
connect() {
|
|
4487
3790
|
return new Promise((resolve3, reject) => {
|
|
4488
3791
|
this.status = "connecting";
|
|
4489
|
-
const sock =
|
|
3792
|
+
const sock = net2.connect(this.opts.port, this.opts.host);
|
|
4490
3793
|
this.socket = sock;
|
|
4491
3794
|
const timeout = setTimeout(() => {
|
|
4492
3795
|
sock.destroy();
|
|
@@ -5657,151 +4960,668 @@ function createQueryBuilder(sql, exec) {
|
|
|
5657
4960
|
const [t, a] = table2.split(/\s+/);
|
|
5658
4961
|
return mkSelect(t, a);
|
|
5659
4962
|
}
|
|
5660
|
-
return mkSelect(table2, alias);
|
|
5661
|
-
},
|
|
5662
|
-
insert: (table2) => mkInsert(table2),
|
|
5663
|
-
update: (table2) => mkUpdate(table2),
|
|
5664
|
-
delete: (table2) => mkDelete(table2)
|
|
5665
|
-
};
|
|
5666
|
-
}
|
|
5667
|
-
|
|
5668
|
-
// src/postgres/client.ts
|
|
5669
|
-
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
5670
|
-
var traceStore = new AsyncLocalStorage();
|
|
5671
|
-
function postgres(options) {
|
|
5672
|
-
const opts = typeof options === "string" ? { connection: options } : options ?? {};
|
|
5673
|
-
const connection = opts.connection ?? process.env.DATABASE_URL;
|
|
5674
|
-
if (!connection) {
|
|
5675
|
-
throw new Error(
|
|
5676
|
-
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
4963
|
+
return mkSelect(table2, alias);
|
|
4964
|
+
},
|
|
4965
|
+
insert: (table2) => mkInsert(table2),
|
|
4966
|
+
update: (table2) => mkUpdate(table2),
|
|
4967
|
+
delete: (table2) => mkDelete(table2)
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4971
|
+
// src/postgres/client.ts
|
|
4972
|
+
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
4973
|
+
var traceStore = new AsyncLocalStorage();
|
|
4974
|
+
function postgres(options) {
|
|
4975
|
+
const opts = typeof options === "string" ? { connection: options } : options ?? {};
|
|
4976
|
+
const connection = opts.connection ?? process.env.DATABASE_URL;
|
|
4977
|
+
if (!connection) {
|
|
4978
|
+
throw new Error(
|
|
4979
|
+
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
4980
|
+
);
|
|
4981
|
+
}
|
|
4982
|
+
const u = new URL(connection);
|
|
4983
|
+
const pool = new PgPool({
|
|
4984
|
+
host: u.hostname,
|
|
4985
|
+
port: Number(u.port || 5432),
|
|
4986
|
+
user: decodeURIComponent(u.username),
|
|
4987
|
+
password: decodeURIComponent(u.password),
|
|
4988
|
+
database: u.pathname.replace(/^\//, ""),
|
|
4989
|
+
poolSize: opts.max ?? opts.poolSize ?? 10,
|
|
4990
|
+
acquireTimeoutMs: opts.acquireTimeoutMs,
|
|
4991
|
+
statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
|
|
4992
|
+
// onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
|
|
4993
|
+
onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
|
|
4994
|
+
const tid = traceStore.getStore();
|
|
4995
|
+
opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
|
|
4996
|
+
} : void 0
|
|
4997
|
+
});
|
|
4998
|
+
const sql = makeSql(pool);
|
|
4999
|
+
const mw = ((req, ctx, next) => {
|
|
5000
|
+
ctx.sql = sql;
|
|
5001
|
+
return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
|
|
5002
|
+
});
|
|
5003
|
+
mw.__meta = { injects: ["sql"], depends: [] };
|
|
5004
|
+
mw.sql = sql;
|
|
5005
|
+
mw.migrate = async () => {
|
|
5006
|
+
await sql.unsafe(`
|
|
5007
|
+
CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
|
|
5008
|
+
name TEXT PRIMARY KEY,
|
|
5009
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
5010
|
+
)
|
|
5011
|
+
`);
|
|
5012
|
+
};
|
|
5013
|
+
mw.markMigrated = async (moduleName) => {
|
|
5014
|
+
await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
|
|
5015
|
+
moduleName
|
|
5016
|
+
]);
|
|
5017
|
+
};
|
|
5018
|
+
mw.isMigrated = async (moduleName) => {
|
|
5019
|
+
const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
|
|
5020
|
+
return rows.length > 0;
|
|
5021
|
+
};
|
|
5022
|
+
mw.transaction = ((fn) => pool.begin(fn));
|
|
5023
|
+
mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
|
|
5024
|
+
mw.close = () => pool.close();
|
|
5025
|
+
return mw;
|
|
5026
|
+
}
|
|
5027
|
+
var TaggedQuery = class {
|
|
5028
|
+
text;
|
|
5029
|
+
params;
|
|
5030
|
+
executor;
|
|
5031
|
+
constructor(text, params, executor) {
|
|
5032
|
+
this.text = text;
|
|
5033
|
+
this.params = params;
|
|
5034
|
+
this.executor = executor;
|
|
5035
|
+
}
|
|
5036
|
+
then(resolve3, reject) {
|
|
5037
|
+
return this.executor(this.text, this.params).then(resolve3, reject);
|
|
5038
|
+
}
|
|
5039
|
+
catch(reject) {
|
|
5040
|
+
return this.executor(this.text, this.params).catch(reject);
|
|
5041
|
+
}
|
|
5042
|
+
finally(fn) {
|
|
5043
|
+
return this.executor(this.text, this.params).finally(fn);
|
|
5044
|
+
}
|
|
5045
|
+
/** 嵌套片段(agent-platform 条件过滤模式) */
|
|
5046
|
+
get __fragment() {
|
|
5047
|
+
return { sql: this.text, params: this.params };
|
|
5048
|
+
}
|
|
5049
|
+
};
|
|
5050
|
+
function makeSql(pool) {
|
|
5051
|
+
const sql = ((strings, ...values) => {
|
|
5052
|
+
const { sql: text, params } = parseTaggedFromPool(strings, values);
|
|
5053
|
+
return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
|
|
5054
|
+
});
|
|
5055
|
+
sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
|
|
5056
|
+
sql.query = createQueryBuilder(sql, async (q) => {
|
|
5057
|
+
const { sql: text, params: p } = compileQuery(q);
|
|
5058
|
+
return wrapError(pool.query(text, p));
|
|
5059
|
+
});
|
|
5060
|
+
sql.raw = (strings, ...values) => rawSql(
|
|
5061
|
+
strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ""), ""),
|
|
5062
|
+
values
|
|
5063
|
+
);
|
|
5064
|
+
sql.close = () => pool.close();
|
|
5065
|
+
return sql;
|
|
5066
|
+
}
|
|
5067
|
+
function parseTaggedFromPool(strings, values) {
|
|
5068
|
+
let sql = strings[0];
|
|
5069
|
+
const params = [];
|
|
5070
|
+
for (let i = 0; i < values.length; i++) {
|
|
5071
|
+
const frag = values[i]?.__fragment;
|
|
5072
|
+
if (frag) {
|
|
5073
|
+
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
5074
|
+
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
5075
|
+
return `$${params.length}`;
|
|
5076
|
+
});
|
|
5077
|
+
sql += renumbered + strings[i + 1];
|
|
5078
|
+
} else {
|
|
5079
|
+
params.push(values[i]);
|
|
5080
|
+
sql += `$${params.length}` + strings[i + 1];
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
return { sql, params };
|
|
5084
|
+
}
|
|
5085
|
+
var PG_ERROR_MAP = {
|
|
5086
|
+
"23505": 409,
|
|
5087
|
+
// unique_violation
|
|
5088
|
+
"23503": 400,
|
|
5089
|
+
// foreign_key_violation
|
|
5090
|
+
"23502": 400,
|
|
5091
|
+
// not_null_violation
|
|
5092
|
+
"23514": 400,
|
|
5093
|
+
// check_violation
|
|
5094
|
+
"22P02": 400,
|
|
5095
|
+
// invalid_text_representation
|
|
5096
|
+
"22003": 400
|
|
5097
|
+
// numeric_value_out_of_range
|
|
5098
|
+
};
|
|
5099
|
+
function wrapError(promise) {
|
|
5100
|
+
return promise.catch((err) => {
|
|
5101
|
+
const code = err?.code;
|
|
5102
|
+
if (code && PG_ERROR_MAP[code]) {
|
|
5103
|
+
throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
|
|
5104
|
+
}
|
|
5105
|
+
throw err;
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
// src/db/redis/connection.ts
|
|
5110
|
+
import net3 from "node:net";
|
|
5111
|
+
|
|
5112
|
+
// src/db/redis/resp.ts
|
|
5113
|
+
var RespError = class extends DbError {
|
|
5114
|
+
constructor(message) {
|
|
5115
|
+
super("protocol", message, { code: "RESP" });
|
|
5116
|
+
this.name = "RespError";
|
|
5117
|
+
}
|
|
5118
|
+
};
|
|
5119
|
+
var IncompleteError = class extends Error {
|
|
5120
|
+
constructor() {
|
|
5121
|
+
super("incomplete RESP message");
|
|
5122
|
+
this.name = "IncompleteError";
|
|
5123
|
+
}
|
|
5124
|
+
};
|
|
5125
|
+
function decodeValue(v, decodeBytes) {
|
|
5126
|
+
if (v instanceof Uint8Array) return decodeBytes ? _decoder2.decode(v) : v;
|
|
5127
|
+
if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
|
|
5128
|
+
return v;
|
|
5129
|
+
}
|
|
5130
|
+
function encodeCommand(args) {
|
|
5131
|
+
const lens = new Array(args.length);
|
|
5132
|
+
let total = headerLen(args.length);
|
|
5133
|
+
for (let i = 0; i < args.length; i++) {
|
|
5134
|
+
const arg = args[i];
|
|
5135
|
+
const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
|
|
5136
|
+
lens[i] = len;
|
|
5137
|
+
total += headerLen(len) + len + 2;
|
|
5138
|
+
}
|
|
5139
|
+
const out = new Uint8Array(total);
|
|
5140
|
+
let off = 0;
|
|
5141
|
+
const head = `*${args.length}\r
|
|
5142
|
+
`;
|
|
5143
|
+
out.set(_encoder2.encode(head), 0);
|
|
5144
|
+
off = head.length;
|
|
5145
|
+
for (let i = 0; i < args.length; i++) {
|
|
5146
|
+
const arg = args[i];
|
|
5147
|
+
const len = lens[i];
|
|
5148
|
+
const lh = `$${len}\r
|
|
5149
|
+
`;
|
|
5150
|
+
out.set(_encoder2.encode(lh), off);
|
|
5151
|
+
off += lh.length;
|
|
5152
|
+
if (typeof arg === "string") {
|
|
5153
|
+
out.set(_encoder2.encode(arg), off);
|
|
5154
|
+
} else if (arg instanceof Buffer) {
|
|
5155
|
+
out.set(arg, off);
|
|
5156
|
+
} else {
|
|
5157
|
+
out.set(_encoder2.encode(String(arg)), off);
|
|
5158
|
+
}
|
|
5159
|
+
off += len;
|
|
5160
|
+
out[off] = 13;
|
|
5161
|
+
out[off + 1] = 10;
|
|
5162
|
+
off += 2;
|
|
5163
|
+
}
|
|
5164
|
+
return out;
|
|
5165
|
+
}
|
|
5166
|
+
function headerLen(n) {
|
|
5167
|
+
if (n < 10) return 4;
|
|
5168
|
+
if (n < 100) return 5;
|
|
5169
|
+
if (n < 1e3) return 6;
|
|
5170
|
+
if (n < 1e4) return 7;
|
|
5171
|
+
return 8;
|
|
5172
|
+
}
|
|
5173
|
+
var _decoder2 = new TextDecoder();
|
|
5174
|
+
var _encoder2 = new TextEncoder();
|
|
5175
|
+
var RespParser = class {
|
|
5176
|
+
buf = new Uint8Array(0);
|
|
5177
|
+
off = 0;
|
|
5178
|
+
/** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
|
|
5179
|
+
push(chunk) {
|
|
5180
|
+
this.append(chunk);
|
|
5181
|
+
const saved = this.off;
|
|
5182
|
+
try {
|
|
5183
|
+
const value = this.parseValue();
|
|
5184
|
+
this.compact();
|
|
5185
|
+
return { value, incomplete: false };
|
|
5186
|
+
} catch (e) {
|
|
5187
|
+
this.off = saved;
|
|
5188
|
+
if (e instanceof IncompleteError) return { value: null, incomplete: true };
|
|
5189
|
+
throw e;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
/** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
|
|
5193
|
+
pushAll(chunk) {
|
|
5194
|
+
this.append(chunk);
|
|
5195
|
+
const out = [];
|
|
5196
|
+
while (true) {
|
|
5197
|
+
const saved = this.off;
|
|
5198
|
+
try {
|
|
5199
|
+
out.push(this.parseValue());
|
|
5200
|
+
} catch (e) {
|
|
5201
|
+
this.off = saved;
|
|
5202
|
+
if (e instanceof IncompleteError) break;
|
|
5203
|
+
throw e;
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
if (out.length > 0) this.compact();
|
|
5207
|
+
return out;
|
|
5208
|
+
}
|
|
5209
|
+
/** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
|
|
5210
|
+
append(chunk) {
|
|
5211
|
+
if (chunk.length === 0) return;
|
|
5212
|
+
const rest = this.buf.length - this.off;
|
|
5213
|
+
if (rest === 0) {
|
|
5214
|
+
this.buf = chunk;
|
|
5215
|
+
this.off = 0;
|
|
5216
|
+
return;
|
|
5217
|
+
}
|
|
5218
|
+
const merged = new Uint8Array(rest + chunk.length);
|
|
5219
|
+
merged.set(this.buf.subarray(this.off), 0);
|
|
5220
|
+
merged.set(chunk, rest);
|
|
5221
|
+
this.buf = merged;
|
|
5222
|
+
this.off = 0;
|
|
5223
|
+
}
|
|
5224
|
+
/** 全部消费后压缩(释放底层) */
|
|
5225
|
+
compact() {
|
|
5226
|
+
if (this.off === this.buf.length) {
|
|
5227
|
+
this.buf = new Uint8Array(0);
|
|
5228
|
+
this.off = 0;
|
|
5229
|
+
} else if (this.off > 0) {
|
|
5230
|
+
this.buf = this.buf.subarray(this.off);
|
|
5231
|
+
this.off = 0;
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
parseValue() {
|
|
5235
|
+
if (this.buf.length - this.off < 3) throw new IncompleteError();
|
|
5236
|
+
const type = String.fromCharCode(this.buf[this.off]);
|
|
5237
|
+
this.off++;
|
|
5238
|
+
switch (type) {
|
|
5239
|
+
case "+": {
|
|
5240
|
+
const line = this.readLine();
|
|
5241
|
+
return line;
|
|
5242
|
+
}
|
|
5243
|
+
case "-": {
|
|
5244
|
+
const line = this.readLine();
|
|
5245
|
+
return new RespError(line);
|
|
5246
|
+
}
|
|
5247
|
+
case ":": {
|
|
5248
|
+
return this.readInt();
|
|
5249
|
+
}
|
|
5250
|
+
case "$": {
|
|
5251
|
+
const len = this.readInt();
|
|
5252
|
+
if (len === -1) return null;
|
|
5253
|
+
return this.readBulkBytes(len);
|
|
5254
|
+
}
|
|
5255
|
+
case "*": {
|
|
5256
|
+
const count = this.readInt();
|
|
5257
|
+
if (count === -1) return null;
|
|
5258
|
+
const items = [];
|
|
5259
|
+
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
5260
|
+
return items;
|
|
5261
|
+
}
|
|
5262
|
+
default:
|
|
5263
|
+
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
/**
|
|
5267
|
+
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
5268
|
+
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
5269
|
+
*/
|
|
5270
|
+
readInt() {
|
|
5271
|
+
const buf = this.buf;
|
|
5272
|
+
let i = this.off;
|
|
5273
|
+
let neg = false;
|
|
5274
|
+
if (i < buf.length && buf[i] === 45) {
|
|
5275
|
+
neg = true;
|
|
5276
|
+
i++;
|
|
5277
|
+
}
|
|
5278
|
+
let n = 0;
|
|
5279
|
+
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
|
|
5280
|
+
n = n * 10 + (buf[i] - 48);
|
|
5281
|
+
i++;
|
|
5282
|
+
}
|
|
5283
|
+
if (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
|
|
5284
|
+
this.off = i + 2;
|
|
5285
|
+
return neg ? -n : n;
|
|
5286
|
+
}
|
|
5287
|
+
throw new IncompleteError();
|
|
5288
|
+
}
|
|
5289
|
+
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
5290
|
+
readLine() {
|
|
5291
|
+
const idx = indexOfCRLF(this.buf, this.off);
|
|
5292
|
+
if (idx === -1) throw new IncompleteError();
|
|
5293
|
+
const line = _decoder2.decode(this.buf.subarray(this.off, idx));
|
|
5294
|
+
this.off = idx + 2;
|
|
5295
|
+
return line;
|
|
5296
|
+
}
|
|
5297
|
+
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
5298
|
+
readBulkBytes(len) {
|
|
5299
|
+
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
5300
|
+
const value = this.buf.subarray(this.off, this.off + len);
|
|
5301
|
+
this.off += len + 2;
|
|
5302
|
+
return value;
|
|
5303
|
+
}
|
|
5304
|
+
};
|
|
5305
|
+
function indexOfCRLF(buf, from = 0) {
|
|
5306
|
+
let i = buf.indexOf(13, from);
|
|
5307
|
+
while (i !== -1) {
|
|
5308
|
+
if (i + 1 < buf.length && buf[i + 1] === 10) return i;
|
|
5309
|
+
i = buf.indexOf(13, i + 1);
|
|
5310
|
+
}
|
|
5311
|
+
return -1;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
// src/db/redis/connection.ts
|
|
5315
|
+
var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
|
|
5316
|
+
var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
|
|
5317
|
+
var RedisConnection = class {
|
|
5318
|
+
ready = false;
|
|
5319
|
+
opts;
|
|
5320
|
+
socket = null;
|
|
5321
|
+
parser = new RespParser();
|
|
5322
|
+
pending = [];
|
|
5323
|
+
/** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
|
|
5324
|
+
pendingHead = 0;
|
|
5325
|
+
offlineQueue = [];
|
|
5326
|
+
subs = /* @__PURE__ */ new Map();
|
|
5327
|
+
psubs = /* @__PURE__ */ new Map();
|
|
5328
|
+
status = "idle";
|
|
5329
|
+
retries = 0;
|
|
5330
|
+
reconnectTimer = null;
|
|
5331
|
+
socketTimeoutTimer = null;
|
|
5332
|
+
connectPromise = null;
|
|
5333
|
+
closed = false;
|
|
5334
|
+
connectedOnce = false;
|
|
5335
|
+
constructor(options = {}) {
|
|
5336
|
+
this.opts = {
|
|
5337
|
+
host: options.host ?? "127.0.0.1",
|
|
5338
|
+
port: options.port ?? 6379,
|
|
5339
|
+
retryDelayMs: options.retryDelayMs ?? 100,
|
|
5340
|
+
maxRetries: options.maxRetries ?? 10,
|
|
5341
|
+
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
5342
|
+
maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
|
|
5343
|
+
commandTimeoutMs: options.commandTimeoutMs ?? 0,
|
|
5344
|
+
socketTimeoutMs: options.socketTimeoutMs ?? 0,
|
|
5345
|
+
onCommand: options.onCommand
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
5349
|
+
connect() {
|
|
5350
|
+
if (this.connectPromise) return this.connectPromise;
|
|
5351
|
+
this.closed = false;
|
|
5352
|
+
this.connectPromise = new Promise((resolve3, reject) => {
|
|
5353
|
+
this.openSocket();
|
|
5354
|
+
this.onceReady = () => resolve3();
|
|
5355
|
+
this.onceFailed = (err) => reject(err);
|
|
5356
|
+
}).finally(() => {
|
|
5357
|
+
this.connectPromise = null;
|
|
5358
|
+
this.onceReady = void 0;
|
|
5359
|
+
this.onceFailed = void 0;
|
|
5360
|
+
});
|
|
5361
|
+
return this.connectPromise;
|
|
5362
|
+
}
|
|
5363
|
+
onceReady;
|
|
5364
|
+
onceFailed;
|
|
5365
|
+
openSocket() {
|
|
5366
|
+
this.status = "connecting";
|
|
5367
|
+
const sock = net3.connect(this.opts.port, this.opts.host);
|
|
5368
|
+
this.socket = sock;
|
|
5369
|
+
sock.on("connect", () => {
|
|
5370
|
+
sock.setNoDelay(true);
|
|
5371
|
+
this.status = "ready";
|
|
5372
|
+
this.retries = 0;
|
|
5373
|
+
if (this.connectedOnce) {
|
|
5374
|
+
for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
|
|
5375
|
+
for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
|
|
5376
|
+
}
|
|
5377
|
+
this.connectedOnce = true;
|
|
5378
|
+
this.flushOffline();
|
|
5379
|
+
this.onceReady?.();
|
|
5380
|
+
});
|
|
5381
|
+
sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
|
|
5382
|
+
sock.on("error", (err) => {
|
|
5383
|
+
if (this.status === "connecting") {
|
|
5384
|
+
this.handleDisconnect(err);
|
|
5385
|
+
}
|
|
5386
|
+
});
|
|
5387
|
+
sock.on("close", () => {
|
|
5388
|
+
this.socket = null;
|
|
5389
|
+
if (this.status !== "closed" && this.status !== "idle") {
|
|
5390
|
+
this.handleDisconnect(new ConnectionError("redis: socket closed"));
|
|
5391
|
+
}
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
5394
|
+
handleDisconnect(err) {
|
|
5395
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5396
|
+
this.pending = [];
|
|
5397
|
+
this.pendingHead = 0;
|
|
5398
|
+
for (const p of queue2) p.reject(err);
|
|
5399
|
+
this.clearSocketTimeout();
|
|
5400
|
+
if (this.closed || this.status === "closed") return;
|
|
5401
|
+
this.retries++;
|
|
5402
|
+
this.status = "connecting";
|
|
5403
|
+
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
5404
|
+
this.status = "closed";
|
|
5405
|
+
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
5406
|
+
this.onceFailed?.(failErr);
|
|
5407
|
+
return;
|
|
5408
|
+
}
|
|
5409
|
+
const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
|
|
5410
|
+
this.reconnectTimer = setTimeout(() => {
|
|
5411
|
+
this.reconnectTimer = null;
|
|
5412
|
+
this.openSocket();
|
|
5413
|
+
}, delay);
|
|
5414
|
+
}
|
|
5415
|
+
onData(chunk) {
|
|
5416
|
+
try {
|
|
5417
|
+
const values = this.parser.pushAll(chunk);
|
|
5418
|
+
for (const raw of values) {
|
|
5419
|
+
const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
|
|
5420
|
+
if (typeof first === "string" && PUSH_TYPES.has(first)) {
|
|
5421
|
+
this.dispatchSubscribe(decodeValue(raw, true));
|
|
5422
|
+
continue;
|
|
5423
|
+
}
|
|
5424
|
+
const p = this.pending[this.pendingHead++];
|
|
5425
|
+
if (!p) break;
|
|
5426
|
+
if (p.timedOut) continue;
|
|
5427
|
+
if (raw instanceof RespError) p.reject(raw);
|
|
5428
|
+
else p.resolve(decodeValue(raw, !p.asBuffer));
|
|
5429
|
+
}
|
|
5430
|
+
if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
|
|
5431
|
+
this.pending = this.pending.slice(this.pendingHead);
|
|
5432
|
+
this.pendingHead = 0;
|
|
5433
|
+
}
|
|
5434
|
+
if (this.opts.socketTimeoutMs > 0) {
|
|
5435
|
+
if (this.pendingHead < this.pending.length) this.armSocketTimeout();
|
|
5436
|
+
else this.clearSocketTimeout();
|
|
5437
|
+
}
|
|
5438
|
+
} catch (e) {
|
|
5439
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5440
|
+
this.pending = [];
|
|
5441
|
+
this.pendingHead = 0;
|
|
5442
|
+
for (const q of queue2) q.reject(e);
|
|
5443
|
+
this.socket?.destroy();
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
/**
|
|
5447
|
+
* 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
|
|
5448
|
+
* opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
|
|
5449
|
+
*/
|
|
5450
|
+
command(name, ...args) {
|
|
5451
|
+
const last = args[args.length - 1];
|
|
5452
|
+
const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
|
|
5453
|
+
if (this.status === "ready" && this.socket && !this.socket.destroyed) {
|
|
5454
|
+
return this.sendNow(name, args, opts?.asBuffer);
|
|
5455
|
+
}
|
|
5456
|
+
if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
|
|
5457
|
+
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
5458
|
+
}
|
|
5459
|
+
if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
|
|
5460
|
+
return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
|
|
5461
|
+
}
|
|
5462
|
+
return new Promise((resolve3, reject) => {
|
|
5463
|
+
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
sendNow(name, args, asBuffer) {
|
|
5467
|
+
const start = performance.now();
|
|
5468
|
+
return new Promise((resolve3, reject) => {
|
|
5469
|
+
const p = {
|
|
5470
|
+
resolve: resolve3,
|
|
5471
|
+
reject,
|
|
5472
|
+
asBuffer,
|
|
5473
|
+
blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
|
|
5474
|
+
};
|
|
5475
|
+
this.armTimeout(p);
|
|
5476
|
+
this.pending.push(p);
|
|
5477
|
+
this.armSocketTimeout();
|
|
5478
|
+
this.socket.write(encodeCommand([name, ...args]), (err) => {
|
|
5479
|
+
if (err) {
|
|
5480
|
+
const idx = this.pending.indexOf(p);
|
|
5481
|
+
if (idx >= 0) this.pending.splice(idx, 1);
|
|
5482
|
+
p.reject(err instanceof Error ? err : new ConnectionError("redis: socket write failed"));
|
|
5483
|
+
}
|
|
5484
|
+
});
|
|
5485
|
+
}).then(
|
|
5486
|
+
(v) => {
|
|
5487
|
+
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
5488
|
+
return v;
|
|
5489
|
+
},
|
|
5490
|
+
(e) => {
|
|
5491
|
+
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
5492
|
+
throw e;
|
|
5493
|
+
}
|
|
5677
5494
|
);
|
|
5678
5495
|
}
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
|
|
5689
|
-
// onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
|
|
5690
|
-
onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
|
|
5691
|
-
const tid = traceStore.getStore();
|
|
5692
|
-
opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
|
|
5693
|
-
} : void 0
|
|
5694
|
-
});
|
|
5695
|
-
const sql = makeSql(pool);
|
|
5696
|
-
const mw = ((req, ctx, next) => {
|
|
5697
|
-
ctx.sql = sql;
|
|
5698
|
-
return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
|
|
5699
|
-
});
|
|
5700
|
-
mw.__meta = { injects: ["sql"], depends: [] };
|
|
5701
|
-
mw.sql = sql;
|
|
5702
|
-
mw.migrate = async () => {
|
|
5703
|
-
await sql.unsafe(`
|
|
5704
|
-
CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
|
|
5705
|
-
name TEXT PRIMARY KEY,
|
|
5706
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
5707
|
-
)
|
|
5708
|
-
`);
|
|
5709
|
-
};
|
|
5710
|
-
mw.markMigrated = async (moduleName) => {
|
|
5711
|
-
await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
|
|
5712
|
-
moduleName
|
|
5713
|
-
]);
|
|
5714
|
-
};
|
|
5715
|
-
mw.isMigrated = async (moduleName) => {
|
|
5716
|
-
const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
|
|
5717
|
-
return rows.length > 0;
|
|
5718
|
-
};
|
|
5719
|
-
mw.transaction = ((fn) => pool.begin(fn));
|
|
5720
|
-
mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
|
|
5721
|
-
mw.close = () => pool.close();
|
|
5722
|
-
return mw;
|
|
5723
|
-
}
|
|
5724
|
-
var TaggedQuery = class {
|
|
5725
|
-
text;
|
|
5726
|
-
params;
|
|
5727
|
-
executor;
|
|
5728
|
-
constructor(text, params, executor) {
|
|
5729
|
-
this.text = text;
|
|
5730
|
-
this.params = params;
|
|
5731
|
-
this.executor = executor;
|
|
5496
|
+
/** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
|
|
5497
|
+
armTimeout(p) {
|
|
5498
|
+
const ms = this.opts.commandTimeoutMs;
|
|
5499
|
+
if (ms <= 0) return;
|
|
5500
|
+
p.timer = setTimeout(() => {
|
|
5501
|
+
p.timedOut = true;
|
|
5502
|
+
if (p.blocking) p.resolve(null);
|
|
5503
|
+
else p.reject(new TimeoutError("redis: command timeout", ms));
|
|
5504
|
+
}, ms);
|
|
5732
5505
|
}
|
|
5733
|
-
|
|
5734
|
-
|
|
5506
|
+
/** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
|
|
5507
|
+
armSocketTimeout() {
|
|
5508
|
+
const ms = this.opts.socketTimeoutMs;
|
|
5509
|
+
if (ms <= 0) return;
|
|
5510
|
+
this.clearSocketTimeout();
|
|
5511
|
+
this.socketTimeoutTimer = setTimeout(() => {
|
|
5512
|
+
this.socketTimeoutTimer = null;
|
|
5513
|
+
const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
|
|
5514
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5515
|
+
this.pending = [];
|
|
5516
|
+
this.pendingHead = 0;
|
|
5517
|
+
for (const q of queue2) q.reject(err);
|
|
5518
|
+
this.clearSocketTimeout();
|
|
5519
|
+
this.socket?.destroy();
|
|
5520
|
+
}, ms);
|
|
5735
5521
|
}
|
|
5736
|
-
|
|
5737
|
-
|
|
5522
|
+
clearSocketTimeout() {
|
|
5523
|
+
if (this.socketTimeoutTimer) {
|
|
5524
|
+
clearTimeout(this.socketTimeoutTimer);
|
|
5525
|
+
this.socketTimeoutTimer = null;
|
|
5526
|
+
}
|
|
5738
5527
|
}
|
|
5739
|
-
|
|
5740
|
-
|
|
5528
|
+
flushOffline() {
|
|
5529
|
+
const queue2 = this.offlineQueue;
|
|
5530
|
+
this.offlineQueue = [];
|
|
5531
|
+
for (const q of queue2) {
|
|
5532
|
+
this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
|
|
5533
|
+
}
|
|
5741
5534
|
}
|
|
5742
|
-
/**
|
|
5743
|
-
|
|
5744
|
-
|
|
5535
|
+
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
|
5536
|
+
batch(payload, count) {
|
|
5537
|
+
if (this.status !== "ready" || !this.socket) {
|
|
5538
|
+
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
5539
|
+
}
|
|
5540
|
+
return new Promise((resolve3, reject) => {
|
|
5541
|
+
const results = [];
|
|
5542
|
+
const batchPending = [];
|
|
5543
|
+
let settled = false;
|
|
5544
|
+
const maybeResolve = () => {
|
|
5545
|
+
if (settled) return;
|
|
5546
|
+
if (results.length === count) {
|
|
5547
|
+
settled = true;
|
|
5548
|
+
if (batchTimer) clearTimeout(batchTimer);
|
|
5549
|
+
resolve3(results);
|
|
5550
|
+
}
|
|
5551
|
+
};
|
|
5552
|
+
for (let i = 0; i < count; i++) {
|
|
5553
|
+
const p = {
|
|
5554
|
+
// 正常响应与错误响应(RespError)都作为结果值收集——管道语义
|
|
5555
|
+
resolve: (v) => {
|
|
5556
|
+
results.push(v);
|
|
5557
|
+
maybeResolve();
|
|
5558
|
+
},
|
|
5559
|
+
reject: (e) => {
|
|
5560
|
+
results.push(e);
|
|
5561
|
+
maybeResolve();
|
|
5562
|
+
}
|
|
5563
|
+
};
|
|
5564
|
+
batchPending.push(p);
|
|
5565
|
+
this.pending.push(p);
|
|
5566
|
+
}
|
|
5567
|
+
let batchTimer;
|
|
5568
|
+
const ms = this.opts.commandTimeoutMs;
|
|
5569
|
+
if (ms > 0) {
|
|
5570
|
+
batchTimer = setTimeout(() => {
|
|
5571
|
+
settled = true;
|
|
5572
|
+
for (const p of batchPending) p.timedOut = true;
|
|
5573
|
+
reject(new TimeoutError("redis: batch timeout", ms));
|
|
5574
|
+
}, ms);
|
|
5575
|
+
}
|
|
5576
|
+
this.socket.write(payload);
|
|
5577
|
+
this.armSocketTimeout();
|
|
5578
|
+
});
|
|
5745
5579
|
}
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
let sql = strings[0];
|
|
5766
|
-
const params = [];
|
|
5767
|
-
for (let i = 0; i < values.length; i++) {
|
|
5768
|
-
const frag = values[i]?.__fragment;
|
|
5769
|
-
if (frag) {
|
|
5770
|
-
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
5771
|
-
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
5772
|
-
return `$${params.length}`;
|
|
5773
|
-
});
|
|
5774
|
-
sql += renumbered + strings[i + 1];
|
|
5775
|
-
} else {
|
|
5776
|
-
params.push(values[i]);
|
|
5777
|
-
sql += `$${params.length}` + strings[i + 1];
|
|
5580
|
+
/** 订阅频道:回调式(channel, message) */
|
|
5581
|
+
async subscribe(channel, fn) {
|
|
5582
|
+
this.subs.set(channel, fn);
|
|
5583
|
+
await this.command("SUBSCRIBE", channel);
|
|
5584
|
+
}
|
|
5585
|
+
/** 订阅模式:回调式(channel, message) */
|
|
5586
|
+
async psubscribe(pattern, fn) {
|
|
5587
|
+
this.psubs.set(pattern, fn);
|
|
5588
|
+
await this.command("PSUBSCRIBE", pattern);
|
|
5589
|
+
}
|
|
5590
|
+
/** 旁路分发订阅消息(RESP 数组路由到回调) */
|
|
5591
|
+
dispatchSubscribe(value) {
|
|
5592
|
+
const [type, a, b] = value;
|
|
5593
|
+
if (type === "message") {
|
|
5594
|
+
const fn = this.subs.get(a);
|
|
5595
|
+
fn?.(a, b);
|
|
5596
|
+
} else if (type === "pmessage") {
|
|
5597
|
+
const fn = this.psubs.get(a);
|
|
5598
|
+
fn?.(b, value[3] ?? "");
|
|
5778
5599
|
}
|
|
5779
5600
|
}
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
"23502": 400,
|
|
5788
|
-
// not_null_violation
|
|
5789
|
-
"23514": 400,
|
|
5790
|
-
// check_violation
|
|
5791
|
-
"22P02": 400,
|
|
5792
|
-
// invalid_text_representation
|
|
5793
|
-
"22003": 400
|
|
5794
|
-
// numeric_value_out_of_range
|
|
5795
|
-
};
|
|
5796
|
-
function wrapError(promise) {
|
|
5797
|
-
return promise.catch((err) => {
|
|
5798
|
-
const code = err?.code;
|
|
5799
|
-
if (code && PG_ERROR_MAP[code]) {
|
|
5800
|
-
throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
|
|
5601
|
+
/** 主动关闭——不再重连 */
|
|
5602
|
+
async close() {
|
|
5603
|
+
this.closed = true;
|
|
5604
|
+
this.status = "closed";
|
|
5605
|
+
if (this.reconnectTimer) {
|
|
5606
|
+
clearTimeout(this.reconnectTimer);
|
|
5607
|
+
this.reconnectTimer = null;
|
|
5801
5608
|
}
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5609
|
+
this.clearSocketTimeout();
|
|
5610
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
5611
|
+
this.pending = [];
|
|
5612
|
+
this.pendingHead = 0;
|
|
5613
|
+
for (const p of queue2) p.reject(new ConnectionError("redis: connection closed"));
|
|
5614
|
+
const oq = this.offlineQueue;
|
|
5615
|
+
this.offlineQueue = [];
|
|
5616
|
+
for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
|
|
5617
|
+
const sock = this.socket;
|
|
5618
|
+
this.socket = null;
|
|
5619
|
+
sock?.destroy();
|
|
5620
|
+
}
|
|
5621
|
+
get connected() {
|
|
5622
|
+
return this.status === "ready";
|
|
5623
|
+
}
|
|
5624
|
+
};
|
|
5805
5625
|
|
|
5806
5626
|
// src/db/redis/pipeline.ts
|
|
5807
5627
|
var RedisPipeline = class {
|