@yuhere/js-jsonrpc2 2.1.8 → 2.1.10

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.
@@ -8013,6 +8013,133 @@ function useMethodDetail(rpc, method) {
8013
8013
  return detail;
8014
8014
  }
8015
8015
  //#endregion
8016
+ //#region webui/src/api/metrics.ts
8017
+ /** 拉取 system.metrics;如果系统方法不可用(withSystemMethods=false),返回 undefined。 */
8018
+ async function fetchMetrics(rpc) {
8019
+ try {
8020
+ const raw = await rpc.call("system.metrics");
8021
+ if (!raw || typeof raw !== "object" || typeof raw.methods !== "object") return void 0;
8022
+ return raw;
8023
+ } catch {
8024
+ return;
8025
+ }
8026
+ }
8027
+ /** 在给定直方图上估算某个分位数(线性插值)。 */
8028
+ function quantileFromHistogram(h, q) {
8029
+ if (h.count === 0) return 0;
8030
+ const target = q * h.count;
8031
+ let prevLe = 0;
8032
+ let prevCount = 0;
8033
+ for (const b of h.buckets) {
8034
+ if (b.le === Number.POSITIVE_INFINITY) {
8035
+ const lo = prevLe;
8036
+ const hi = h.max;
8037
+ const span = b.count - prevCount || 1;
8038
+ const ratio = (target - prevCount) / span;
8039
+ return lo + (hi - lo) * ratio;
8040
+ }
8041
+ if (b.count >= target) {
8042
+ const lo = prevLe;
8043
+ const hi = b.le;
8044
+ const span = b.count - prevCount || 1;
8045
+ const ratio = (target - prevCount) / span;
8046
+ return lo + (hi - lo) * ratio;
8047
+ }
8048
+ prevLe = b.le;
8049
+ prevCount = b.count;
8050
+ }
8051
+ return h.max;
8052
+ }
8053
+ function deriveMethod(method, stats, prev, dtSeconds) {
8054
+ const totalErrors = Object.values(stats.errorsByCode).reduce((a, b) => a + b, 0);
8055
+ const opened = stats.streams.opened || 0;
8056
+ const errorRate = stats.calls > 0 ? totalErrors / stats.calls : 0;
8057
+ const callsPerSecond = prev && dtSeconds && dtSeconds > 0 ? Math.max(0, (stats.calls - prev.calls) / dtSeconds) : void 0;
8058
+ return {
8059
+ method,
8060
+ stats,
8061
+ ...callsPerSecond !== void 0 ? { callsPerSecond } : {},
8062
+ errorRate,
8063
+ p50: quantileFromHistogram(stats.duration, .5),
8064
+ p95: quantileFromHistogram(stats.duration, .95),
8065
+ p99: quantileFromHistogram(stats.duration, .99),
8066
+ streamCompletionRate: opened > 0 ? stats.streams.completed / opened : 0,
8067
+ streamAbortRate: opened > 0 ? stats.streams.aborted / opened : 0,
8068
+ ttfbP50: quantileFromHistogram(stats.streamTimeToFirstChunk, .5),
8069
+ ttfbP95: quantileFromHistogram(stats.streamTimeToFirstChunk, .95),
8070
+ avgChunksPerStream: opened > 0 ? stats.streamChunksTotal / (stats.streams.completed + stats.streams.failed + stats.streams.aborted || 1) : 0
8071
+ };
8072
+ }
8073
+ //#endregion
8074
+ //#region webui/src/hooks/useMetrics.ts
8075
+ function useMetrics(rpc, enabled) {
8076
+ const [snapshot, setSnapshot] = (0, import_react.useState)(void 0);
8077
+ const [previous, setPrevious] = (0, import_react.useState)(void 0);
8078
+ const [fetchedAt, setFetchedAt] = (0, import_react.useState)(void 0);
8079
+ const [loading, setLoading] = (0, import_react.useState)(false);
8080
+ const [error, setError] = (0, import_react.useState)(null);
8081
+ const [available, setAvailable] = (0, import_react.useState)(true);
8082
+ const [autoRefresh, setAutoRefresh] = (0, import_react.useState)(true);
8083
+ const [intervalMs, setIntervalMs] = (0, import_react.useState)(3e3);
8084
+ const inFlight = (0, import_react.useRef)(false);
8085
+ const refresh = (0, import_react.useCallback)(async () => {
8086
+ if (inFlight.current) return;
8087
+ inFlight.current = true;
8088
+ setLoading(true);
8089
+ setError(null);
8090
+ try {
8091
+ const next = await fetchMetrics(rpc);
8092
+ if (!next) {
8093
+ setAvailable(false);
8094
+ return;
8095
+ }
8096
+ setAvailable(true);
8097
+ const now = Date.now();
8098
+ setSnapshot((curr) => {
8099
+ if (curr) setPrevious({
8100
+ snapshot: curr,
8101
+ fetchedAt: fetchedAt ?? now
8102
+ });
8103
+ return next;
8104
+ });
8105
+ setFetchedAt(now);
8106
+ } catch (e) {
8107
+ setError(e instanceof Error ? e.message : String(e));
8108
+ } finally {
8109
+ setLoading(false);
8110
+ inFlight.current = false;
8111
+ }
8112
+ }, [rpc, fetchedAt]);
8113
+ (0, import_react.useEffect)(() => {
8114
+ if (!enabled) return;
8115
+ refresh();
8116
+ }, [enabled, rpc]);
8117
+ (0, import_react.useEffect)(() => {
8118
+ if (!enabled || !autoRefresh || !available) return;
8119
+ const t = setInterval(() => void refresh(), intervalMs);
8120
+ return () => clearInterval(t);
8121
+ }, [
8122
+ enabled,
8123
+ autoRefresh,
8124
+ available,
8125
+ intervalMs,
8126
+ refresh
8127
+ ]);
8128
+ return {
8129
+ snapshot,
8130
+ previous,
8131
+ fetchedAt,
8132
+ loading,
8133
+ error,
8134
+ refresh,
8135
+ autoRefresh,
8136
+ setAutoRefresh,
8137
+ intervalMs,
8138
+ setIntervalMs,
8139
+ available
8140
+ };
8141
+ }
8142
+ //#endregion
8016
8143
  //#region node_modules/react/cjs/react-jsx-runtime.production.min.js
8017
8144
  /**
8018
8145
  * @license React
@@ -8026,6 +8153,7 @@ function useMethodDetail(rpc, method) {
8026
8153
  var require_react_jsx_runtime_production_min = /* @__PURE__ */ __commonJSMin(((exports) => {
8027
8154
  var f = require_react();
8028
8155
  var k = Symbol.for("react.element");
8156
+ var l = Symbol.for("react.fragment");
8029
8157
  var m = Object.prototype.hasOwnProperty;
8030
8158
  var n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
8031
8159
  var p = {
@@ -8050,6 +8178,7 @@ var require_react_jsx_runtime_production_min = /* @__PURE__ */ __commonJSMin(((e
8050
8178
  _owner: n.current
8051
8179
  };
8052
8180
  }
8181
+ exports.Fragment = l;
8053
8182
  exports.jsx = q;
8054
8183
  exports.jsxs = q;
8055
8184
  }));
@@ -8280,8 +8409,8 @@ var KIND_COLORS$1 = {
8280
8409
  record: "var(--syn-object)",
8281
8410
  union: "var(--syn-union)",
8282
8411
  enum: "var(--syn-union)",
8283
- nullable: "var(--syn-muted)",
8284
8412
  unknown: "var(--syn-muted)",
8413
+ null: "var(--syn-muted)",
8285
8414
  stream: "var(--syn-container)"
8286
8415
  };
8287
8416
  function colorOf$1(kind) {
@@ -8323,8 +8452,8 @@ function SchemaNode({ schema, depth, defaultOpen, label }) {
8323
8452
  children: "<stream>"
8324
8453
  }),
8325
8454
  schema.nullable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
8326
- className: "schema-nullable",
8327
- children: "| null"
8455
+ className: "schema-nullable-badge",
8456
+ children: "nullable"
8328
8457
  }),
8329
8458
  constraintsText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
8330
8459
  className: "schema-constraints",
@@ -8457,10 +8586,11 @@ function isSchema$1(v) {
8457
8586
  }
8458
8587
  /**
8459
8588
  * 根据 schema 生成一个合理的示例值。
8589
+ * - nullable 优先返回 null
8590
+ * - null 直接返回 null
8460
8591
  * - 原始类型返回零值(''、0、false)
8461
8592
  * - literal 返回固定值
8462
8593
  * - enum 返回第一个枚举值
8463
- * - nullable 返回 null
8464
8594
  * - 容器类型递归展开
8465
8595
  * - unknown 返回 null
8466
8596
  */
@@ -8471,12 +8601,12 @@ function sampleFromSchema(schema) {
8471
8601
  case "string": return "";
8472
8602
  case "number": return 0;
8473
8603
  case "boolean": return false;
8604
+ case "null": return null;
8474
8605
  case "literal": return schema.constraints?.value ?? null;
8475
8606
  case "enum": {
8476
8607
  const values = schema.constraints?.values;
8477
8608
  return Array.isArray(values) && values.length > 0 ? values[0] : "";
8478
8609
  }
8479
- case "nullable": return isSchema$1(c.inner) ? sampleFromSchema(c.inner) : null;
8480
8610
  case "array": return [isSchema$1(c.items) ? sampleFromSchema(c.items) : null];
8481
8611
  case "tuple": return (Array.isArray(c.items) ? c.items : []).map((it) => isSchema$1(it) ? sampleFromSchema(it) : null);
8482
8612
  case "union": {
@@ -8506,8 +8636,8 @@ var KIND_COLORS = {
8506
8636
  record: "var(--syn-object)",
8507
8637
  union: "var(--syn-union)",
8508
8638
  enum: "var(--syn-union)",
8509
- nullable: "var(--syn-muted)",
8510
- unknown: "var(--syn-muted)"
8639
+ unknown: "var(--syn-muted)",
8640
+ null: "var(--syn-muted)"
8511
8641
  };
8512
8642
  function colorOf(kind) {
8513
8643
  return KIND_COLORS[kind] ?? "var(--fg)";
@@ -8536,16 +8666,7 @@ function SchemaForm({ params, values, onChange, disabled }) {
8536
8666
  function SchemaField({ schema, value, onChange, label, depth = 0, defaultOpen = true, disabled }) {
8537
8667
  const [open, setOpen] = (0, import_react.useState)(defaultOpen);
8538
8668
  const kind = schema.kind;
8539
- if (kind === "nullable") {
8540
- const inner = schema.children && isSchema$1(schema.children.inner) ? schema.children.inner : void 0;
8541
- if (!inner) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UnknownLeaf, {
8542
- schema,
8543
- value,
8544
- onChange,
8545
- label,
8546
- depth,
8547
- disabled
8548
- });
8669
+ if (schema.nullable) {
8549
8670
  const isNull = value === null || value === void 0;
8550
8671
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
8551
8672
  className: "sf-node sf-row",
@@ -8559,9 +8680,17 @@ function SchemaField({ schema, value, onChange, label, depth = 0, defaultOpen =
8559
8680
  }),
8560
8681
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
8561
8682
  className: "sf-kind",
8562
- style: { color: colorOf("nullable") },
8683
+ style: { color: colorOf(kind) },
8684
+ children: kind
8685
+ }),
8686
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
8687
+ className: "sf-nullable-badge",
8563
8688
  children: "nullable"
8564
8689
  }),
8690
+ schema.description && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
8691
+ className: "sf-desc",
8692
+ children: ["— ", schema.description]
8693
+ }),
8565
8694
  !isNull && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
8566
8695
  type: "button",
8567
8696
  className: "tiny",
@@ -8573,7 +8702,10 @@ function SchemaField({ schema, value, onChange, label, depth = 0, defaultOpen =
8573
8702
  type: "button",
8574
8703
  className: "tiny",
8575
8704
  disabled,
8576
- onClick: () => onChange(sampleFromSchema(inner)),
8705
+ onClick: () => onChange(sampleFromSchema({
8706
+ ...schema,
8707
+ nullable: false
8708
+ })),
8577
8709
  children: "填值"
8578
8710
  })
8579
8711
  ]
@@ -8583,7 +8715,10 @@ function SchemaField({ schema, value, onChange, label, depth = 0, defaultOpen =
8583
8715
  className: "sf-null-indicator",
8584
8716
  children: "null"
8585
8717
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SchemaField, {
8586
- schema: inner,
8718
+ schema: {
8719
+ ...schema,
8720
+ nullable: false
8721
+ },
8587
8722
  value,
8588
8723
  onChange,
8589
8724
  depth: 0,
@@ -8705,7 +8840,7 @@ function SchemaField({ schema, value, onChange, label, depth = 0, defaultOpen =
8705
8840
  });
8706
8841
  }
8707
8842
  function isScalarKind(kind) {
8708
- return kind === "string" || kind === "number" || kind === "boolean" || kind === "enum" || kind === "literal";
8843
+ return kind === "string" || kind === "number" || kind === "boolean" || kind === "enum" || kind === "literal" || kind === "null";
8709
8844
  }
8710
8845
  function ScalarInput({ schema, value, onChange, disabled }) {
8711
8846
  switch (schema.kind) {
@@ -8748,6 +8883,16 @@ function ScalarInput({ schema, value, onChange, disabled }) {
8748
8883
  children: "(固定值)"
8749
8884
  })]
8750
8885
  });
8886
+ case "null": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
8887
+ className: "sf-scalar",
8888
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", {
8889
+ className: "literal-value",
8890
+ children: "null"
8891
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
8892
+ className: "hint",
8893
+ children: "(固定值)"
8894
+ })]
8895
+ });
8751
8896
  default: return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
8752
8897
  className: "sf-input",
8753
8898
  type: "text",
@@ -9275,6 +9420,449 @@ function formatError(e) {
9275
9420
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
9276
9421
  }
9277
9422
  //#endregion
9423
+ //#region webui/src/components/MetricsPanel.tsx
9424
+ function formatDuration(seconds) {
9425
+ if (!Number.isFinite(seconds) || seconds <= 0) return "—";
9426
+ if (seconds < .001) return `${(seconds * 1e6).toFixed(0)} µs`;
9427
+ if (seconds < 1) return `${(seconds * 1e3).toFixed(1)} ms`;
9428
+ if (seconds < 60) return `${seconds.toFixed(2)} s`;
9429
+ return `${(seconds / 60).toFixed(1)} min`;
9430
+ }
9431
+ function formatRate(v) {
9432
+ if (v === void 0) return "—";
9433
+ if (v >= 100) return v.toFixed(0);
9434
+ if (v >= 10) return v.toFixed(1);
9435
+ return v.toFixed(2);
9436
+ }
9437
+ function formatPercent(v) {
9438
+ if (!Number.isFinite(v)) return "—";
9439
+ return `${(v * 100).toFixed(1)}%`;
9440
+ }
9441
+ function rateClass(v) {
9442
+ if (v === void 0) return "";
9443
+ if (v > 100) return "rate-hot";
9444
+ if (v > 10) return "rate-warm";
9445
+ return "rate-cold";
9446
+ }
9447
+ function MetricsPanel({ snapshot, previous, fetchedAt, loading, error, available, onRefresh, autoRefresh, onAutoRefreshChange, intervalMs, onIntervalChange }) {
9448
+ const derived = (0, import_react.useMemo)(() => {
9449
+ if (!snapshot) return [];
9450
+ const dtSeconds = previous && fetchedAt ? (fetchedAt - previous.fetchedAt) / 1e3 : void 0;
9451
+ const rows = [];
9452
+ for (const [method, stats] of Object.entries(snapshot.methods)) {
9453
+ const prev = previous?.snapshot.methods[method];
9454
+ rows.push(deriveMethod(method, stats, prev, dtSeconds));
9455
+ }
9456
+ rows.sort((a, b) => b.stats.calls - a.stats.calls);
9457
+ return rows;
9458
+ }, [
9459
+ snapshot,
9460
+ previous,
9461
+ fetchedAt
9462
+ ]);
9463
+ const streamMethods = derived.filter((r) => r.stats.streams.opened > 0);
9464
+ const nonStreamMethods = derived.filter((r) => r.stats.streams.opened === 0);
9465
+ const totals = (0, import_react.useMemo)(() => {
9466
+ if (!snapshot) return {
9467
+ calls: 0,
9468
+ errors: 0,
9469
+ methods: 0,
9470
+ streams: 0
9471
+ };
9472
+ let calls = 0;
9473
+ let errors = 0;
9474
+ let streams = 0;
9475
+ for (const s of Object.values(snapshot.methods)) {
9476
+ calls += s.calls;
9477
+ errors += Object.values(s.errorsByCode).reduce((a, b) => a + b, 0);
9478
+ streams += s.streams.opened;
9479
+ }
9480
+ return {
9481
+ calls,
9482
+ errors,
9483
+ methods: Object.keys(snapshot.methods).length,
9484
+ streams
9485
+ };
9486
+ }, [snapshot]);
9487
+ if (!available) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
9488
+ className: "metrics",
9489
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Metrics" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
9490
+ className: "hint",
9491
+ children: [
9492
+ "服务端未启用 ",
9493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "system.metrics" }),
9494
+ "(",
9495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "withSystemMethods: false" }),
9496
+ " 或未注册)。"
9497
+ ]
9498
+ })]
9499
+ });
9500
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
9501
+ className: "metrics",
9502
+ children: [
9503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
9504
+ className: "metrics-header",
9505
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Metrics" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
9506
+ className: "metrics-controls",
9507
+ children: [
9508
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
9509
+ className: "metrics-auto",
9510
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
9511
+ type: "checkbox",
9512
+ checked: autoRefresh,
9513
+ onChange: (e) => onAutoRefreshChange(e.target.checked)
9514
+ }), "自动刷新"]
9515
+ }),
9516
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
9517
+ className: "sf-input",
9518
+ value: intervalMs,
9519
+ onChange: (e) => onIntervalChange(Number(e.target.value)),
9520
+ disabled: !autoRefresh,
9521
+ title: "刷新间隔",
9522
+ children: [
9523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
9524
+ value: 1e3,
9525
+ children: "1s"
9526
+ }),
9527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
9528
+ value: 3e3,
9529
+ children: "3s"
9530
+ }),
9531
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
9532
+ value: 5e3,
9533
+ children: "5s"
9534
+ }),
9535
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
9536
+ value: 1e4,
9537
+ children: "10s"
9538
+ })
9539
+ ]
9540
+ }),
9541
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
9542
+ onClick: onRefresh,
9543
+ disabled: loading,
9544
+ children: loading ? "刷新中…" : "立即刷新"
9545
+ }),
9546
+ snapshot && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
9547
+ className: "hint",
9548
+ children: ["uptime ", formatDuration(snapshot.uptimeMs / 1e3)]
9549
+ })
9550
+ ]
9551
+ })]
9552
+ }),
9553
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
9554
+ className: "error",
9555
+ children: error
9556
+ }),
9557
+ !snapshot && !error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
9558
+ className: "hint",
9559
+ children: "加载中…"
9560
+ }),
9561
+ snapshot && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
9562
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
9563
+ className: "metrics-cards",
9564
+ children: [
9565
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricCard, {
9566
+ label: "Methods",
9567
+ value: String(totals.methods)
9568
+ }),
9569
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricCard, {
9570
+ label: "Calls",
9571
+ value: String(totals.calls)
9572
+ }),
9573
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricCard, {
9574
+ label: "Streams",
9575
+ value: String(totals.streams)
9576
+ }),
9577
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricCard, {
9578
+ label: "Errors",
9579
+ value: String(totals.errors),
9580
+ tone: totals.errors > 0 ? "danger" : void 0
9581
+ }),
9582
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricCard, {
9583
+ label: "Error rate",
9584
+ value: totals.calls > 0 ? formatPercent(totals.errors / totals.calls) : "—",
9585
+ tone: totals.calls > 0 && totals.errors / totals.calls > .05 ? "danger" : void 0
9586
+ }),
9587
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricCard, {
9588
+ label: "Invalid requests",
9589
+ value: String(snapshot.invalidRequests),
9590
+ tone: snapshot.invalidRequests > 0 ? "warn" : void 0
9591
+ })
9592
+ ]
9593
+ }),
9594
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: ["Non-stream methods", /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
9595
+ className: "hint",
9596
+ children: [
9597
+ " (",
9598
+ nonStreamMethods.length,
9599
+ ")"
9600
+ ]
9601
+ })] }),
9602
+ nonStreamMethods.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
9603
+ className: "hint",
9604
+ children: "暂无数据。"
9605
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NonStreamTable, { rows: nonStreamMethods }),
9606
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: ["Stream methods", /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
9607
+ className: "hint",
9608
+ children: [
9609
+ " (",
9610
+ streamMethods.length,
9611
+ ")"
9612
+ ]
9613
+ })] }),
9614
+ streamMethods.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
9615
+ className: "hint",
9616
+ children: "暂无流式调用。"
9617
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StreamTable, { rows: streamMethods })
9618
+ ] })
9619
+ ]
9620
+ });
9621
+ }
9622
+ function MetricCard({ label, value, tone }) {
9623
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
9624
+ className: `metric-card${tone ? ` metric-card-${tone}` : ""}`,
9625
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
9626
+ className: "metric-card-label",
9627
+ children: label
9628
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
9629
+ className: "metric-card-value",
9630
+ children: value
9631
+ })]
9632
+ });
9633
+ }
9634
+ function NonStreamTable({ rows }) {
9635
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
9636
+ className: "metrics-table",
9637
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
9638
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { children: "method" }),
9639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9640
+ className: "num",
9641
+ children: "calls"
9642
+ }),
9643
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9644
+ className: "num",
9645
+ children: "rate/s"
9646
+ }),
9647
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9648
+ className: "num",
9649
+ children: "errors"
9650
+ }),
9651
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9652
+ className: "num",
9653
+ children: "err%"
9654
+ }),
9655
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9656
+ className: "num",
9657
+ children: "p50"
9658
+ }),
9659
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9660
+ className: "num",
9661
+ children: "p95"
9662
+ }),
9663
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9664
+ className: "num",
9665
+ children: "p99"
9666
+ }),
9667
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9668
+ className: "num",
9669
+ children: "avg"
9670
+ })
9671
+ ] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: rows.map((r) => {
9672
+ const errorCount = Object.values(r.stats.errorsByCode).reduce((a, b) => a + b, 0);
9673
+ const avg = r.stats.duration.count > 0 ? r.stats.duration.sum / r.stats.duration.count : 0;
9674
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
9675
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9676
+ className: "method-name",
9677
+ title: r.method,
9678
+ children: r.method
9679
+ }),
9680
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9681
+ className: "num",
9682
+ children: r.stats.calls
9683
+ }),
9684
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9685
+ className: `num ${rateClass(r.callsPerSecond)}`,
9686
+ children: formatRate(r.callsPerSecond)
9687
+ }),
9688
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9689
+ className: `num${errorCount > 0 ? " error" : ""}`,
9690
+ children: errorCount
9691
+ }),
9692
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9693
+ className: `num${r.errorRate > .05 ? " error" : ""}`,
9694
+ children: formatPercent(r.errorRate)
9695
+ }),
9696
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9697
+ className: "num",
9698
+ children: formatDuration(r.p50)
9699
+ }),
9700
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9701
+ className: "num",
9702
+ children: formatDuration(r.p95)
9703
+ }),
9704
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9705
+ className: "num",
9706
+ children: formatDuration(r.p99)
9707
+ }),
9708
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9709
+ className: "num",
9710
+ children: formatDuration(avg)
9711
+ })
9712
+ ] }, r.method);
9713
+ }) })]
9714
+ });
9715
+ }
9716
+ function StreamTable({ rows }) {
9717
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
9718
+ className: "metrics-table",
9719
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
9720
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { children: "method" }),
9721
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9722
+ className: "num",
9723
+ children: "opened"
9724
+ }),
9725
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9726
+ className: "num",
9727
+ children: "completed"
9728
+ }),
9729
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9730
+ className: "num",
9731
+ children: "failed"
9732
+ }),
9733
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9734
+ className: "num",
9735
+ children: "aborted"
9736
+ }),
9737
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9738
+ className: "num",
9739
+ children: "complete%"
9740
+ }),
9741
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9742
+ className: "num",
9743
+ children: "abort%"
9744
+ }),
9745
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9746
+ className: "num",
9747
+ children: "avg chunks"
9748
+ }),
9749
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9750
+ className: "num",
9751
+ children: "ttfb p50"
9752
+ }),
9753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9754
+ className: "num",
9755
+ children: "ttfb p95"
9756
+ }),
9757
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
9758
+ className: "num",
9759
+ children: "duration avg"
9760
+ })
9761
+ ] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: rows.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
9762
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9763
+ className: "method-name",
9764
+ title: r.method,
9765
+ children: r.method
9766
+ }),
9767
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9768
+ className: "num",
9769
+ children: r.stats.streams.opened
9770
+ }),
9771
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9772
+ className: "num",
9773
+ children: r.stats.streams.completed
9774
+ }),
9775
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9776
+ className: `num${r.stats.streams.failed > 0 ? " error" : ""}`,
9777
+ children: r.stats.streams.failed
9778
+ }),
9779
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9780
+ className: `num${r.stats.streams.aborted > 0 ? " warn" : ""}`,
9781
+ children: r.stats.streams.aborted
9782
+ }),
9783
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9784
+ className: "num",
9785
+ children: formatPercent(r.streamCompletionRate)
9786
+ }),
9787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9788
+ className: "num",
9789
+ children: formatPercent(r.streamAbortRate)
9790
+ }),
9791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9792
+ className: "num",
9793
+ children: r.avgChunksPerStream.toFixed(1)
9794
+ }),
9795
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9796
+ className: "num",
9797
+ children: formatDuration(r.ttfbP50)
9798
+ }),
9799
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9800
+ className: "num",
9801
+ children: formatDuration(r.ttfbP95)
9802
+ }),
9803
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
9804
+ className: "num",
9805
+ children: formatDuration(r.stats.streamDuration.count > 0 ? r.stats.streamDuration.sum / r.stats.streamDuration.count : 0)
9806
+ })
9807
+ ] }, r.method)) })]
9808
+ });
9809
+ }
9810
+ //#endregion
9811
+ //#region webui/src/api/glob.ts
9812
+ function globToRegExp(glob) {
9813
+ let re = "^";
9814
+ for (let i = 0; i < glob.length; i++) {
9815
+ const ch = glob[i];
9816
+ if (ch === "*") {
9817
+ while (i + 1 < glob.length && glob[i + 1] === "*") i++;
9818
+ re += ".*";
9819
+ } else if (ch === "?") re += ".";
9820
+ else if ("\\^$+{}()|[]".includes(ch)) re += "\\" + ch;
9821
+ else re += ch;
9822
+ }
9823
+ re += "$";
9824
+ return new RegExp(re);
9825
+ }
9826
+ function compileGlobFilter(input) {
9827
+ const raw = input ?? "";
9828
+ const trimmed = raw.trim();
9829
+ if (trimmed.length === 0) return {
9830
+ includes: [],
9831
+ excludes: [],
9832
+ raw,
9833
+ passthrough: true
9834
+ };
9835
+ const includes = [];
9836
+ const excludes = [];
9837
+ for (const part of trimmed.split(",")) {
9838
+ const pat = part.trim();
9839
+ if (pat.length === 0) continue;
9840
+ if (pat.startsWith("!")) {
9841
+ const body = pat.slice(1).trim();
9842
+ if (body.length > 0) excludes.push(globToRegExp(body));
9843
+ } else includes.push(globToRegExp(pat));
9844
+ }
9845
+ return {
9846
+ includes,
9847
+ excludes,
9848
+ raw,
9849
+ passthrough: false
9850
+ };
9851
+ }
9852
+ function matchMethod(filter, method) {
9853
+ if (filter.passthrough) return true;
9854
+ if (filter.excludes.some((re) => re.test(method))) return false;
9855
+ if (filter.includes.length === 0) return true;
9856
+ return filter.includes.some((re) => re.test(method));
9857
+ }
9858
+ /** 便捷:直接对 MetricsSnapshot 的 methods 做过滤,返回浅拷贝。 */
9859
+ function filterSnapshotMethods(filter, methods) {
9860
+ if (filter.passthrough) return methods;
9861
+ const out = {};
9862
+ for (const [method, stats] of Object.entries(methods)) if (matchMethod(filter, method)) out[method] = stats;
9863
+ return out;
9864
+ }
9865
+ //#endregion
9278
9866
  //#region webui/src/api/docGen.ts
9279
9867
  function isSchema(v) {
9280
9868
  return !!v && typeof v === "object" && typeof v.kind === "string";
@@ -9411,17 +9999,25 @@ function App() {
9411
9999
  const [transport, setTransport] = (0, import_react.useState)("http");
9412
10000
  const [selected, setSelected] = (0, import_react.useState)("");
9413
10001
  const [generatingDocs, setGeneratingDocs] = (0, import_react.useState)(false);
9414
- const rpc = (0, import_react.useMemo)(() => {
9415
- return createRpc({
9416
- transport,
9417
- endpoint: ENDPOINT
9418
- });
9419
- }, [transport]);
9420
- (0, import_react.useEffect)(() => {
9421
- return () => rpc.close();
9422
- }, [rpc]);
10002
+ const [view, setView] = (0, import_react.useState)("methods");
10003
+ const [methodFilter, setMethodFilter] = (0, import_react.useState)("");
10004
+ const rpc = (0, import_react.useMemo)(() => createRpc({
10005
+ transport,
10006
+ endpoint: ENDPOINT
10007
+ }), [transport]);
10008
+ (0, import_react.useEffect)(() => () => rpc.close(), [rpc]);
9423
10009
  const { methods, loading, error, refresh } = useMethods(rpc);
9424
10010
  const detail = useMethodDetail(rpc, selected);
10011
+ const metrics = useMetrics(rpc, view === "metrics");
10012
+ const compiledFilter = (0, import_react.useMemo)(() => compileGlobFilter(methodFilter), [methodFilter]);
10013
+ const filteredSnapshot = (0, import_react.useMemo)(() => {
10014
+ if (!metrics.snapshot) return void 0;
10015
+ if (compiledFilter.passthrough) return metrics.snapshot;
10016
+ return {
10017
+ ...metrics.snapshot,
10018
+ methods: filterSnapshotMethods(compiledFilter, metrics.snapshot.methods)
10019
+ };
10020
+ }, [metrics.snapshot, compiledFilter]);
9425
10021
  const generateDocs = (0, import_react.useCallback)(async () => {
9426
10022
  if (methods.length === 0) {
9427
10023
  alert("方法列表为空,请先刷新方法列表。");
@@ -9449,14 +10045,87 @@ function App() {
9449
10045
  generatingDocs
9450
10046
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
9451
10047
  className: "sidebar",
9452
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Methods" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MethodTree, {
10048
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
10049
+ className: "view-switch segmented",
10050
+ role: "group",
10051
+ "aria-label": "视图",
10052
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
10053
+ type: "button",
10054
+ className: view === "methods" ? "active" : "",
10055
+ onClick: () => setView("methods"),
10056
+ children: "Methods"
10057
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
10058
+ type: "button",
10059
+ className: view === "metrics" ? "active" : "",
10060
+ onClick: () => setView("metrics"),
10061
+ children: "Metrics"
10062
+ })]
10063
+ }), view === "methods" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Methods" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MethodTree, {
9453
10064
  methods,
9454
10065
  selected,
9455
10066
  onSelect: setSelected
10067
+ })] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
10068
+ className: "metrics-sidebar",
10069
+ children: [
10070
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Metrics" }),
10071
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
10072
+ className: "hint",
10073
+ style: { margin: "0 0 8px" },
10074
+ children: "📊 服务端 RPC 调用统计。"
10075
+ }),
10076
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
10077
+ className: "metrics-filter",
10078
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
10079
+ className: "metrics-filter-label",
10080
+ children: "Filter"
10081
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
10082
+ className: "sf-input",
10083
+ type: "text",
10084
+ placeholder: "user.*, !system.*",
10085
+ value: methodFilter,
10086
+ onChange: (e) => setMethodFilter(e.target.value),
10087
+ spellCheck: false
10088
+ })]
10089
+ }),
10090
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
10091
+ className: "hint metrics-filter-help",
10092
+ children: [
10093
+ "支持 ",
10094
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "*" }),
10095
+ " / ",
10096
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "?" }),
10097
+ " / ",
10098
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "!" }),
10099
+ ",多个模式用 ",
10100
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "," }),
10101
+ " 分隔。",
10102
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}),
10103
+ "例:",
10104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "user.*" }),
10105
+ "、",
10106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "*.get" }),
10107
+ "、",
10108
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "!system.*" }),
10109
+ "、",
10110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "user.*,!user.secret" })
10111
+ ]
10112
+ }),
10113
+ metrics.snapshot && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
10114
+ className: "hint metrics-filter-stat",
10115
+ children: [
10116
+ "匹配 ",
10117
+ Object.keys(filteredSnapshot?.methods ?? {}).length,
10118
+ " /",
10119
+ " ",
10120
+ Object.keys(metrics.snapshot.methods).length,
10121
+ " 个方法"
10122
+ ]
10123
+ })
10124
+ ]
9456
10125
  })]
9457
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
10126
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
9458
10127
  className: "content",
9459
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MethodDetail, {
10128
+ children: view === "methods" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MethodDetail, {
9460
10129
  method: selected,
9461
10130
  detail
9462
10131
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TestPanel, {
@@ -9465,7 +10134,19 @@ function App() {
9465
10134
  rpc,
9466
10135
  transport,
9467
10136
  endpoint: ENDPOINT
9468
- })]
10137
+ })] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricsPanel, {
10138
+ snapshot: filteredSnapshot,
10139
+ previous: metrics.previous,
10140
+ fetchedAt: metrics.fetchedAt,
10141
+ loading: metrics.loading,
10142
+ error: metrics.error,
10143
+ available: metrics.available,
10144
+ onRefresh: () => void metrics.refresh(),
10145
+ autoRefresh: metrics.autoRefresh,
10146
+ onAutoRefreshChange: metrics.setAutoRefresh,
10147
+ intervalMs: metrics.intervalMs,
10148
+ onIntervalChange: metrics.setIntervalMs
10149
+ })
9469
10150
  })] })]
9470
10151
  });
9471
10152
  }
@@ -9474,4 +10155,4 @@ function App() {
9474
10155
  import_client.createRoot(document.getElementById("root")).render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.StrictMode, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(App, {}) }) }));
9475
10156
  //#endregion
9476
10157
 
9477
- //# sourceMappingURL=index-sVQlhxjI.js.map
10158
+ //# sourceMappingURL=index-Cr0Jnrgb.js.map