diff --git a/CHANGELOG.md b/CHANGELOG.md index dc13e08..20db855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.11.0 + +### Added / 新增 +- `ZeroInspectorKit.dispose()`:完整释放检查器占用的进程级资源(持久化刷盘 Timer、`FlutterError.onError` 接管、内存 / FPS 定时器、throttle notifier、持久化落盘),集成测试结束或宿主 App 运行时关闭采集(如隐私合规开关)后可彻底停采,再用 `init` 重新启用。 / `ZeroInspectorKit.dispose()` fully releases the inspector's process-wide resources (persistence flush Timer, `FlutterError.onError` hook, memory / FPS timers, throttle notifiers, on-disk persistence) so collection can be switched off entirely at runtime and re-enabled via `init`. +- 可编辑重放编辑器 `InspectorReplayEditor`(`showReplayEditor`)与 `buildReplayRequest` 工具:详情页点 Replay 弹出底部面板,URL / Header / Body 只读,仅查询参数可编辑,重发时用修改后的参数重建 URL,并在面板内预览响应(状态码 / 耗时 / 前 400 字节)。 / An editable replay editor `InspectorReplayEditor` (`showReplayEditor`) plus the `buildReplayRequest` helper: a bottom sheet from the request detail lets you edit only the URL query parameters (URL / headers / body stay read-only), rebuilds the URL from the edited params on send, and previews the response in-panel. +- 新增公开工具:`sensitive_data`、`inspector_version`;`fps_service` 由内部提升为公开导出,便于外部直接引用。 / New exported utilities: `sensitive_data`, `inspector_version`; `fps_service` is promoted to a public export. +- FPS 分相耗时:单帧记录新增 `buildDurationUs` 与 `rasterDurationUs`(GPU 光栅化),掉帧列表项同时展示 build / raster 分项,便于定位是构建还是光栅化卡顿。 / FPS phased timing: each frame record now carries `buildDurationUs` and `rasterDurationUs` (GPU raster), and the janky-frame list shows the build / raster split so you can tell whether build or raster is the bottleneck. +- 自适应卡顿阈值:掉帧阈值改按设备标称刷新率换算(60Hz≈16.7ms、120Hz≈8.3ms),高刷屏同样严格,不再被固定 16ms 放过。 / Adaptive jank threshold: the per-frame budget is now derived from the display refresh rate (≈16.7ms at 60Hz, ≈8.3ms at 120Hz) so high-refresh displays keep a tight bar. +- `PlatformDispatcher.onError` 接入:异常聚合服务现在同时接管 `PlatformDispatcher.onError`(接管 + 还原),捕获 `FlutterError.onError` 漏掉的框架边界外 / 平台通道回包异常,一并聚合进 Errors Tab。 / `PlatformDispatcher.onError` hook: the error aggregator now also hooks `PlatformDispatcher.onError` (with save/restore), catching errors that `FlutterError.onError` misses — those thrown outside the framework boundary or from platform-channel replies — and aggregating them too. +- 告警持久化:告警事件随日志/网络/异常一起异步落盘到 SQLite,崩溃或重启后可在 Alerts 标签回看;会话存档导出也包含 `alerts` 段。新增 `AlertService.restore` 与 `PersistenceService.loadAlerts`。 / Alert persistence: alert events now flush to disk alongside logs/network/errors, so they survive crashes and restarts and can be reviewed in the Alerts tab; the session archive export also includes an `alerts` section. New `AlertService.restore` and `PersistenceService.loadAlerts`. + +### Fixed / 修复 +- 拦截器请求体缓冲无上限导致大 body 经 `add` / `write` 上传 OOM(上限此前只实现在 `addStream` 上):现 `add` / `write` 也按 512KB 上限缓冲,超限后直传底层请求,重放不再重复写入或截断大 body。 / Interceptor request-body buffering was unbounded, so uploading a large body through `add` / `write` could OOM (the cap only existed on `addStream`); `add` / `write` now also honor the 512KB cap and switch to pass-through, and replay no longer duplicates or truncates large bodies. +- 二进制响应(gzip / brotli / protobuf / 图片)解码失败会抛异常、整条记录(含状态码)丢失:现降级为「字节数 + 前若干字节 hex 预览」,小体积二进制改用 base64 承载,面板可直接预览图片。 / Binary responses (gzip / brotli / protobuf / images) used to throw and lose the whole record (status code included); they now fall back to a byte count + leading hex preview, and small binary bodies are carried as base64 so the panel can preview them. +- `AlertService` 节流表(`(source, message)`,source 多为完整 URL)长跑无界增长,造成确定性内存泄漏:现设 512 项硬上限 + 软阈值过期清扫,按插入顺序淘汰最旧项。 / `AlertService`'s throttle map (keyed by `(source, message)`, source often a full URL) grew unbounded over long runs — a deterministic leak; it now has a 512-entry hard cap plus an expiry sweep, evicting the oldest entries by insertion order. +- Hive / SharedPreferences 查看器同键混含 `int` 与 `String` 时 `List.sort()` 抛 `TypeError`:抽出共享 `KeyValueQuery` 工具做安全排序与分页,永不抛异常。 / The Hive / SharedPreferences viewers threw `TypeError` from `List.sort()` when keys mixed `int` and `String`; a shared `KeyValueQuery` helper now does safe sorting and paging that never throws. +- `LogLevel` 缩写文本此前在 `LogEntry` 与 `ExportService` 各有一份相同 switch,新增级别易漏改:收敛为单一 `LogLevelText.of`。 / The `LogLevel` abbreviation switch was duplicated in `LogEntry` and `ExportService`; it is now a single `LogLevelText.of` implementation. + ## 1.10.1 ### Changed / 变更 diff --git a/README.md b/README.md index ee375ea..cb3306a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ An in-app developer console for Flutter: inspect HTTP, WebSocket & gRPC traffic, [![Dart](https://img.shields.io/badge/Dart-✓-0175C2?logo=dart)](https://dart.dev) [![Style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart) -> **🔔 Upgrade recommended:** This release updates the open-source license — the plugin is now **MPL-2.0** (commercial and closed-source use allowed; modified files must be published in source form). The `LICENSE` file is now the clean upstream MPL-2.0 text so pub.dev detects it, with the copyright notice and additional statements moved to [NOTICE](NOTICE). All users are encouraged to upgrade to the latest version (`^1.10.1`). +> **🔔 Upgrade recommended:** This release adds an editable replay editor (edit only query params — URL/headers/body stay read-only), phased FPS timing (build + raster split) with an adaptive jank threshold, `PlatformDispatcher.onError` capture, and on-disk alert persistence that survives restarts. It also adds `ZeroInspectorKit.dispose()` for a full runtime teardown. All users are encouraged to upgrade to the latest version (`^1.11.0`). 🌐 **[Official Website](https://www.zerolabsco.com/)**  ·  📦 **[View on pub.dev](https://pub.dev/packages/zero_inspector_kit)**  ·  🔗 **[View on GitHub](https://github.com/zero-labsco/zero_inspector_kit)** @@ -57,16 +57,16 @@ An in-app developer console for Flutter: inspect HTTP, WebSocket & gRPC traffic, ## Features - **Zero-Invasion Integration** — One line of code, no changes to existing project code. -- **Network Inspector** — Real-time capture of all HTTP (http & Dio) requests; modify bodies/headers via interceptor rules; batch cURL copy; sensitive-header masking; filterable by method/status/interception. +- **Network Inspector** — Real-time capture of all HTTP (http & Dio) requests; modify bodies/headers via interceptor rules; in-app **replay editor** (edit only query params — URL/headers/body stay read-only — and preview the response); batch cURL copy; sensitive-header masking; filterable by method/status/interception. - **WebSocket / gRPC Capture** — Opt-in streaming-protocol capture (off by default, runtime toggle like Memory/FPS); WebSocket frames and gRPC calls appear in the Network list. - **Logging System** — Auto-captures `print()`, `debugPrint()`, and custom logs across multiple levels; integrates with third-party log libraries; auto-scroll (pausable), regex search, tag filtering and one-tap copy of a single log entry. - **Error Monitor** — Dedicated Errors tab (since v1.9.0): hooks `FlutterError.onError` + `runZonedGuarded`, aggregates & dedups crashes by type + stack signature with count and first/last seen; a red count badge sits on the Errors tab icon. -- **Session Persistence** — Logs / network / errors are asynchronously flushed into the inspector's own `zero_inspector_kit.db` (since v1.9.0; a disk ring buffer that also shows up in the Database tab); on launch logs & errors replay into their tabs; export & share the full session archive from the panel header. +- **Session Persistence** — Logs / network / errors / **alerts** are asynchronously flushed into the inspector's own `zero_inspector_kit.db` (since v1.9.0; a disk ring buffer that also shows up in the Database tab); on launch logs, errors & alerts replay into their tabs; export & share the full session archive (now including alerts) from the panel header. - **Database Viewer** — Inspect SQLite and other databases via custom providers. - **Memory Monitor** — Trend chart, Dart Heap, Native memory breakdown, leak detection, image-cache & storage stats (master switch to avoid overhead). Since v1.9.0 the leak detector also bridges Flutter's official `FlutterMemoryAllocations` to cut false positives. -- **FPS Monitor** — Real-time FPS, jank detection, trend chart, frame records (master switch to avoid overhead). -- **Route Tracker** — Navigation history and current route. -- **Alert System** — Rule-based alerts on network/logs/memory/FPS with unread badge and throttling. +- **FPS Monitor** — Real-time FPS, jank detection, trend chart, frame records with **phased build + raster timing** and an **adaptive jank threshold** derived from the display refresh rate (so 120Hz screens stay strict). Master switch to avoid overhead. +- **Route Tracker** — Navigation history and current route (`navigatorObservers: [InspectorRouteObserver()]`). +- **Alert System** — Rule-based alerts on network/logs/memory/FPS with unread badge and throttling; alerts are persisted to disk and survive app restarts. - **Floating Button** — Breathing-animation overlay button that auto-docks to screen edges, avoiding back-gesture conflicts. - **One-Click Bug Report** — Tap the bug icon in the panel header to share a ready-to-file snapshot (device model + OS + current memory + recent logs + recent network) via the system share sheet. - **Modern UI** — Dark theme with gradients and centralized, customizable colors. @@ -102,7 +102,7 @@ An in-app developer console for Flutter: inspect HTTP, WebSocket & gRPC traffic, ```yaml dependencies: - zero_inspector_kit: ^1.10.1 + zero_inspector_kit: ^1.11.0 ``` ### GitHub @@ -112,7 +112,7 @@ dependencies: zero_inspector_kit: git: url: https://github.com/zero-labsco/zero_inspector_kit.git - ref: release/v1.10.1 # replace 1.10.1 with the version you need + ref: release/v1.11.0 # replace 1.11.0 with the version you need ``` --- @@ -243,7 +243,7 @@ InspectorLogInterceptor.instance.onLogCaptured = (entry) { > Available since v1.9.0 -The **Errors** tab answers "is the same crash happening repeatedly?" `ErrorService` hooks `FlutterError.onError` (keeping the default red-screen behavior) plus `runZonedGuarded` inside `runAppWithInspector()`, then aggregates each exception by **type + stack signature**: repeated crashes merge into one record with a **×N** count and first/last-seen time. Tap a row to expand the full stack sample, filter by search, and copy individual stacks. A red count badge on the Errors tab icon shows the number of aggregated records while the panel is open. +The **Errors** tab answers "is the same crash happening repeatedly?" `ErrorService` hooks `FlutterError.onError` and `PlatformDispatcher.onError` (both with save/restore, default red-screen behavior kept) plus `runZonedGuarded` inside `runAppWithInspector()`, then aggregates each exception by **type + stack signature**: repeated crashes merge into one record with a **×N** count and first/last-seen time. Tap a row to expand the full stack sample, filter by search, and copy individual stacks. A red count badge on the Errors tab icon shows the number of aggregated records while the panel is open. ```dart import 'package:zero_inspector_kit/zero_inspector_kit.dart'; @@ -264,7 +264,7 @@ Toggle with `enableErrorCapture` in `init()` (default `true`). Full details on t > Available since v1.9.0 -Logs, network requests, and aggregated errors are asynchronously flushed into the inspector's own database file, **`zero_inspector_kit.db`** — a SQLite-backed disk ring buffer that also appears in the **Database** tab. On the next launch, **logs and aggregated errors replay into their tabs**; network requests stay archived on disk for later export. Data therefore survives app restarts even if you never opened the panel. Tap the **storage icon** in the panel header to open the **Persisted data** manager: see row counts per category, export the **full session archive** as JSON (share sheet), or clear the disk. +Logs, network requests, aggregated errors and **alerts** are asynchronously flushed into the inspector's own database file, **`zero_inspector_kit.db`** — a SQLite-backed disk ring buffer that also appears in the **Database** tab. On the next launch, **logs, aggregated errors and alerts replay into their tabs**; network requests stay archived on disk for later export. Data therefore survives app restarts even if you never opened the panel. Tap the **storage icon** in the panel header to open the **Persisted data** manager: see row counts per category, export the **full session archive** (now including an `alerts` section) as JSON (share sheet), or clear the disk. ```dart // Reading persisted data programmatically (optional) @@ -376,8 +376,9 @@ MemoryInspectorService.instance.clearLeakRecords(); // clear all Real-time frame analysis with a master switch (off by default). - **Master Switch:** top toggle; off = no timings callback, zero overhead. -- **Metrics:** current FPS, jank rate, total frames, 30-second trend chart (60 points), janky-frame list (>16ms). +- **Metrics:** current FPS, jank rate, total frames, 30-second trend chart (60 points), janky-frame list with **build + raster split** (each janky frame shows its build vs raster duration so you can tell which phase stalled). - **Accuracy (since v1.2.1):** uses real `buildStart` timestamps (not `DateTime.now()`) and `rasterFinish - buildStart` duration, catching GPU raster jank. +- **Adaptive threshold:** the jank cutoff is derived from the display refresh rate (`1000ms / refreshRate`), so 60Hz≈16.7ms while a 120Hz screen uses a tight ≈8.3ms bar instead of being let through by a fixed 16ms. ```dart FpsService.instance.start(); @@ -440,6 +441,18 @@ Sensitive headers are masked the same way as in the Network tab (toggle "Sensiti ## API Reference +### ZeroInspectorKit + +The top-level entry class. Call `init()` (or `runAppWithInspector()`) once to start collection; call `dispose()` to fully release the inspector's process-wide resources (persistence flush timer, `FlutterError.onError` / `PlatformDispatcher.onError` hooks, memory / FPS timers, throttle notifiers and on-disk persistence) so collection can be switched off entirely at runtime — e.g. behind a privacy-compliance toggle — and re-enabled later via `init()`. + +```dart +// Fully stop collection (releases every process-wide resource). +ZeroInspectorKit.instance.dispose(); + +// Re-enable later with the same or different options. +ZeroInspectorKit.instance.init(); +``` + ### FloatingInspectorButton | Parameter | Type | Description | diff --git a/README_zh.md b/README_zh.md index e2dc82d..145bad0 100644 --- a/README_zh.md +++ b/README_zh.md @@ -19,7 +19,7 @@ [![Dart](https://img.shields.io/badge/Dart-✓-0175C2?logo=dart)](https://dart.dev) [![Style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart) -> **🔔 推荐升级:** 本次更新开源协议——插件改用 **MPL-2.0**(允许商用与闭源使用,修改过的文件需以源码形式公开);`LICENSE` 改为标准 MPL-2.0 全文以便 pub.dev 正确识别,版权与附加声明移至 [NOTICE](NOTICE)。建议所有用户升级到最新版本(`^1.10.1`)。 +> **🔔 推荐升级:** 本次新增可编辑重放编辑器(仅可编辑查询参数——URL/请求头/请求体保持只读)、FPS 分阶段耗时(build + raster 分项)与自适应卡顿阈值、`PlatformDispatcher.onError` 捕获,以及可跨重启存活的告警持久化;并提供 `ZeroInspectorKit.dispose()` 做完整的运行时资源释放。建议所有用户升级到最新版本(`^1.11.0`)。 🌐 **[官方网站](https://www.zerolabsco.com/)**  ·  📦 **[在 pub.dev 查看](https://pub.dev/packages/zero_inspector_kit)**  ·  🔗 **[查看 GitHub 仓库](https://github.com/zero-labsco/zero_inspector_kit)** @@ -56,16 +56,16 @@ ## 功能特性 - **零侵入集成**:一行代码,无需改动现有项目代码。 -- **网络检查器**:实时捕获所有 HTTP(http & Dio)请求;通过拦截规则修改请求体/请求头;批量复制 cURL;敏感请求头遮蔽;可按方法/状态码/拦截状态筛选。 +- **网络检查器**:实时捕获所有 HTTP(http & Dio)请求;通过拦截规则修改请求体/请求头;应用内**重放编辑器**(仅可编辑查询参数——URL/请求头/请求体保持只读——并预览响应);批量复制 cURL;敏感请求头遮蔽;可按方法/状态码/拦截状态筛选。 - **WebSocket / gRPC 抓取**:可选的流式协议抓取(默认关闭,运行时开关,与 Memory/FPS 一致);WebSocket 帧与 gRPC 调用出现在 Network 列表中。 - **日志系统**:自动捕获 `print()`、`debugPrint()` 及自定义日志,支持多级别与第三方日志库集成;自动滚动(可暂停)、正则搜索、按标签过滤与单条日志一键复制。 -- **异常监控**:独立的 Errors 标签页(v1.9.0 起):接管 `FlutterError.onError` + `runZonedGuarded`,按类型 + 堆栈签名去重聚合崩溃,记录次数与首末次时间;Errors 标签图标带红色计数。 -- **会话持久化**:日志 / 网络 / 异常异步写入插件自有的 `zero_inspector_kit.db` 数据库(v1.9.0 起;磁盘环形缓冲,也会出现在 Database 标签页);启动时日志与异常回放入各自标签页,网络请求留档供导出;可从面板头部导出完整会话存档并分享。 +- **异常监控**:独立的 Errors 标签页(v1.9.0 起):接管 `FlutterError.onError` + `PlatformDispatcher.onError` + `runZonedGuarded`,按类型 + 堆栈签名去重聚合崩溃,记录次数与首末次时间;Errors 标签图标带红色计数。 +- **会话持久化**:日志 / 网络 / 异常 / **告警**异步写入插件自有的 `zero_inspector_kit.db` 数据库(v1.9.0 起;磁盘环形缓冲,也会出现在 Database 标签页);启动时日志、异常与告警回放入各自标签页,网络请求留档供导出;可从面板头部导出完整会话存档(含告警段)并分享。 - **数据库查看器**:支持 SQLite 及其他数据库,可自定义提供者。 - **内存监控**:趋势图、Dart Heap、Native 内存分项、泄漏检测、图片缓存与存储统计(总开关避免开销)。v1.9.0 起泄漏检测额外桥接 Flutter 官方 `FlutterMemoryAllocations`,降低误报。 -- **FPS 监控**:实时帧率、掉帧检测、趋势图、帧记录(总开关避免开销)。 -- **路由追踪器**:导航历史与当前路由。 -- **告警系统**:针对网络/日志/内存/FPS 的告警规则,带未读红点与节流。 +- **FPS 监控**:实时帧率、掉帧检测、趋势图,带 **build + raster 分项耗时**的帧记录与按设备刷新率换算的**自适应卡顿阈值**(120Hz 屏幕同样严格)。总开关避免开销。 +- **路由追踪器**:导航历史与当前路由(`navigatorObservers: [InspectorRouteObserver()]`)。 +- **告警系统**:针对网络/日志/内存/FPS 的告警规则,带未读红点与节流;告警会持久化到磁盘并可跨启动存活。 - **悬浮按钮**:呼吸动画的 Overlay 按钮,自动吸附屏幕边缘,避免返回手势冲突。 - **一键 Bug 报告**:点击面板头部的虫子图标,即可一键生成并分享一份可直接贴进 issue 的快照(设备型号 + 系统 + 当前内存 + 最近日志 + 最近网络)。 - **现代化 UI**:深色主题 + 渐变,集中式可定制配色。 @@ -101,7 +101,7 @@ ```yaml dependencies: - zero_inspector_kit: ^1.10.1 + zero_inspector_kit: ^1.11.0 ``` ### GitHub @@ -111,7 +111,7 @@ dependencies: zero_inspector_kit: git: url: https://github.com/zero-labsco/zero_inspector_kit.git - ref: release/v1.10.1 # 将 1.10.1 替换为你需要的版本号 + ref: release/v1.11.0 # 将 1.11.0 替换为你需要的版本号 ``` --- @@ -242,7 +242,7 @@ InspectorLogInterceptor.instance.onLogCaptured = (entry) { > v1.9.0 起可用 -**Errors** 标签页回答的是"同一处崩溃是否反复出现?"。`ErrorService` 接管 `FlutterError.onError`(保留默认红色报错行为)与 `runAppWithInspector()` 内的 `runZonedGuarded`,将每次异常按**类型 + 堆栈签名**聚合:重复崩溃合并为一条记录,显示 **×N** 次数与首次/末次时间。点击行展开完整堆栈样本,支持搜索过滤与单条复制。面板打开时,Errors 标签图标上的红色计数会显示聚合记录条数。 +**Errors** 标签页回答的是"同一处崩溃是否反复出现?"。`ErrorService` 接管 `FlutterError.onError` 与 `PlatformDispatcher.onError`(均带还原、保留默认红色报错行为)以及 `runAppWithInspector()` 内的 `runZonedGuarded`,将每次异常按**类型 + 堆栈签名**聚合:重复崩溃合并为一条记录,显示 **×N** 次数与首次/末次时间。点击行展开完整堆栈样本,支持搜索过滤与单条复制。面板打开时,Errors 标签图标上的红色计数会显示聚合记录条数。 ```dart import 'package:zero_inspector_kit/zero_inspector_kit.dart'; @@ -263,7 +263,7 @@ ErrorService.instance.clear(); > v1.9.0 起可用 -日志、网络请求与聚合异常会被异步写入插件自有的数据库文件 **`zero_inspector_kit.db`**——一个基于 SQLite 的磁盘环形缓冲,也会出现在 **Database** 标签页中。**下次启动时,日志与聚合异常会回放入各自标签页**;网络请求保留在磁盘存档,供之后导出。因此即使从未打开过面板,数据也能跨重启保留。点击面板头部的**存储图标**打开 **Persisted data** 管理弹层:查看各类别行数、导出**完整会话存档** JSON(系统分享)或清空磁盘。 +日志、网络请求、聚合异常与**告警**会被异步写入插件自有的数据库文件 **`zero_inspector_kit.db`**——一个基于 SQLite 的磁盘环形缓冲,也会出现在 **Database** 标签页中。**下次启动时,日志、聚合异常与告警会回放入各自标签页**;网络请求保留在磁盘存档,供之后导出。因此即使从未打开过面板,数据也能跨重启保留。点击面板头部的**存储图标**打开 **Persisted data** 管理弹层:查看各类别行数、导出**完整会话存档**(现含 `alerts` 段)JSON(系统分享)或清空磁盘。 ```dart // 编程方式读取持久化数据(可选) @@ -376,8 +376,9 @@ MemoryInspectorService.instance.clearLeakRecords(); // 清空所有 实时帧性能分析,顶部总开关控制数据采集(默认关闭以避免性能开销)。 - **总开关**:FPS 面板顶部开关;关闭时无帧回调、零开销。 -- **指标**:当前 FPS、掉帧率、总帧数、30 秒趋势图(60 点)、掉帧列表(>16ms)。 +- **指标**:当前 FPS、掉帧率、总帧数、30 秒趋势图(60 点)、带 **build + raster 分项**的掉帧列表(每条掉帧会展示 build 与 raster 各自的耗时,便于定位是构建还是光栅化卡顿)。 - **准确度(v1.2.1 起)**:使用真实 `buildStart` 时间戳(非 `DateTime.now()`),帧耗时取 `rasterFinish - buildStart`,可检测 GPU 光栅化卡顿。 +- **自适应阈值**:掉帧阈值按设备标称刷新率换算(`1000ms / 刷新率`),60Hz≈16.7ms,而 120Hz 屏幕使用严格的 ≈8.3ms,不再被固定 16ms 放过。 ```dart FpsService.instance.start(); @@ -440,6 +441,18 @@ ZeroInspectorKit.runAppWithInspector( ## API 参考 +### ZeroInspectorKit + +顶层入口类。调用一次 `init()`(或 `runAppWithInspector()`)开始采集;调用 `dispose()` 完整释放检查器占用的进程级资源(持久化刷盘定时器、`FlutterError.onError` / `PlatformDispatcher.onError` 接管、内存 / FPS 定时器、节流 notifier 与磁盘持久化),从而在运行时彻底停采——例如配合隐私合规开关——之后可用 `init()` 重新启用。 + +```dart +// 彻底停止采集(释放全部进程级资源) +ZeroInspectorKit.instance.dispose(); + +// 之后用相同或不同参数重新启用 +ZeroInspectorKit.instance.init(); +``` + ### FloatingInspectorButton | 参数 | 类型 | 描述 | diff --git a/TODO.md b/TODO.md index 46eb46d..7316091 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1,66 @@ # TODO / 待办事项 -> 历史待办已清理(2026-08-26):剩余未完成项(悬浮按钮位置持久化、内存历史窗口可配置、拦截规则增强、iOS VM Service 研究、图片内存详细信息、路由观察者单元测试补充)经评估暂不推进——其中悬浮按钮持久化需引入额外持久化依赖,与其余项一同暂缓。以下仅保留已完成功能作为项目历史记录。 +> 本文件记录 `zero_inspector_kit` 的中长期工程待办与已规划但尚未动工的大型功能。 +> 短期、已排期进入具体版本的功能见各版本 `CHANGELOG.md`。 +> This file tracks the long-term engineering backlog and planned-but-not-started +> large features. Short-term, version-bound work lives in `CHANGELOG.md`. + +## 已规划 · 大型工程待办 / Planned · Large Engineering Backlog + +### 网络 / Network +- [ ] **拦截规则增强**:正则匹配、按域/路径前缀分组、命中计数与最近命中时间、规则导入导出(`InterceptorRule` 序列化)。 +- [ ] **请求/响应差异对比**:同 URL 两次请求体的 diff 视图,定位接口字段漂移。 +- [ ] **重放历史**:已重放的请求与响应归档,支持"再次重放同一份响应"。 +- [ ] **WebSocket 流量统计**:按连接聚合收/发字节、帧数、吞吐曲线。 + +### 日志 / Logs +- [ ] **日志级别实时过滤开关**:面板内按 V/D/I/W/E 即时筛选,替代仅 tag 过滤。 +- [ ] **日志导出裁剪**:按时间区间 / 级别 / tag 组合导出,避免导出整库。 +- [ ] **结构化日志(jsonLog)解析**:自动展开 JSON 字段,可折叠树形查看。 + +### 内存 / Memory +- [ ] **图片内存详细信息**:`Image` / `ImageProvider` 尺寸、解码后字节、缓存命中,定位内存大户。 +- [ ] **内存历史窗口可配置**:保留时长 / 采样间隔作为 `init` 参数暴露(当前硬编码)。 +- [ ] **泄漏对象引用链**:结合 `FlutterMemoryAllocations` 给出从根到泄漏对象的保留路径。 + +### 性能 / FPS +- [ ] **分阶段耗时下钻**:build/layout/paint/raster 四段分别计时(当前仅 build/raster 两段),定位具体卡顿阶段。 +- [ ] **帧耗时火焰图**:连续帧的 build/raster 耗时叠加,识别周期性尖刺。 +- [ ] **着色器编译卡顿标记**:首次构建帧标记为 warm-up,不计入稳态 jank。 + +### 错误 / Errors +- [ ] **崩溃会话快照**:捕获未处理异常时自动导出最近 N 条日志 + 网络 + 内存快照为单文件。 +- [ ] **错误去重智能合并**:跨重启按堆栈签名合并,长期累计历史出现次数。 +- [ ] **自定义上报钩子**:`onErrorReport` 回调,接入 Sentry / Firebase Crashlytics。 + +### 路由 / Routes +- [ ] **路由观察者单元测试补充**:覆盖嵌套 Navigator、命名路由、Dialog/Overlay 场景。 +- [ ] **路由耗时**:页面 `initState` → `build` → 首帧 的端到端耗时统计。 + +### 持久化 / Persistence +- [ ] **导出格式增强**:cURL/HAR 已支持,补充 OpenAPI 推断与 `har2postman`。 +- [ ] **磁盘配额**:持久化按字节上限裁剪(当前仅行数 + 保留时长)。 +- [ ] **Web / 桌面后端**:`PersistenceService` 在桌面/Web 用 `sqlite3` / `indexed_db` 后端替代 sqflite。 + +### 工程化 / Engineering +- [ ] **悬浮按钮位置持久化**:保存并恢复拖拽位置,需引入轻量持久化(评估是否值得引入依赖)。 +- [ ] **自动化截屏回归**:`test/` 下补充 `golden` 测试,覆盖九大查看器标签页。 +- [ ] **pub.dev 评分专项**:补全 API 文档覆盖率、示例工程 `example/`,目标 pana 140/140。 +- [ ] **CI 矩阵**:扩展 `ci.yml` 在 stable + beta 双 Flutter 版本上跑 analyze/test。 ## 已完成功能 / Completed Features - **Dart VM Heap 内存监控** — HTTP 轮询连接 VM Service + `ProcessInfo.currentRss` 降级;趋势图、新生代/老生代、手动 GC、历史清理。 - **内存泄漏检测** — `WeakReference` 弱引用 + 四状态机(tracking → verifying → leaked/released),自动 GC 验证。 -- **FPS / 帧率监控** — `addTimingsCallback` 采集帧数据,实时 FPS + 掉帧预警。 +- **FPS / 帧率监控** — `addTimingsCallback` 采集帧数据,实时 FPS + 掉帧预警;单帧 build/raster 分项耗时 + 自适应刷新率阈值。 - **数据导出与分享** — 日志/网络导出 JSON/文本/CSV/HAR/cURL;剪贴板复制;`share_plus` 系统分享 + 敏感头遮蔽。 - **内存查看器** — 趋势图触摸交互(十字准线、拖动手跟手浏览、自适应 Y 轴)。 -- **网络查看器** — Timeline 瀑布图、按 Method/状态码/拦截状态三维筛选。 +- **网络查看器** — Timeline 瀑布图、按 Method/状态码/拦截状态三维筛选;可编辑重放编辑器(仅查询参数可改,URL/Header/Body 只读)。 - **日志查看器增强** — 自动滚动(可暂停)、正则搜索、按 tag 过滤、单条复制、主视图内详情页。 - **SharedPreferences 查看器** — 以自定义 DB 源并入 Database 体系,抽象 `SharedPrefsLike` 不依赖 `shared_preferences`。 - **Widget 检查器** — 渲染树一次性快照 + 面包屑导航浏览。 - **路由追踪穿透包装** — 构建期穿过中间壳 Widget 注入 `navigatorObservers`,兼容非直接 `MaterialApp`。 +- **异常聚合(Errors Tab)** — 接管 `FlutterError.onError` 与 `PlatformDispatcher.onError`,按类型+堆栈签名去重。 +- **告警服务** — 命中规则入队、未读红点、节流防风暴、磁盘环形缓冲持久化(跨重启回放)。 +- **持久化环形缓冲** — 日志/网络/异常/告警异步落盘到 SQLite,按行数 + 保留时长滚动裁剪,会话存档导出。 +- **`ZeroInspectorKit.dispose()`** — 完整释放进程级资源,支持运行时彻底停采。 diff --git a/docs/404.html b/docs/404.html index 9cf5ccf..7300ae0 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1 +1 @@ -404: This page could not be found

404

This page could not be found.

\ No newline at end of file +404: This page could not be found

404

This page could not be found.

\ No newline at end of file diff --git a/docs/404/index.html b/docs/404/index.html index 9cf5ccf..7300ae0 100644 --- a/docs/404/index.html +++ b/docs/404/index.html @@ -1 +1 @@ -404: This page could not be found

404

This page could not be found.

\ No newline at end of file +404: This page could not be found

404

This page could not be found.

\ No newline at end of file diff --git a/docs/Alerts/index.html b/docs/Alerts/index.html index 1ee73ed..e35e8df 100644 --- a/docs/Alerts/index.html +++ b/docs/Alerts/index.html @@ -1,4 +1,4 @@ -
🔔 Alerts

Alerts / 告警系统

+
🔔 Alerts

Alerts / 告警系统

Overview / 概述

Available since v1.3.0

@@ -60,5 +60,6 @@

Notes / 备注

  • Alerts are evaluated in-process and shown in the developer console only; they do not send data off-device / 告警仅在本机开发者控制台内评估与展示,不会将数据发送到设备外
  • +
  • Persisted to disk (since v1.11.0): triggered alerts are flushed to the SQLite ring buffer and replay into the Alerts tab after an app restart, and are included in the session archive export / 已落盘持久化(v1.11.0 起):已触发的告警会写入 SQLite 环形缓冲,重启后回放至 Alerts 标签页,并包含在会话存档导出中
  • The alert system is tree-shaken out in release builds, like the rest of the inspector / 与检查器其余部分一样,告警系统在 release 构建中被 tree-shake 移除
  • -

Zero Inspector Kit · MPL-2.0
\ No newline at end of file +
\ No newline at end of file diff --git a/docs/Configuration/index.html b/docs/Configuration/index.html index bd833b0..ed39f79 100644 --- a/docs/Configuration/index.html +++ b/docs/Configuration/index.html @@ -1,4 +1,4 @@ -
⚙️ Configuration

Configuration / 配置说明

+
⚙️ Configuration

Configuration / 配置说明

ZeroInspectorKit.init() Parameters / 初始化参数

@@ -567,4 +567,4 @@

PropertyTypeDescriptionisRunningboolWhether monitoring is currently active / 是否正在监控currentFpsdoubleCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)jankRatedoubleJank rate as percentage / 卡顿率(百分比)totalFrameCountintTotal frames captured / 总帧数totalJankyCountintTotal janky frames (>16ms) / 总卡顿帧数(>16ms)lastFrameJankyboolWhether the most recent frame was janky / 最近一帧是否卡顿fpsHistoryList<double>Recent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)frameRecordsList<FrameRecord>Recent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)

See FPS Viewer for full feature details.

-

完整功能详情见 FPS Viewer。


Zero Inspector Kit · MPL-2.0
\ No newline at end of file +

完整功能详情见 FPS Viewer。

\ No newline at end of file diff --git a/docs/Custom-Database-Provider/index.html b/docs/Custom-Database-Provider/index.html index 6c7ca7e..6da36ac 100644 --- a/docs/Custom-Database-Provider/index.html +++ b/docs/Custom-Database-Provider/index.html @@ -1,4 +1,4 @@ -
🔌 Custom Database Provider

Custom Database Provider / 自定义数据库提供者

+
🔌 Custom Database Provider

Custom Database Provider / 自定义数据库提供者

Overview / 概述

Zero Inspector Kit supports extending database inspection beyond SQLite through the DatabaseProvider interface.

Zero Inspector Kit 通过 DatabaseProvider 接口支持扩展数据库检查功能到 SQLite 以外的数据库。

@@ -129,4 +129,4 @@

插件内置 SqliteDatabaseProvider,当 enableDatabaseScan 为 true(默认)时自动注册。

You can also register it manually:

也可以手动注册:

-
DatabaseRegistry.instance.registerProvider(SqliteDatabaseProvider());


Zero Inspector Kit · MPL-2.0
\ No newline at end of file +
DatabaseRegistry.instance.registerProvider(SqliteDatabaseProvider());
\ No newline at end of file diff --git a/docs/Database-Viewer/index.html b/docs/Database-Viewer/index.html index 7bbdddb..f3cebc0 100644 --- a/docs/Database-Viewer/index.html +++ b/docs/Database-Viewer/index.html @@ -1,4 +1,4 @@ -
💾 Database Viewer

Database Viewer / 数据库查看器

+

Zero Inspector Kit · MPL-2.0
\ No newline at end of file +

对于非 SQLite 数据库,请参阅 自定义数据库提供者。

\ No newline at end of file diff --git a/docs/Errors/index.html b/docs/Errors/index.html index 9fb63cc..a499cb6 100644 --- a/docs/Errors/index.html +++ b/docs/Errors/index.html @@ -1,10 +1,10 @@ -
🚨 Errors

Errors / 异常聚合查看器

+
🚨 Errors

Errors / 异常聚合查看器

Overview / 概述

Available since v1.9.0

v1.9.0 起可用

-

The Errors tab surfaces “the same crash happening repeatedly”. When the inspector is running, ErrorService hooks FlutterError.onError (keeping the default red-screen / console behavior) and aggregates every exception by type + stack signature. Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces.

+

The Errors tab surfaces “the same crash happening repeatedly”. When the inspector is running, ErrorService hooks both FlutterError.onError and PlatformDispatcher.onError (keeping the default red-screen / console behavior for both) and aggregates every exception by type + stack signature. Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces.

Errors 标签页用来快速发现”同一处崩溃反复出现”的问题。检查器运行时,ErrorService 会接管 FlutterError.onError(保留默认的红色报错与控制台行为),把每次异常按类型 + 堆栈签名去重聚合:同一处崩溃的多次发生会合并为一条记录,展示累计次数与首末次时间——而不是用成百上千条相同的堆栈刷屏日志。

What Gets Captured / 捕获来源

@@ -28,7 +28,11 @@

Source / 来源How / 方式Flutter framework errors / Flutter 框架异常Hooks FlutterError.onError (default handler kept) / 接管 FlutterError.onError(保留默认处理)Uncaught async errors / 未捕获异步异常runZonedGuarded inside runAppWithInspector() / runAppWithInspector() 内部的 runZonedGuardedManual reports / 手动上报ErrorService.instance.report(exception, stackTrace) — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道 + + + + +
Source / 来源How / 方式
Flutter framework errors / Flutter 框架异常Hooks FlutterError.onError (default handler kept) / 接管 FlutterError.onError(保留默认处理)
Framework-boundary & platform-channel errors / 框架边界外与平台通道异常PlatformDispatcher.onError (with save/restore) / PlatformDispatcher.onError(接管 + 还原)
Uncaught async errors / 未捕获异步异常runZonedGuarded inside runAppWithInspector() / runAppWithInspector() 内部的 runZonedGuarded
Manual reports / 手动上报ErrorService.instance.report(exception, stackTrace) — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道

Captured records are also persisted to disk (see Session Persistence below), so aggregated errors from previous sessions are replayed on the next launch.

捕获到的记录也会落盘持久化(见下文会话持久化),下次启动时会回放上次会话的聚合异常。

Deduplication / 去重规则

@@ -106,7 +110,7 @@

MemberDescriptionerrorsList<ErrorRecord> — aggregated records, newest first / 聚合记录,最新在前errorCountint — number of aggregated records / 聚合记录条数isEnabledbool — capture toggle (programmatic) / 抓取开关(代码控制)report(exception, stack, [context])Manually report an exception / 手动上报异常restore(records)Restore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过)clear()Clear all aggregated records / 清空全部记录install() / uninstall()Hook / restore FlutterError.onError / 接管 / 还原 FlutterError.onError +
MemberDescription
errorsList<ErrorRecord> — aggregated records, newest first / 聚合记录,最新在前
errorCountint — number of aggregated records / 聚合记录条数
isEnabledbool — capture toggle (programmatic) / 抓取开关(代码控制)
report(exception, stack, [context])Manually report an exception / 手动上报异常
restore(records)Restore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过)
clear()Clear all aggregated records / 清空全部记录
install() / uninstall()Hook / restore FlutterError.onError and PlatformDispatcher.onError / 接管 / 还原 FlutterError.onError 与 PlatformDispatcher.onError

ErrorRecord / 异常记录

@@ -139,8 +143,8 @@

FieldDescriptiontypeException type name (e.g. _TypeError) / 异常类型名messageException message / 异常消息countTotal occurrences / 累计出现次数firstSeen / lastSeenFirst / last occurrence time / 首次 / 末次出现时间sampleStackOne full stack sample (may be truncated) / 一条完整堆栈样本(可能截断)

Session Persistence / 会话持久化

-

Errors — together with logs and network requests — are asynchronously flushed to a local SQLite ring buffer (zero_inspector_kit.db). On the next launch, logs and aggregated errors replay into their tabs, so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the storage icon in the panel header to open the Persisted data manager: see the current row counts, export the full session archive as JSON, or clear the disk. See Configuration (PersistenceService section) for details and the tuning parameters.

-

异常与日志、网络请求一起被异步写入本地 SQLite 环形缓冲(zero_inspector_kit.db)。下次启动时,日志与聚合异常会回放入各自标签页——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看当前行数、导出完整会话存档 JSON,或清空磁盘。详见 Configuration(PersistenceService 一节)的参数说明。

+

Errors — together with logs, network requests and alerts — are asynchronously flushed to a local SQLite ring buffer (zero_inspector_kit.db). On the next launch, logs, aggregated errors and alerts replay into their tabs, so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the storage icon in the panel header to open the Persisted data manager: see the current row counts, export the full session archive as JSON, or clear the disk. See Configuration (PersistenceService section) for details and the tuning parameters.

+

异常与日志、网络请求、告警一起被异步写入本地 SQLite 环形缓冲(zero_inspector_kit.db)。下次启动时,日志、聚合异常与告警会回放入各自标签页——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看当前行数、导出完整会话存档 JSON,或清空磁盘。详见 Configuration(PersistenceService 一节)的参数说明。

Enable / Disable / 开关

  • enableErrorCapture (default true) in ZeroInspectorKit.init() controls error aggregation / init() 的 enableErrorCapture(默认 true)控制异常聚合
  • @@ -156,4 +160,4 @@

Zero Inspector Kit · MPL-2.0
\ No newline at end of file +
\ No newline at end of file diff --git a/docs/FAQ/index.html b/docs/FAQ/index.html index dc12474..48ffb5f 100644 --- a/docs/FAQ/index.html +++ b/docs/FAQ/index.html @@ -1,4 +1,4 @@ -
❓ FAQ

FAQ / 常见问题

+
❓ FAQ

FAQ / 常见问题

General / 通用

Q: Does the inspector affect production builds? / 检查器会影响生产构建吗?

A: No. The inspector is automatically disabled in release mode via kReleaseMode. Flutter’s tree-shaking removes all inspector-related code from production builds. You don’t need to remove any code.

@@ -132,4 +132,4 @@

可以。本项目采用 MPL-2.0 许可证,可用于闭源商业 App——未修改直接使用时无需公开任何源码;若修改了插件源文件,被修改的文件必须以 MPL-2.0 公开源码(你自己的 App 仍无需开源)。

Q: Are you liable for issues in modified versions? / 修改版出问题你们负责吗?

A: This plugin is provided “as is”, without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.

-

本插件按”原样”提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。


Zero Inspector Kit · MPL-2.0
\ No newline at end of file +

本插件按”原样”提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。

\ No newline at end of file diff --git a/docs/FPS-Viewer/index.html b/docs/FPS-Viewer/index.html index da2cfde..fb1b0a5 100644 --- a/docs/FPS-Viewer/index.html +++ b/docs/FPS-Viewer/index.html @@ -1,4 +1,4 @@ -
🎯 FPS Viewer

FPS Viewer / FPS 监控面板

+
🎯 FPS Viewer

FPS Viewer / FPS 监控面板

The FPS Viewer provides real-time frame performance analysis, including current FPS, jank rate, frame duration stats, and an FPS trend chart.

FPS 监控面板提供实时帧性能分析,包括当前 FPS、卡顿率、帧耗时统计和 FPS 趋势图。

@@ -37,7 +37,7 @@

1. Current Stats / 当前统计

  • Current FPS — Updated every 500ms / 每 500ms 更新
  • -
  • Jank Rate — Percentage of janky frames (>16ms) / 卡顿帧占比(>16ms 视为卡顿)
  • +
  • Jank Rate — Percentage of janky frames (duration exceeds the per-frame budget, derived from the display refresh rate) / 卡顿帧占比(帧耗时超过由屏幕刷新率换算的逐帧预算)
  • Total Frame Count — All frames captured since start / 自启动以来的总帧数
  • Total Janky Count — All janky frames captured / 自启动以来的总卡顿帧数
  • Last Frame Janky — Whether the most recent frame was janky / 最近一帧是否卡顿
  • @@ -53,11 +53,11 @@

    实时折线图,30 秒历史窗口(60 个数据点 × 500ms)。Y 轴动态缩放为 max(60, maxFps) 避免 FPS 飙升时溢出。卡顿区域以红色标记。

    3. Janky Frame List / 卡顿帧列表

      -
    • Lists frames exceeding 16ms duration / 列出耗时超过 16ms 的帧
    • -
    • Each item shows frame duration and timestamp / 每项显示帧耗时和时间戳
    • +
    • Lists janky frames — duration exceeds the per-frame budget, derived from the display refresh rate (~16.7ms at 60Hz, ~8.3ms at 120Hz) / 列出卡顿帧——帧耗时超过由屏幕刷新率换算的逐帧预算(60Hz≈16.7ms、120Hz≈8.3ms)
    • +
    • Each item shows frame duration and timestamp, plus the build / raster split (buildDurationUs + rasterDurationUs) so you can tell whether build or GPU raster is the bottleneck / 每项显示帧耗时、时间戳,以及 build / raster 分项(buildDurationUs + rasterDurationUs),便于判断是构建还是 GPU 光栅化卡顿
    • Helps identify specific jank spikes / 帮助定位具体卡顿点
    -

    列出耗时超过 16ms 的帧,每项显示帧耗时和时间戳,帮助定位具体卡顿点。

    +

    列出卡顿帧(帧耗时超过由屏幕刷新率换算的逐帧预算,60Hz≈16.7ms、120Hz≈8.3ms),每项显示帧耗时、时间戳与 build / raster 分项,帮助定位具体卡顿点。

    4. Reset / 重置

    • “Reset” button clears all statistics and historical data / “Reset” 按钮清空所有统计和历史数据
    • @@ -67,8 +67,10 @@

      How It Works / 工作原理

      FPS monitoring uses Flutter’s WidgetsBinding.instance.addTimingsCallback to receive frame timing information from the engine. The callback is batched — it may return multiple FrameTiming objects per call, so each frame is recorded individually inside the loop to ensure accurate FPS calculation.

      FPS 监控使用 Flutter 的 WidgetsBinding.instance.addTimingsCallback 接收引擎的帧时序信息。该回调是批量的——每次调用可能返回多个 FrameTiming 对象,因此在循环内部为每帧单独记录时间戳,确保 FPS 计算准确。

      -

      Jank threshold / 卡顿阈值: A frame is considered janky if its duration exceeds 16ms (the 60 FPS budget of ~16.67ms per frame).

      -

      卡顿阈值:帧耗时超过 16ms(60 FPS 每帧预算约 16.67ms)即视为卡顿。

      +

      Jank threshold / 卡顿阈值: A frame is janky when its total duration (build + raster) exceeds the per-frame budget — 1000ms ÷ refreshRate (≈16.7ms at 60Hz, ≈8.3ms at 120Hz). Since v1.11.0 the budget is derived from the device’s refresh rate, so high-refresh (120Hz) screens are held to a tight ≈8.3ms instead of being forgiven by a fixed 16ms.

      +

      卡顿阈值:帧的总耗时(build + raster)超过逐帧预算即视为卡顿——1000ms ÷ 刷新率(60Hz≈16.7ms、120Hz≈8.3ms)。自 v1.11.0 起预算按设备刷新率换算,高刷(120Hz)屏会被严格约束在 ≈8.3ms,而非被固定 16ms 放过。

      +

      Phased timing / 分阶段耗时: Each FrameRecord carries buildDurationUs and rasterDurationUs (GPU raster); the janky-frame list shows the build / raster split per row.

      +

      分阶段耗时:每条 FrameRecord 都带有 buildDurationUs 与 rasterDurationUs(GPU 光栅化),掉帧列表项会展示 build / raster 分项。

      Programmatic Control (Optional) / 编程式控制(可选)

      If you need to start/stop monitoring from code (e.g., for automated testing), use FpsService:

      如需从代码控制监控(如自动化测试),使用 FpsService:

      @@ -168,7 +170,7 @@

      PropertyTypeDescriptionisRunningboolWhether monitoring is currently active / 是否正在监控currentFpsdoubleCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)jankRatedoubleJank rate as percentage / 卡顿率(百分比)totalFrameCountintTotal frames captured / 总帧数totalJankyCountintTotal janky frames captured (>16ms) / 总卡顿帧数(>16ms)lastFrameJankyboolWhether the most recent frame was janky / 最近一帧是否卡顿fpsHistoryList<double>Recent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)frameRecordsList<FrameRecord>Recent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条) +
      PropertyTypeDescription
      isRunningboolWhether monitoring is currently active / 是否正在监控
      currentFpsdoubleCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)
      jankRatedoubleJank rate as percentage / 卡顿率(百分比)
      totalFrameCountintTotal frames captured / 总帧数
      totalJankyCountintTotal janky frames captured (per-frame budget) / 总卡顿帧数(按逐帧预算)
      lastFrameJankyboolWhether the most recent frame was janky / 最近一帧是否卡顿
      fpsHistoryList<double>Recent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)
      frameRecordsList<FrameRecord>Recent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)

      Performance Considerations / 性能说明

      • Off by default / 默认关闭: Zero overhead when disabled / 关闭时零开销
      • @@ -237,4 +239,4 @@

Zero Inspector Kit · MPL-2.0
\ No newline at end of file +
\ No newline at end of file diff --git a/docs/Getting-Started/index.html b/docs/Getting-Started/index.html index a54679a..79974f3 100644 --- a/docs/Getting-Started/index.html +++ b/docs/Getting-Started/index.html @@ -1,4 +1,4 @@ -
🚀 Getting Started

Getting Started / 快速开始

+
🚀 Getting Started

Getting Started / 快速开始

Quick Start / 快速开始

Integrate with just 1 line of code:

仅需 1 行代码 即可完成集成:

@@ -84,4 +84,4 @@

Installation — Detailed installation methods / 详细安装方式
  • Usage — Full usage guide / 完整使用指南
  • Configuration — Configuration options / 配置选项
  • -


    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file +
    \ No newline at end of file diff --git a/docs/Installation/index.html b/docs/Installation/index.html index cfb2f02..8b99bef 100644 --- a/docs/Installation/index.html +++ b/docs/Installation/index.html @@ -1,9 +1,9 @@ -
    📦 Installation

    Installation / 安装

    +

    Then run:

    然后运行:

    flutter pub get
    @@ -14,7 +14,7 @@

    zero_inspector_kit: git: url: https://github.com/zero-labsco/zero_inspector_kit.git - ref: release/v1.10.1

    + ref: release/v1.11.0

    Platform Setup / 平台配置

    Android

    No additional configuration needed.

    @@ -47,4 +47,4 @@

  • Getting Started — Quick start guide / 快速开始
  • Usage — Full usage guide / 完整使用指南
  • -

    \ No newline at end of file +
    🚀 Getting Started📖 Usage
    \ No newline at end of file diff --git a/docs/Log-Viewer/index.html b/docs/Log-Viewer/index.html index d1ac560..249a52d 100644 --- a/docs/Log-Viewer/index.html +++ b/docs/Log-Viewer/index.html @@ -1,4 +1,4 @@ -
    📝 Log Viewer

    Log Viewer / 日志查看器

    +
    📝 Log Viewer

    Log Viewer / 日志查看器

    Overview / 概述

    The Log Viewer automatically captures logs from multiple sources with zero configuration.

    日志查看器自动从多个来源捕获日志,无需配置。

    @@ -162,4 +162,4 @@

    Starting the Log Interceptor / 启动日志拦截器

    If using runAppWithInspector(), the log interceptor starts automatically. Otherwise:

    如果使用 runAppWithInspector(),日志拦截器会自动启动。否则:

    -
    InspectorLogInterceptor.instance.start();

    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file +
    InspectorLogInterceptor.instance.start();
    \ No newline at end of file diff --git a/docs/Memory-Viewer/index.html b/docs/Memory-Viewer/index.html index 249c428..6741cab 100644 --- a/docs/Memory-Viewer/index.html +++ b/docs/Memory-Viewer/index.html @@ -1,4 +1,4 @@ -
    📊 Memory Viewer

    Memory Viewer / 内存监控面板

    +
    📊 Memory Viewer

    Memory Viewer / 内存监控面板

    The Memory Viewer provides comprehensive in-app memory analysis, including trend charts, Dart Heap details, Native memory breakdown, memory leak detection, image cache monitoring, and storage statistics.

    内存监控面板提供应用内全面内存分析,包括趋势图、Dart Heap 详情、Native 内存分项、内存泄漏检测、图片缓存监控和存储统计。

    @@ -262,4 +262,4 @@

    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file +
    \ No newline at end of file diff --git a/docs/Network-Inspector/index.html b/docs/Network-Inspector/index.html index 3a24228..2804ef3 100644 --- a/docs/Network-Inspector/index.html +++ b/docs/Network-Inspector/index.html @@ -1,4 +1,4 @@ -
    🌐 Network Inspector

    Network Inspector / 网络检查器

    +
    🌐 Network Inspector

    Network Inspector / 网络检查器

    Overview / 概述

    The Network Inspector automatically captures all HTTP requests made via the http package and Dio, with zero configuration needed.

    网络检查器自动捕获所有通过 http 包 和 Dio 发送的 HTTP 请求,无需任何配置。

    @@ -154,6 +154,22 @@

    From the request detail view, tap Copy as cURL to copy the request as a ready-to-run cURL command (method, URL, headers, body). Respects the sensitive-field masking toggle above.

    在请求详情页点击 Copy as cURL,即可将请求复制为可直接运行的 cURL 命令(方法、URL、请求头、请求体),并遵循上方的敏感字段遮蔽开关。

    +

    Replay Editor / 重放编辑器

    +
    +

    Available since v1.11.0

    +

    v1.11.0 起可用

    +
    +

    Every request detail view has a replay action (↻ icon). It opens an editable sheet that lets you re-issue the captured request in-app and preview the response — handy for quickly re-running an endpoint without leaving the app.

    +

    每个请求详情页都有重放操作(↻ 图标)。它会打开一个可编辑弹层,让你在 App 内重新发出该请求并预览响应——无需离开应用即可快速复跑某个接口。

    +
      +
    • Only the URL query parameters are editable — add / edit / remove key=value rows; the URL is rebuilt from them on send / 仅 URL 查询参数可编辑——可增删改 key=value 行,发送时用其重建 URL
    • +
    • URL, request headers and request body are read-only by design (the captured values are re-sent verbatim) / URL、请求头与请求体按设计只读(以捕获到的原值原样重发)
    • +
    • On Send, the request is re-issued with the original method / headers / body and the edited query string; the sheet shows the returned status code, elapsed time, and a response preview / 点击 Send 后用原始方法 / 请求头 / 请求体加上修改后的查询串重新发出,弹层展示返回状态码、耗时与响应预览
    • +
    +
    +

    This is distinct from the interceptor (which builds a durable modification rule applied to future matching requests). The replay editor makes a one-off call with editable query params — including GET requests, whose params can’t be changed by the interceptor.

    +

    它与拦截器不同(拦截器生成作用于后续匹配请求的持久规则)。重放编辑器发起的是一次性调用,仅查询参数可改——包括 GET 请求(其参数拦截器无法修改)。

    +

    Status Code Colors / 状态码颜色

    @@ -314,4 +330,4 @@

    Create / edit / delete rules / 创建 / 编辑 / 删除规则
  • Enable / disable individual rules / 启用 / 禁用单条规则
  • View rule status indicators in the request list / 在请求列表中查看规则状态标识
  • -


    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file +
    \ No newline at end of file diff --git a/docs/Route-Tracker/index.html b/docs/Route-Tracker/index.html index 32008cf..9fb40b0 100644 --- a/docs/Route-Tracker/index.html +++ b/docs/Route-Tracker/index.html @@ -1,4 +1,4 @@ -
    🧭 Route Tracker

    Route Tracker / 路由追踪

    +
    🧭 Route Tracker

    Route Tracker / 路由追踪

    Overview / 概述

    The Route Tracker monitors navigation history, recording all route push, pop, and replacement events.

    路由追踪器监控导航历史,记录所有路由 push、pop 和替换事件。

    @@ -62,4 +62,4 @@

    MaterialApp(
       navigatorObservers: [InspectorRouteObserver()],
       home: MyHomePage(),
    -)

    \ No newline at end of file +)
    💾 Database Viewer📊 Memory Viewer
    \ No newline at end of file diff --git a/docs/Usage/index.html b/docs/Usage/index.html index 3b21aef..65806d8 100644 --- a/docs/Usage/index.html +++ b/docs/Usage/index.html @@ -1,4 +1,4 @@ -
    📖 Usage

    Usage / 使用指南

    +
    📖 Usage

    Usage / 使用指南

    Integration Methods / 集成方式

    void main() {
    @@ -202,4 +202,4 @@ 

    Session Persistence / 会话持久化

    Logs, network requests, and aggregated errors are asynchronously flushed to a local SQLite ring buffer. On the next launch, logs and aggregated errors replay into their tabs; network requests stay archived on disk for later export. Data therefore survives app restarts even if the inspector panel was never opened. Tap the storage icon in the panel header to open the Persisted data manager: view row counts per category, export the full session archive, or clear the disk. See Configuration (PersistenceService section) for the API and tuning parameters.

    -

    日志、网络请求与聚合异常会被异步写入本地 SQLite 环形缓冲。下次启动时,日志与聚合异常会回放入各自标签页;网络请求保留在磁盘存档,供之后导出。因此即使从未打开过检查器面板,数据也能跨重启保留。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看各类别行数、导出完整会话存档或清空磁盘。API 与调参详见 Configuration(PersistenceService 一节)。


    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file +

    日志、网络请求与聚合异常会被异步写入本地 SQLite 环形缓冲。下次启动时,日志与聚合异常会回放入各自标签页;网络请求保留在磁盘存档,供之后导出。因此即使从未打开过检查器面板,数据也能跨重启保留。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看各类别行数、导出完整会话存档或清空磁盘。API 与调参详见 Configuration(PersistenceService 一节)。

    \ No newline at end of file diff --git a/docs/_next/static/Ho8dPUk7o9kXcEclnMtW5/_buildManifest.js b/docs/_next/static/CXWehaxueihmLb2ZUFhrT/_buildManifest.js similarity index 80% rename from docs/_next/static/Ho8dPUk7o9kXcEclnMtW5/_buildManifest.js rename to docs/_next/static/CXWehaxueihmLb2ZUFhrT/_buildManifest.js index d539090..4b6c18c 100644 --- a/docs/_next/static/Ho8dPUk7o9kXcEclnMtW5/_buildManifest.js +++ b/docs/_next/static/CXWehaxueihmLb2ZUFhrT/_buildManifest.js @@ -1 +1 @@ -self.__BUILD_MANIFEST=function(e){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[e,"static/chunks/pages/index-2690f32ccd636754.js"],"/Alerts":[e,"static/chunks/pages/Alerts-303580b721158a8d.js"],"/Configuration":[e,"static/chunks/pages/Configuration-c728c0430f76605b.js"],"/Custom-Database-Provider":[e,"static/chunks/pages/Custom-Database-Provider-838fb9c0a6151981.js"],"/Database-Viewer":[e,"static/chunks/pages/Database-Viewer-5b3fb1bd8161d7e8.js"],"/Errors":[e,"static/chunks/pages/Errors-d6acd286d99365c4.js"],"/FAQ":[e,"static/chunks/pages/FAQ-3f5854cf7971f23b.js"],"/FPS-Viewer":[e,"static/chunks/pages/FPS-Viewer-93d1796fa44f218a.js"],"/Getting-Started":[e,"static/chunks/pages/Getting-Started-cb7caccb4a04217f.js"],"/Installation":[e,"static/chunks/pages/Installation-2cb5055fa0be64c8.js"],"/Log-Viewer":[e,"static/chunks/pages/Log-Viewer-f91ed29d9fff37fb.js"],"/Memory-Viewer":[e,"static/chunks/pages/Memory-Viewer-5f8bed1aa6f7d6b5.js"],"/Network-Inspector":[e,"static/chunks/pages/Network-Inspector-2a15a53da68a93b6.js"],"/Route-Tracker":[e,"static/chunks/pages/Route-Tracker-8de8d462ef1f1c4e.js"],"/Usage":[e,"static/chunks/pages/Usage-d81feeeea27e1c63.js"],"/_error":["static/chunks/pages/_error-7a92967bea80186d.js"],"/_meta":["static/chunks/pages/_meta-b47e7a01bb6b7929.js"],sortedPages:["/","/Alerts","/Configuration","/Custom-Database-Provider","/Database-Viewer","/Errors","/FAQ","/FPS-Viewer","/Getting-Started","/Installation","/Log-Viewer","/Memory-Viewer","/Network-Inspector","/Route-Tracker","/Usage","/_app","/_error","/_meta"]}}("static/chunks/812-3fcedf6f3c3b1908.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); \ No newline at end of file +self.__BUILD_MANIFEST=function(e){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[e,"static/chunks/pages/index-2690f32ccd636754.js"],"/Alerts":[e,"static/chunks/pages/Alerts-a0f5430c00bb2cc2.js"],"/Configuration":[e,"static/chunks/pages/Configuration-c728c0430f76605b.js"],"/Custom-Database-Provider":[e,"static/chunks/pages/Custom-Database-Provider-838fb9c0a6151981.js"],"/Database-Viewer":[e,"static/chunks/pages/Database-Viewer-5b3fb1bd8161d7e8.js"],"/Errors":[e,"static/chunks/pages/Errors-7cf1ee698701533d.js"],"/FAQ":[e,"static/chunks/pages/FAQ-3f5854cf7971f23b.js"],"/FPS-Viewer":[e,"static/chunks/pages/FPS-Viewer-b850e87e45780b99.js"],"/Getting-Started":[e,"static/chunks/pages/Getting-Started-cb7caccb4a04217f.js"],"/Installation":[e,"static/chunks/pages/Installation-95d3b8e64e9f547a.js"],"/Log-Viewer":[e,"static/chunks/pages/Log-Viewer-f91ed29d9fff37fb.js"],"/Memory-Viewer":[e,"static/chunks/pages/Memory-Viewer-5f8bed1aa6f7d6b5.js"],"/Network-Inspector":[e,"static/chunks/pages/Network-Inspector-e55229099cd437f6.js"],"/Route-Tracker":[e,"static/chunks/pages/Route-Tracker-8de8d462ef1f1c4e.js"],"/Usage":[e,"static/chunks/pages/Usage-d81feeeea27e1c63.js"],"/_error":["static/chunks/pages/_error-7a92967bea80186d.js"],"/_meta":["static/chunks/pages/_meta-b47e7a01bb6b7929.js"],sortedPages:["/","/Alerts","/Configuration","/Custom-Database-Provider","/Database-Viewer","/Errors","/FAQ","/FPS-Viewer","/Getting-Started","/Installation","/Log-Viewer","/Memory-Viewer","/Network-Inspector","/Route-Tracker","/Usage","/_app","/_error","/_meta"]}}("static/chunks/812-3fcedf6f3c3b1908.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); \ No newline at end of file diff --git a/docs/_next/static/Ho8dPUk7o9kXcEclnMtW5/_ssgManifest.js b/docs/_next/static/CXWehaxueihmLb2ZUFhrT/_ssgManifest.js similarity index 100% rename from docs/_next/static/Ho8dPUk7o9kXcEclnMtW5/_ssgManifest.js rename to docs/_next/static/CXWehaxueihmLb2ZUFhrT/_ssgManifest.js diff --git a/docs/_next/static/chunks/nextra-data-en-US.json b/docs/_next/static/chunks/nextra-data-en-US.json index 0d8d8da..59646d9 100644 --- a/docs/_next/static/chunks/nextra-data-en-US.json +++ b/docs/_next/static/chunks/nextra-data-en-US.json @@ -1 +1 @@ -{"/Alerts":{"title":"Alerts / 告警系统","data":{"overview--概述#Overview / 概述":"Available since v1.3.0v1.3.0 起可用\nThe alert system lets you define rules that proactively surface problems across network requests, logs, memory, and FPS — without manually scanning the panels.告警系统允许你定义规则,主动暴露网络请求、日志、内存、FPS 方面的问题,无需手动逐个面板排查。","how-it-works--工作原理#How It Works / 工作原理":"Rules are evaluated by AlertService whenever a relevant event occurs / 每当相关事件发生时,AlertService 会评估规则\nMatched rules produce alerts, and the unread count is exposed via a ValueNotifier / 命中的规则会生成告警,未读数通过 ValueNotifier 暴露\nThe floating button shows an unread-count badge; opening the panel clears it / 悬浮球显示未读数量角标,打开面板即清零\nAn Alerts tab in the inspector panel lists all triggered alerts / 检查器面板的 Alerts 标签页列出所有已触发的告警","rule-types--规则类型#Rule Types / 规则类型":"Type\tTrigger / 触发条件\tExample / 示例\tNetwork\tResponse status code / 响应状态码\tAlert when status ≥ 400 / 状态码 ≥ 400 时告警\tLog\tLog level or message / 日志级别或内容\tAlert on error logs / 出现 error 日志时告警\tMemory\tDart heap / Native memory threshold / 内存阈值\tAlert when Dart heap > 100 MB / Dart 堆 > 100 MB 时告警\tFPS\tFrame rate / 帧率\tAlert when FPS < 50 / FPS < 50 时告警","ui-features--ui-功能#UI Features / UI 功能":"","floating-button-badge--悬浮球角标#Floating Button Badge / 悬浮球角标":"A red badge shows the current unread alert count (clamped 0–99) / 红色角标显示当前未读告警数(0–99)\nThe number is drawn inside the button so it is never clipped at screen edges / 数字直接绘制在按钮内部,吸附屏幕边缘时不会被裁切\nOpening the inspector panel clears the unread count / 打开检查器面板即清零未读数","alerts-tab--alerts-标签页#Alerts Tab / Alerts 标签页":"Lists triggered alerts with type, condition, and timestamp / 列出已触发告警的类型、条件与时间\nTap an alert to jump to the related panel (network / log / memory / FPS) / 点击告警可跳转到相关面板(网络 / 日志 / 内存 / FPS)","notes--备注#Notes / 备注":"Alerts are evaluated in-process and shown in the developer console only; they do not send data off-device / 告警仅在本机开发者控制台内评估与展示,不会将数据发送到设备外\nThe alert system is tree-shaken out in release builds, like the rest of the inspector / 与检查器其余部分一样,告警系统在 release 构建中被 tree-shake 移除"}},"/Configuration":{"title":"Configuration / 配置说明","data":{"zeroinspectorkitinit-parameters--初始化参数#ZeroInspectorKit.init() Parameters / 初始化参数":"Parameter\tType\tDefault\tDescription\tenable\tbool\ttrue\tEnable inspector (auto false in release mode) / 启用检查器\tenableLogCapture\tbool\ttrue\tEnable log capture / 启用日志捕获\tenableNetworkCapture\tbool\ttrue\tEnable network interception / 启用网络拦截\tenableErrorCapture\tbool\ttrue\tEnable error aggregation (Errors tab) / 启用异常聚合(Errors 标签页)\tenablePersistence\tbool\ttrue\tPersist logs/network/errors to a SQLite ring buffer; logs & errors replay on launch / 将日志/网络/异常落盘到 SQLite 环形缓冲,启动时回放日志与异常\tenableFlutterLeakTracker\tbool\ttrue\tBridge Flutter's official MemoryAllocations as a second leak-detection source / 桥接 Flutter 官方 MemoryAllocations 作为泄漏检测第二来源\tenableDatabaseScan\tbool\ttrue\tEnable database scan / 启用数据库扫描\tenableRouteTracking\tbool\ttrue\tEnable route tracking / 启用路由追踪\tenableWidgetInspector\tbool\ttrue\tEnable Widget tree snapshot / 启用 Widget 树快照\tenableNetworkTimeline\tbool\ttrue\tPrefer the network timeline (waterfall) in request details / 网络详情页默认展示时间轴(瀑布图)\tcustomButton\tWidget?\tnull\tCustom floating button widget / 自定义悬浮按钮\tonLogCaptured\tvoid Function(LogEntry)?\tnull\tLog capture callback for third-party integration / 日志捕获回调\tmaxNetworkItems\tint?\t100\tNetwork request cache cap / 网络请求缓存上限\tmaxLogItems\tint?\t500\tLog entry cache cap / 日志条目缓存上限\tmaxRouteItems\tint?\t200\tRoute record cache cap / 路由记录缓存上限\tmaxBodyPreviewBytes\tint?\t32KB\tBody preview cap, longer bodies truncated / body 预览字节上限,超出截断\t\nenableWidgetInspector / enableNetworkTimeline are also exposed on runAppWithInspector(). (wrapApp() takes only enable.)enableWidgetInspector / enableNetworkTimeline 也可在 runAppWithInspector() 上设置(wrapApp() 仅接受 enable)。","usage-examples--使用示例#Usage Examples / 使用示例":"","disable-specific-features--禁用特定功能#Disable Specific Features / 禁用特定功能":"ZeroInspectorKit.init(\n enableLogCapture: true,\n enableNetworkCapture: false, // Disable network monitoring / 禁用网络监控\n enableDatabaseScan: true,\n enableRouteTracking: false, // Disable route tracking / 禁用路由追踪\n);","with-log-callback--带日志回调#With Log Callback / 带日志回调":"ZeroInspectorKit.init(\n onLogCaptured: (entry) {\n // Forward to your logging service / 转发到你的日志服务\n myLogger.log(entry.message);\n },\n);","conditionalinspector--条件检查器组件#ConditionalInspector / 条件检查器组件":"A convenience widget that automatically shows/hides the inspector based on build mode.根据构建模式自动显示/隐藏检查器的便利组件。\nConditionalInspector(\n child: YourAppWidget(),\n)\nParameter\tType\tDefault\tDescription\tchild\tWidget\trequired\tChild widget / 子组件\tenabled\tbool\ttrue\tEnable inspector / 启用检查器","floatinginspectorbutton--悬浮检查器按钮#FloatingInspectorButton / 悬浮检查器按钮":"Parameter\tType\tDefault\tDescription\tenabled\tbool\ttrue\tEnable button (auto false in release mode) / 启用按钮","inspectorloginterceptor--日志拦截器#InspectorLogInterceptor / 日志拦截器":"Method\tDescription\tstart()\tStart capturing logs / 开始捕获日志\tstop()\tStop capturing logs / 停止捕获日志\tlog(level, message, tag)\tAdd a log entry / 添加日志条目\tverbose(message, tag)\tAdd verbose log / 添加详细日志\tdebug(message, tag)\tAdd debug log / 添加调试日志\tinfo(message, tag)\tAdd info log / 添加信息日志\twarning(message, tag)\tAdd warning log / 添加警告日志\terror(message, tag)\tAdd error log / 添加错误日志\t\nProperty\tType\tDescription\tonLogCaptured\tvoid Function(LogEntry)?\tCallback when a log is captured / 日志捕获回调","inspectorrouteobserver--路由观察者#InspectorRouteObserver / 路由观察者":"Navigator observer for tracking route changes. Auto-injected when using runAppWithInspector() or wrapApp().用于追踪路由变化的 Navigator 观察者。使用 runAppWithInspector() 或 wrapApp() 时自动注入。\nMaterialApp(\n navigatorObservers: [InspectorRouteObserver()],\n home: MyHomePage(),\n)","databaseregistry--数据库注册表#DatabaseRegistry / 数据库注册表":"Register custom database providers:注册自定义数据库提供者:\nDatabaseRegistry.instance.registerProvider(SqliteDatabaseProvider());\nSee Custom Database Provider for more details.详见 自定义数据库提供者。","inspectorlog--简化日志-api#InspectorLog / 简化日志 API":"Available since v1.1.2 / v1.1.2 起可用\nA static wrapper around InspectorLogInterceptor.instance for shorter log calls.InspectorLogInterceptor.instance 的静态包装,用于更简短的日志调用。\nInspectorLog.v('Verbose log');\nInspectorLog.d('Debug log');\nInspectorLog.i('Info log', tag: 'Auth');\nInspectorLog.w('Warning log');\nInspectorLog.e('Error log');\nMethod\tDescription\tstart()\tStart capturing logs / 开始捕获日志\tstop()\tStop capturing logs / 停止捕获日志\tlog(level, message, {tag})\tAdd a log entry / 添加日志条目\tv(message, {tag})\tAdd verbose log / 添加详细日志\td(message, {tag})\tAdd debug log / 添加调试日志\ti(message, {tag})\tAdd info log / 添加信息日志\tw(message, {tag})\tAdd warning log / 添加警告日志\te(message, {tag})\tAdd error log / 添加错误日志\t\nProperty\tType\tDescription\tisRunning\tbool\tWhether log capture is currently active / 日志捕获是否正在运行","memoryinspectorservice--内存监控服务#MemoryInspectorService / 内存监控服务":"Available since v1.1.0 / v1.1.0 起可用\nSingleton service for memory monitoring and leak detection, extends ChangeNotifier.内存监控与泄漏检测单例服务,继承 ChangeNotifier。","full-api--完整接口#Full API / 完整接口":"// Leak tracking (full form) / 泄漏追踪(完整写法)\nMemoryInspectorService.instance.trackObject(\n myController,\n tag: 'HomeController_textController',\n expectedReleaseAfter: const Duration(seconds: 30),\n);\nMemoryInspectorService.instance.untrackObject(myController);\nMemoryInspectorService.instance.clearLeakRecords();","simplified-api--简化接口#Simplified API / 简化接口":"Available since v1.1.2 / v1.1.2 起可用\n// Extension method on Object / Object 上的扩展方法\nmyBloc.trackMemoryLeak(tag: 'HomePage_myBloc');\n// Top-level function / 顶层函数\ntrackMemoryLeak(myBloc, tag: 'HomePage_myBloc');\n// Cancel tracking / 取消追踪\nmyBloc.untrackMemoryLeak();\nSee Memory Viewer for full feature details.完整功能详情见 Memory Viewer。","flutter-memoryallocations-bridge--官方泄漏追踪桥接#Flutter MemoryAllocations Bridge / 官方泄漏追踪桥接":"Available since v1.9.0 / v1.9.0 起可用\nThe leak detector's own WeakReference state machine has a blind spot: an object whose dispose() ran but which has not been GC'd yet still resolves through the weak reference and is flagged as a suspected leak (false positive). LeakTrackerBridge subscribes to Flutter's official FlutterMemoryAllocations (the data source behind leak_tracker) as a second source: once the official stream reports disposed, the object is treated as released (awaiting GC) even if the weak reference still resolves — cutting false positives.自研泄漏检测的 WeakReference 状态机有一个盲点:对象已调用 dispose() 但尚未被 GC 时,弱引用仍存活,会被判定为\"疑似泄漏\"(误报)。LeakTrackerBridge 订阅 Flutter 官方的 FlutterMemoryAllocations(leak_tracker 背后的数据源)作为第二来源:只要官方上报了 disposed,即便弱引用仍存活也判定为已释放(等待 GC),从而显著降低误报。\nEnabled by default via ZeroInspectorKit.init()'s enableFlutterLeakTracker / 通过 init() 的 enableFlutterLeakTracker 默认启用\nOfficial events only cover framework types that report to FlutterMemoryAllocations (Image / Picture / Layer …) — it supplements, not replaces, the custom detector / 官方事件只覆盖会上报给 FlutterMemoryAllocations 的框架类型(如 Image / Picture / Layer),因此是补充而非替代自研方案","errorservice--异常聚合服务#ErrorService / 异常聚合服务":"Available since v1.9.0 / v1.9.0 起可用\nSingleton service for aggregated error capture, extends ChangeNotifier. Hooks FlutterError.onError (keeping the default red error behavior) and dedups each exception by type + stack signature. See Errors for full feature details.异常聚合单例服务,继承 ChangeNotifier。接管 FlutterError.onError(保留默认红色报错行为),按类型 + 堆栈签名去重聚合。完整功能见 Errors。\n// Report an exception manually (gRPC / custom protocols) / 手动上报异常\nErrorService.instance.report(error, stackTrace, 'myModule');\n// Read aggregated records (newest first) / 读取聚合记录(最新在前)\nfinal ErrorRecord r = ErrorService.instance.errors.first;\n// Toggle capture / 切换抓取开关\nErrorService.instance.isEnabled = false;\n// Clear all records / 清空全部记录\nErrorService.instance.clear();\nMember\tDescription\terrors\tList — aggregated records, newest first / 聚合记录,最新在前\terrorCount\tint — aggregated record count / 聚合记录条数\tisEnabled\tbool — capture toggle (programmatic) / 抓取开关(代码控制)\treport(exception, stack, [context])\tManually report an exception / 手动上报异常\trestore(records)\tRestore persisted records (replay on launch) / 恢复持久化记录(启动回放)\tclear()\tClear all aggregated records / 清空所有聚合记录\tinstall() / uninstall()\tHook / restore FlutterError.onError / 接管 / 还原 FlutterError.onError","persistenceservice--持久化服务#PersistenceService / 持久化服务":"Available since v1.9.0 / v1.9.0 起可用\nSingleton service that async-flushes logs, network requests, and aggregated errors to a local SQLite ring buffer (zero_inspector_kit.db) so data survives app restarts. On launch, logs and aggregated errors replay into their tabs; network requests stay archived on disk for later export. Everything is guarded and degrades gracefully: if the DB is unavailable (e.g. desktop without sqflite FFI) isEnabled is false and writes become no-ops, never affecting the host app.单例服务,将日志、网络请求与聚合异常异步落盘到本地 SQLite 环形缓冲(zero_inspector_kit.db),使数据跨重启不丢。启动时日志与聚合异常回放入各自标签页;网络请求保留在磁盘存档,供之后导出。所有操作都有保护并优雅降级:数据库不可用(如桌面端未配置 sqflite FFI)时 isEnabled 为 false,写入变为空操作,绝不影响宿主应用。","tuning--调参#Tuning / 调参":"await PersistenceService.instance.init(\n maxRowsPerTable: 5000, // rows kept per table / 每表保留行数\n retention: const Duration(days: 7), // retention window / 保留时长\n flushInterval: const Duration(seconds: 2), // disk flush interval / 刷盘间隔\n);\nEnabled by default via ZeroInspectorKit.init()'s enablePersistence — the ring buffer flushes automatically, so no manual init() call is needed in normal use.通过 init() 的 enablePersistence 默认启用——环形缓冲会自动刷盘,常规使用无需手动调用 init()。\nMember\tDescription\tisEnabled\tbool — whether the DB is available / 数据库是否可用\tmaxRowsPerTable\tint — row cap per table (ring-buffer trim line) / 每表行数上限\tenqueueLog(e) / enqueueNetwork(r) / enqueueError(e)\tEnqueue an item for async flush / 入队待异步落盘\tflush()\tFlush the buffer to disk and trim / 刷盘并按环形缓冲裁剪\tloadLogs() / loadErrors() / loadNetworkJson()\tLoad persisted data, newest first / 读取已持久化数据(最新在前)\tbuildSessionArchiveJson()\tBuild the full-session archive JSON (logs + errors + network) / 构建完整会话存档 JSON\texportSessionArchiveAndShare()\tExport & share the full-session archive via the system share sheet / 导出并分享完整会话存档\tclearAll()\tClear persisted data on disk / 清空磁盘上的持久化数据\tdispose()\tFlush then close / 先刷盘再关闭","persisted-data-manager--持久化数据管理#Persisted Data Manager / 持久化数据管理":"Tap the storage icon in the panel header to open the Persisted data sheet: it shows the current row counts per category (logs / network / errors), the per-table cap, and whether the data will be replayed on the next launch. Actions:点击面板头部的存储图标打开 Persisted data 管理弹层:展示各类别当前行数(logs / network / errors)、每表上限,以及下次启动是否会回放。支持的操作:\nExport & share session archive — share a JSON snapshot of the whole session / 导出并分享会话存档——分享本次会话完整 JSON 快照\nClear disk — wipe persisted rows so the next launch replays nothing / 清空磁盘——清空持久化数据,下次启动干净\nClear disk & lists — wipe disk and the in-memory lists together / 同时清空磁盘与列表——连同内存列表一并清空","fpsservice--fps-监控服务#FpsService / FPS 监控服务":"Available since v1.2.0 / v1.2.0 起可用\nSingleton service for FPS monitoring, extends ChangeNotifier.FPS 监控单例服务,继承 ChangeNotifier。\nFpsService.instance.start();\nFpsService.instance.stop();\nFpsService.instance.clear();\nfinal fps = FpsService.instance.currentFps;\nfinal jankRate = FpsService.instance.jankRate;\nMethod\tDescription\tstart()\tStart FPS monitoring / 开始 FPS 监控\tstop()\tStop FPS monitoring / 停止 FPS 监控\tclear()\tClear all historical data and counters / 清空所有历史数据和计数器\t\nProperty\tType\tDescription\tisRunning\tbool\tWhether monitoring is currently active / 是否正在监控\tcurrentFps\tdouble\tCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)\tjankRate\tdouble\tJank rate as percentage / 卡顿率(百分比)\ttotalFrameCount\tint\tTotal frames captured / 总帧数\ttotalJankyCount\tint\tTotal janky frames (>16ms) / 总卡顿帧数(>16ms)\tlastFrameJanky\tbool\tWhether the most recent frame was janky / 最近一帧是否卡顿\tfpsHistory\tList\tRecent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)\tframeRecords\tList\tRecent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)\t\nSee FPS Viewer for full feature details.完整功能详情见 FPS Viewer。"}},"/Custom-Database-Provider":{"title":"Custom Database Provider / 自定义数据库提供者","data":{"overview--概述#Overview / 概述":"Zero Inspector Kit supports extending database inspection beyond SQLite through the DatabaseProvider interface.Zero Inspector Kit 通过 DatabaseProvider 接口支持扩展数据库检查功能到 SQLite 以外的数据库。","databaseprovider-interface--接口定义#DatabaseProvider Interface / 接口定义":"abstract class DatabaseProvider {\n /// Provider name / 提供者名称\n String get name;\n /// Get list of databases / 获取数据库列表\n Future> getDatabases();\n /// Query table data / 查询表数据\n Future queryTable(String dbPath, String tableName, {int limit = 50});\n}","data-models--数据模型#Data Models / 数据模型":"","databaseinfo#DatabaseInfo":"Field\tType\tDescription\tname\tString\tDatabase name / 数据库名称\tpath\tString\tDatabase file path / 数据库文件路径\ttables\tList\tList of tables / 表列表","tableinfo#TableInfo":"Field\tType\tDescription\tname\tString\tTable name / 表名\trowCount\tint\tRow count / 行数","queryresult#QueryResult":"Field\tType\tDescription\tcolumns\tList\tColumn names / 列名\trows\tList>\tRow data / 行数据","example-custom-provider--示例自定义提供者#Example: Custom Provider / 示例:自定义提供者":"class MyCustomDatabaseProvider implements DatabaseProvider {\n @override\n String get name => 'CustomDB';\n @override\n Future> getDatabases() async {\n // Return your custom databases / 返回你的自定义数据库列表\n return [\n DatabaseInfo(\n name: 'my_database',\n path: '/path/to/my_database',\n tables: [\n TableInfo(name: 'users', rowCount: 100),\n TableInfo(name: 'orders', rowCount: 500),\n ],\n ),\n ];\n }\n @override\n Future queryTable(\n String dbPath,\n String tableName, {\n int limit = 50,\n }) async {\n // Execute query and return results / 执行查询并返回结果\n return QueryResult(\n columns: ['id', 'name', 'value'],\n rows: [\n {'id': 1, 'name': 'item1', 'value': '100'},\n {'id': 2, 'name': 'item2', 'value': '200'},\n ],\n );\n }\n}","register-provider--注册提供者#Register Provider / 注册提供者":"// Register your custom provider / 注册自定义提供者\nDatabaseRegistry.instance.registerProvider(MyCustomDatabaseProvider());","built-in-provider--内置提供者#Built-in Provider / 内置提供者":"The plugin includes SqliteDatabaseProvider which is auto-registered when enableDatabaseScan is true (default).插件内置 SqliteDatabaseProvider,当 enableDatabaseScan 为 true(默认)时自动注册。You can also register it manually:也可以手动注册:\nDatabaseRegistry.instance.registerProvider(SqliteDatabaseProvider());"}},"/Database-Viewer":{"title":"Database Viewer / 数据库查看器","data":{"overview--概述#Overview / 概述":"The Database Viewer inspects SQLite databases in your app with a two-level navigation system.数据库查看器通过双层导航系统检查应用中的 SQLite 数据库。","auto-scan--自动扫描#Auto-Scan / 自动扫描":"The inspector automatically scans the following directories for .db and .sqlite files:检查器自动扫描以下目录中的 .db 和 .sqlite 文件:\ngetApplicationDocumentsDirectory() / 应用文档目录\ngetDatabasesPath() / 数据库目录","two-level-navigation--双层导航#Two-Level Navigation / 双层导航":"","level-1-database-list-global--第一层数据库列表全局#Level 1: Database List (Global) / 第一层:数据库列表(全局)":"Shows all discovered databases / 显示所有发现的数据库\nEach database shows name and table count / 每个数据库显示名称和表数量\nGlobal search: Search database names and table names / 全局搜索:搜索数据库名和表名","level-2-database-detail--第二层数据库详情#Level 2: Database Detail / 第二层:数据库详情":"Click a database to enter its detail view / 点击数据库进入详情视图\nLeft panel: table list with row counts / 左侧面板:表列表(含行数)\nRight panel: table data in DataTable format / 右侧面板:DataTable 格式的表数据\nBack button to return to database list / 返回按钮返回数据库列表\nIn-database search: Search table names AND all column data / 数据库内搜索:搜索表名和所有列数据","search--搜索#Search / 搜索":"Level\tSearch Scope\tGlobal (database list)\tDatabase names, table names / 数据库名、表名\tIn-database\tTable names, all column values in all tables / 表名、所有表的所有列值","search-highlights--搜索高亮#Search Highlights / 搜索高亮":"When searching within a database, matching cell values are highlighted in accent color.在数据库内搜索时,匹配的单元格值以强调色高亮显示。","ui-features--ui-功能#UI Features / UI 功能":"Color-coded table icons / 带颜色的表图标\nRow count badges for each table / 每个表的行数徽章\nSelected table highlighted / 选中表高亮\nHorizontal and vertical scrollable data table / 水平和垂直可滚动的数据表\nRow count display (filtered / total) when searching / 搜索时显示行数(过滤/总数)","custom-database-provider--自定义数据库提供者#Custom Database Provider / 自定义数据库提供者":"For non-SQLite databases, see Custom Database Provider.对于非 SQLite 数据库,请参阅 自定义数据库提供者。"}},"/Errors":{"title":"Errors / 异常聚合查看器","data":{"overview--概述#Overview / 概述":"Available since v1.9.0v1.9.0 起可用\nThe Errors tab surfaces \"the same crash happening repeatedly\". When the inspector is running, ErrorService hooks FlutterError.onError (keeping the default red-screen / console behavior) and aggregates every exception by type + stack signature. Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces.Errors 标签页用来快速发现\"同一处崩溃反复出现\"的问题。检查器运行时,ErrorService 会接管 FlutterError.onError(保留默认的红色报错与控制台行为),把每次异常按类型 + 堆栈签名去重聚合:同一处崩溃的多次发生会合并为一条记录,展示累计次数与首末次时间——而不是用成百上千条相同的堆栈刷屏日志。","what-gets-captured--捕获来源#What Gets Captured / 捕获来源":"Source / 来源\tHow / 方式\tFlutter framework errors / Flutter 框架异常\tHooks FlutterError.onError (default handler kept) / 接管 FlutterError.onError(保留默认处理)\tUncaught async errors / 未捕获异步异常\trunZonedGuarded inside runAppWithInspector() / runAppWithInspector() 内部的 runZonedGuarded\tManual reports / 手动上报\tErrorService.instance.report(exception, stackTrace) — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道\t\nCaptured records are also persisted to disk (see Session Persistence below), so aggregated errors from previous sessions are replayed on the next launch.捕获到的记录也会落盘持久化(见下文会话持久化),下次启动时会回放上次会话的聚合异常。","deduplication--去重规则#Deduplication / 去重规则":"Records are keyed by exception type plus a stack signature (first frames with line/address noise stripped) / 按异常类型 + 堆栈签名(取前若干帧并去掉行号/地址噪声)去重\nA new occurrence of an existing signature bumps its count and updates lastSeen instead of adding a new row / 同签名的再次发生只累加 count 并更新 lastSeen,不新增行\nRing buffer capped at 200 aggregated records (oldest dropped) / 环形缓冲上限 200 条聚合记录(超出丢弃最旧)","ui-features--ui-功能#UI Features / UI 功能":"","errors-tab--errors-标签页#Errors Tab / Errors 标签页":"Lists aggregated errors newest-first, showing the exception type, message, first seen / last seen time, and a ×N badge when the same crash recurred / 按最新在前列出聚合异常,展示异常类型、消息、首次/末次出现时间,同崩溃复发时显示 ×N 徽章\nTap a row to expand the full stack sample (truncated at 8000 chars); tap again to collapse / 点击行展开完整堆栈样本(8000 字符截断);再点收起\nCopy stack per row / 行内复制堆栈\nSearch filters by exception type or message / 搜索按异常类型或消息过滤\nClear empties the aggregated list / 清除清空聚合列表","tab-badge--标签页红点#Tab Badge / 标签页红点":"The Errors tab icon shows a red count badge while there are aggregated errors (capped at 99+), so recurring crashes are visible at a glance / Errors 标签页图标在有聚合异常时显示红色计数(99+ 封顶),一眼可见反复崩溃\nOpening the Errors tab is not required to see the badge — it reflects ErrorService.instance.errorCount whenever the panel is open / 只要面板打开,标签红点即反映 ErrorService.instance.errorCount,无需先进入 Errors 页\nNote: the floating ball itself stays clean — its red count (if any) is the Alerts unread badge (AlertService), not error aggregation.注意:悬浮球本身保持纯粹——球上的红色数字(如有)来自告警未读数(AlertService),与异常聚合无关。","api--接口#API / 接口":"The full service is re-exported from the package root — no need to import lib/src/.完整服务已从包根导出,无需 import lib/src/。\nimport 'package:zero_inspector_kit/zero_inspector_kit.dart';\n// Report an exception manually (gRPC/custom protocol/your own error paths) / 手动上报异常\nErrorService.instance.report(error, stackTrace, 'myModule');\n// Read the aggregated view (newest first) / 读取聚合视图(最新在前)\nfinal ErrorRecord latest = ErrorService.instance.errors.first;\nprint('${latest.type} x${latest.count} — first ${latest.firstSeen} / last ${latest.lastSeen}');\n// Clear all records / 清空所有记录\nErrorService.instance.clear();\nMember\tDescription\terrors\tList — aggregated records, newest first / 聚合记录,最新在前\terrorCount\tint — number of aggregated records / 聚合记录条数\tisEnabled\tbool — capture toggle (programmatic) / 抓取开关(代码控制)\treport(exception, stack, [context])\tManually report an exception / 手动上报异常\trestore(records)\tRestore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过)\tclear()\tClear all aggregated records / 清空全部记录\tinstall() / uninstall()\tHook / restore FlutterError.onError / 接管 / 还原 FlutterError.onError","errorrecord--异常记录#ErrorRecord / 异常记录":"Field\tDescription\ttype\tException type name (e.g. _TypeError) / 异常类型名\tmessage\tException message / 异常消息\tcount\tTotal occurrences / 累计出现次数\tfirstSeen / lastSeen\tFirst / last occurrence time / 首次 / 末次出现时间\tsampleStack\tOne full stack sample (may be truncated) / 一条完整堆栈样本(可能截断)","session-persistence--会话持久化#Session Persistence / 会话持久化":"Errors — together with logs and network requests — are asynchronously flushed to a local SQLite ring buffer (zero_inspector_kit.db). On the next launch, logs and aggregated errors replay into their tabs, so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the storage icon in the panel header to open the Persisted data manager: see the current row counts, export the full session archive as JSON, or clear the disk. See Configuration (PersistenceService section) for details and the tuning parameters.异常与日志、网络请求一起被异步写入本地 SQLite 环形缓冲(zero_inspector_kit.db)。下次启动时,日志与聚合异常会回放入各自标签页——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看当前行数、导出完整会话存档 JSON,或清空磁盘。详见 Configuration(PersistenceService 一节)的参数说明。","enable--disable--开关#Enable / Disable / 开关":"enableErrorCapture (default true) in ZeroInspectorKit.init() controls error aggregation / init() 的 enableErrorCapture(默认 true)控制异常聚合\nenablePersistence (default true) controls the disk ring buffer / enablePersistence(默认 true)控制磁盘环形缓冲\nSet both to false to keep everything in memory only / 两者都设为 false 时数据仅保留在内存\nZeroInspectorKit.init(\n enableErrorCapture: true,\n enablePersistence: true, // false → memory-only / 关闭后仅内存\n);","related--相关#Related / 相关":"Log Viewer — raw log stream including error lines / 原始日志流(含错误行)\nConfiguration — init parameters and full service APIs / 初始化参数与完整服务接口\nUsage — general usage guide / 使用指南"}},"/FAQ":{"title":"FAQ / 常见问题","data":{"general--通用#General / 通用":"","q-does-the-inspector-affect-production-builds--检查器会影响生产构建吗#Q: Does the inspector affect production builds? / 检查器会影响生产构建吗?":"A: No. The inspector is automatically disabled in release mode via kReleaseMode. Flutter's tree-shaking removes all inspector-related code from production builds. You don't need to remove any code.不会。 检查器在 release 模式下通过 kReleaseMode 自动禁用。Flutter 的 tree-shaking 会移除所有检查器相关代码,无需手动移除。","q-what-platforms-are-supported--支持哪些平台#Q: What platforms are supported? / 支持哪些平台?":"A: Android and iOS.支持 Android 和 iOS。","q-what-flutterdart-versions-are-required--需要什么-flutterdart-版本#Q: What Flutter/Dart versions are required? / 需要什么 Flutter/Dart 版本?":"A: Flutter >= 3.3.0, Dart SDK >= 3.11.0 < 4.0.0.Flutter >= 3.3.0,Dart SDK >= 3.11.0 < 4.0.0。","network--网络#Network / 网络":"","q-do-i-need-to-manually-add-interceptors-for-dio--需要为-dio-手动添加拦截器吗#Q: Do I need to manually add interceptors for Dio? / 需要为 Dio 手动添加拦截器吗?":"A: No. Dio uses IOHttpClientAdapter internally, which uses dart:io's HttpClient. The inspector captures all requests via HttpOverrides automatically, making it truly zero-invasion for both http package and Dio.不需要。 Dio 内部使用 IOHttpClientAdapter,底层使用 dart:io 的 HttpClient。检查器通过 HttpOverrides 自动捕获所有请求,对 http 包和 Dio 都是真正的零侵入。","q-why-do-i-see-duplicate-dio-requests--为什么看到重复的-dio-请求#Q: Why do I see duplicate Dio requests? / 为什么看到重复的 Dio 请求?":"A: If you use both InspectorDioInterceptor and the auto-capture (HttpOverrides), Dio requests will be recorded twice. Simply remove the manual InspectorDioInterceptor — auto-capture handles everything.如果同时使用了 InspectorDioInterceptor 和自动捕获(HttpOverrides),Dio 请求会被记录两次。移除手动 InspectorDioInterceptor 即可,自动捕获会处理一切。","q-can-i-modify-requests-during-testing--测试时可以修改请求吗#Q: Can I modify requests during testing? / 测试时可以修改请求吗?":"A: Yes (since v1.0.7). The inspector supports intercepting and modifying requests via rules. Open a request detail and tap the Interceptor icon to configure a rule. You can modify the request body and request headers only — response fields (status code, response body) are read-only (since v1.0.8). GET requests cannot be modified (no request body). The master toggle is on the Network list page; rules only apply when modification mode is enabled.可以(v1.0.7 起)。检查器支持通过规则拦截并修改请求。打开请求详情,点击拦截器图标配置规则。仅可修改请求体和请求头——响应字段(状态码、响应体)只读(v1.0.8 起)。GET 请求不可修改(无请求体)。总开关在 Network 列表页,规则仅在启用修改模式时生效。See Network Inspector > Request Interceptor for details.详见 Network Inspector > 请求拦截修改。","logging--日志#Logging / 日志":"","q-can-i-use-my-existing-logging-library--可以使用现有的日志库吗#Q: Can I use my existing logging library? / 可以使用现有的日志库吗?":"A: Yes! The inspector automatically captures logs from any library that uses print() or debugPrint(). No configuration needed. Third-party logs are categorized as Info level.可以! 检查器会自动捕获任何使用 print() 或 debugPrint() 的日志库的日志,无需配置。第三方日志统一归类为 Info 级别。","q-how-to-sync-inspector-logs-to-my-logger--如何将检查器日志同步到我的日志库#Q: How to sync inspector logs to my logger? / 如何将检查器日志同步到我的日志库?":"A: Use the onLogCaptured callback:使用 onLogCaptured 回调:\nInspectorLogInterceptor.instance.onLogCaptured = (entry) {\n yourLogger.log(entry.message);\n};\nWarning: Do NOT call print() or logging methods inside this callback, as it will cause infinite recursion.\n警告:不要在此回调中调用 print() 或日志方法,否则会导致无限递归。","database--数据库#Database / 数据库":"","q-why-dont-i-see-any-databases--为什么看不到数据库#Q: Why don't I see any databases? / 为什么看不到数据库?":"A: The inspector scans getApplicationDocumentsDirectory() and getDatabasesPath() for .db and .sqlite files. If your database is stored elsewhere, you can implement a custom DatabaseProvider.检查器扫描 getApplicationDocumentsDirectory() 和 getDatabasesPath() 目录中的 .db 和 .sqlite 文件。如果你的数据库存储在其他位置,可以实现自定义 DatabaseProvider。","memory--内存#Memory / 内存":"","q-why-does-dart-heap-show-na-when-debugging-via-pc--通过-pc-调试时为什么-dart-heap-显示-na#Q: Why does Dart Heap show \"N/A\" when debugging via PC? / 通过 PC 调试时为什么 Dart Heap 显示 \"N/A\"?":"A: This is an expected behavior. When using flutter run to debug via PC, the flutter tool sets up port forwarding between PC and device via adb reverse, allowing PC-side DevTools to access the device's VM Service. However, Service.getInfo() returns a serverUri from the PC's perspective; when the app process internally accesses 127.0.0.1:PC_port, the device doesn't have that port listening locally, resulting in Connection refused and VM Service showing OFF.这是预期行为。 使用 flutter run 连接 PC 调试时,flutter tool 会通过 adb reverse 在 PC 和设备之间做端口转发,让 PC 上的 DevTools 能访问设备的 VM Service。但应用进程内部 Service.getInfo() 返回的 serverUri 是 PC 视角的端口,应用进程访问 127.0.0.1:PC端口 时设备本地并没有监听该端口,导致 Connection refused,VM Service 显示 OFF。Workarounds / 解决方案:\nOpen the debug app directly without PC connection — VM Service works normally / 直接打开 debug 应用(不连 PC),VM Service 正常工作\nUse Native memory data (Android PSS / iOS physicalFootprint) as fallback — always available / 使用 Native 内存数据(Android PSS / iOS physicalFootprint)作为降级,始终可用\nProcess RSS is always available regardless of VM Service / 进程 RSS 无论 VM Service 状态都可用\nSee Memory Viewer > VM Service Availability for details.详见 Memory Viewer > VM Service 可用性。","q-does-memory-monitoring-affect-performance--内存监控会影响性能吗#Q: Does memory monitoring affect performance? / 内存监控会影响性能吗?":"A: Memory monitoring is off by default (since v1.1.0). When disabled, all timers are stopped and VM Service connection is cleared, leaving zero overhead. When enabled, refresh intervals are optimized:内存监控默认关闭(v1.1.0 起)。关闭时所有定时器停止、VM Service 连接清空,零开销。开启时刷新间隔已优化:\nProcess RSS / Dart Heap: 500ms\nNative Memory: 3000ms\nStorage Stats: 3000ms\nLeak Detection: 2000ms\nYou can toggle the switch at the top of the Memory panel anytime.可随时在 Memory 面板顶部切换开关。","q-does-leak-detection-require-code-modification--泄漏检测需要侵入代码吗#Q: Does leak detection require code modification? / 泄漏检测需要侵入代码吗?":"A: The current implementation uses WeakReference + Finalizer, which requires calling trackObject() to register objects for tracking. This is a mild invasion (one line of code per tracked object).当前实现使用 WeakReference + Finalizer,需要调用 trackObject() 注册要追踪的对象。这是轻度侵入(每个追踪对象一行代码)。Trade-offs / 取舍:\nPros: 100% reliable, doesn't depend on VM Service, works in release mode / 100% 可靠,不依赖 VM Service,release 模式也可用\nCons: Requires user to know which objects to track / 需要用户知道要追踪哪些对象\nA zero-invasion Heap Snapshot comparison feature (based on VM Service) is technically possible but would strongly depend on VM Service availability (unavailable when debugging via PC), with significant performance overhead. Not currently implemented.零侵入的 Heap Snapshot 对比功能(基于 VM Service)技术上可行,但会强依赖 VM Service 可用性(PC 调试时不可用),且有显著性能开销。目前未实现。See Memory Viewer > Memory Leak Detection for API details.详见 Memory Viewer > 内存泄漏检测 了解 API 详情。","fps--帧率#FPS / 帧率":"","q-why-does-fps-show-a-very-low-value-eg-9-10-fps--为什么-fps-显示很低如-9-10#Q: Why does FPS show a very low value (e.g. 9-10 FPS)? / 为什么 FPS 显示很低(如 9-10)?":"A: This was a bug fixed in v1.2.0. The root cause was that _recentFrameTimestamps.add(now) was placed outside the for-loop in _onFrameTimings; since Flutter engine's addTimingsCallback is batched (may return multiple frames per call), only one timestamp was recorded per batch, undercounting FPS by 6-10x. Upgrade to ^1.2.0 to fix this.这是 v1.2.0 已修复的 bug。根因是 _onFrameTimings 中 _recentFrameTimestamps.add(now) 在 for 循环外;Flutter 引擎的 addTimingsCallback 是批量回调(一次可能返回多帧),但每批只记录 1 个时间戳,导致 FPS 计算偏低 6-10 倍。升级到 ^1.2.0 即可修复。See FPS Viewer for details.详见 FPS Viewer。","q-does-fps-monitoring-affect-performance--fps-监控会影响性能吗#Q: Does FPS monitoring affect performance? / FPS 监控会影响性能吗?":"A: FPS monitoring is off by default (since v1.2.0). When disabled, no frame timings callbacks and no timers — zero overhead. When enabled, it only processes lightweight frame timing data with bounded history (60 trend points, up to 3600 frame records). You can toggle the switch at the top of the FPS panel anytime.FPS 监控默认关闭(v1.2.0 起)。关闭时无帧回调、无定时器——零开销。开启时仅处理轻量帧时序数据,历史有界(趋势 60 个点,帧记录最多 3600 条)。可随时在 FPS 面板顶部切换开关。","ui--界面#UI / 界面":"","q-the-inspector-panel-gets-pushed-up-when-keyboard-appears--键盘弹出时检查器面板被顶起来了#Q: The inspector panel gets pushed up when keyboard appears. / 键盘弹出时检查器面板被顶起来了。":"A: This was fixed in v1.0.6. The floating button and panel are rendered via Overlay, which is independent of the page layout and not affected by keyboard.此问题已在 v1.0.6 修复。悬浮按钮和面板通过 Overlay 渲染,独立于页面布局,不受键盘影响。","q-how-to-use-search--如何使用搜索#Q: How to use search? / 如何使用搜索?":"A: Each viewer has its own search bar at the top. Database viewer has two-level search: global search (database list) and in-database search (table names + all column data).每个查看器顶部都有搜索栏。数据库查看器有双层搜索:全局搜索(数据库列表)和数据库内搜索(表名 + 所有列数据)。","q-the-floating-button-disappeared--cant-be-opened-after-enabling-fps--开启-fps-后悬浮按钮消失无法打开#Q: The floating button disappeared / can't be opened after enabling FPS. / 开启 FPS 后悬浮按钮消失/无法打开。":"A: This was a bug fixed in v1.2.0. The root cause was that FpsService.notifyListeners() triggered Overlay rebuild; combined with the original Overlay lookup failure (using Overlay.of(context, rootOverlay: true) which returned null), the button State was destroyed and could not recover. Upgrade to ^1.2.0 which uses navigatorState.overlay instead.这是 v1.2.0 已修复的 bug。根因是 FpsService.notifyListeners() 触发 Overlay 重建,叠加原 Overlay 查找失败(使用 Overlay.of(context, rootOverlay: true) 返回 null),导致按钮 State 被销毁且无法恢复。升级到 ^1.2.0,改用 navigatorState.overlay 即可。","q-how-does-the-floating-button-edge-docking-work--悬浮按钮的边缘吸附怎么用#Q: How does the floating button edge docking work? / 悬浮按钮的边缘吸附怎么用?":"A: Since v1.2.0, when you drag the button near a screen edge and release, it auto-docks and tucks into the edge, leaving only a 24px peek visible. The icon becomes a directional chevron hinting you can tap to pull it out:\nTap the peek → smoothly pulls out to fully visible (panel NOT opened, avoids accidental open) / 平滑拉出到完全可见(不打开面板,避免误触)\nTap when fully visible → opens the inspector panel / 打开检查器面板\nThis design avoids conflicts with system back gestures (Android/iOS edge swipe to go back) when pulling out from the docked state.v1.2.0 起,拖动按钮靠近屏幕边缘松手即自动吸附并\"收入\"边缘,仅露 24px。图标变为方向箭头,提示可点击拉出:\n点击露出部分 → 平滑拉出到完全可见(不打开面板,避免误触)\n完全可见时点击 → 打开检查器面板\n此设计避免了从吸附态拖出时与系统返回手势(Android/iOS 边缘右滑退出)的冲突。See Usage > Edge Docking for details.详见 Usage > 边缘吸附。","license--许可证#License / 许可证":"","q-can-i-use-this-in-a-commercial-project--可以在商业项目中使用吗#Q: Can I use this in a commercial project? / 可以在商业项目中使用吗?":"A: Yes. This project is licensed under MPL-2.0. You can use it in closed-source commercial apps — if you use it unmodified, no source disclosure is required. If you modify the plugin's source files, those modified files must be published in source form under MPL-2.0 (your own app still does not need to be open-sourced).可以。本项目采用 MPL-2.0 许可证,可用于闭源商业 App——未修改直接使用时无需公开任何源码;若修改了插件源文件,被修改的文件必须以 MPL-2.0 公开源码(你自己的 App 仍无需开源)。","q-are-you-liable-for-issues-in-modified-versions--修改版出问题你们负责吗#Q: Are you liable for issues in modified versions? / 修改版出问题你们负责吗?":"A: This plugin is provided \"as is\", without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.本插件按\"原样\"提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。"}},"/FPS-Viewer":{"title":"FPS Viewer / FPS 监控面板","data":{"":"The FPS Viewer provides real-time frame performance analysis, including current FPS, jank rate, frame duration stats, and an FPS trend chart.FPS 监控面板提供实时帧性能分析,包括当前 FPS、卡顿率、帧耗时统计和 FPS 趋势图。\nAvailable since v1.2.0v1.2.0 起可用","overview--概览#Overview / 概览":"The FPS Viewer is integrated into the inspector panel. Tap the floating inspector button → \"FPS\" tab to access.FPS 监控面板集成在检查器面板中。点击悬浮检查器按钮 → \"FPS\" 标签即可访问。⚠️ Important: FPS monitoring is off by default. You must toggle on the switch at the top of the panel to start collecting data.⚠️ 重要:FPS 监控默认关闭。必须在面板顶部打开开关才会开始采集数据。","master-switch--总开关#Master Switch / 总开关":"A switch at the top of the FPS panel controls whether monitoring is enabled.FPS 面板顶部有一个开关,控制是否启用监控。\nState\tBehavior\tOFF (default)\tNo frame timings callbacks, no timer, zero overhead / 无帧回调、无定时器、零开销\tON\tStarts collecting frame data via WidgetsBinding.instance.addTimingsCallback / 通过 addTimingsCallback 开始采集帧数据\t\nWhen turned off, all callbacks and timers are cancelled, leaving no residual overhead.关闭时所有回调和定时器会被取消,不会残留开销。","features--功能#Features / 功能":"","1-current-stats--当前统计#1. Current Stats / 当前统计":"Current FPS — Updated every 500ms / 每 500ms 更新\nJank Rate — Percentage of janky frames (>16ms) / 卡顿帧占比(>16ms 视为卡顿)\nTotal Frame Count — All frames captured since start / 自启动以来的总帧数\nTotal Janky Count — All janky frames captured / 自启动以来的总卡顿帧数\nLast Frame Janky — Whether the most recent frame was janky / 最近一帧是否卡顿\n显示当前 FPS、卡顿率、总帧数、总卡顿帧数、最近一帧是否卡顿。","2-fps-trend-chart--fps-趋势图#2. FPS Trend Chart / FPS 趋势图":"Real-time line chart with 30-second history window (60 data points × 500ms)\nY-axis dynamically scales to max(60, maxFps) to avoid overflow when FPS spikes\nJanky frame region marked in red\nTime axis labels: -30s / -15s / Now\n实时折线图,30 秒历史窗口(60 个数据点 × 500ms)。Y 轴动态缩放为 max(60, maxFps) 避免 FPS 飙升时溢出。卡顿区域以红色标记。","3-janky-frame-list--卡顿帧列表#3. Janky Frame List / 卡顿帧列表":"Lists frames exceeding 16ms duration / 列出耗时超过 16ms 的帧\nEach item shows frame duration and timestamp / 每项显示帧耗时和时间戳\nHelps identify specific jank spikes / 帮助定位具体卡顿点\n列出耗时超过 16ms 的帧,每项显示帧耗时和时间戳,帮助定位具体卡顿点。","4-reset--重置#4. Reset / 重置":"\"Reset\" button clears all statistics and historical data / \"Reset\" 按钮清空所有统计和历史数据\nUseful for measuring a specific interaction scenario / 适合测量特定交互场景\n\"Reset\" 按钮清空所有统计和历史数据,适合测量特定交互场景。","how-it-works--工作原理#How It Works / 工作原理":"FPS monitoring uses Flutter's WidgetsBinding.instance.addTimingsCallback to receive frame timing information from the engine. The callback is batched — it may return multiple FrameTiming objects per call, so each frame is recorded individually inside the loop to ensure accurate FPS calculation.FPS 监控使用 Flutter 的 WidgetsBinding.instance.addTimingsCallback 接收引擎的帧时序信息。该回调是批量的——每次调用可能返回多个 FrameTiming 对象,因此在循环内部为每帧单独记录时间戳,确保 FPS 计算准确。Jank threshold / 卡顿阈值: A frame is considered janky if its duration exceeds 16ms (the 60 FPS budget of ~16.67ms per frame).卡顿阈值:帧耗时超过 16ms(60 FPS 每帧预算约 16.67ms)即视为卡顿。","programmatic-control-optional--编程式控制可选#Programmatic Control (Optional) / 编程式控制(可选)":"If you need to start/stop monitoring from code (e.g., for automated testing), use FpsService:如需从代码控制监控(如自动化测试),使用 FpsService:\n// Start / Stop FPS monitoring / 开始 / 停止 FPS 监控\nFpsService.instance.start();\nFpsService.instance.stop();\n// Read current stats / 读取当前统计\nfinal fps = FpsService.instance.currentFps;\nfinal jankRate = FpsService.instance.jankRate;\nfinal totalFrames = FpsService.instance.totalFrameCount;\nfinal jankyFrames = FpsService.instance.totalJankyCount;\n// Clear historical data / 清空历史数据\nFpsService.instance.clear();\n// Listen to updates / 监听更新\nFpsService.instance.addListener(() {\n // Update your own UI / 更新你自己的 UI\n});\n// Historical data access / 历史数据访问\nfinal history = FpsService.instance.fpsHistory; // List, 60 entries / 60 条\nfinal records = FpsService.instance.frameRecords; // List, unmodifiable / 不可变,最多 3600 条","fpsservice-api--fpsservice-接口#FpsService API / FpsService 接口":"Singleton service extending ChangeNotifier.继承 ChangeNotifier 的单例服务。\nMethod\tDescription\tstart()\tStart FPS monitoring / 开始 FPS 监控\tstop()\tStop FPS monitoring / 停止 FPS 监控\tclear()\tClear all historical data and counters / 清空所有历史数据和计数器\t\nProperty\tType\tDescription\tisRunning\tbool\tWhether monitoring is currently active / 是否正在监控\tcurrentFps\tdouble\tCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)\tjankRate\tdouble\tJank rate as percentage / 卡顿率(百分比)\ttotalFrameCount\tint\tTotal frames captured / 总帧数\ttotalJankyCount\tint\tTotal janky frames captured (>16ms) / 总卡顿帧数(>16ms)\tlastFrameJanky\tbool\tWhether the most recent frame was janky / 最近一帧是否卡顿\tfpsHistory\tList\tRecent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)\tframeRecords\tList\tRecent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)","performance-considerations--性能说明#Performance Considerations / 性能说明":"Off by default / 默认关闭: Zero overhead when disabled / 关闭时零开销\nLightweight callbacks / 轻量回调: Only processes frame timing data, no heavy computation / 仅处理帧时序数据,无重计算\nBounded history / 有界历史: Trend chart keeps only 60 points, frame records capped at 3600 / 趋势图仅保留 60 个点,帧记录上限 3600 条\nSafe with other features / 与其他功能兼容: Can run alongside Memory Viewer monitoring / 可与 Memory Viewer 监控同时运行","platform-support--平台支持#Platform Support / 平台支持":"Feature\tAndroid\tiOS\tFPS Monitoring\t✅\t✅\tJank Detection\t✅\t✅\tTrend Chart\t✅\t✅\t\nFPS monitoring uses Flutter engine APIs and works on Android and iOS (the two platforms this plugin targets).FPS 监控使用 Flutter 引擎 API,在本插件支持的两个平台 Android 与 iOS 上均可用。","demo--演示#Demo / 演示":"The Example App includes an FPS demo module with three scenarios:Example App 包含 FPS 演示模块,提供三种场景:\nDemo\tDescription\tTrigger Jank\tBlocks the main thread for 100-500ms to simulate jank / 阻塞主线程 100-500ms 模拟卡顿\tHeavy Animations\t80 simultaneously rotating+scaling widgets to intentionally trigger jank / 80 个同时旋转+缩放的 widget 故意触发卡顿\tSmooth Animation\tSingle lightweight compound animation, demonstrates stable 60 FPS / 单个轻量复合动画,演示稳定 60 FPS","related--相关#Related / 相关":"Usage — General usage guide / 通用使用指南\nFAQ — Common questions / 常见问题\nConfiguration — Configuration options / 配置选项"}},"/Getting-Started":{"title":"Getting Started / 快速开始","data":{"quick-start--快速开始#Quick Start / 快速开始":"Integrate with just 1 line of code:仅需 1 行代码 即可完成集成:\nimport 'package:flutter/material.dart';\nimport 'package:zero_inspector_kit/zero_inspector_kit.dart';\nvoid main() {\n // Single line: Initialize inspector, capture print() via Zone, and display floating button\n // 一行代码:初始化检查器、通过 Zone 捕获 print()、自动显示悬浮按钮\n ZeroInspectorKit.runAppWithInspector(const MyApp());\n}\nclass MyApp extends StatelessWidget {\n const MyApp({super.key});\n @override\n Widget build(BuildContext context) {\n return MaterialApp(\n home: Scaffold(\n appBar: AppBar(title: const Text('App')),\n body: const Center(child: Text('Hello World')),\n ),\n );\n }\n}","what-happens-automatically--自动完成的工作#What Happens Automatically / 自动完成的工作":"After integration, the inspector automatically does the following without modifying any other project code:集成后,检查器会自动完成以下工作,无需修改项目其他代码:\nFeature\tDescription\t✅ Log Capture\tAuto-capture all print(), debugPrint() via Zone / 通过 Zone 自动捕获所有日志\t✅ Network Interception\tAuto-intercept all http and Dio requests via HttpOverrides / 自动拦截所有网络请求\t✅ Error Capture\tAggregate Flutter framework errors & unhandled exceptions, deduped by type + stack (Errors tab) / 聚合 Flutter 框架异常与未捕获异常,按类型+堆栈去重\t✅ Session Persistence\tFlush logs/network/errors to a disk ring buffer; logs & errors replay on next launch / 日志/网络/异常落盘环形缓冲,启动时回放日志与异常\t✅ Database Scan\tAuto-scan and register SQLite databases / 自动扫描注册数据库\t✅ Floating Button\tAuto-displayed via Overlay, not affected by keyboard / 通过 Overlay 自动显示\t✅ Route Tracking\tAuto-inject InspectorRouteObserver into MaterialApp / 自动注入路由观察者\t\nNote / 说明: The Memory and FPS panels are available as tabs but are off by default to avoid performance overhead. Toggle the switch at the top of each panel to start collecting data. See Memory Viewer and FPS Viewer for details.Memory 和 FPS 面板作为标签页可用,但默认关闭以避免性能开销。在各自面板顶部打开开关才会开始采集数据。详见 Memory Viewer 和 FPS Viewer。","production-build--生产构建#Production Build / 生产构建":"The inspector is automatically disabled in release mode. You don't need to remove any code — Flutter's tree-shaking will remove all inspector-related code from production builds.检查器在 release 模式下会自动禁用,无需移除任何代码,Flutter 的 tree-shaking 会自动移除所有检查器相关代码。","alternative-integration--替代集成方式#Alternative Integration / 替代集成方式":"If you prefer more control, use the two-line approach:如果需要更多控制权,可以使用两行代码方式:\nvoid main() {\n ZeroInspectorKit.init();\n runApp(ZeroInspectorKit.wrapApp(const MyApp()));\n}","next-steps--下一步#Next Steps / 下一步":"Installation — Detailed installation methods / 详细安装方式\nUsage — Full usage guide / 完整使用指南\nConfiguration — Configuration options / 配置选项"}},"/Installation":{"title":"Installation / 安装","data":{"from-pubdev-recommended--从-pubdev-安装推荐#From pub.dev (Recommended) / 从 pub.dev 安装(推荐)":"Add the following to your pubspec.yaml:在 pubspec.yaml 中添加以下依赖:\ndependencies:\n zero_inspector_kit: ^1.10.1\nThen run:然后运行:\nflutter pub get","from-github--从-github-安装#From GitHub / 从 GitHub 安装":"Alternatively, install from GitHub:或者从 GitHub 安装:\ndependencies:\n zero_inspector_kit:\n git:\n url: https://github.com/zero-labsco/zero_inspector_kit.git\n ref: release/v1.10.1","platform-setup--平台配置#Platform Setup / 平台配置":"","android#Android":"No additional configuration needed.无需额外配置。","ios#iOS":"No additional configuration needed.无需额外配置。","import--导入#Import / 导入":"import 'package:zero_inspector_kit/zero_inspector_kit.dart';","requirements--环境要求#Requirements / 环境要求":"Requirement\tVersion\tFlutter\t>= 3.3.0\tDart SDK\t>= 3.11.0 < 4.0.0","next-steps--下一步#Next Steps / 下一步":"Getting Started — Quick start guide / 快速开始\nUsage — Full usage guide / 完整使用指南"}},"/Network-Inspector":{"title":"Network Inspector / 网络检查器","data":{"overview--概述#Overview / 概述":"The Network Inspector automatically captures all HTTP requests made via the http package and Dio, with zero configuration needed.网络检查器自动捕获所有通过 http 包 和 Dio 发送的 HTTP 请求,无需任何配置。","how-it-works--工作原理#How It Works / 工作原理":"The inspector uses Flutter's HttpOverrides to intercept all HTTP traffic at the dart:io level. This means:检查器通过 Flutter 的 HttpOverrides 在 dart:io 层面拦截所有 HTTP 流量。这意味着:\nhttp package: Auto-captured ✅ / 自动捕获 ✅\nDio: Auto-captured (uses IOHttpClientAdapter → HttpClient) ✅ / 自动捕获 ✅\nNo manual interceptor setup needed / 无需手动添加拦截器","websocket--grpc-capture--websocket-与-grpc-抓取#WebSocket & gRPC Capture / WebSocket 与 gRPC 抓取":"Available since v1.7.0 (opt-in, off by default)v1.7.0 起可用(可选开启,默认关闭)\nHTTP/HTTPS traffic is captured automatically, but the HttpOverrides interceptor does not cover streaming protocols like WebSocket and gRPC. For those, enable the opt-in capture so frames and calls show up as WS / gRPC rows in the Network tab.HTTP/HTTPS 流量会自动捕获,但 HttpOverrides 拦截器无法覆盖 WebSocket、gRPC 这类流式协议。针对它们需开启可选的抓取,开启后收发帧/调用会以 WS / gRPC 行的形式出现在 Network 标签页。","enable--开启方式#Enable / 开启方式":"Toggle the WS switch in the Network tab toolbar, or / 在 Network 标签页工具栏点击 WS 开关,或\nSet it programmatically: / 通过代码开启:\n// on / 开启\nWsInspectorService.instance.enable();\n// off / 关闭\nWsInspectorService.instance.disable();\nCapture is off by default and only records while enabled — apps that don't use these protocols pay nothing.抓取默认关闭,且只在开启时记录;不使用这类协议的应用零开销。","two-usage-modes--两种使用方式#Two Usage Modes / 两种使用方式":"1. Transparent wrapper — InspectorWebSocketReplace WebSocket.connect with InspectorWebSocket.connect. Frames are auto-recorded (→ out, ← in) when capture is on; when off it passes through with zero overhead.将 WebSocket.connect 替换为 InspectorWebSocket.connect。开启抓取时会自动记录收发帧(→ 出站、← 入站);关闭时零开销透传。\nfinal ws = await InspectorWebSocket.connect('wss://echo.websocket.events');\nws.listen((msg) => print('received: $msg'));\nws.add('hello'); // recorded as an outgoing frame / 记录为出站帧\n2. Manual hook — recordCallFor stacks not transparently interceptable by dart:io (gRPC, web_socket_channel, custom protocols), call recordCall to log a request/response pair. No-ops when capture is disabled.对于无法被 dart:io 透明拦截的栈(gRPC、web_socket_channel、自定义协议),调用 recordCall 记录一次请求/响应。关闭抓取时为空操作。\nWsInspectorService.instance.recordCall(\n name: 'user.UserService/GetUser',\n request: '{ \"id\": 1 }',\n response: '{ \"name\": \"Ada\" }',\n protocol: 'gRPC', // appears in the method column / 显示在 method 列\n);","what-you-see--查看方式#What You See / 查看方式":"Network tab lists a WS (or gRPC) entry per connection/call / Network 标签页按连接/调用列出 WS(或 gRPC)记录\nOpen the detail view to see the frame log (outgoing → / incoming ←), accumulated in the response body / 进入详情页查看帧日志(出站 → / 入站 ←),累积显示在响应体中\nA [connection closed] marker is appended when the socket closes / 连接关闭后会追加 [connection closed] 标记","captured-information--捕获的信息#Captured Information / 捕获的信息":"Field\tDescription\tMethod\tGET, POST, PUT, DELETE, PATCH\tURL\tFull request URL\tStatus Code\tHTTP response status code\tDuration\tRequest duration\tRequest Headers\tAll request headers\tRequest Body\tRequest payload (JSON formatted)\tResponse Body\tResponse payload (JSON formatted)\tHost\tParsed from URL","ui-features--ui-功能#UI Features / UI 功能":"","request-list--请求列表#Request List / 请求列表":"Color-coded by HTTP method / 按 HTTP 方法着色\nStatus code badge / 状态码徽章\nDuration display / 耗时显示\nLeft border color indicates status / 左侧边框颜色表示状态","request-detail--请求详情#Request Detail / 请求详情":"Click a request to enter detail view / 点击请求进入详情视图\nBack button to return to list / 返回按钮返回列表\nRequest and response sections / 请求和响应分段显示\nJSON formatted body / JSON 格式化显示","search--搜索#Search / 搜索":"Fuzzy search by URL or method / 按 URL 或方法模糊搜索\nSearch bar hidden in detail view / 详情视图隐藏搜索栏","batch-operations--批量操作#Batch Operations / 批量操作":"Available since v1.3.0v1.3.0 起可用\nThe request list supports a selection mode for operating on multiple requests at once.请求列表支持选择模式,可一次性操作多条请求。\nTap the selection icon in the toolbar to enter selection mode / 点击工具栏的选择图标进入选择模式\nCheck/uncheck items; a top batch bar shows Select all / Cancel / 勾选/取消勾选;顶部批量条提供全选 / 取消\nBatch \"Copy as cURL\": copies all selected requests as cURL commands / 批量「Copy as cURL」:将所有选中请求复制为 cURL 命令\nBatch delete: removes selected requests from the list / 批量删除:从列表中移除选中请求","export--sensitive-field-masking--导出与敏感字段遮蔽#Export & Sensitive-field Masking / 导出与敏感字段遮蔽":"Available since v1.3.0v1.3.0 起可用\nThe toolbar includes an eye toggle that controls whether sensitive headers are masked on export.工具栏包含眼睛开关,控制导出时是否遮蔽敏感请求头。\nDefault: off (fully visible) — cURL / JSON / HAR reflect the real request verbatim / 默认关闭(完整可见):cURL / JSON / HAR 原样反映真实请求\nOn: toCurl / netToJson / netToHar / copyNet / exportNetToFile mask these headers / 开启后:以下请求头被遮蔽:\nAuthorization, Cookie, Set-Cookie, Proxy-Authorization, X-Auth-Token, X-CSRF-Token, X-XSRF-Token\nThe detail page \"Copy as cURL\" / \"Copy as JSON\" follow the same toggle / 详情页「Copy as cURL」「Copy as JSON」跟随同一开关\nWhen masking is active, the snackbar notes (sensitive hidden) / 遮蔽开启时,snackbar 提示 (sensitive hidden)\nTip: keep masking on before pasting cURL/JSON into terminals, chat, or issue trackers to avoid leaking credentials.提示:将 cURL/JSON 粘贴到终端、聊天或 issue 前,建议开启遮蔽,避免凭据泄露。","copy-as-curl--复制为-curl#Copy as cURL / 复制为 cURL":"Available since v1.3.0v1.3.0 起可用\nFrom the request detail view, tap Copy as cURL to copy the request as a ready-to-run cURL command (method, URL, headers, body). Respects the sensitive-field masking toggle above.在请求详情页点击 Copy as cURL,即可将请求复制为可直接运行的 cURL 命令(方法、URL、请求头、请求体),并遵循上方的敏感字段遮蔽开关。","status-code-colors--状态码颜色#Status Code Colors / 状态码颜色":"Range\tColor\tDescription\t2xx\tGreen\tSuccess / 成功\t3xx\tBlue\tRedirect / 重定向\t4xx\tOrange\tClient error / 客户端错误\t5xx\tRed\tServer error / 服务器错误","http-method-colors--http-方法颜色#HTTP Method Colors / HTTP 方法颜色":"Method\tColor\tGET\tBlue / 蓝色\tPOST\tGreen / 绿色\tPUT\tOrange / 橙色\tDELETE\tRed / 红色\tPATCH\tPurple / 紫色","usage-example--使用示例#Usage Example / 使用示例":"// http package - auto-captured / http 包 - 自动捕获\nfinal response = await http.get(\n Uri.parse('https://api.example.com/data'),\n);\n// Dio - auto-captured / Dio - 自动捕获\nfinal response = await dio.post(\n 'https://api.example.com/data',\n data: {'key': 'value'},\n);\nNo additional setup required! All requests will appear in the Network tab.无需额外配置!所有请求都会出现在 Network 标签页中。","request-interceptor--请求拦截修改#Request Interceptor / 请求拦截修改":"Available since v1.0.7 (response fields locked to read-only since v1.0.8)v1.0.7 起可用(v1.0.8 起响应字段锁定为只读)\nThe inspector supports intercepting and modifying network requests via rules. This is useful for testing different request parameters without modifying app code.检查器支持通过规则拦截并修改网络请求,适合在不修改应用代码的情况下测试不同的请求参数。","workflow--工作流程#Workflow / 工作流程":"Send a request normally (it will be captured in the Network panel) / 正常发送请求(会被捕获到 Network 面板)\nOpen the request detail and tap the Interceptor icon / 打开请求详情,点击拦截器图标\nConfigure the modification rule (URL pattern, HTTP method, request modifications) / 配置修改规则(URL 模式、HTTP 方法、请求修改)\nSave the rule — subsequent matching requests will use the modified parameters / 保存规则——后续匹配的请求将使用修改后的参数","supported-modifications--支持的修改#Supported Modifications / 支持的修改":"Field\tEditable\tNotes\tRequest Body\t✅\tOnly for requests with body (POST, PUT, PATCH, etc.) / 仅适用于有 body 的请求\tRequest Headers\t✅\tAdd / modify / remove headers / 新增 / 修改 / 删除请求头\tURL\t❌\tGrayed out, read-only / 灰色不可编辑\tResponse Status Code\t❌\tRead-only since v1.0.8 / v1.0.8 起只读\tResponse Body\t❌\tRead-only since v1.0.8 / v1.0.8 起只读\t\nThe interception edit panel only allows modifying the request body and request headers. Response fields (status code, response body) are grayed out and uneditable.拦截编辑面板仅允许修改请求体和请求头。响应字段(状态码、响应体)灰色不可编辑。","rule-matching--规则匹配#Rule Matching / 规则匹配":"URL pattern matching: exact match or regex / URL 模式匹配:精确匹配或正则匹配\nHTTP method filtering: GET, POST, PUT, DELETE, PATCH, HEAD, or Any / HTTP 方法过滤","why-get-requests-cannot-be-modified--为什么-get-请求不能修改#Why GET Requests Cannot Be Modified / 为什么 GET 请求不能修改":"The interceptor currently supports modifying request body and headers only / 拦截器目前仅支持修改请求体和请求头\nGET requests don't have a request body / GET 请求没有请求体\nModifying GET request parameters would require URL modification / 修改 GET 请求参数需要修改 URL\nURL modification may cause unexpected issues with request routing and parameter encoding / 修改 URL 可能导致请求路由和参数编码的意外问题\nGET request detail pages do not display interception edit buttons, making them unmodifiable.GET 请求详情页不显示拦截编辑按钮,因此无法修改。","master-toggle--拦截总开关#Master Toggle / 拦截总开关":"The interception master toggle is displayed only on the Network list page, not in the detail page / 拦截总开关只显示在 Network 列表页,不在详情页\nRules are only applied when modification mode is enabled / 规则仅在启用修改模式时生效\nWhen no rules are configured or rules are disabled, all requests are sent normally without any modification / 未配置规则或禁用规则时,所有请求正常发送,不做任何修改","rule-management--规则管理#Rule Management / 规则管理":"The network panel includes an interceptor rule editor where you can:网络面板包含拦截规则编辑器,可以:\nCreate / edit / delete rules / 创建 / 编辑 / 删除规则\nEnable / disable individual rules / 启用 / 禁用单条规则\nView rule status indicators in the request list / 在请求列表中查看规则状态标识"}},"/Log-Viewer":{"title":"Log Viewer / 日志查看器","data":{"overview--概述#Overview / 概述":"The Log Viewer automatically captures logs from multiple sources with zero configuration.日志查看器自动从多个来源捕获日志,无需配置。","log-sources--日志来源#Log Sources / 日志来源":"Source\tCapture Method\tprint()\tZone specification override / Zone 规范覆盖\tdebugPrint()\tdebugPrint override / debugPrint 覆盖\tFlutter errors\tFlutterError.onError hook / 接管 FlutterError.onError\tUnhandled exceptions\trunZonedGuarded / runZonedGuarded 捕获\tThird-party libraries\tVia print() capture / 通过 print() 捕获\t\nSince v1.9.0, Flutter framework errors and unhandled exceptions are also aggregated & deduplicated in the dedicated Errors tab (keyed by type + stack signature) — while this Log Viewer keeps showing them as error-level lines in the chronological stream. Use Errors for \"is this crash repeating?\"; use Log Viewer for the raw timeline.自 v1.9.0 起,Flutter 框架异常与未捕获异常会同时进入独立的 Errors 标签页去重聚合(按类型 + 堆栈签名归并);本 Log Viewer 仍会在原始时间流中把它们显示为错误级日志。回答\"该崩溃是否反复出现\"请用 Errors;查看原始时间线请用 Log Viewer。","log-levels--日志级别#Log Levels / 日志级别":"Level\tAbbreviation\tColor\tDescription\tVerbose\tV\tGray\tDetailed information / 详细信息\tDebug\tD\tBlue\tDebug information / 调试信息\tInfo\tI\tGreen\tGeneral information / 一般信息\tWarning\tW\tOrange\tWarning messages / 警告信息\tError\tE\tRed\tError messages / 错误信息\t\nThird-party library logs are categorized as Info level.\n第三方日志库的日志统一归类为 Info 级别。","ui-features--ui-功能#UI Features / UI 功能":"","filter-bar--过滤栏#Filter Bar / 过滤栏":"All: Show all logs / 使用 All 显示所有日志\nV / D / I / W / E: Filter by level / 按级别过滤\nTag dropdown: Filter by any captured tag / 按任意已捕获标签过滤\nSingle-select mode / 单选模式","toolbar--工具栏#Toolbar / 工具栏":"Auto-scroll toggle (default on): Jump to the newest log as new entries arrive; pause to keep history still for inspection / 自动滚动开关(默认开启):新日志到达时自动跳到最新;暂停可稳定查看历史\nCopy as JSON: Copy all currently filtered logs as JSON / 复制为 JSON:将当前过滤后的全部日志复制为 JSON\nShare as Text: Share filtered logs as plain text / 分享为文本:将过滤后的日志以纯文本分享\nClear: Clear all logs / 清除:清空全部日志","log-list--日志列表#Log List / 日志列表":"Level badge with color / 带颜色的级别徽章\nTimestamp display / 时间戳显示\nTag display (if available) / 标签显示(如有)\nError/warning rows have subtle background tint / 错误/警告行有淡色背景\nLeft border color indicates level / 左侧边框颜色表示级别\nTap a row to open the in-view detail page with full message and copy; use the back button to return / 点击行进入详情页(主视图内切换),可查看完整消息并复制,点返回按钮回到列表\nPer-row copy button copies that single log as JSON / 行内复制按钮将该条日志以 JSON 复制","search--搜索#Search / 搜索":"Fuzzy search by message content or tag / 按消息内容或标签模糊搜索\nToggle regex mode (. * button) to search with RegExp (case-insensitive); invalid patterns degrade gracefully instead of crashing / 点击 正则模式(. * 按钮)使用 RegExp 搜索(不区分大小写);非法图案优雅降级,不会崩溃\nCombined with level and tag filters / 可与级别、标签过滤组合使用","manual-logging--手动记录日志#Manual Logging / 手动记录日志":"Quick shorthand (recommended) / 简化写法(推荐) — available since v1.1.2 / v1.1.2 起可用:\nInspectorLog.v('Verbose message / 详细消息');\nInspectorLog.d('Debug message / 调试消息');\nInspectorLog.i('Info message / 信息消息');\nInspectorLog.w('Warning message / 警告消息');\nInspectorLog.e('Error message / 错误消息');\n// With tag / 带标签\nInspectorLog.i('User logged in', tag: 'Auth');\nFull form / 完整写法:\nInspectorLogInterceptor.instance.verbose('Verbose message / 详细消息');\nInspectorLogInterceptor.instance.debug('Debug message / 调试消息');\nInspectorLogInterceptor.instance.info('Info message / 信息消息');\nInspectorLogInterceptor.instance.warning('Warning message / 警告消息');\nInspectorLogInterceptor.instance.error('Error message / 错误消息');\n// With tag / 带标签\nInspectorLogInterceptor.instance.info('User logged in', tag: 'Auth');\nInspectorLog is a static wrapper around InspectorLogInterceptor.instance for shorter log calls.InspectorLog 是 InspectorLogInterceptor.instance 的静态包装,用于更简短的日志调用。","third-party-library-integration--第三方日志库集成#Third-Party Library Integration / 第三方日志库集成":"","auto-capture-inbound--自动捕获入站#Auto-Capture (Inbound) / 自动捕获(入站)":"No configuration needed. Any library using print() or debugPrint() is automatically captured.无需配置。任何使用 print() 或 debugPrint() 的库都会被自动捕获。","bidirectional-sync-optional--双向同步可选#Bidirectional Sync (Optional) / 双向同步(可选)":"To forward inspector logs to your third-party logger:将检查器日志转发到第三方日志库:\nimport 'package:logger/logger.dart';\nfinal logger = Logger();\nInspectorLogInterceptor.instance.onLogCaptured = (entry) {\n logger.log(\n _mapLogLevel(entry.level),\n '${entry.tag != null ? '[${entry.tag}] ' : ''}${entry.message}',\n );\n};\nNote: Do NOT call logging methods inside onLogCaptured, as this will cause infinite recursion.\n注意:不要在 onLogCaptured 内部调用日志方法,否则会导致无限递归。","starting-the-log-interceptor--启动日志拦截器#Starting the Log Interceptor / 启动日志拦截器":"If using runAppWithInspector(), the log interceptor starts automatically. Otherwise:如果使用 runAppWithInspector(),日志拦截器会自动启动。否则:\nInspectorLogInterceptor.instance.start();"}},"/Route-Tracker":{"title":"Route Tracker / 路由追踪","data":{"overview--概述#Overview / 概述":"The Route Tracker monitors navigation history, recording all route push, pop, and replacement events.路由追踪器监控导航历史,记录所有路由 push、pop 和替换事件。","auto-injection--自动注入#Auto-Injection / 自动注入":"When using runAppWithInspector() or wrapApp(), the InspectorRouteObserver is automatically injected into MaterialApp's navigatorObservers.使用 runAppWithInspector() 或 wrapApp() 时,InspectorRouteObserver 会自动注入到 MaterialApp 的 navigatorObservers 中。","tracked-route-actions--追踪的路由操作#Tracked Route Actions / 追踪的路由操作":"Action\tColor\tDescription\tpush\tBlue\tNavigator.push() / 推入新路由\tpushNamed\tBlue\tNavigator.pushNamed() / 按名称推入\tpop\tOrange\tNavigator.pop() / 弹出路由\tpopUntil\tOrange\tNavigator.popUntil() / 弹出直到\tpushReplacement\tPurple\tNavigator.pushReplacement() / 替换路由","ui-features--ui-功能#UI Features / UI 功能":"","route-list--路由列表#Route List / 路由列表":"Action badge with color / 带颜色的操作徽章\nRoute name display / 路由名称显示\nTimestamp display / 时间戳显示\nLeft border color indicates action type / 左侧边框颜色表示操作类型","route-detail--路由详情#Route Detail / 路由详情":"Click a route to view details / 点击路由查看详情\nShows: Action, Route Name, Timestamp, Arguments / 显示:操作、路由名、时间戳、参数\nArguments displayed as formatted JSON / 参数以 JSON 格式显示","manual-setup-optional--手动设置可选#Manual Setup (Optional) / 手动设置(可选)":"If you use a custom Navigator or don't use MaterialApp, add the observer manually:如果使用自定义 Navigator 或不使用 MaterialApp,请手动添加观察者:\nMaterialApp(\n navigatorObservers: [InspectorRouteObserver()],\n home: MyHomePage(),\n)"}},"/Memory-Viewer":{"title":"Memory Viewer / 内存监控面板","data":{"":"The Memory Viewer provides comprehensive in-app memory analysis, including trend charts, Dart Heap details, Native memory breakdown, memory leak detection, image cache monitoring, and storage statistics.内存监控面板提供应用内全面内存分析,包括趋势图、Dart Heap 详情、Native 内存分项、内存泄漏检测、图片缓存监控和存储统计。\nAvailable since v1.1.0v1.1.1 起可用","overview--概览#Overview / 概览":"The Memory Viewer is integrated into the inspector panel. Tap the floating inspector button → \"Memory\" tab to access.内存监控面板集成在检查器面板中。点击悬浮检查器按钮 → \"Memory\" 标签即可访问。⚠️ Important: Memory monitoring is off by default. You must toggle on the switch at the top of the panel to start collecting data.⚠️ 重要:内存监控默认关闭。必须在面板顶部打开开关才会开始采集数据。","master-switch--总开关#Master Switch / 总开关":"A switch at the top of the Memory panel controls whether monitoring is enabled.Memory 面板顶部有一个开关,控制是否启用监控。\nState\tBehavior\tOFF (default)\tNo timers, no VM Service connection, zero overhead / 无定时器、无 VM Service 连接、零开销\tON\tStarts RSS collection (500ms), Native memory (3s), storage stats (3s), leak detection (2s), and VM Service connection attempt / 启动 RSS 采集(500ms)、Native 内存(3s)、存储统计(3s)、泄漏检测(2s),并尝试连接 VM Service\t\nWhen turned off, all timers are cancelled and the VM Service connection state is cleared, leaving no residual WebSocket overhead.关闭时所有定时器会被取消,VM Service 连接状态会被清空,不会残留 WebSocket 开销。","features--功能#Features / 功能":"","1-memory-trend-chart--内存趋势图#1. Memory Trend Chart / 内存趋势图":"Real-time line chart with 2-minute history window (240 snapshots × 500ms)\nSwitchable between 4 metrics via the metric selector chips:\nProcess RSS — Process-level RSS (always available)\nDart Heap — Total Dart Heap usage (requires VM Service)\nNew Space — New generation usage (requires VM Service)\nOld Space — Old generation usage (requires VM Service)\n实时折线图,2 分钟历史窗口(240 条快照 × 500ms)。可通过指标选择芯片切换 4 种指标。","2-native-memory--native-内存真机-100-可用#2. Native Memory / Native 内存(真机 100% 可用)":"Does not depend on VM Service. Data is collected via Platform Channel:不依赖 VM Service,通过 Platform Channel 采集:Android (Debug.MemoryInfo + /proc/self/status):\nTotal PSS / Dalvik PSS / Native PSS / Native Private Dirty\nProcess RSS (from /proc/self/status)\nDevice memory status (availMem, threshold, lowMemory)\niOS (mach task_info):\nPhysical Footprint (Apple's recommended metric)\nInternal / Compressed / Resident Size\nProcess RSS\nDevice available memory","3-dart-heap-overview--dart-heap-概览需要-vm-service#3. Dart Heap Overview / Dart Heap 概览(需要 VM Service)":"Heap Usage / Capacity / external usage\nProgress bar showing heap usage ratio\nThree core metrics with bilingual labels\n显示 Heap Usage / Capacity / External 三个核心指标 + 进度条。","4-heap-generations--堆分代详情需要-vm-service#4. Heap Generations / 堆分代详情(需要 VM Service)":"New Space: usage / capacity / external\nOld Space: usage / capacity / external\nHelps identify allocation patterns (frequent new-space churn vs old-space growth)\n显示新生代/老生代的 Usage / Capacity / External 详细数据。","5-manual-gc--手动触发-gc需要-vm-service#5. Manual GC / 手动触发 GC(需要 VM Service)":"\"Trigger GC\" button forces a full garbage collection\nDisabled (grayed out) when VM Service is unavailable\n\"Trigger GC\" 按钮强制触发完整垃圾回收。VM Service 不可用时按钮变灰禁用。","6-memory-leak-detection--内存泄漏检测#6. Memory Leak Detection / 内存泄漏检测":"Based on Dart 2.17+ WeakReference and Finalizer. Does NOT depend on VM Service.基于 Dart 2.17+ 的 WeakReference 和 Finalizer,不依赖 VM Service。Four-state transition / 四状态流转:\nState\tMeaning\ttracking\tObject registered, waiting for expected release time / 对象已注册,等待预期释放时间\tverifying\tExceeded expected release time, GC verification triggered (if VM Service available) / 超过预期释放时间,触发 GC 验证(VM Service 可用时)\tleaked\tObject still exists after GC — suspected leak / GC 后对象仍存在——疑似泄漏\treleased\tObject has been garbage collected / 对象已被垃圾回收\t\nAPI / 接口:Quick shorthand (recommended) / 简化写法(推荐) — available since v1.1.2 / v1.1.2 起可用:\n// Extension method on Object / Object 上的扩展方法\nmyBloc.trackMemoryLeak(tag: 'HomePage_myBloc');\n// Or top-level function / 或使用顶层函数\ntrackMemoryLeak(myBloc, tag: 'HomePage_myBloc');\n// Cancel tracking / 取消追踪\nmyBloc.untrackMemoryLeak();\nFull form / 完整写法:\n// Register an object for leak tracking / 注册对象进行泄漏追踪\nMemoryInspectorService.instance.trackObject(\n myController,\n tag: 'HomeController_textController', // optional identifier\n expectedReleaseAfter: const Duration(seconds: 30), // expected release time\n);\n// Stop tracking a specific object / 停止追踪特定对象\nMemoryInspectorService.instance.untrackObject(myController);\n// Clear all records / 清空所有记录\nMemoryInspectorService.instance.clearLeakRecords();\nLimits / 限制:\nMax 500 tracked objects (LRU eviction)\nDetection interval: 2 seconds\ntrackObject() requires user code modification (mild invasion)\nFlutter MemoryAllocations bridge (since v1.9.0) / 官方泄漏追踪桥接(v1.9.0 起)The WeakReference state machine has a blind spot: an object whose dispose() ran but that has not been GC'd yet still resolves through the weak reference, producing a false \"leaked\" verdict. Since v1.9.0, MemoryInspectorService also subscribes to Flutter's official FlutterMemoryAllocations (the data source behind leak_tracker) via LeakTrackerBridge: once the official stream reports disposed, the object is treated as released (awaiting GC) even if the weak reference still resolves — cutting false positives. Enabled by default through enableFlutterLeakTracker in init(), and toggled in code via MemoryInspectorService.instance.flutterLeakTrackerEnabled.WeakReference 状态机有一个盲点:对象已调用 dispose() 但尚未被 GC 时弱引用仍存活,会产生\"疑似泄漏\"的误报。v1.9.0 起,MemoryInspectorService 通过 LeakTrackerBridge 额外订阅 Flutter 官方的 FlutterMemoryAllocations(leak_tracker 背后的数据源):只要官方上报 disposed,即便弱引用仍存活也判定为已释放(等待 GC),从而显著降低误报。由 init() 的 enableFlutterLeakTracker 默认启用,也可用 MemoryInspectorService.instance.flutterLeakTrackerEnabled 在代码中开关。","7-image-cache--图片缓存#7. Image Cache / 图片缓存":"Real-time Flutter image cache size and count\nPending (loading) / Live (in use) image counts\nOne-click clear all image cache\n实时显示 Flutter 图片缓存大小、数量、加载中/使用中状态。一键清理图片缓存。","8-app-storage--应用存储#8. App Storage / 应用存储":"Documents directory size\nTemp cache directory size\nTotal database file size\nOne-click clear temp cache\n显示文档目录、临时缓存、数据库文件大小。一键清理临时缓存。","️-vm-service-availability--vm-service-可用性#⚠️ VM Service Availability / VM Service 可用性":"Dart Heap data and manual GC require VM Service connection. When VM Service is unavailable, these features gracefully degrade to show \"N/A\".Dart Heap 数据和手动 GC 需要 VM Service 连接。 VM Service 不可用时,这些功能优雅降级显示 \"N/A\"。","when-debugging-via-pc-with-flutter-run--通过-pc-用-flutter-run-调试时#When debugging via PC with flutter run / 通过 PC 用 flutter run 调试时":"Dart VM Heap data may be unavailable (VM: OFF).Dart VM Heap 数据可能不可用(VM: OFF)。Reason / 原因: When using flutter run to debug via PC, the flutter tool sets up port forwarding between PC and device via adb reverse, allowing PC-side DevTools to access the device's VM Service. However, Service.getInfo() returns a serverUri from the PC's perspective; when the app process internally accesses 127.0.0.1:PC_port, the device doesn't have that port listening locally, resulting in Connection refused and VM Service showing OFF.原因:使用 flutter run 连接 PC 调试时,flutter tool 会通过 adb reverse 在 PC 和设备之间做端口转发,让 PC 上的 DevTools 能访问设备的 VM Service。但应用进程内部 Service.getInfo() 返回的 serverUri 是 PC 视角的端口,应用进程访问 127.0.0.1:PC端口 时设备本地并没有监听该端口,导致 Connection refused,VM Service 显示 OFF。","when-opening-debug-app-directly-no-pc--直接打开-debug-应用不连-pc#When opening debug app directly (no PC) / 直接打开 debug 应用(不连 PC)":"VM Service works normally. No flutter tool is involved, VM Service listens directly on the device's local port, the app can connect normally, and Dart Heap data displays correctly.VM Service 正常工作。 没有 flutter tool 介入,VM Service 直接监听设备本地端口,应用能正常连接,Dart Heap 数据正常显示。","fallback--降级方案#Fallback / 降级方案":"When VM Service is unavailable:\n✅ Native memory (Android PSS / iOS physicalFootprint) — still available\n✅ Process RSS — always available\n✅ Image cache / storage stats — still available\n✅ Leak detection — still available (doesn't depend on VM Service)\n❌ Dart Heap details — shows N/A\n❌ Manual GC — button disabled","platform-support--平台支持#Platform Support / 平台支持":"Feature\tAndroid\tiOS\tProcess RSS\t✅\t✅\tNative Memory\t✅\t✅\tDart Heap (VM Service)\t✅ (no PC)\t✅ (no PC)\tManual GC\t✅ (no PC)\t✅ (no PC)\tLeak Detection\t✅\t✅\tImage Cache\t✅\t✅\tStorage Stats\t✅\t✅","refresh-intervals--刷新间隔#Refresh Intervals / 刷新间隔":"Data Source\tInterval\tProcess RSS / Dart Heap\t500ms\tNative Memory (Android/iOS)\t3000ms\tStorage Stats\t3000ms\tLeak Detection\t2000ms","related--相关#Related / 相关":"Usage — General usage guide\nFAQ — Common questions\nConfiguration — Configuration options"}},"/":{"title":"Zero Inspector Kit","data":{"":"A powerful Flutter plugin for in-app developer console, providing real-time debugging tools including network request inspection, logging, error aggregation, database viewing, and route tracking.一个功能强大的 Flutter 插件,提供应用内开发者控制台,包括网络请求检查、日志记录、异常聚合、数据库查看和路由追踪。","-features--功能特性#✨ Features / 功能特性":"Feature\tDescription\tZero Invasion\tIntegrate with 1 line of code / 一行代码集成\tNetwork Inspector\tReal-time HTTP request viewing (http + Dio) / 实时网络请求查看\tLog Viewer\tAuto-capture print()/debugPrint() and third-party logs / 自动捕获日志\tErrors\tAggregate & dedupe crashes by type + stack, with count and first/last seen / 按类型+堆栈去重聚合崩溃,记录次数与首末次时间\tDatabase Viewer\tSQLite inspection with table data / 数据库查看\tMemory Viewer\tTrend chart, Dart Heap, Native memory, leak detection (incl. Flutter MemoryAllocations bridge) / 内存趋势图、Dart Heap、Native 内存、泄漏检测(含官方 MemoryAllocations 桥接)\tFPS Monitor\tReal-time FPS, jank rate, trend chart / 实时 FPS、卡顿率、趋势图\tRoute Tracker\tNavigation history tracking / 路由追踪\tSession Persistence\tLogs/network/errors survive restarts via a disk ring buffer; logs & errors replay on launch; one-tap session archive export / 日志/网络/异常通过磁盘环形缓冲跨重启保留,启动时回放日志与异常;一键导出会话存档\tAlerts\tRule-based alerts (network/log/memory/FPS) with unread badge / 基于规则的告警(网络/日志/内存/FPS)与未读角标\tSensitive Masking & cURL\tMask secrets on export; one-click cURL copy; batch ops / 导出遮蔽敏感字段、一键复制 cURL、批量操作\tFuzzy Search\tSearch in all viewers / 各查看器模糊搜索\tCross-platform\tAndroid, iOS / 跨平台支持","-table-of-contents--目录#📚 Table of Contents / 目录":"Page\tDescription\tGetting Started\tQuick start guide / 快速开始\tInstallation\tHow to install / 安装方式\tUsage\tDetailed usage / 详细使用\tNetwork Inspector\tNetwork request viewing / 网络检查器\tLog Viewer\tLog capturing and viewing / 日志查看器\tErrors\tAggregated error viewing / 异常聚合查看\tDatabase Viewer\tDatabase inspection / 数据库查看器\tRoute Tracker\tRoute tracking / 路由追踪\tMemory Viewer\tMemory monitoring & leak detection / 内存监控与泄漏检测\tFPS Viewer\tFPS monitoring & jank detection / FPS 监控与卡顿检测\tAlerts\tRule-based alerting / 基于规则的告警\tConfiguration\tConfiguration options / 配置说明\tCustom Database Provider\tExtend database support / 自定义数据库提供者\tFAQ\tFrequently asked questions / 常见问题","-links--链接#🔗 Links / 链接":"GitHub\nOfficial Website\npub.dev","-license--许可证#📄 License / 许可证":"This project is licensed under the GNU General Public License v3.0.本项目采用 GNU General Public License v3.0 许可证。This plugin is provided \"as is\", without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.本插件按\"原样\"提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。"}},"/Usage":{"title":"Usage / 使用指南","data":{"integration-methods--集成方式#Integration Methods / 集成方式":"","1-one-line-integration-recommended--一行代码集成推荐#1. One-Line Integration (Recommended) / 一行代码集成(推荐)":"void main() {\n ZeroInspectorKit.runAppWithInspector(const MyApp());\n}\nThis method:\nAuto-initializes inspector / 自动初始化检查器\nCaptures print() via Zone / 通过 Zone 捕获 print()\nDisplays floating button via Overlay / 通过 Overlay 显示悬浮按钮\nAuto-injects route observer / 自动注入路由观察者","2-two-line-integration--两行代码集成#2. Two-Line Integration / 两行代码集成":"void main() {\n ZeroInspectorKit.init();\n runApp(ZeroInspectorKit.wrapApp(const MyApp()));\n}","inspector-panel--检查器面板#Inspector Panel / 检查器面板":"The inspector panel contains 9 tabs (as of v1.9.0):检查器面板包含 9 个标签页(v1.9.0 起):\nTab\tIcon\tFeature\tNetwork\t🌐\tHTTP request viewing + interceptor rules / 网络请求查看 + 拦截修改\tLogs\t📝\tLog viewing with level filter / 日志查看\tErrors\t🚨\tAggregated & deduped crash viewing / 去重聚合的异常查看\tDatabase\t💾\tDatabase and table inspection / 数据库查看\tMemory\t📊\tMemory trend, Dart Heap, Native memory, leak detection / 内存趋势、Dart Heap、Native 内存、泄漏检测\tFPS\t🎯\tReal-time FPS, jank rate, trend chart / 实时 FPS、卡顿率、趋势图\tRoutes\t🧭\tRoute navigation tracking / 路由追踪\tWidgets\t🔍\tWidget tree snapshot for the current route / 当前路由的 Widget 树快照\tAlerts\t🔔\tRule-based alerts with unread badge / 基于规则的告警与未读角标\t\nThe Memory and FPS monitors are off by default to avoid performance overhead. Toggle the switch at the top of each panel to start collecting data.Memory 与 FPS 监控默认关闭以避免性能开销。在各自面板顶部打开开关才会开始采集数据。","one-click-bug-report--一键-bug-报告#One-Click Bug Report / 一键 Bug 报告":"Tap the bug icon in the inspector panel header to generate and share a bug report in one tap — ideal for QA to attach environment context when filing issues.点击检查器面板头部的虫子图标,即可一键生成并分享一份 Bug 报告——非常适合 QA 在提 issue 时附上环境上下文。The shared text snapshot includes / 分享的文本快照包含:\nDevice / 设备: real model (e.g. Pixel 8 Pro / iPhone (iPhone16,1)), OS & version, locale, Dart runtime, CPU cores.\nMemory / 内存: current heap usage and whether Native memory is supported.\nRecent logs / 最近日志: the latest captured log entries.\nRecent network / 最近网络: the latest captured requests.\nSensitive headers are masked the same way as in the Network tab. No data leaves the device except through the share target you choose.敏感请求头会与网络标签页一样被遮蔽。除你选择的分享目标外,数据不会离开设备。\nRequires no extra setup — it works as soon as the inspector is running. / 无需额外配置——检查器运行后即可使用。","floating-button--悬浮按钮#Floating Button / 悬浮按钮":"The floating button appears after 1 second delay / 悬浮按钮延迟 1 秒出现\nDrag to move it along the screen edge / 拖动 可沿屏幕边缘移动\nTap (when fully visible) to open/close the inspector panel / 点击(完全可见时)打开/关闭检查器面板\nButton auto-snaps to the nearest screen edge / 按钮自动吸附到最近的屏幕边缘\nBreathing animation when idle / 空闲时有呼吸动画","edge-docking-since-v120--边缘吸附v120-起#Edge Docking (since v1.2.0) / 边缘吸附(v1.2.0 起)":"When released near a screen edge, the button auto-docks and tucks into the edge, leaving only a 24px peek visible:拖动松手后,按钮会自动吸附到最近边缘并\"收入\"边缘,仅露出 24px 小弧边:\nState\tBehavior / 行为\tDocked (tucked in)\tOnly 24px peek visible; icon becomes a directional chevron (left dock → ➡, right dock → ⬅) hinting at tap-to-pull-out / 仅露出 24px;图标变为方向箭头提示可点击拉出\tTap docked peek\tSmoothly pulls out to fully visible (panel NOT opened, avoids accidental open) / 平滑拉出到完全可见(不打开面板,避免误触)\tTap fully visible\tOpens the inspector panel / 打开检查器面板\t\nThis design avoids conflicts with system back gestures (Android/iOS edge swipe to go back) when pulling out from the docked state.此设计避免了从吸附态拖出时与系统返回手势(Android/iOS 边缘右滑退出)的冲突。","search--搜索#Search / 搜索":"The main viewers support fuzzy search:各查看器均支持模糊搜索:\nViewer\tSearch Scope\tNetwork\tURL, HTTP method / URL、请求方法\tLogs\tMessage, tag / 消息、标签\tErrors\tException type, message / 异常类型、消息\tDatabase (global)\tDatabase name, table name / 数据库名、表名\tDatabase (in-database)\tTable name, all column data / 表名、所有列数据","manual-logging-optional--手动记录日志可选#Manual Logging (Optional) / 手动记录日志(可选)":"The inspector auto-captures print() output. You can also use manual log methods for precise level control:检查器会自动捕获 print() 输出。也可以使用手动日志方法进行精确级别控制:Quick shorthand (recommended) / 简化写法(推荐):\nInspectorLog.v('Verbose log / 详细日志');\nInspectorLog.d('Debug log / 调试日志');\nInspectorLog.i('Info log / 信息日志');\nInspectorLog.w('Warning log / 警告日志');\nInspectorLog.e('Error log / 错误日志');\nFull form / 完整写法:\nInspectorLogInterceptor.instance.verbose('Verbose log / 详细日志');\nInspectorLogInterceptor.instance.debug('Debug log / 调试日志');\nInspectorLogInterceptor.instance.info('Info log / 信息日志');\nInspectorLogInterceptor.instance.warning('Warning log / 警告日志');\nInspectorLogInterceptor.instance.error('Error log / 错误日志');","third-party-log-integration--第三方日志库集成#Third-Party Log Integration / 第三方日志库集成":"No configuration needed! The plugin automatically captures logs from third-party logging libraries (e.g., logger, flutter_logger) that use print() or debugPrint().无需配置! 插件会自动捕获所有使用 print() 的第三方日志库的日志。These logs are categorized as INFO level.这些日志统一归类为 INFO 级别。","bidirectional-sync-optional--双向同步可选#Bidirectional Sync (Optional) / 双向同步(可选)":"To sync inspector-captured logs to your third-party logger:将检查器捕获的日志同步到第三方日志库:\nInspectorLogInterceptor.instance.onLogCaptured = (entry) {\n yourLogger.log(entry.message);\n};","feature-pages--功能详情#Feature Pages / 功能详情":"Network Inspector — Network request details + interceptor rules / 网络检查器详情 + 拦截修改\nLog Viewer — Log viewing details / 日志查看器详情\nErrors — Aggregated error viewing / 异常聚合查看\nDatabase Viewer — Database inspection details / 数据库查看器详情\nRoute Tracker — Route tracking details / 路由追踪详情\nMemory Viewer — Memory monitoring & leak detection / 内存监控与泄漏检测\nFPS Viewer — FPS monitoring & jank detection / FPS 监控与卡顿检测","session-persistence--会话持久化#Session Persistence / 会话持久化":"Logs, network requests, and aggregated errors are asynchronously flushed to a local SQLite ring buffer. On the next launch, logs and aggregated errors replay into their tabs; network requests stay archived on disk for later export. Data therefore survives app restarts even if the inspector panel was never opened. Tap the storage icon in the panel header to open the Persisted data manager: view row counts per category, export the full session archive, or clear the disk. See Configuration (PersistenceService section) for the API and tuning parameters.日志、网络请求与聚合异常会被异步写入本地 SQLite 环形缓冲。下次启动时,日志与聚合异常会回放入各自标签页;网络请求保留在磁盘存档,供之后导出。因此即使从未打开过检查器面板,数据也能跨重启保留。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看各类别行数、导出完整会话存档或清空磁盘。API 与调参详见 Configuration(PersistenceService 一节)。"}}} \ No newline at end of file +{"/Configuration":{"title":"Configuration / 配置说明","data":{"zeroinspectorkitinit-parameters--初始化参数#ZeroInspectorKit.init() Parameters / 初始化参数":"Parameter\tType\tDefault\tDescription\tenable\tbool\ttrue\tEnable inspector (auto false in release mode) / 启用检查器\tenableLogCapture\tbool\ttrue\tEnable log capture / 启用日志捕获\tenableNetworkCapture\tbool\ttrue\tEnable network interception / 启用网络拦截\tenableErrorCapture\tbool\ttrue\tEnable error aggregation (Errors tab) / 启用异常聚合(Errors 标签页)\tenablePersistence\tbool\ttrue\tPersist logs/network/errors to a SQLite ring buffer; logs & errors replay on launch / 将日志/网络/异常落盘到 SQLite 环形缓冲,启动时回放日志与异常\tenableFlutterLeakTracker\tbool\ttrue\tBridge Flutter's official MemoryAllocations as a second leak-detection source / 桥接 Flutter 官方 MemoryAllocations 作为泄漏检测第二来源\tenableDatabaseScan\tbool\ttrue\tEnable database scan / 启用数据库扫描\tenableRouteTracking\tbool\ttrue\tEnable route tracking / 启用路由追踪\tenableWidgetInspector\tbool\ttrue\tEnable Widget tree snapshot / 启用 Widget 树快照\tenableNetworkTimeline\tbool\ttrue\tPrefer the network timeline (waterfall) in request details / 网络详情页默认展示时间轴(瀑布图)\tcustomButton\tWidget?\tnull\tCustom floating button widget / 自定义悬浮按钮\tonLogCaptured\tvoid Function(LogEntry)?\tnull\tLog capture callback for third-party integration / 日志捕获回调\tmaxNetworkItems\tint?\t100\tNetwork request cache cap / 网络请求缓存上限\tmaxLogItems\tint?\t500\tLog entry cache cap / 日志条目缓存上限\tmaxRouteItems\tint?\t200\tRoute record cache cap / 路由记录缓存上限\tmaxBodyPreviewBytes\tint?\t32KB\tBody preview cap, longer bodies truncated / body 预览字节上限,超出截断\t\nenableWidgetInspector / enableNetworkTimeline are also exposed on runAppWithInspector(). (wrapApp() takes only enable.)enableWidgetInspector / enableNetworkTimeline 也可在 runAppWithInspector() 上设置(wrapApp() 仅接受 enable)。","usage-examples--使用示例#Usage Examples / 使用示例":"","disable-specific-features--禁用特定功能#Disable Specific Features / 禁用特定功能":"ZeroInspectorKit.init(\n enableLogCapture: true,\n enableNetworkCapture: false, // Disable network monitoring / 禁用网络监控\n enableDatabaseScan: true,\n enableRouteTracking: false, // Disable route tracking / 禁用路由追踪\n);","with-log-callback--带日志回调#With Log Callback / 带日志回调":"ZeroInspectorKit.init(\n onLogCaptured: (entry) {\n // Forward to your logging service / 转发到你的日志服务\n myLogger.log(entry.message);\n },\n);","conditionalinspector--条件检查器组件#ConditionalInspector / 条件检查器组件":"A convenience widget that automatically shows/hides the inspector based on build mode.根据构建模式自动显示/隐藏检查器的便利组件。\nConditionalInspector(\n child: YourAppWidget(),\n)\nParameter\tType\tDefault\tDescription\tchild\tWidget\trequired\tChild widget / 子组件\tenabled\tbool\ttrue\tEnable inspector / 启用检查器","floatinginspectorbutton--悬浮检查器按钮#FloatingInspectorButton / 悬浮检查器按钮":"Parameter\tType\tDefault\tDescription\tenabled\tbool\ttrue\tEnable button (auto false in release mode) / 启用按钮","inspectorloginterceptor--日志拦截器#InspectorLogInterceptor / 日志拦截器":"Method\tDescription\tstart()\tStart capturing logs / 开始捕获日志\tstop()\tStop capturing logs / 停止捕获日志\tlog(level, message, tag)\tAdd a log entry / 添加日志条目\tverbose(message, tag)\tAdd verbose log / 添加详细日志\tdebug(message, tag)\tAdd debug log / 添加调试日志\tinfo(message, tag)\tAdd info log / 添加信息日志\twarning(message, tag)\tAdd warning log / 添加警告日志\terror(message, tag)\tAdd error log / 添加错误日志\t\nProperty\tType\tDescription\tonLogCaptured\tvoid Function(LogEntry)?\tCallback when a log is captured / 日志捕获回调","inspectorrouteobserver--路由观察者#InspectorRouteObserver / 路由观察者":"Navigator observer for tracking route changes. Auto-injected when using runAppWithInspector() or wrapApp().用于追踪路由变化的 Navigator 观察者。使用 runAppWithInspector() 或 wrapApp() 时自动注入。\nMaterialApp(\n navigatorObservers: [InspectorRouteObserver()],\n home: MyHomePage(),\n)","databaseregistry--数据库注册表#DatabaseRegistry / 数据库注册表":"Register custom database providers:注册自定义数据库提供者:\nDatabaseRegistry.instance.registerProvider(SqliteDatabaseProvider());\nSee Custom Database Provider for more details.详见 自定义数据库提供者。","inspectorlog--简化日志-api#InspectorLog / 简化日志 API":"Available since v1.1.2 / v1.1.2 起可用\nA static wrapper around InspectorLogInterceptor.instance for shorter log calls.InspectorLogInterceptor.instance 的静态包装,用于更简短的日志调用。\nInspectorLog.v('Verbose log');\nInspectorLog.d('Debug log');\nInspectorLog.i('Info log', tag: 'Auth');\nInspectorLog.w('Warning log');\nInspectorLog.e('Error log');\nMethod\tDescription\tstart()\tStart capturing logs / 开始捕获日志\tstop()\tStop capturing logs / 停止捕获日志\tlog(level, message, {tag})\tAdd a log entry / 添加日志条目\tv(message, {tag})\tAdd verbose log / 添加详细日志\td(message, {tag})\tAdd debug log / 添加调试日志\ti(message, {tag})\tAdd info log / 添加信息日志\tw(message, {tag})\tAdd warning log / 添加警告日志\te(message, {tag})\tAdd error log / 添加错误日志\t\nProperty\tType\tDescription\tisRunning\tbool\tWhether log capture is currently active / 日志捕获是否正在运行","memoryinspectorservice--内存监控服务#MemoryInspectorService / 内存监控服务":"Available since v1.1.0 / v1.1.0 起可用\nSingleton service for memory monitoring and leak detection, extends ChangeNotifier.内存监控与泄漏检测单例服务,继承 ChangeNotifier。","full-api--完整接口#Full API / 完整接口":"// Leak tracking (full form) / 泄漏追踪(完整写法)\nMemoryInspectorService.instance.trackObject(\n myController,\n tag: 'HomeController_textController',\n expectedReleaseAfter: const Duration(seconds: 30),\n);\nMemoryInspectorService.instance.untrackObject(myController);\nMemoryInspectorService.instance.clearLeakRecords();","simplified-api--简化接口#Simplified API / 简化接口":"Available since v1.1.2 / v1.1.2 起可用\n// Extension method on Object / Object 上的扩展方法\nmyBloc.trackMemoryLeak(tag: 'HomePage_myBloc');\n// Top-level function / 顶层函数\ntrackMemoryLeak(myBloc, tag: 'HomePage_myBloc');\n// Cancel tracking / 取消追踪\nmyBloc.untrackMemoryLeak();\nSee Memory Viewer for full feature details.完整功能详情见 Memory Viewer。","flutter-memoryallocations-bridge--官方泄漏追踪桥接#Flutter MemoryAllocations Bridge / 官方泄漏追踪桥接":"Available since v1.9.0 / v1.9.0 起可用\nThe leak detector's own WeakReference state machine has a blind spot: an object whose dispose() ran but which has not been GC'd yet still resolves through the weak reference and is flagged as a suspected leak (false positive). LeakTrackerBridge subscribes to Flutter's official FlutterMemoryAllocations (the data source behind leak_tracker) as a second source: once the official stream reports disposed, the object is treated as released (awaiting GC) even if the weak reference still resolves — cutting false positives.自研泄漏检测的 WeakReference 状态机有一个盲点:对象已调用 dispose() 但尚未被 GC 时,弱引用仍存活,会被判定为\"疑似泄漏\"(误报)。LeakTrackerBridge 订阅 Flutter 官方的 FlutterMemoryAllocations(leak_tracker 背后的数据源)作为第二来源:只要官方上报了 disposed,即便弱引用仍存活也判定为已释放(等待 GC),从而显著降低误报。\nEnabled by default via ZeroInspectorKit.init()'s enableFlutterLeakTracker / 通过 init() 的 enableFlutterLeakTracker 默认启用\nOfficial events only cover framework types that report to FlutterMemoryAllocations (Image / Picture / Layer …) — it supplements, not replaces, the custom detector / 官方事件只覆盖会上报给 FlutterMemoryAllocations 的框架类型(如 Image / Picture / Layer),因此是补充而非替代自研方案","errorservice--异常聚合服务#ErrorService / 异常聚合服务":"Available since v1.9.0 / v1.9.0 起可用\nSingleton service for aggregated error capture, extends ChangeNotifier. Hooks FlutterError.onError (keeping the default red error behavior) and dedups each exception by type + stack signature. See Errors for full feature details.异常聚合单例服务,继承 ChangeNotifier。接管 FlutterError.onError(保留默认红色报错行为),按类型 + 堆栈签名去重聚合。完整功能见 Errors。\n// Report an exception manually (gRPC / custom protocols) / 手动上报异常\nErrorService.instance.report(error, stackTrace, 'myModule');\n// Read aggregated records (newest first) / 读取聚合记录(最新在前)\nfinal ErrorRecord r = ErrorService.instance.errors.first;\n// Toggle capture / 切换抓取开关\nErrorService.instance.isEnabled = false;\n// Clear all records / 清空全部记录\nErrorService.instance.clear();\nMember\tDescription\terrors\tList — aggregated records, newest first / 聚合记录,最新在前\terrorCount\tint — aggregated record count / 聚合记录条数\tisEnabled\tbool — capture toggle (programmatic) / 抓取开关(代码控制)\treport(exception, stack, [context])\tManually report an exception / 手动上报异常\trestore(records)\tRestore persisted records (replay on launch) / 恢复持久化记录(启动回放)\tclear()\tClear all aggregated records / 清空所有聚合记录\tinstall() / uninstall()\tHook / restore FlutterError.onError / 接管 / 还原 FlutterError.onError","persistenceservice--持久化服务#PersistenceService / 持久化服务":"Available since v1.9.0 / v1.9.0 起可用\nSingleton service that async-flushes logs, network requests, and aggregated errors to a local SQLite ring buffer (zero_inspector_kit.db) so data survives app restarts. On launch, logs and aggregated errors replay into their tabs; network requests stay archived on disk for later export. Everything is guarded and degrades gracefully: if the DB is unavailable (e.g. desktop without sqflite FFI) isEnabled is false and writes become no-ops, never affecting the host app.单例服务,将日志、网络请求与聚合异常异步落盘到本地 SQLite 环形缓冲(zero_inspector_kit.db),使数据跨重启不丢。启动时日志与聚合异常回放入各自标签页;网络请求保留在磁盘存档,供之后导出。所有操作都有保护并优雅降级:数据库不可用(如桌面端未配置 sqflite FFI)时 isEnabled 为 false,写入变为空操作,绝不影响宿主应用。","tuning--调参#Tuning / 调参":"await PersistenceService.instance.init(\n maxRowsPerTable: 5000, // rows kept per table / 每表保留行数\n retention: const Duration(days: 7), // retention window / 保留时长\n flushInterval: const Duration(seconds: 2), // disk flush interval / 刷盘间隔\n);\nEnabled by default via ZeroInspectorKit.init()'s enablePersistence — the ring buffer flushes automatically, so no manual init() call is needed in normal use.通过 init() 的 enablePersistence 默认启用——环形缓冲会自动刷盘,常规使用无需手动调用 init()。\nMember\tDescription\tisEnabled\tbool — whether the DB is available / 数据库是否可用\tmaxRowsPerTable\tint — row cap per table (ring-buffer trim line) / 每表行数上限\tenqueueLog(e) / enqueueNetwork(r) / enqueueError(e)\tEnqueue an item for async flush / 入队待异步落盘\tflush()\tFlush the buffer to disk and trim / 刷盘并按环形缓冲裁剪\tloadLogs() / loadErrors() / loadNetworkJson()\tLoad persisted data, newest first / 读取已持久化数据(最新在前)\tbuildSessionArchiveJson()\tBuild the full-session archive JSON (logs + errors + network) / 构建完整会话存档 JSON\texportSessionArchiveAndShare()\tExport & share the full-session archive via the system share sheet / 导出并分享完整会话存档\tclearAll()\tClear persisted data on disk / 清空磁盘上的持久化数据\tdispose()\tFlush then close / 先刷盘再关闭","persisted-data-manager--持久化数据管理#Persisted Data Manager / 持久化数据管理":"Tap the storage icon in the panel header to open the Persisted data sheet: it shows the current row counts per category (logs / network / errors), the per-table cap, and whether the data will be replayed on the next launch. Actions:点击面板头部的存储图标打开 Persisted data 管理弹层:展示各类别当前行数(logs / network / errors)、每表上限,以及下次启动是否会回放。支持的操作:\nExport & share session archive — share a JSON snapshot of the whole session / 导出并分享会话存档——分享本次会话完整 JSON 快照\nClear disk — wipe persisted rows so the next launch replays nothing / 清空磁盘——清空持久化数据,下次启动干净\nClear disk & lists — wipe disk and the in-memory lists together / 同时清空磁盘与列表——连同内存列表一并清空","fpsservice--fps-监控服务#FpsService / FPS 监控服务":"Available since v1.2.0 / v1.2.0 起可用\nSingleton service for FPS monitoring, extends ChangeNotifier.FPS 监控单例服务,继承 ChangeNotifier。\nFpsService.instance.start();\nFpsService.instance.stop();\nFpsService.instance.clear();\nfinal fps = FpsService.instance.currentFps;\nfinal jankRate = FpsService.instance.jankRate;\nMethod\tDescription\tstart()\tStart FPS monitoring / 开始 FPS 监控\tstop()\tStop FPS monitoring / 停止 FPS 监控\tclear()\tClear all historical data and counters / 清空所有历史数据和计数器\t\nProperty\tType\tDescription\tisRunning\tbool\tWhether monitoring is currently active / 是否正在监控\tcurrentFps\tdouble\tCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)\tjankRate\tdouble\tJank rate as percentage / 卡顿率(百分比)\ttotalFrameCount\tint\tTotal frames captured / 总帧数\ttotalJankyCount\tint\tTotal janky frames (>16ms) / 总卡顿帧数(>16ms)\tlastFrameJanky\tbool\tWhether the most recent frame was janky / 最近一帧是否卡顿\tfpsHistory\tList\tRecent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)\tframeRecords\tList\tRecent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)\t\nSee FPS Viewer for full feature details.完整功能详情见 FPS Viewer。"}},"/Alerts":{"title":"Alerts / 告警系统","data":{"overview--概述#Overview / 概述":"Available since v1.3.0v1.3.0 起可用\nThe alert system lets you define rules that proactively surface problems across network requests, logs, memory, and FPS — without manually scanning the panels.告警系统允许你定义规则,主动暴露网络请求、日志、内存、FPS 方面的问题,无需手动逐个面板排查。","how-it-works--工作原理#How It Works / 工作原理":"Rules are evaluated by AlertService whenever a relevant event occurs / 每当相关事件发生时,AlertService 会评估规则\nMatched rules produce alerts, and the unread count is exposed via a ValueNotifier / 命中的规则会生成告警,未读数通过 ValueNotifier 暴露\nThe floating button shows an unread-count badge; opening the panel clears it / 悬浮球显示未读数量角标,打开面板即清零\nAn Alerts tab in the inspector panel lists all triggered alerts / 检查器面板的 Alerts 标签页列出所有已触发的告警","rule-types--规则类型#Rule Types / 规则类型":"Type\tTrigger / 触发条件\tExample / 示例\tNetwork\tResponse status code / 响应状态码\tAlert when status ≥ 400 / 状态码 ≥ 400 时告警\tLog\tLog level or message / 日志级别或内容\tAlert on error logs / 出现 error 日志时告警\tMemory\tDart heap / Native memory threshold / 内存阈值\tAlert when Dart heap > 100 MB / Dart 堆 > 100 MB 时告警\tFPS\tFrame rate / 帧率\tAlert when FPS < 50 / FPS < 50 时告警","ui-features--ui-功能#UI Features / UI 功能":"","floating-button-badge--悬浮球角标#Floating Button Badge / 悬浮球角标":"A red badge shows the current unread alert count (clamped 0–99) / 红色角标显示当前未读告警数(0–99)\nThe number is drawn inside the button so it is never clipped at screen edges / 数字直接绘制在按钮内部,吸附屏幕边缘时不会被裁切\nOpening the inspector panel clears the unread count / 打开检查器面板即清零未读数","alerts-tab--alerts-标签页#Alerts Tab / Alerts 标签页":"Lists triggered alerts with type, condition, and timestamp / 列出已触发告警的类型、条件与时间\nTap an alert to jump to the related panel (network / log / memory / FPS) / 点击告警可跳转到相关面板(网络 / 日志 / 内存 / FPS)","notes--备注#Notes / 备注":"Alerts are evaluated in-process and shown in the developer console only; they do not send data off-device / 告警仅在本机开发者控制台内评估与展示,不会将数据发送到设备外\nPersisted to disk (since v1.11.0): triggered alerts are flushed to the SQLite ring buffer and replay into the Alerts tab after an app restart, and are included in the session archive export / 已落盘持久化(v1.11.0 起):已触发的告警会写入 SQLite 环形缓冲,重启后回放至 Alerts 标签页,并包含在会话存档导出中\nThe alert system is tree-shaken out in release builds, like the rest of the inspector / 与检查器其余部分一样,告警系统在 release 构建中被 tree-shake 移除"}},"/Custom-Database-Provider":{"title":"Custom Database Provider / 自定义数据库提供者","data":{"overview--概述#Overview / 概述":"Zero Inspector Kit supports extending database inspection beyond SQLite through the DatabaseProvider interface.Zero Inspector Kit 通过 DatabaseProvider 接口支持扩展数据库检查功能到 SQLite 以外的数据库。","databaseprovider-interface--接口定义#DatabaseProvider Interface / 接口定义":"abstract class DatabaseProvider {\n /// Provider name / 提供者名称\n String get name;\n /// Get list of databases / 获取数据库列表\n Future> getDatabases();\n /// Query table data / 查询表数据\n Future queryTable(String dbPath, String tableName, {int limit = 50});\n}","data-models--数据模型#Data Models / 数据模型":"","databaseinfo#DatabaseInfo":"Field\tType\tDescription\tname\tString\tDatabase name / 数据库名称\tpath\tString\tDatabase file path / 数据库文件路径\ttables\tList\tList of tables / 表列表","tableinfo#TableInfo":"Field\tType\tDescription\tname\tString\tTable name / 表名\trowCount\tint\tRow count / 行数","queryresult#QueryResult":"Field\tType\tDescription\tcolumns\tList\tColumn names / 列名\trows\tList>\tRow data / 行数据","example-custom-provider--示例自定义提供者#Example: Custom Provider / 示例:自定义提供者":"class MyCustomDatabaseProvider implements DatabaseProvider {\n @override\n String get name => 'CustomDB';\n @override\n Future> getDatabases() async {\n // Return your custom databases / 返回你的自定义数据库列表\n return [\n DatabaseInfo(\n name: 'my_database',\n path: '/path/to/my_database',\n tables: [\n TableInfo(name: 'users', rowCount: 100),\n TableInfo(name: 'orders', rowCount: 500),\n ],\n ),\n ];\n }\n @override\n Future queryTable(\n String dbPath,\n String tableName, {\n int limit = 50,\n }) async {\n // Execute query and return results / 执行查询并返回结果\n return QueryResult(\n columns: ['id', 'name', 'value'],\n rows: [\n {'id': 1, 'name': 'item1', 'value': '100'},\n {'id': 2, 'name': 'item2', 'value': '200'},\n ],\n );\n }\n}","register-provider--注册提供者#Register Provider / 注册提供者":"// Register your custom provider / 注册自定义提供者\nDatabaseRegistry.instance.registerProvider(MyCustomDatabaseProvider());","built-in-provider--内置提供者#Built-in Provider / 内置提供者":"The plugin includes SqliteDatabaseProvider which is auto-registered when enableDatabaseScan is true (default).插件内置 SqliteDatabaseProvider,当 enableDatabaseScan 为 true(默认)时自动注册。You can also register it manually:也可以手动注册:\nDatabaseRegistry.instance.registerProvider(SqliteDatabaseProvider());"}},"/Database-Viewer":{"title":"Database Viewer / 数据库查看器","data":{"overview--概述#Overview / 概述":"The Database Viewer inspects SQLite databases in your app with a two-level navigation system.数据库查看器通过双层导航系统检查应用中的 SQLite 数据库。","auto-scan--自动扫描#Auto-Scan / 自动扫描":"The inspector automatically scans the following directories for .db and .sqlite files:检查器自动扫描以下目录中的 .db 和 .sqlite 文件:\ngetApplicationDocumentsDirectory() / 应用文档目录\ngetDatabasesPath() / 数据库目录","two-level-navigation--双层导航#Two-Level Navigation / 双层导航":"","level-1-database-list-global--第一层数据库列表全局#Level 1: Database List (Global) / 第一层:数据库列表(全局)":"Shows all discovered databases / 显示所有发现的数据库\nEach database shows name and table count / 每个数据库显示名称和表数量\nGlobal search: Search database names and table names / 全局搜索:搜索数据库名和表名","level-2-database-detail--第二层数据库详情#Level 2: Database Detail / 第二层:数据库详情":"Click a database to enter its detail view / 点击数据库进入详情视图\nLeft panel: table list with row counts / 左侧面板:表列表(含行数)\nRight panel: table data in DataTable format / 右侧面板:DataTable 格式的表数据\nBack button to return to database list / 返回按钮返回数据库列表\nIn-database search: Search table names AND all column data / 数据库内搜索:搜索表名和所有列数据","search--搜索#Search / 搜索":"Level\tSearch Scope\tGlobal (database list)\tDatabase names, table names / 数据库名、表名\tIn-database\tTable names, all column values in all tables / 表名、所有表的所有列值","search-highlights--搜索高亮#Search Highlights / 搜索高亮":"When searching within a database, matching cell values are highlighted in accent color.在数据库内搜索时,匹配的单元格值以强调色高亮显示。","ui-features--ui-功能#UI Features / UI 功能":"Color-coded table icons / 带颜色的表图标\nRow count badges for each table / 每个表的行数徽章\nSelected table highlighted / 选中表高亮\nHorizontal and vertical scrollable data table / 水平和垂直可滚动的数据表\nRow count display (filtered / total) when searching / 搜索时显示行数(过滤/总数)","custom-database-provider--自定义数据库提供者#Custom Database Provider / 自定义数据库提供者":"For non-SQLite databases, see Custom Database Provider.对于非 SQLite 数据库,请参阅 自定义数据库提供者。"}},"/Errors":{"title":"Errors / 异常聚合查看器","data":{"overview--概述#Overview / 概述":"Available since v1.9.0v1.9.0 起可用\nThe Errors tab surfaces \"the same crash happening repeatedly\". When the inspector is running, ErrorService hooks both FlutterError.onError and PlatformDispatcher.onError (keeping the default red-screen / console behavior for both) and aggregates every exception by type + stack signature. Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces.Errors 标签页用来快速发现\"同一处崩溃反复出现\"的问题。检查器运行时,ErrorService 会接管 FlutterError.onError(保留默认的红色报错与控制台行为),把每次异常按类型 + 堆栈签名去重聚合:同一处崩溃的多次发生会合并为一条记录,展示累计次数与首末次时间——而不是用成百上千条相同的堆栈刷屏日志。","what-gets-captured--捕获来源#What Gets Captured / 捕获来源":"Source / 来源\tHow / 方式\tFlutter framework errors / Flutter 框架异常\tHooks FlutterError.onError (default handler kept) / 接管 FlutterError.onError(保留默认处理)\tFramework-boundary & platform-channel errors / 框架边界外与平台通道异常\tPlatformDispatcher.onError (with save/restore) / PlatformDispatcher.onError(接管 + 还原)\tUncaught async errors / 未捕获异步异常\trunZonedGuarded inside runAppWithInspector() / runAppWithInspector() 内部的 runZonedGuarded\tManual reports / 手动上报\tErrorService.instance.report(exception, stackTrace) — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道\t\nCaptured records are also persisted to disk (see Session Persistence below), so aggregated errors from previous sessions are replayed on the next launch.捕获到的记录也会落盘持久化(见下文会话持久化),下次启动时会回放上次会话的聚合异常。","deduplication--去重规则#Deduplication / 去重规则":"Records are keyed by exception type plus a stack signature (first frames with line/address noise stripped) / 按异常类型 + 堆栈签名(取前若干帧并去掉行号/地址噪声)去重\nA new occurrence of an existing signature bumps its count and updates lastSeen instead of adding a new row / 同签名的再次发生只累加 count 并更新 lastSeen,不新增行\nRing buffer capped at 200 aggregated records (oldest dropped) / 环形缓冲上限 200 条聚合记录(超出丢弃最旧)","ui-features--ui-功能#UI Features / UI 功能":"","errors-tab--errors-标签页#Errors Tab / Errors 标签页":"Lists aggregated errors newest-first, showing the exception type, message, first seen / last seen time, and a ×N badge when the same crash recurred / 按最新在前列出聚合异常,展示异常类型、消息、首次/末次出现时间,同崩溃复发时显示 ×N 徽章\nTap a row to expand the full stack sample (truncated at 8000 chars); tap again to collapse / 点击行展开完整堆栈样本(8000 字符截断);再点收起\nCopy stack per row / 行内复制堆栈\nSearch filters by exception type or message / 搜索按异常类型或消息过滤\nClear empties the aggregated list / 清除清空聚合列表","tab-badge--标签页红点#Tab Badge / 标签页红点":"The Errors tab icon shows a red count badge while there are aggregated errors (capped at 99+), so recurring crashes are visible at a glance / Errors 标签页图标在有聚合异常时显示红色计数(99+ 封顶),一眼可见反复崩溃\nOpening the Errors tab is not required to see the badge — it reflects ErrorService.instance.errorCount whenever the panel is open / 只要面板打开,标签红点即反映 ErrorService.instance.errorCount,无需先进入 Errors 页\nNote: the floating ball itself stays clean — its red count (if any) is the Alerts unread badge (AlertService), not error aggregation.注意:悬浮球本身保持纯粹——球上的红色数字(如有)来自告警未读数(AlertService),与异常聚合无关。","api--接口#API / 接口":"The full service is re-exported from the package root — no need to import lib/src/.完整服务已从包根导出,无需 import lib/src/。\nimport 'package:zero_inspector_kit/zero_inspector_kit.dart';\n// Report an exception manually (gRPC/custom protocol/your own error paths) / 手动上报异常\nErrorService.instance.report(error, stackTrace, 'myModule');\n// Read the aggregated view (newest first) / 读取聚合视图(最新在前)\nfinal ErrorRecord latest = ErrorService.instance.errors.first;\nprint('${latest.type} x${latest.count} — first ${latest.firstSeen} / last ${latest.lastSeen}');\n// Clear all records / 清空所有记录\nErrorService.instance.clear();\nMember\tDescription\terrors\tList — aggregated records, newest first / 聚合记录,最新在前\terrorCount\tint — number of aggregated records / 聚合记录条数\tisEnabled\tbool — capture toggle (programmatic) / 抓取开关(代码控制)\treport(exception, stack, [context])\tManually report an exception / 手动上报异常\trestore(records)\tRestore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过)\tclear()\tClear all aggregated records / 清空全部记录\tinstall() / uninstall()\tHook / restore FlutterError.onError and PlatformDispatcher.onError / 接管 / 还原 FlutterError.onError 与 PlatformDispatcher.onError","errorrecord--异常记录#ErrorRecord / 异常记录":"Field\tDescription\ttype\tException type name (e.g. _TypeError) / 异常类型名\tmessage\tException message / 异常消息\tcount\tTotal occurrences / 累计出现次数\tfirstSeen / lastSeen\tFirst / last occurrence time / 首次 / 末次出现时间\tsampleStack\tOne full stack sample (may be truncated) / 一条完整堆栈样本(可能截断)","session-persistence--会话持久化#Session Persistence / 会话持久化":"Errors — together with logs, network requests and alerts — are asynchronously flushed to a local SQLite ring buffer (zero_inspector_kit.db). On the next launch, logs, aggregated errors and alerts replay into their tabs, so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the storage icon in the panel header to open the Persisted data manager: see the current row counts, export the full session archive as JSON, or clear the disk. See Configuration (PersistenceService section) for details and the tuning parameters.异常与日志、网络请求、告警一起被异步写入本地 SQLite 环形缓冲(zero_inspector_kit.db)。下次启动时,日志、聚合异常与告警会回放入各自标签页——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看当前行数、导出完整会话存档 JSON,或清空磁盘。详见 Configuration(PersistenceService 一节)的参数说明。","enable--disable--开关#Enable / Disable / 开关":"enableErrorCapture (default true) in ZeroInspectorKit.init() controls error aggregation / init() 的 enableErrorCapture(默认 true)控制异常聚合\nenablePersistence (default true) controls the disk ring buffer / enablePersistence(默认 true)控制磁盘环形缓冲\nSet both to false to keep everything in memory only / 两者都设为 false 时数据仅保留在内存\nZeroInspectorKit.init(\n enableErrorCapture: true,\n enablePersistence: true, // false → memory-only / 关闭后仅内存\n);","related--相关#Related / 相关":"Log Viewer — raw log stream including error lines / 原始日志流(含错误行)\nConfiguration — init parameters and full service APIs / 初始化参数与完整服务接口\nUsage — general usage guide / 使用指南"}},"/FAQ":{"title":"FAQ / 常见问题","data":{"general--通用#General / 通用":"","q-does-the-inspector-affect-production-builds--检查器会影响生产构建吗#Q: Does the inspector affect production builds? / 检查器会影响生产构建吗?":"A: No. The inspector is automatically disabled in release mode via kReleaseMode. Flutter's tree-shaking removes all inspector-related code from production builds. You don't need to remove any code.不会。 检查器在 release 模式下通过 kReleaseMode 自动禁用。Flutter 的 tree-shaking 会移除所有检查器相关代码,无需手动移除。","q-what-platforms-are-supported--支持哪些平台#Q: What platforms are supported? / 支持哪些平台?":"A: Android and iOS.支持 Android 和 iOS。","q-what-flutterdart-versions-are-required--需要什么-flutterdart-版本#Q: What Flutter/Dart versions are required? / 需要什么 Flutter/Dart 版本?":"A: Flutter >= 3.3.0, Dart SDK >= 3.11.0 < 4.0.0.Flutter >= 3.3.0,Dart SDK >= 3.11.0 < 4.0.0。","network--网络#Network / 网络":"","q-do-i-need-to-manually-add-interceptors-for-dio--需要为-dio-手动添加拦截器吗#Q: Do I need to manually add interceptors for Dio? / 需要为 Dio 手动添加拦截器吗?":"A: No. Dio uses IOHttpClientAdapter internally, which uses dart:io's HttpClient. The inspector captures all requests via HttpOverrides automatically, making it truly zero-invasion for both http package and Dio.不需要。 Dio 内部使用 IOHttpClientAdapter,底层使用 dart:io 的 HttpClient。检查器通过 HttpOverrides 自动捕获所有请求,对 http 包和 Dio 都是真正的零侵入。","q-why-do-i-see-duplicate-dio-requests--为什么看到重复的-dio-请求#Q: Why do I see duplicate Dio requests? / 为什么看到重复的 Dio 请求?":"A: If you use both InspectorDioInterceptor and the auto-capture (HttpOverrides), Dio requests will be recorded twice. Simply remove the manual InspectorDioInterceptor — auto-capture handles everything.如果同时使用了 InspectorDioInterceptor 和自动捕获(HttpOverrides),Dio 请求会被记录两次。移除手动 InspectorDioInterceptor 即可,自动捕获会处理一切。","q-can-i-modify-requests-during-testing--测试时可以修改请求吗#Q: Can I modify requests during testing? / 测试时可以修改请求吗?":"A: Yes (since v1.0.7). The inspector supports intercepting and modifying requests via rules. Open a request detail and tap the Interceptor icon to configure a rule. You can modify the request body and request headers only — response fields (status code, response body) are read-only (since v1.0.8). GET requests cannot be modified (no request body). The master toggle is on the Network list page; rules only apply when modification mode is enabled.可以(v1.0.7 起)。检查器支持通过规则拦截并修改请求。打开请求详情,点击拦截器图标配置规则。仅可修改请求体和请求头——响应字段(状态码、响应体)只读(v1.0.8 起)。GET 请求不可修改(无请求体)。总开关在 Network 列表页,规则仅在启用修改模式时生效。See Network Inspector > Request Interceptor for details.详见 Network Inspector > 请求拦截修改。","logging--日志#Logging / 日志":"","q-can-i-use-my-existing-logging-library--可以使用现有的日志库吗#Q: Can I use my existing logging library? / 可以使用现有的日志库吗?":"A: Yes! The inspector automatically captures logs from any library that uses print() or debugPrint(). No configuration needed. Third-party logs are categorized as Info level.可以! 检查器会自动捕获任何使用 print() 或 debugPrint() 的日志库的日志,无需配置。第三方日志统一归类为 Info 级别。","q-how-to-sync-inspector-logs-to-my-logger--如何将检查器日志同步到我的日志库#Q: How to sync inspector logs to my logger? / 如何将检查器日志同步到我的日志库?":"A: Use the onLogCaptured callback:使用 onLogCaptured 回调:\nInspectorLogInterceptor.instance.onLogCaptured = (entry) {\n yourLogger.log(entry.message);\n};\nWarning: Do NOT call print() or logging methods inside this callback, as it will cause infinite recursion.\n警告:不要在此回调中调用 print() 或日志方法,否则会导致无限递归。","database--数据库#Database / 数据库":"","q-why-dont-i-see-any-databases--为什么看不到数据库#Q: Why don't I see any databases? / 为什么看不到数据库?":"A: The inspector scans getApplicationDocumentsDirectory() and getDatabasesPath() for .db and .sqlite files. If your database is stored elsewhere, you can implement a custom DatabaseProvider.检查器扫描 getApplicationDocumentsDirectory() 和 getDatabasesPath() 目录中的 .db 和 .sqlite 文件。如果你的数据库存储在其他位置,可以实现自定义 DatabaseProvider。","memory--内存#Memory / 内存":"","q-why-does-dart-heap-show-na-when-debugging-via-pc--通过-pc-调试时为什么-dart-heap-显示-na#Q: Why does Dart Heap show \"N/A\" when debugging via PC? / 通过 PC 调试时为什么 Dart Heap 显示 \"N/A\"?":"A: This is an expected behavior. When using flutter run to debug via PC, the flutter tool sets up port forwarding between PC and device via adb reverse, allowing PC-side DevTools to access the device's VM Service. However, Service.getInfo() returns a serverUri from the PC's perspective; when the app process internally accesses 127.0.0.1:PC_port, the device doesn't have that port listening locally, resulting in Connection refused and VM Service showing OFF.这是预期行为。 使用 flutter run 连接 PC 调试时,flutter tool 会通过 adb reverse 在 PC 和设备之间做端口转发,让 PC 上的 DevTools 能访问设备的 VM Service。但应用进程内部 Service.getInfo() 返回的 serverUri 是 PC 视角的端口,应用进程访问 127.0.0.1:PC端口 时设备本地并没有监听该端口,导致 Connection refused,VM Service 显示 OFF。Workarounds / 解决方案:\nOpen the debug app directly without PC connection — VM Service works normally / 直接打开 debug 应用(不连 PC),VM Service 正常工作\nUse Native memory data (Android PSS / iOS physicalFootprint) as fallback — always available / 使用 Native 内存数据(Android PSS / iOS physicalFootprint)作为降级,始终可用\nProcess RSS is always available regardless of VM Service / 进程 RSS 无论 VM Service 状态都可用\nSee Memory Viewer > VM Service Availability for details.详见 Memory Viewer > VM Service 可用性。","q-does-memory-monitoring-affect-performance--内存监控会影响性能吗#Q: Does memory monitoring affect performance? / 内存监控会影响性能吗?":"A: Memory monitoring is off by default (since v1.1.0). When disabled, all timers are stopped and VM Service connection is cleared, leaving zero overhead. When enabled, refresh intervals are optimized:内存监控默认关闭(v1.1.0 起)。关闭时所有定时器停止、VM Service 连接清空,零开销。开启时刷新间隔已优化:\nProcess RSS / Dart Heap: 500ms\nNative Memory: 3000ms\nStorage Stats: 3000ms\nLeak Detection: 2000ms\nYou can toggle the switch at the top of the Memory panel anytime.可随时在 Memory 面板顶部切换开关。","q-does-leak-detection-require-code-modification--泄漏检测需要侵入代码吗#Q: Does leak detection require code modification? / 泄漏检测需要侵入代码吗?":"A: The current implementation uses WeakReference + Finalizer, which requires calling trackObject() to register objects for tracking. This is a mild invasion (one line of code per tracked object).当前实现使用 WeakReference + Finalizer,需要调用 trackObject() 注册要追踪的对象。这是轻度侵入(每个追踪对象一行代码)。Trade-offs / 取舍:\nPros: 100% reliable, doesn't depend on VM Service, works in release mode / 100% 可靠,不依赖 VM Service,release 模式也可用\nCons: Requires user to know which objects to track / 需要用户知道要追踪哪些对象\nA zero-invasion Heap Snapshot comparison feature (based on VM Service) is technically possible but would strongly depend on VM Service availability (unavailable when debugging via PC), with significant performance overhead. Not currently implemented.零侵入的 Heap Snapshot 对比功能(基于 VM Service)技术上可行,但会强依赖 VM Service 可用性(PC 调试时不可用),且有显著性能开销。目前未实现。See Memory Viewer > Memory Leak Detection for API details.详见 Memory Viewer > 内存泄漏检测 了解 API 详情。","fps--帧率#FPS / 帧率":"","q-why-does-fps-show-a-very-low-value-eg-9-10-fps--为什么-fps-显示很低如-9-10#Q: Why does FPS show a very low value (e.g. 9-10 FPS)? / 为什么 FPS 显示很低(如 9-10)?":"A: This was a bug fixed in v1.2.0. The root cause was that _recentFrameTimestamps.add(now) was placed outside the for-loop in _onFrameTimings; since Flutter engine's addTimingsCallback is batched (may return multiple frames per call), only one timestamp was recorded per batch, undercounting FPS by 6-10x. Upgrade to ^1.2.0 to fix this.这是 v1.2.0 已修复的 bug。根因是 _onFrameTimings 中 _recentFrameTimestamps.add(now) 在 for 循环外;Flutter 引擎的 addTimingsCallback 是批量回调(一次可能返回多帧),但每批只记录 1 个时间戳,导致 FPS 计算偏低 6-10 倍。升级到 ^1.2.0 即可修复。See FPS Viewer for details.详见 FPS Viewer。","q-does-fps-monitoring-affect-performance--fps-监控会影响性能吗#Q: Does FPS monitoring affect performance? / FPS 监控会影响性能吗?":"A: FPS monitoring is off by default (since v1.2.0). When disabled, no frame timings callbacks and no timers — zero overhead. When enabled, it only processes lightweight frame timing data with bounded history (60 trend points, up to 3600 frame records). You can toggle the switch at the top of the FPS panel anytime.FPS 监控默认关闭(v1.2.0 起)。关闭时无帧回调、无定时器——零开销。开启时仅处理轻量帧时序数据,历史有界(趋势 60 个点,帧记录最多 3600 条)。可随时在 FPS 面板顶部切换开关。","ui--界面#UI / 界面":"","q-the-inspector-panel-gets-pushed-up-when-keyboard-appears--键盘弹出时检查器面板被顶起来了#Q: The inspector panel gets pushed up when keyboard appears. / 键盘弹出时检查器面板被顶起来了。":"A: This was fixed in v1.0.6. The floating button and panel are rendered via Overlay, which is independent of the page layout and not affected by keyboard.此问题已在 v1.0.6 修复。悬浮按钮和面板通过 Overlay 渲染,独立于页面布局,不受键盘影响。","q-how-to-use-search--如何使用搜索#Q: How to use search? / 如何使用搜索?":"A: Each viewer has its own search bar at the top. Database viewer has two-level search: global search (database list) and in-database search (table names + all column data).每个查看器顶部都有搜索栏。数据库查看器有双层搜索:全局搜索(数据库列表)和数据库内搜索(表名 + 所有列数据)。","q-the-floating-button-disappeared--cant-be-opened-after-enabling-fps--开启-fps-后悬浮按钮消失无法打开#Q: The floating button disappeared / can't be opened after enabling FPS. / 开启 FPS 后悬浮按钮消失/无法打开。":"A: This was a bug fixed in v1.2.0. The root cause was that FpsService.notifyListeners() triggered Overlay rebuild; combined with the original Overlay lookup failure (using Overlay.of(context, rootOverlay: true) which returned null), the button State was destroyed and could not recover. Upgrade to ^1.2.0 which uses navigatorState.overlay instead.这是 v1.2.0 已修复的 bug。根因是 FpsService.notifyListeners() 触发 Overlay 重建,叠加原 Overlay 查找失败(使用 Overlay.of(context, rootOverlay: true) 返回 null),导致按钮 State 被销毁且无法恢复。升级到 ^1.2.0,改用 navigatorState.overlay 即可。","q-how-does-the-floating-button-edge-docking-work--悬浮按钮的边缘吸附怎么用#Q: How does the floating button edge docking work? / 悬浮按钮的边缘吸附怎么用?":"A: Since v1.2.0, when you drag the button near a screen edge and release, it auto-docks and tucks into the edge, leaving only a 24px peek visible. The icon becomes a directional chevron hinting you can tap to pull it out:\nTap the peek → smoothly pulls out to fully visible (panel NOT opened, avoids accidental open) / 平滑拉出到完全可见(不打开面板,避免误触)\nTap when fully visible → opens the inspector panel / 打开检查器面板\nThis design avoids conflicts with system back gestures (Android/iOS edge swipe to go back) when pulling out from the docked state.v1.2.0 起,拖动按钮靠近屏幕边缘松手即自动吸附并\"收入\"边缘,仅露 24px。图标变为方向箭头,提示可点击拉出:\n点击露出部分 → 平滑拉出到完全可见(不打开面板,避免误触)\n完全可见时点击 → 打开检查器面板\n此设计避免了从吸附态拖出时与系统返回手势(Android/iOS 边缘右滑退出)的冲突。See Usage > Edge Docking for details.详见 Usage > 边缘吸附。","license--许可证#License / 许可证":"","q-can-i-use-this-in-a-commercial-project--可以在商业项目中使用吗#Q: Can I use this in a commercial project? / 可以在商业项目中使用吗?":"A: Yes. This project is licensed under MPL-2.0. You can use it in closed-source commercial apps — if you use it unmodified, no source disclosure is required. If you modify the plugin's source files, those modified files must be published in source form under MPL-2.0 (your own app still does not need to be open-sourced).可以。本项目采用 MPL-2.0 许可证,可用于闭源商业 App——未修改直接使用时无需公开任何源码;若修改了插件源文件,被修改的文件必须以 MPL-2.0 公开源码(你自己的 App 仍无需开源)。","q-are-you-liable-for-issues-in-modified-versions--修改版出问题你们负责吗#Q: Are you liable for issues in modified versions? / 修改版出问题你们负责吗?":"A: This plugin is provided \"as is\", without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.本插件按\"原样\"提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。"}},"/FPS-Viewer":{"title":"FPS Viewer / FPS 监控面板","data":{"":"The FPS Viewer provides real-time frame performance analysis, including current FPS, jank rate, frame duration stats, and an FPS trend chart.FPS 监控面板提供实时帧性能分析,包括当前 FPS、卡顿率、帧耗时统计和 FPS 趋势图。\nAvailable since v1.2.0v1.2.0 起可用","overview--概览#Overview / 概览":"The FPS Viewer is integrated into the inspector panel. Tap the floating inspector button → \"FPS\" tab to access.FPS 监控面板集成在检查器面板中。点击悬浮检查器按钮 → \"FPS\" 标签即可访问。⚠️ Important: FPS monitoring is off by default. You must toggle on the switch at the top of the panel to start collecting data.⚠️ 重要:FPS 监控默认关闭。必须在面板顶部打开开关才会开始采集数据。","master-switch--总开关#Master Switch / 总开关":"A switch at the top of the FPS panel controls whether monitoring is enabled.FPS 面板顶部有一个开关,控制是否启用监控。\nState\tBehavior\tOFF (default)\tNo frame timings callbacks, no timer, zero overhead / 无帧回调、无定时器、零开销\tON\tStarts collecting frame data via WidgetsBinding.instance.addTimingsCallback / 通过 addTimingsCallback 开始采集帧数据\t\nWhen turned off, all callbacks and timers are cancelled, leaving no residual overhead.关闭时所有回调和定时器会被取消,不会残留开销。","features--功能#Features / 功能":"","1-current-stats--当前统计#1. Current Stats / 当前统计":"Current FPS — Updated every 500ms / 每 500ms 更新\nJank Rate — Percentage of janky frames (duration exceeds the per-frame budget, derived from the display refresh rate) / 卡顿帧占比(帧耗时超过由屏幕刷新率换算的逐帧预算)\nTotal Frame Count — All frames captured since start / 自启动以来的总帧数\nTotal Janky Count — All janky frames captured / 自启动以来的总卡顿帧数\nLast Frame Janky — Whether the most recent frame was janky / 最近一帧是否卡顿\n显示当前 FPS、卡顿率、总帧数、总卡顿帧数、最近一帧是否卡顿。","2-fps-trend-chart--fps-趋势图#2. FPS Trend Chart / FPS 趋势图":"Real-time line chart with 30-second history window (60 data points × 500ms)\nY-axis dynamically scales to max(60, maxFps) to avoid overflow when FPS spikes\nJanky frame region marked in red\nTime axis labels: -30s / -15s / Now\n实时折线图,30 秒历史窗口(60 个数据点 × 500ms)。Y 轴动态缩放为 max(60, maxFps) 避免 FPS 飙升时溢出。卡顿区域以红色标记。","3-janky-frame-list--卡顿帧列表#3. Janky Frame List / 卡顿帧列表":"Lists janky frames — duration exceeds the per-frame budget, derived from the display refresh rate (~16.7ms at 60Hz, ~8.3ms at 120Hz) / 列出卡顿帧——帧耗时超过由屏幕刷新率换算的逐帧预算(60Hz≈16.7ms、120Hz≈8.3ms)\nEach item shows frame duration and timestamp, plus the build / raster split (buildDurationUs + rasterDurationUs) so you can tell whether build or GPU raster is the bottleneck / 每项显示帧耗时、时间戳,以及 build / raster 分项(buildDurationUs + rasterDurationUs),便于判断是构建还是 GPU 光栅化卡顿\nHelps identify specific jank spikes / 帮助定位具体卡顿点\n列出卡顿帧(帧耗时超过由屏幕刷新率换算的逐帧预算,60Hz≈16.7ms、120Hz≈8.3ms),每项显示帧耗时、时间戳与 build / raster 分项,帮助定位具体卡顿点。","4-reset--重置#4. Reset / 重置":"\"Reset\" button clears all statistics and historical data / \"Reset\" 按钮清空所有统计和历史数据\nUseful for measuring a specific interaction scenario / 适合测量特定交互场景\n\"Reset\" 按钮清空所有统计和历史数据,适合测量特定交互场景。","how-it-works--工作原理#How It Works / 工作原理":"FPS monitoring uses Flutter's WidgetsBinding.instance.addTimingsCallback to receive frame timing information from the engine. The callback is batched — it may return multiple FrameTiming objects per call, so each frame is recorded individually inside the loop to ensure accurate FPS calculation.FPS 监控使用 Flutter 的 WidgetsBinding.instance.addTimingsCallback 接收引擎的帧时序信息。该回调是批量的——每次调用可能返回多个 FrameTiming 对象,因此在循环内部为每帧单独记录时间戳,确保 FPS 计算准确。Jank threshold / 卡顿阈值: A frame is janky when its total duration (build + raster) exceeds the per-frame budget — 1000ms ÷ refreshRate (≈16.7ms at 60Hz, ≈8.3ms at 120Hz). Since v1.11.0 the budget is derived from the device's refresh rate, so high-refresh (120Hz) screens are held to a tight ≈8.3ms instead of being forgiven by a fixed 16ms.卡顿阈值:帧的总耗时(build + raster)超过逐帧预算即视为卡顿——1000ms ÷ 刷新率(60Hz≈16.7ms、120Hz≈8.3ms)。自 v1.11.0 起预算按设备刷新率换算,高刷(120Hz)屏会被严格约束在 ≈8.3ms,而非被固定 16ms 放过。Phased timing / 分阶段耗时: Each FrameRecord carries buildDurationUs and rasterDurationUs (GPU raster); the janky-frame list shows the build / raster split per row.分阶段耗时:每条 FrameRecord 都带有 buildDurationUs 与 rasterDurationUs(GPU 光栅化),掉帧列表项会展示 build / raster 分项。","programmatic-control-optional--编程式控制可选#Programmatic Control (Optional) / 编程式控制(可选)":"If you need to start/stop monitoring from code (e.g., for automated testing), use FpsService:如需从代码控制监控(如自动化测试),使用 FpsService:\n// Start / Stop FPS monitoring / 开始 / 停止 FPS 监控\nFpsService.instance.start();\nFpsService.instance.stop();\n// Read current stats / 读取当前统计\nfinal fps = FpsService.instance.currentFps;\nfinal jankRate = FpsService.instance.jankRate;\nfinal totalFrames = FpsService.instance.totalFrameCount;\nfinal jankyFrames = FpsService.instance.totalJankyCount;\n// Clear historical data / 清空历史数据\nFpsService.instance.clear();\n// Listen to updates / 监听更新\nFpsService.instance.addListener(() {\n // Update your own UI / 更新你自己的 UI\n});\n// Historical data access / 历史数据访问\nfinal history = FpsService.instance.fpsHistory; // List, 60 entries / 60 条\nfinal records = FpsService.instance.frameRecords; // List, unmodifiable / 不可变,最多 3600 条","fpsservice-api--fpsservice-接口#FpsService API / FpsService 接口":"Singleton service extending ChangeNotifier.继承 ChangeNotifier 的单例服务。\nMethod\tDescription\tstart()\tStart FPS monitoring / 开始 FPS 监控\tstop()\tStop FPS monitoring / 停止 FPS 监控\tclear()\tClear all historical data and counters / 清空所有历史数据和计数器\t\nProperty\tType\tDescription\tisRunning\tbool\tWhether monitoring is currently active / 是否正在监控\tcurrentFps\tdouble\tCurrent FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)\tjankRate\tdouble\tJank rate as percentage / 卡顿率(百分比)\ttotalFrameCount\tint\tTotal frames captured / 总帧数\ttotalJankyCount\tint\tTotal janky frames captured (per-frame budget) / 总卡顿帧数(按逐帧预算)\tlastFrameJanky\tbool\tWhether the most recent frame was janky / 最近一帧是否卡顿\tfpsHistory\tList\tRecent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)\tframeRecords\tList\tRecent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)","performance-considerations--性能说明#Performance Considerations / 性能说明":"Off by default / 默认关闭: Zero overhead when disabled / 关闭时零开销\nLightweight callbacks / 轻量回调: Only processes frame timing data, no heavy computation / 仅处理帧时序数据,无重计算\nBounded history / 有界历史: Trend chart keeps only 60 points, frame records capped at 3600 / 趋势图仅保留 60 个点,帧记录上限 3600 条\nSafe with other features / 与其他功能兼容: Can run alongside Memory Viewer monitoring / 可与 Memory Viewer 监控同时运行","platform-support--平台支持#Platform Support / 平台支持":"Feature\tAndroid\tiOS\tFPS Monitoring\t✅\t✅\tJank Detection\t✅\t✅\tTrend Chart\t✅\t✅\t\nFPS monitoring uses Flutter engine APIs and works on Android and iOS (the two platforms this plugin targets).FPS 监控使用 Flutter 引擎 API,在本插件支持的两个平台 Android 与 iOS 上均可用。","demo--演示#Demo / 演示":"The Example App includes an FPS demo module with three scenarios:Example App 包含 FPS 演示模块,提供三种场景:\nDemo\tDescription\tTrigger Jank\tBlocks the main thread for 100-500ms to simulate jank / 阻塞主线程 100-500ms 模拟卡顿\tHeavy Animations\t80 simultaneously rotating+scaling widgets to intentionally trigger jank / 80 个同时旋转+缩放的 widget 故意触发卡顿\tSmooth Animation\tSingle lightweight compound animation, demonstrates stable 60 FPS / 单个轻量复合动画,演示稳定 60 FPS","related--相关#Related / 相关":"Usage — General usage guide / 通用使用指南\nFAQ — Common questions / 常见问题\nConfiguration — Configuration options / 配置选项"}},"/Getting-Started":{"title":"Getting Started / 快速开始","data":{"quick-start--快速开始#Quick Start / 快速开始":"Integrate with just 1 line of code:仅需 1 行代码 即可完成集成:\nimport 'package:flutter/material.dart';\nimport 'package:zero_inspector_kit/zero_inspector_kit.dart';\nvoid main() {\n // Single line: Initialize inspector, capture print() via Zone, and display floating button\n // 一行代码:初始化检查器、通过 Zone 捕获 print()、自动显示悬浮按钮\n ZeroInspectorKit.runAppWithInspector(const MyApp());\n}\nclass MyApp extends StatelessWidget {\n const MyApp({super.key});\n @override\n Widget build(BuildContext context) {\n return MaterialApp(\n home: Scaffold(\n appBar: AppBar(title: const Text('App')),\n body: const Center(child: Text('Hello World')),\n ),\n );\n }\n}","what-happens-automatically--自动完成的工作#What Happens Automatically / 自动完成的工作":"After integration, the inspector automatically does the following without modifying any other project code:集成后,检查器会自动完成以下工作,无需修改项目其他代码:\nFeature\tDescription\t✅ Log Capture\tAuto-capture all print(), debugPrint() via Zone / 通过 Zone 自动捕获所有日志\t✅ Network Interception\tAuto-intercept all http and Dio requests via HttpOverrides / 自动拦截所有网络请求\t✅ Error Capture\tAggregate Flutter framework errors & unhandled exceptions, deduped by type + stack (Errors tab) / 聚合 Flutter 框架异常与未捕获异常,按类型+堆栈去重\t✅ Session Persistence\tFlush logs/network/errors to a disk ring buffer; logs & errors replay on next launch / 日志/网络/异常落盘环形缓冲,启动时回放日志与异常\t✅ Database Scan\tAuto-scan and register SQLite databases / 自动扫描注册数据库\t✅ Floating Button\tAuto-displayed via Overlay, not affected by keyboard / 通过 Overlay 自动显示\t✅ Route Tracking\tAuto-inject InspectorRouteObserver into MaterialApp / 自动注入路由观察者\t\nNote / 说明: The Memory and FPS panels are available as tabs but are off by default to avoid performance overhead. Toggle the switch at the top of each panel to start collecting data. See Memory Viewer and FPS Viewer for details.Memory 和 FPS 面板作为标签页可用,但默认关闭以避免性能开销。在各自面板顶部打开开关才会开始采集数据。详见 Memory Viewer 和 FPS Viewer。","production-build--生产构建#Production Build / 生产构建":"The inspector is automatically disabled in release mode. You don't need to remove any code — Flutter's tree-shaking will remove all inspector-related code from production builds.检查器在 release 模式下会自动禁用,无需移除任何代码,Flutter 的 tree-shaking 会自动移除所有检查器相关代码。","alternative-integration--替代集成方式#Alternative Integration / 替代集成方式":"If you prefer more control, use the two-line approach:如果需要更多控制权,可以使用两行代码方式:\nvoid main() {\n ZeroInspectorKit.init();\n runApp(ZeroInspectorKit.wrapApp(const MyApp()));\n}","next-steps--下一步#Next Steps / 下一步":"Installation — Detailed installation methods / 详细安装方式\nUsage — Full usage guide / 完整使用指南\nConfiguration — Configuration options / 配置选项"}},"/Installation":{"title":"Installation / 安装","data":{"from-pubdev-recommended--从-pubdev-安装推荐#From pub.dev (Recommended) / 从 pub.dev 安装(推荐)":"Add the following to your pubspec.yaml:在 pubspec.yaml 中添加以下依赖:\ndependencies:\n zero_inspector_kit: ^1.11.0\nThen run:然后运行:\nflutter pub get","from-github--从-github-安装#From GitHub / 从 GitHub 安装":"Alternatively, install from GitHub:或者从 GitHub 安装:\ndependencies:\n zero_inspector_kit:\n git:\n url: https://github.com/zero-labsco/zero_inspector_kit.git\n ref: release/v1.11.0","platform-setup--平台配置#Platform Setup / 平台配置":"","android#Android":"No additional configuration needed.无需额外配置。","ios#iOS":"No additional configuration needed.无需额外配置。","import--导入#Import / 导入":"import 'package:zero_inspector_kit/zero_inspector_kit.dart';","requirements--环境要求#Requirements / 环境要求":"Requirement\tVersion\tFlutter\t>= 3.3.0\tDart SDK\t>= 3.11.0 < 4.0.0","next-steps--下一步#Next Steps / 下一步":"Getting Started — Quick start guide / 快速开始\nUsage — Full usage guide / 完整使用指南"}},"/Log-Viewer":{"title":"Log Viewer / 日志查看器","data":{"overview--概述#Overview / 概述":"The Log Viewer automatically captures logs from multiple sources with zero configuration.日志查看器自动从多个来源捕获日志,无需配置。","log-sources--日志来源#Log Sources / 日志来源":"Source\tCapture Method\tprint()\tZone specification override / Zone 规范覆盖\tdebugPrint()\tdebugPrint override / debugPrint 覆盖\tFlutter errors\tFlutterError.onError hook / 接管 FlutterError.onError\tUnhandled exceptions\trunZonedGuarded / runZonedGuarded 捕获\tThird-party libraries\tVia print() capture / 通过 print() 捕获\t\nSince v1.9.0, Flutter framework errors and unhandled exceptions are also aggregated & deduplicated in the dedicated Errors tab (keyed by type + stack signature) — while this Log Viewer keeps showing them as error-level lines in the chronological stream. Use Errors for \"is this crash repeating?\"; use Log Viewer for the raw timeline.自 v1.9.0 起,Flutter 框架异常与未捕获异常会同时进入独立的 Errors 标签页去重聚合(按类型 + 堆栈签名归并);本 Log Viewer 仍会在原始时间流中把它们显示为错误级日志。回答\"该崩溃是否反复出现\"请用 Errors;查看原始时间线请用 Log Viewer。","log-levels--日志级别#Log Levels / 日志级别":"Level\tAbbreviation\tColor\tDescription\tVerbose\tV\tGray\tDetailed information / 详细信息\tDebug\tD\tBlue\tDebug information / 调试信息\tInfo\tI\tGreen\tGeneral information / 一般信息\tWarning\tW\tOrange\tWarning messages / 警告信息\tError\tE\tRed\tError messages / 错误信息\t\nThird-party library logs are categorized as Info level.\n第三方日志库的日志统一归类为 Info 级别。","ui-features--ui-功能#UI Features / UI 功能":"","filter-bar--过滤栏#Filter Bar / 过滤栏":"All: Show all logs / 使用 All 显示所有日志\nV / D / I / W / E: Filter by level / 按级别过滤\nTag dropdown: Filter by any captured tag / 按任意已捕获标签过滤\nSingle-select mode / 单选模式","toolbar--工具栏#Toolbar / 工具栏":"Auto-scroll toggle (default on): Jump to the newest log as new entries arrive; pause to keep history still for inspection / 自动滚动开关(默认开启):新日志到达时自动跳到最新;暂停可稳定查看历史\nCopy as JSON: Copy all currently filtered logs as JSON / 复制为 JSON:将当前过滤后的全部日志复制为 JSON\nShare as Text: Share filtered logs as plain text / 分享为文本:将过滤后的日志以纯文本分享\nClear: Clear all logs / 清除:清空全部日志","log-list--日志列表#Log List / 日志列表":"Level badge with color / 带颜色的级别徽章\nTimestamp display / 时间戳显示\nTag display (if available) / 标签显示(如有)\nError/warning rows have subtle background tint / 错误/警告行有淡色背景\nLeft border color indicates level / 左侧边框颜色表示级别\nTap a row to open the in-view detail page with full message and copy; use the back button to return / 点击行进入详情页(主视图内切换),可查看完整消息并复制,点返回按钮回到列表\nPer-row copy button copies that single log as JSON / 行内复制按钮将该条日志以 JSON 复制","search--搜索#Search / 搜索":"Fuzzy search by message content or tag / 按消息内容或标签模糊搜索\nToggle regex mode (. * button) to search with RegExp (case-insensitive); invalid patterns degrade gracefully instead of crashing / 点击 正则模式(. * 按钮)使用 RegExp 搜索(不区分大小写);非法图案优雅降级,不会崩溃\nCombined with level and tag filters / 可与级别、标签过滤组合使用","manual-logging--手动记录日志#Manual Logging / 手动记录日志":"Quick shorthand (recommended) / 简化写法(推荐) — available since v1.1.2 / v1.1.2 起可用:\nInspectorLog.v('Verbose message / 详细消息');\nInspectorLog.d('Debug message / 调试消息');\nInspectorLog.i('Info message / 信息消息');\nInspectorLog.w('Warning message / 警告消息');\nInspectorLog.e('Error message / 错误消息');\n// With tag / 带标签\nInspectorLog.i('User logged in', tag: 'Auth');\nFull form / 完整写法:\nInspectorLogInterceptor.instance.verbose('Verbose message / 详细消息');\nInspectorLogInterceptor.instance.debug('Debug message / 调试消息');\nInspectorLogInterceptor.instance.info('Info message / 信息消息');\nInspectorLogInterceptor.instance.warning('Warning message / 警告消息');\nInspectorLogInterceptor.instance.error('Error message / 错误消息');\n// With tag / 带标签\nInspectorLogInterceptor.instance.info('User logged in', tag: 'Auth');\nInspectorLog is a static wrapper around InspectorLogInterceptor.instance for shorter log calls.InspectorLog 是 InspectorLogInterceptor.instance 的静态包装,用于更简短的日志调用。","third-party-library-integration--第三方日志库集成#Third-Party Library Integration / 第三方日志库集成":"","auto-capture-inbound--自动捕获入站#Auto-Capture (Inbound) / 自动捕获(入站)":"No configuration needed. Any library using print() or debugPrint() is automatically captured.无需配置。任何使用 print() 或 debugPrint() 的库都会被自动捕获。","bidirectional-sync-optional--双向同步可选#Bidirectional Sync (Optional) / 双向同步(可选)":"To forward inspector logs to your third-party logger:将检查器日志转发到第三方日志库:\nimport 'package:logger/logger.dart';\nfinal logger = Logger();\nInspectorLogInterceptor.instance.onLogCaptured = (entry) {\n logger.log(\n _mapLogLevel(entry.level),\n '${entry.tag != null ? '[${entry.tag}] ' : ''}${entry.message}',\n );\n};\nNote: Do NOT call logging methods inside onLogCaptured, as this will cause infinite recursion.\n注意:不要在 onLogCaptured 内部调用日志方法,否则会导致无限递归。","starting-the-log-interceptor--启动日志拦截器#Starting the Log Interceptor / 启动日志拦截器":"If using runAppWithInspector(), the log interceptor starts automatically. Otherwise:如果使用 runAppWithInspector(),日志拦截器会自动启动。否则:\nInspectorLogInterceptor.instance.start();"}},"/Memory-Viewer":{"title":"Memory Viewer / 内存监控面板","data":{"":"The Memory Viewer provides comprehensive in-app memory analysis, including trend charts, Dart Heap details, Native memory breakdown, memory leak detection, image cache monitoring, and storage statistics.内存监控面板提供应用内全面内存分析,包括趋势图、Dart Heap 详情、Native 内存分项、内存泄漏检测、图片缓存监控和存储统计。\nAvailable since v1.1.0v1.1.1 起可用","overview--概览#Overview / 概览":"The Memory Viewer is integrated into the inspector panel. Tap the floating inspector button → \"Memory\" tab to access.内存监控面板集成在检查器面板中。点击悬浮检查器按钮 → \"Memory\" 标签即可访问。⚠️ Important: Memory monitoring is off by default. You must toggle on the switch at the top of the panel to start collecting data.⚠️ 重要:内存监控默认关闭。必须在面板顶部打开开关才会开始采集数据。","master-switch--总开关#Master Switch / 总开关":"A switch at the top of the Memory panel controls whether monitoring is enabled.Memory 面板顶部有一个开关,控制是否启用监控。\nState\tBehavior\tOFF (default)\tNo timers, no VM Service connection, zero overhead / 无定时器、无 VM Service 连接、零开销\tON\tStarts RSS collection (500ms), Native memory (3s), storage stats (3s), leak detection (2s), and VM Service connection attempt / 启动 RSS 采集(500ms)、Native 内存(3s)、存储统计(3s)、泄漏检测(2s),并尝试连接 VM Service\t\nWhen turned off, all timers are cancelled and the VM Service connection state is cleared, leaving no residual WebSocket overhead.关闭时所有定时器会被取消,VM Service 连接状态会被清空,不会残留 WebSocket 开销。","features--功能#Features / 功能":"","1-memory-trend-chart--内存趋势图#1. Memory Trend Chart / 内存趋势图":"Real-time line chart with 2-minute history window (240 snapshots × 500ms)\nSwitchable between 4 metrics via the metric selector chips:\nProcess RSS — Process-level RSS (always available)\nDart Heap — Total Dart Heap usage (requires VM Service)\nNew Space — New generation usage (requires VM Service)\nOld Space — Old generation usage (requires VM Service)\n实时折线图,2 分钟历史窗口(240 条快照 × 500ms)。可通过指标选择芯片切换 4 种指标。","2-native-memory--native-内存真机-100-可用#2. Native Memory / Native 内存(真机 100% 可用)":"Does not depend on VM Service. Data is collected via Platform Channel:不依赖 VM Service,通过 Platform Channel 采集:Android (Debug.MemoryInfo + /proc/self/status):\nTotal PSS / Dalvik PSS / Native PSS / Native Private Dirty\nProcess RSS (from /proc/self/status)\nDevice memory status (availMem, threshold, lowMemory)\niOS (mach task_info):\nPhysical Footprint (Apple's recommended metric)\nInternal / Compressed / Resident Size\nProcess RSS\nDevice available memory","3-dart-heap-overview--dart-heap-概览需要-vm-service#3. Dart Heap Overview / Dart Heap 概览(需要 VM Service)":"Heap Usage / Capacity / external usage\nProgress bar showing heap usage ratio\nThree core metrics with bilingual labels\n显示 Heap Usage / Capacity / External 三个核心指标 + 进度条。","4-heap-generations--堆分代详情需要-vm-service#4. Heap Generations / 堆分代详情(需要 VM Service)":"New Space: usage / capacity / external\nOld Space: usage / capacity / external\nHelps identify allocation patterns (frequent new-space churn vs old-space growth)\n显示新生代/老生代的 Usage / Capacity / External 详细数据。","5-manual-gc--手动触发-gc需要-vm-service#5. Manual GC / 手动触发 GC(需要 VM Service)":"\"Trigger GC\" button forces a full garbage collection\nDisabled (grayed out) when VM Service is unavailable\n\"Trigger GC\" 按钮强制触发完整垃圾回收。VM Service 不可用时按钮变灰禁用。","6-memory-leak-detection--内存泄漏检测#6. Memory Leak Detection / 内存泄漏检测":"Based on Dart 2.17+ WeakReference and Finalizer. Does NOT depend on VM Service.基于 Dart 2.17+ 的 WeakReference 和 Finalizer,不依赖 VM Service。Four-state transition / 四状态流转:\nState\tMeaning\ttracking\tObject registered, waiting for expected release time / 对象已注册,等待预期释放时间\tverifying\tExceeded expected release time, GC verification triggered (if VM Service available) / 超过预期释放时间,触发 GC 验证(VM Service 可用时)\tleaked\tObject still exists after GC — suspected leak / GC 后对象仍存在——疑似泄漏\treleased\tObject has been garbage collected / 对象已被垃圾回收\t\nAPI / 接口:Quick shorthand (recommended) / 简化写法(推荐) — available since v1.1.2 / v1.1.2 起可用:\n// Extension method on Object / Object 上的扩展方法\nmyBloc.trackMemoryLeak(tag: 'HomePage_myBloc');\n// Or top-level function / 或使用顶层函数\ntrackMemoryLeak(myBloc, tag: 'HomePage_myBloc');\n// Cancel tracking / 取消追踪\nmyBloc.untrackMemoryLeak();\nFull form / 完整写法:\n// Register an object for leak tracking / 注册对象进行泄漏追踪\nMemoryInspectorService.instance.trackObject(\n myController,\n tag: 'HomeController_textController', // optional identifier\n expectedReleaseAfter: const Duration(seconds: 30), // expected release time\n);\n// Stop tracking a specific object / 停止追踪特定对象\nMemoryInspectorService.instance.untrackObject(myController);\n// Clear all records / 清空所有记录\nMemoryInspectorService.instance.clearLeakRecords();\nLimits / 限制:\nMax 500 tracked objects (LRU eviction)\nDetection interval: 2 seconds\ntrackObject() requires user code modification (mild invasion)\nFlutter MemoryAllocations bridge (since v1.9.0) / 官方泄漏追踪桥接(v1.9.0 起)The WeakReference state machine has a blind spot: an object whose dispose() ran but that has not been GC'd yet still resolves through the weak reference, producing a false \"leaked\" verdict. Since v1.9.0, MemoryInspectorService also subscribes to Flutter's official FlutterMemoryAllocations (the data source behind leak_tracker) via LeakTrackerBridge: once the official stream reports disposed, the object is treated as released (awaiting GC) even if the weak reference still resolves — cutting false positives. Enabled by default through enableFlutterLeakTracker in init(), and toggled in code via MemoryInspectorService.instance.flutterLeakTrackerEnabled.WeakReference 状态机有一个盲点:对象已调用 dispose() 但尚未被 GC 时弱引用仍存活,会产生\"疑似泄漏\"的误报。v1.9.0 起,MemoryInspectorService 通过 LeakTrackerBridge 额外订阅 Flutter 官方的 FlutterMemoryAllocations(leak_tracker 背后的数据源):只要官方上报 disposed,即便弱引用仍存活也判定为已释放(等待 GC),从而显著降低误报。由 init() 的 enableFlutterLeakTracker 默认启用,也可用 MemoryInspectorService.instance.flutterLeakTrackerEnabled 在代码中开关。","7-image-cache--图片缓存#7. Image Cache / 图片缓存":"Real-time Flutter image cache size and count\nPending (loading) / Live (in use) image counts\nOne-click clear all image cache\n实时显示 Flutter 图片缓存大小、数量、加载中/使用中状态。一键清理图片缓存。","8-app-storage--应用存储#8. App Storage / 应用存储":"Documents directory size\nTemp cache directory size\nTotal database file size\nOne-click clear temp cache\n显示文档目录、临时缓存、数据库文件大小。一键清理临时缓存。","️-vm-service-availability--vm-service-可用性#⚠️ VM Service Availability / VM Service 可用性":"Dart Heap data and manual GC require VM Service connection. When VM Service is unavailable, these features gracefully degrade to show \"N/A\".Dart Heap 数据和手动 GC 需要 VM Service 连接。 VM Service 不可用时,这些功能优雅降级显示 \"N/A\"。","when-debugging-via-pc-with-flutter-run--通过-pc-用-flutter-run-调试时#When debugging via PC with flutter run / 通过 PC 用 flutter run 调试时":"Dart VM Heap data may be unavailable (VM: OFF).Dart VM Heap 数据可能不可用(VM: OFF)。Reason / 原因: When using flutter run to debug via PC, the flutter tool sets up port forwarding between PC and device via adb reverse, allowing PC-side DevTools to access the device's VM Service. However, Service.getInfo() returns a serverUri from the PC's perspective; when the app process internally accesses 127.0.0.1:PC_port, the device doesn't have that port listening locally, resulting in Connection refused and VM Service showing OFF.原因:使用 flutter run 连接 PC 调试时,flutter tool 会通过 adb reverse 在 PC 和设备之间做端口转发,让 PC 上的 DevTools 能访问设备的 VM Service。但应用进程内部 Service.getInfo() 返回的 serverUri 是 PC 视角的端口,应用进程访问 127.0.0.1:PC端口 时设备本地并没有监听该端口,导致 Connection refused,VM Service 显示 OFF。","when-opening-debug-app-directly-no-pc--直接打开-debug-应用不连-pc#When opening debug app directly (no PC) / 直接打开 debug 应用(不连 PC)":"VM Service works normally. No flutter tool is involved, VM Service listens directly on the device's local port, the app can connect normally, and Dart Heap data displays correctly.VM Service 正常工作。 没有 flutter tool 介入,VM Service 直接监听设备本地端口,应用能正常连接,Dart Heap 数据正常显示。","fallback--降级方案#Fallback / 降级方案":"When VM Service is unavailable:\n✅ Native memory (Android PSS / iOS physicalFootprint) — still available\n✅ Process RSS — always available\n✅ Image cache / storage stats — still available\n✅ Leak detection — still available (doesn't depend on VM Service)\n❌ Dart Heap details — shows N/A\n❌ Manual GC — button disabled","platform-support--平台支持#Platform Support / 平台支持":"Feature\tAndroid\tiOS\tProcess RSS\t✅\t✅\tNative Memory\t✅\t✅\tDart Heap (VM Service)\t✅ (no PC)\t✅ (no PC)\tManual GC\t✅ (no PC)\t✅ (no PC)\tLeak Detection\t✅\t✅\tImage Cache\t✅\t✅\tStorage Stats\t✅\t✅","refresh-intervals--刷新间隔#Refresh Intervals / 刷新间隔":"Data Source\tInterval\tProcess RSS / Dart Heap\t500ms\tNative Memory (Android/iOS)\t3000ms\tStorage Stats\t3000ms\tLeak Detection\t2000ms","related--相关#Related / 相关":"Usage — General usage guide\nFAQ — Common questions\nConfiguration — Configuration options"}},"/Network-Inspector":{"title":"Network Inspector / 网络检查器","data":{"overview--概述#Overview / 概述":"The Network Inspector automatically captures all HTTP requests made via the http package and Dio, with zero configuration needed.网络检查器自动捕获所有通过 http 包 和 Dio 发送的 HTTP 请求,无需任何配置。","how-it-works--工作原理#How It Works / 工作原理":"The inspector uses Flutter's HttpOverrides to intercept all HTTP traffic at the dart:io level. This means:检查器通过 Flutter 的 HttpOverrides 在 dart:io 层面拦截所有 HTTP 流量。这意味着:\nhttp package: Auto-captured ✅ / 自动捕获 ✅\nDio: Auto-captured (uses IOHttpClientAdapter → HttpClient) ✅ / 自动捕获 ✅\nNo manual interceptor setup needed / 无需手动添加拦截器","websocket--grpc-capture--websocket-与-grpc-抓取#WebSocket & gRPC Capture / WebSocket 与 gRPC 抓取":"Available since v1.7.0 (opt-in, off by default)v1.7.0 起可用(可选开启,默认关闭)\nHTTP/HTTPS traffic is captured automatically, but the HttpOverrides interceptor does not cover streaming protocols like WebSocket and gRPC. For those, enable the opt-in capture so frames and calls show up as WS / gRPC rows in the Network tab.HTTP/HTTPS 流量会自动捕获,但 HttpOverrides 拦截器无法覆盖 WebSocket、gRPC 这类流式协议。针对它们需开启可选的抓取,开启后收发帧/调用会以 WS / gRPC 行的形式出现在 Network 标签页。","enable--开启方式#Enable / 开启方式":"Toggle the WS switch in the Network tab toolbar, or / 在 Network 标签页工具栏点击 WS 开关,或\nSet it programmatically: / 通过代码开启:\n// on / 开启\nWsInspectorService.instance.enable();\n// off / 关闭\nWsInspectorService.instance.disable();\nCapture is off by default and only records while enabled — apps that don't use these protocols pay nothing.抓取默认关闭,且只在开启时记录;不使用这类协议的应用零开销。","two-usage-modes--两种使用方式#Two Usage Modes / 两种使用方式":"1. Transparent wrapper — InspectorWebSocketReplace WebSocket.connect with InspectorWebSocket.connect. Frames are auto-recorded (→ out, ← in) when capture is on; when off it passes through with zero overhead.将 WebSocket.connect 替换为 InspectorWebSocket.connect。开启抓取时会自动记录收发帧(→ 出站、← 入站);关闭时零开销透传。\nfinal ws = await InspectorWebSocket.connect('wss://echo.websocket.events');\nws.listen((msg) => print('received: $msg'));\nws.add('hello'); // recorded as an outgoing frame / 记录为出站帧\n2. Manual hook — recordCallFor stacks not transparently interceptable by dart:io (gRPC, web_socket_channel, custom protocols), call recordCall to log a request/response pair. No-ops when capture is disabled.对于无法被 dart:io 透明拦截的栈(gRPC、web_socket_channel、自定义协议),调用 recordCall 记录一次请求/响应。关闭抓取时为空操作。\nWsInspectorService.instance.recordCall(\n name: 'user.UserService/GetUser',\n request: '{ \"id\": 1 }',\n response: '{ \"name\": \"Ada\" }',\n protocol: 'gRPC', // appears in the method column / 显示在 method 列\n);","what-you-see--查看方式#What You See / 查看方式":"Network tab lists a WS (or gRPC) entry per connection/call / Network 标签页按连接/调用列出 WS(或 gRPC)记录\nOpen the detail view to see the frame log (outgoing → / incoming ←), accumulated in the response body / 进入详情页查看帧日志(出站 → / 入站 ←),累积显示在响应体中\nA [connection closed] marker is appended when the socket closes / 连接关闭后会追加 [connection closed] 标记","captured-information--捕获的信息#Captured Information / 捕获的信息":"Field\tDescription\tMethod\tGET, POST, PUT, DELETE, PATCH\tURL\tFull request URL\tStatus Code\tHTTP response status code\tDuration\tRequest duration\tRequest Headers\tAll request headers\tRequest Body\tRequest payload (JSON formatted)\tResponse Body\tResponse payload (JSON formatted)\tHost\tParsed from URL","ui-features--ui-功能#UI Features / UI 功能":"","request-list--请求列表#Request List / 请求列表":"Color-coded by HTTP method / 按 HTTP 方法着色\nStatus code badge / 状态码徽章\nDuration display / 耗时显示\nLeft border color indicates status / 左侧边框颜色表示状态","request-detail--请求详情#Request Detail / 请求详情":"Click a request to enter detail view / 点击请求进入详情视图\nBack button to return to list / 返回按钮返回列表\nRequest and response sections / 请求和响应分段显示\nJSON formatted body / JSON 格式化显示","search--搜索#Search / 搜索":"Fuzzy search by URL or method / 按 URL 或方法模糊搜索\nSearch bar hidden in detail view / 详情视图隐藏搜索栏","batch-operations--批量操作#Batch Operations / 批量操作":"Available since v1.3.0v1.3.0 起可用\nThe request list supports a selection mode for operating on multiple requests at once.请求列表支持选择模式,可一次性操作多条请求。\nTap the selection icon in the toolbar to enter selection mode / 点击工具栏的选择图标进入选择模式\nCheck/uncheck items; a top batch bar shows Select all / Cancel / 勾选/取消勾选;顶部批量条提供全选 / 取消\nBatch \"Copy as cURL\": copies all selected requests as cURL commands / 批量「Copy as cURL」:将所有选中请求复制为 cURL 命令\nBatch delete: removes selected requests from the list / 批量删除:从列表中移除选中请求","export--sensitive-field-masking--导出与敏感字段遮蔽#Export & Sensitive-field Masking / 导出与敏感字段遮蔽":"Available since v1.3.0v1.3.0 起可用\nThe toolbar includes an eye toggle that controls whether sensitive headers are masked on export.工具栏包含眼睛开关,控制导出时是否遮蔽敏感请求头。\nDefault: off (fully visible) — cURL / JSON / HAR reflect the real request verbatim / 默认关闭(完整可见):cURL / JSON / HAR 原样反映真实请求\nOn: toCurl / netToJson / netToHar / copyNet / exportNetToFile mask these headers / 开启后:以下请求头被遮蔽:\nAuthorization, Cookie, Set-Cookie, Proxy-Authorization, X-Auth-Token, X-CSRF-Token, X-XSRF-Token\nThe detail page \"Copy as cURL\" / \"Copy as JSON\" follow the same toggle / 详情页「Copy as cURL」「Copy as JSON」跟随同一开关\nWhen masking is active, the snackbar notes (sensitive hidden) / 遮蔽开启时,snackbar 提示 (sensitive hidden)\nTip: keep masking on before pasting cURL/JSON into terminals, chat, or issue trackers to avoid leaking credentials.提示:将 cURL/JSON 粘贴到终端、聊天或 issue 前,建议开启遮蔽,避免凭据泄露。","copy-as-curl--复制为-curl#Copy as cURL / 复制为 cURL":"Available since v1.3.0v1.3.0 起可用\nFrom the request detail view, tap Copy as cURL to copy the request as a ready-to-run cURL command (method, URL, headers, body). Respects the sensitive-field masking toggle above.在请求详情页点击 Copy as cURL,即可将请求复制为可直接运行的 cURL 命令(方法、URL、请求头、请求体),并遵循上方的敏感字段遮蔽开关。","replay-editor--重放编辑器#Replay Editor / 重放编辑器":"Available since v1.11.0v1.11.0 起可用\nEvery request detail view has a replay action (↻ icon). It opens an editable sheet that lets you re-issue the captured request in-app and preview the response — handy for quickly re-running an endpoint without leaving the app.每个请求详情页都有重放操作(↻ 图标)。它会打开一个可编辑弹层,让你在 App 内重新发出该请求并预览响应——无需离开应用即可快速复跑某个接口。\nOnly the URL query parameters are editable — add / edit / remove key=value rows; the URL is rebuilt from them on send / 仅 URL 查询参数可编辑——可增删改 key=value 行,发送时用其重建 URL\nURL, request headers and request body are read-only by design (the captured values are re-sent verbatim) / URL、请求头与请求体按设计只读(以捕获到的原值原样重发)\nOn Send, the request is re-issued with the original method / headers / body and the edited query string; the sheet shows the returned status code, elapsed time, and a response preview / 点击 Send 后用原始方法 / 请求头 / 请求体加上修改后的查询串重新发出,弹层展示返回状态码、耗时与响应预览\nThis is distinct from the interceptor (which builds a durable modification rule applied to future matching requests). The replay editor makes a one-off call with editable query params — including GET requests, whose params can't be changed by the interceptor.它与拦截器不同(拦截器生成作用于后续匹配请求的持久规则)。重放编辑器发起的是一次性调用,仅查询参数可改——包括 GET 请求(其参数拦截器无法修改)。","status-code-colors--状态码颜色#Status Code Colors / 状态码颜色":"Range\tColor\tDescription\t2xx\tGreen\tSuccess / 成功\t3xx\tBlue\tRedirect / 重定向\t4xx\tOrange\tClient error / 客户端错误\t5xx\tRed\tServer error / 服务器错误","http-method-colors--http-方法颜色#HTTP Method Colors / HTTP 方法颜色":"Method\tColor\tGET\tBlue / 蓝色\tPOST\tGreen / 绿色\tPUT\tOrange / 橙色\tDELETE\tRed / 红色\tPATCH\tPurple / 紫色","usage-example--使用示例#Usage Example / 使用示例":"// http package - auto-captured / http 包 - 自动捕获\nfinal response = await http.get(\n Uri.parse('https://api.example.com/data'),\n);\n// Dio - auto-captured / Dio - 自动捕获\nfinal response = await dio.post(\n 'https://api.example.com/data',\n data: {'key': 'value'},\n);\nNo additional setup required! All requests will appear in the Network tab.无需额外配置!所有请求都会出现在 Network 标签页中。","request-interceptor--请求拦截修改#Request Interceptor / 请求拦截修改":"Available since v1.0.7 (response fields locked to read-only since v1.0.8)v1.0.7 起可用(v1.0.8 起响应字段锁定为只读)\nThe inspector supports intercepting and modifying network requests via rules. This is useful for testing different request parameters without modifying app code.检查器支持通过规则拦截并修改网络请求,适合在不修改应用代码的情况下测试不同的请求参数。","workflow--工作流程#Workflow / 工作流程":"Send a request normally (it will be captured in the Network panel) / 正常发送请求(会被捕获到 Network 面板)\nOpen the request detail and tap the Interceptor icon / 打开请求详情,点击拦截器图标\nConfigure the modification rule (URL pattern, HTTP method, request modifications) / 配置修改规则(URL 模式、HTTP 方法、请求修改)\nSave the rule — subsequent matching requests will use the modified parameters / 保存规则——后续匹配的请求将使用修改后的参数","supported-modifications--支持的修改#Supported Modifications / 支持的修改":"Field\tEditable\tNotes\tRequest Body\t✅\tOnly for requests with body (POST, PUT, PATCH, etc.) / 仅适用于有 body 的请求\tRequest Headers\t✅\tAdd / modify / remove headers / 新增 / 修改 / 删除请求头\tURL\t❌\tGrayed out, read-only / 灰色不可编辑\tResponse Status Code\t❌\tRead-only since v1.0.8 / v1.0.8 起只读\tResponse Body\t❌\tRead-only since v1.0.8 / v1.0.8 起只读\t\nThe interception edit panel only allows modifying the request body and request headers. Response fields (status code, response body) are grayed out and uneditable.拦截编辑面板仅允许修改请求体和请求头。响应字段(状态码、响应体)灰色不可编辑。","rule-matching--规则匹配#Rule Matching / 规则匹配":"URL pattern matching: exact match or regex / URL 模式匹配:精确匹配或正则匹配\nHTTP method filtering: GET, POST, PUT, DELETE, PATCH, HEAD, or Any / HTTP 方法过滤","why-get-requests-cannot-be-modified--为什么-get-请求不能修改#Why GET Requests Cannot Be Modified / 为什么 GET 请求不能修改":"The interceptor currently supports modifying request body and headers only / 拦截器目前仅支持修改请求体和请求头\nGET requests don't have a request body / GET 请求没有请求体\nModifying GET request parameters would require URL modification / 修改 GET 请求参数需要修改 URL\nURL modification may cause unexpected issues with request routing and parameter encoding / 修改 URL 可能导致请求路由和参数编码的意外问题\nGET request detail pages do not display interception edit buttons, making them unmodifiable.GET 请求详情页不显示拦截编辑按钮,因此无法修改。","master-toggle--拦截总开关#Master Toggle / 拦截总开关":"The interception master toggle is displayed only on the Network list page, not in the detail page / 拦截总开关只显示在 Network 列表页,不在详情页\nRules are only applied when modification mode is enabled / 规则仅在启用修改模式时生效\nWhen no rules are configured or rules are disabled, all requests are sent normally without any modification / 未配置规则或禁用规则时,所有请求正常发送,不做任何修改","rule-management--规则管理#Rule Management / 规则管理":"The network panel includes an interceptor rule editor where you can:网络面板包含拦截规则编辑器,可以:\nCreate / edit / delete rules / 创建 / 编辑 / 删除规则\nEnable / disable individual rules / 启用 / 禁用单条规则\nView rule status indicators in the request list / 在请求列表中查看规则状态标识"}},"/Route-Tracker":{"title":"Route Tracker / 路由追踪","data":{"overview--概述#Overview / 概述":"The Route Tracker monitors navigation history, recording all route push, pop, and replacement events.路由追踪器监控导航历史,记录所有路由 push、pop 和替换事件。","auto-injection--自动注入#Auto-Injection / 自动注入":"When using runAppWithInspector() or wrapApp(), the InspectorRouteObserver is automatically injected into MaterialApp's navigatorObservers.使用 runAppWithInspector() 或 wrapApp() 时,InspectorRouteObserver 会自动注入到 MaterialApp 的 navigatorObservers 中。","tracked-route-actions--追踪的路由操作#Tracked Route Actions / 追踪的路由操作":"Action\tColor\tDescription\tpush\tBlue\tNavigator.push() / 推入新路由\tpushNamed\tBlue\tNavigator.pushNamed() / 按名称推入\tpop\tOrange\tNavigator.pop() / 弹出路由\tpopUntil\tOrange\tNavigator.popUntil() / 弹出直到\tpushReplacement\tPurple\tNavigator.pushReplacement() / 替换路由","ui-features--ui-功能#UI Features / UI 功能":"","route-list--路由列表#Route List / 路由列表":"Action badge with color / 带颜色的操作徽章\nRoute name display / 路由名称显示\nTimestamp display / 时间戳显示\nLeft border color indicates action type / 左侧边框颜色表示操作类型","route-detail--路由详情#Route Detail / 路由详情":"Click a route to view details / 点击路由查看详情\nShows: Action, Route Name, Timestamp, Arguments / 显示:操作、路由名、时间戳、参数\nArguments displayed as formatted JSON / 参数以 JSON 格式显示","manual-setup-optional--手动设置可选#Manual Setup (Optional) / 手动设置(可选)":"If you use a custom Navigator or don't use MaterialApp, add the observer manually:如果使用自定义 Navigator 或不使用 MaterialApp,请手动添加观察者:\nMaterialApp(\n navigatorObservers: [InspectorRouteObserver()],\n home: MyHomePage(),\n)"}},"/":{"title":"Zero Inspector Kit","data":{"":"A powerful Flutter plugin for in-app developer console, providing real-time debugging tools including network request inspection, logging, error aggregation, database viewing, and route tracking.一个功能强大的 Flutter 插件,提供应用内开发者控制台,包括网络请求检查、日志记录、异常聚合、数据库查看和路由追踪。","-features--功能特性#✨ Features / 功能特性":"Feature\tDescription\tZero Invasion\tIntegrate with 1 line of code / 一行代码集成\tNetwork Inspector\tReal-time HTTP request viewing (http + Dio) / 实时网络请求查看\tLog Viewer\tAuto-capture print()/debugPrint() and third-party logs / 自动捕获日志\tErrors\tAggregate & dedupe crashes by type + stack, with count and first/last seen / 按类型+堆栈去重聚合崩溃,记录次数与首末次时间\tDatabase Viewer\tSQLite inspection with table data / 数据库查看\tMemory Viewer\tTrend chart, Dart Heap, Native memory, leak detection (incl. Flutter MemoryAllocations bridge) / 内存趋势图、Dart Heap、Native 内存、泄漏检测(含官方 MemoryAllocations 桥接)\tFPS Monitor\tReal-time FPS, jank rate, trend chart / 实时 FPS、卡顿率、趋势图\tRoute Tracker\tNavigation history tracking / 路由追踪\tSession Persistence\tLogs/network/errors survive restarts via a disk ring buffer; logs & errors replay on launch; one-tap session archive export / 日志/网络/异常通过磁盘环形缓冲跨重启保留,启动时回放日志与异常;一键导出会话存档\tAlerts\tRule-based alerts (network/log/memory/FPS) with unread badge / 基于规则的告警(网络/日志/内存/FPS)与未读角标\tSensitive Masking & cURL\tMask secrets on export; one-click cURL copy; batch ops / 导出遮蔽敏感字段、一键复制 cURL、批量操作\tFuzzy Search\tSearch in all viewers / 各查看器模糊搜索\tCross-platform\tAndroid, iOS / 跨平台支持","-table-of-contents--目录#📚 Table of Contents / 目录":"Page\tDescription\tGetting Started\tQuick start guide / 快速开始\tInstallation\tHow to install / 安装方式\tUsage\tDetailed usage / 详细使用\tNetwork Inspector\tNetwork request viewing / 网络检查器\tLog Viewer\tLog capturing and viewing / 日志查看器\tErrors\tAggregated error viewing / 异常聚合查看\tDatabase Viewer\tDatabase inspection / 数据库查看器\tRoute Tracker\tRoute tracking / 路由追踪\tMemory Viewer\tMemory monitoring & leak detection / 内存监控与泄漏检测\tFPS Viewer\tFPS monitoring & jank detection / FPS 监控与卡顿检测\tAlerts\tRule-based alerting / 基于规则的告警\tConfiguration\tConfiguration options / 配置说明\tCustom Database Provider\tExtend database support / 自定义数据库提供者\tFAQ\tFrequently asked questions / 常见问题","-links--链接#🔗 Links / 链接":"GitHub\nOfficial Website\npub.dev","-license--许可证#📄 License / 许可证":"This project is licensed under the GNU General Public License v3.0.本项目采用 GNU General Public License v3.0 许可证。This plugin is provided \"as is\", without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.本插件按\"原样\"提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。"}},"/Usage":{"title":"Usage / 使用指南","data":{"integration-methods--集成方式#Integration Methods / 集成方式":"","1-one-line-integration-recommended--一行代码集成推荐#1. One-Line Integration (Recommended) / 一行代码集成(推荐)":"void main() {\n ZeroInspectorKit.runAppWithInspector(const MyApp());\n}\nThis method:\nAuto-initializes inspector / 自动初始化检查器\nCaptures print() via Zone / 通过 Zone 捕获 print()\nDisplays floating button via Overlay / 通过 Overlay 显示悬浮按钮\nAuto-injects route observer / 自动注入路由观察者","2-two-line-integration--两行代码集成#2. Two-Line Integration / 两行代码集成":"void main() {\n ZeroInspectorKit.init();\n runApp(ZeroInspectorKit.wrapApp(const MyApp()));\n}","inspector-panel--检查器面板#Inspector Panel / 检查器面板":"The inspector panel contains 9 tabs (as of v1.9.0):检查器面板包含 9 个标签页(v1.9.0 起):\nTab\tIcon\tFeature\tNetwork\t🌐\tHTTP request viewing + interceptor rules / 网络请求查看 + 拦截修改\tLogs\t📝\tLog viewing with level filter / 日志查看\tErrors\t🚨\tAggregated & deduped crash viewing / 去重聚合的异常查看\tDatabase\t💾\tDatabase and table inspection / 数据库查看\tMemory\t📊\tMemory trend, Dart Heap, Native memory, leak detection / 内存趋势、Dart Heap、Native 内存、泄漏检测\tFPS\t🎯\tReal-time FPS, jank rate, trend chart / 实时 FPS、卡顿率、趋势图\tRoutes\t🧭\tRoute navigation tracking / 路由追踪\tWidgets\t🔍\tWidget tree snapshot for the current route / 当前路由的 Widget 树快照\tAlerts\t🔔\tRule-based alerts with unread badge / 基于规则的告警与未读角标\t\nThe Memory and FPS monitors are off by default to avoid performance overhead. Toggle the switch at the top of each panel to start collecting data.Memory 与 FPS 监控默认关闭以避免性能开销。在各自面板顶部打开开关才会开始采集数据。","one-click-bug-report--一键-bug-报告#One-Click Bug Report / 一键 Bug 报告":"Tap the bug icon in the inspector panel header to generate and share a bug report in one tap — ideal for QA to attach environment context when filing issues.点击检查器面板头部的虫子图标,即可一键生成并分享一份 Bug 报告——非常适合 QA 在提 issue 时附上环境上下文。The shared text snapshot includes / 分享的文本快照包含:\nDevice / 设备: real model (e.g. Pixel 8 Pro / iPhone (iPhone16,1)), OS & version, locale, Dart runtime, CPU cores.\nMemory / 内存: current heap usage and whether Native memory is supported.\nRecent logs / 最近日志: the latest captured log entries.\nRecent network / 最近网络: the latest captured requests.\nSensitive headers are masked the same way as in the Network tab. No data leaves the device except through the share target you choose.敏感请求头会与网络标签页一样被遮蔽。除你选择的分享目标外,数据不会离开设备。\nRequires no extra setup — it works as soon as the inspector is running. / 无需额外配置——检查器运行后即可使用。","floating-button--悬浮按钮#Floating Button / 悬浮按钮":"The floating button appears after 1 second delay / 悬浮按钮延迟 1 秒出现\nDrag to move it along the screen edge / 拖动 可沿屏幕边缘移动\nTap (when fully visible) to open/close the inspector panel / 点击(完全可见时)打开/关闭检查器面板\nButton auto-snaps to the nearest screen edge / 按钮自动吸附到最近的屏幕边缘\nBreathing animation when idle / 空闲时有呼吸动画","edge-docking-since-v120--边缘吸附v120-起#Edge Docking (since v1.2.0) / 边缘吸附(v1.2.0 起)":"When released near a screen edge, the button auto-docks and tucks into the edge, leaving only a 24px peek visible:拖动松手后,按钮会自动吸附到最近边缘并\"收入\"边缘,仅露出 24px 小弧边:\nState\tBehavior / 行为\tDocked (tucked in)\tOnly 24px peek visible; icon becomes a directional chevron (left dock → ➡, right dock → ⬅) hinting at tap-to-pull-out / 仅露出 24px;图标变为方向箭头提示可点击拉出\tTap docked peek\tSmoothly pulls out to fully visible (panel NOT opened, avoids accidental open) / 平滑拉出到完全可见(不打开面板,避免误触)\tTap fully visible\tOpens the inspector panel / 打开检查器面板\t\nThis design avoids conflicts with system back gestures (Android/iOS edge swipe to go back) when pulling out from the docked state.此设计避免了从吸附态拖出时与系统返回手势(Android/iOS 边缘右滑退出)的冲突。","search--搜索#Search / 搜索":"The main viewers support fuzzy search:各查看器均支持模糊搜索:\nViewer\tSearch Scope\tNetwork\tURL, HTTP method / URL、请求方法\tLogs\tMessage, tag / 消息、标签\tErrors\tException type, message / 异常类型、消息\tDatabase (global)\tDatabase name, table name / 数据库名、表名\tDatabase (in-database)\tTable name, all column data / 表名、所有列数据","manual-logging-optional--手动记录日志可选#Manual Logging (Optional) / 手动记录日志(可选)":"The inspector auto-captures print() output. You can also use manual log methods for precise level control:检查器会自动捕获 print() 输出。也可以使用手动日志方法进行精确级别控制:Quick shorthand (recommended) / 简化写法(推荐):\nInspectorLog.v('Verbose log / 详细日志');\nInspectorLog.d('Debug log / 调试日志');\nInspectorLog.i('Info log / 信息日志');\nInspectorLog.w('Warning log / 警告日志');\nInspectorLog.e('Error log / 错误日志');\nFull form / 完整写法:\nInspectorLogInterceptor.instance.verbose('Verbose log / 详细日志');\nInspectorLogInterceptor.instance.debug('Debug log / 调试日志');\nInspectorLogInterceptor.instance.info('Info log / 信息日志');\nInspectorLogInterceptor.instance.warning('Warning log / 警告日志');\nInspectorLogInterceptor.instance.error('Error log / 错误日志');","third-party-log-integration--第三方日志库集成#Third-Party Log Integration / 第三方日志库集成":"No configuration needed! The plugin automatically captures logs from third-party logging libraries (e.g., logger, flutter_logger) that use print() or debugPrint().无需配置! 插件会自动捕获所有使用 print() 的第三方日志库的日志。These logs are categorized as INFO level.这些日志统一归类为 INFO 级别。","bidirectional-sync-optional--双向同步可选#Bidirectional Sync (Optional) / 双向同步(可选)":"To sync inspector-captured logs to your third-party logger:将检查器捕获的日志同步到第三方日志库:\nInspectorLogInterceptor.instance.onLogCaptured = (entry) {\n yourLogger.log(entry.message);\n};","feature-pages--功能详情#Feature Pages / 功能详情":"Network Inspector — Network request details + interceptor rules / 网络检查器详情 + 拦截修改\nLog Viewer — Log viewing details / 日志查看器详情\nErrors — Aggregated error viewing / 异常聚合查看\nDatabase Viewer — Database inspection details / 数据库查看器详情\nRoute Tracker — Route tracking details / 路由追踪详情\nMemory Viewer — Memory monitoring & leak detection / 内存监控与泄漏检测\nFPS Viewer — FPS monitoring & jank detection / FPS 监控与卡顿检测","session-persistence--会话持久化#Session Persistence / 会话持久化":"Logs, network requests, and aggregated errors are asynchronously flushed to a local SQLite ring buffer. On the next launch, logs and aggregated errors replay into their tabs; network requests stay archived on disk for later export. Data therefore survives app restarts even if the inspector panel was never opened. Tap the storage icon in the panel header to open the Persisted data manager: view row counts per category, export the full session archive, or clear the disk. See Configuration (PersistenceService section) for the API and tuning parameters.日志、网络请求与聚合异常会被异步写入本地 SQLite 环形缓冲。下次启动时,日志与聚合异常会回放入各自标签页;网络请求保留在磁盘存档,供之后导出。因此即使从未打开过检查器面板,数据也能跨重启保留。点击面板头部的存储图标可打开 Persisted data 管理弹层:查看各类别行数、导出完整会话存档或清空磁盘。API 与调参详见 Configuration(PersistenceService 一节)。"}}} \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/Alerts-303580b721158a8d.js b/docs/_next/static/chunks/pages/Alerts-a0f5430c00bb2cc2.js similarity index 62% rename from docs/_next/static/chunks/pages/Alerts-303580b721158a8d.js rename to docs/_next/static/chunks/pages/Alerts-a0f5430c00bb2cc2.js index d5733e7..f04cd76 100644 --- a/docs/_next/static/chunks/pages/Alerts-303580b721158a8d.js +++ b/docs/_next/static/chunks/pages/Alerts-a0f5430c00bb2cc2.js @@ -1 +1 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[713],{6399:function(e,t,r){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Alerts",function(){return r(4571)}])},4571:function(e,t,r){"use strict";r.r(t),r.d(t,{useTOC:function(){return l}});var n=r(5893),i=r(7812),s=r(7080),a=r(8925);function l(e){return[{value:"Overview / 概述",id:"overview--概述",depth:2},{value:"How It Works / 工作原理",id:"how-it-works--工作原理",depth:2},{value:"Rule Types / 规则类型",id:"rule-types--规则类型",depth:2},{value:"UI Features / UI 功能",id:"ui-features--ui-功能",depth:2},{value:"Floating Button Badge / 悬浮球角标",id:"floating-button-badge--悬浮球角标",depth:3},{value:"Alerts Tab / Alerts 标签页",id:"alerts-tab--alerts-标签页",depth:3},{value:"Notes / 备注",id:"notes--备注",depth:2}]}t.default=(0,i.c)(function(e){let{toc:t=l(e)}=e,r={blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(r.h1,{children:"Alerts / 告警系统"}),"\n",(0,n.jsx)(r.h2,{id:t[0].id,children:t[0].value}),"\n",(0,n.jsxs)(r.blockquote,{children:["\n",(0,n.jsx)(r.p,{children:(0,n.jsx)(r.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(r.p,{children:(0,n.jsx)(r.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsx)(r.p,{children:"The alert system lets you define rules that proactively surface problems across network requests, logs, memory, and FPS — without manually scanning the panels."}),"\n",(0,n.jsx)(r.p,{children:"告警系统允许你定义规则,主动暴露网络请求、日志、内存、FPS 方面的问题,无需手动逐个面板排查。"}),"\n",(0,n.jsx)(r.h2,{id:t[1].id,children:t[1].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsxs)(r.li,{children:["Rules are evaluated by ",(0,n.jsx)(r.code,{children:"AlertService"})," whenever a relevant event occurs / 每当相关事件发生时,",(0,n.jsx)(r.code,{children:"AlertService"})," 会评估规则"]}),"\n",(0,n.jsxs)(r.li,{children:["Matched rules produce alerts, and the unread count is exposed via a ",(0,n.jsx)(r.code,{children:"ValueNotifier"})," / 命中的规则会生成告警,未读数通过 ",(0,n.jsx)(r.code,{children:"ValueNotifier"})," 暴露"]}),"\n",(0,n.jsx)(r.li,{children:"The floating button shows an unread-count badge; opening the panel clears it / 悬浮球显示未读数量角标,打开面板即清零"}),"\n",(0,n.jsxs)(r.li,{children:["An ",(0,n.jsx)(r.strong,{children:"Alerts"})," tab in the inspector panel lists all triggered alerts / 检查器面板的 ",(0,n.jsx)(r.strong,{children:"Alerts"})," 标签页列出所有已触发的告警"]}),"\n"]}),"\n",(0,n.jsx)(r.h2,{id:t[2].id,children:t[2].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(r.table,{children:[(0,n.jsx)(r.thead,{children:(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.th,{children:"Type"}),(0,n.jsx)(r.th,{children:"Trigger / 触发条件"}),(0,n.jsx)(r.th,{children:"Example / 示例"})]})}),(0,n.jsxs)(r.tbody,{children:[(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"Network"})}),(0,n.jsx)(r.td,{children:"Response status code / 响应状态码"}),(0,n.jsx)(r.td,{children:"Alert when status ≥ 400 / 状态码 ≥ 400 时告警"})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"Log"})}),(0,n.jsx)(r.td,{children:"Log level or message / 日志级别或内容"}),(0,n.jsx)(r.td,{children:"Alert on error logs / 出现 error 日志时告警"})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"Memory"})}),(0,n.jsx)(r.td,{children:"Dart heap / Native memory threshold / 内存阈值"}),(0,n.jsx)(r.td,{children:"Alert when Dart heap > 100 MB / Dart 堆 > 100 MB 时告警"})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"FPS"})}),(0,n.jsx)(r.td,{children:"Frame rate / 帧率"}),(0,n.jsx)(r.td,{children:"Alert when FPS < 50 / FPS < 50 时告警"})]})]})]}),"\n",(0,n.jsx)(r.h2,{id:t[3].id,children:t[3].value}),"\n",(0,n.jsx)(r.h3,{id:t[4].id,children:t[4].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsx)(r.li,{children:"A red badge shows the current unread alert count (clamped 0–99) / 红色角标显示当前未读告警数(0–99)"}),"\n",(0,n.jsx)(r.li,{children:"The number is drawn inside the button so it is never clipped at screen edges / 数字直接绘制在按钮内部,吸附屏幕边缘时不会被裁切"}),"\n",(0,n.jsx)(r.li,{children:"Opening the inspector panel clears the unread count / 打开检查器面板即清零未读数"}),"\n"]}),"\n",(0,n.jsx)(r.h3,{id:t[5].id,children:t[5].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsx)(r.li,{children:"Lists triggered alerts with type, condition, and timestamp / 列出已触发告警的类型、条件与时间"}),"\n",(0,n.jsx)(r.li,{children:"Tap an alert to jump to the related panel (network / log / memory / FPS) / 点击告警可跳转到相关面板(网络 / 日志 / 内存 / FPS)"}),"\n"]}),"\n",(0,n.jsx)(r.h2,{id:t[6].id,children:t[6].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsx)(r.li,{children:"Alerts are evaluated in-process and shown in the developer console only; they do not send data off-device / 告警仅在本机开发者控制台内评估与展示,不会将数据发送到设备外"}),"\n",(0,n.jsx)(r.li,{children:"The alert system is tree-shaken out in release builds, like the rest of the inspector / 与检查器其余部分一样,告警系统在 release 构建中被 tree-shake 移除"}),"\n"]})]})},"/Alerts",{filePath:"pages/Alerts.md",timestamp:1786656379e3,pageMap:s.v,frontMatter:{},title:"Alerts / 告警系统"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,t,r){"use strict";r.d(t,{v:function(){return n}});let n=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=6399)}),_N_E=e.O()}]); \ No newline at end of file +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[713],{6399:function(e,t,r){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Alerts",function(){return r(4571)}])},4571:function(e,t,r){"use strict";r.r(t),r.d(t,{useTOC:function(){return l}});var n=r(5893),s=r(7812),i=r(7080),a=r(8925);function l(e){return[{value:"Overview / 概述",id:"overview--概述",depth:2},{value:"How It Works / 工作原理",id:"how-it-works--工作原理",depth:2},{value:"Rule Types / 规则类型",id:"rule-types--规则类型",depth:2},{value:"UI Features / UI 功能",id:"ui-features--ui-功能",depth:2},{value:"Floating Button Badge / 悬浮球角标",id:"floating-button-badge--悬浮球角标",depth:3},{value:"Alerts Tab / Alerts 标签页",id:"alerts-tab--alerts-标签页",depth:3},{value:"Notes / 备注",id:"notes--备注",depth:2}]}t.default=(0,s.c)(function(e){let{toc:t=l(e)}=e,r={blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(r.h1,{children:"Alerts / 告警系统"}),"\n",(0,n.jsx)(r.h2,{id:t[0].id,children:t[0].value}),"\n",(0,n.jsxs)(r.blockquote,{children:["\n",(0,n.jsx)(r.p,{children:(0,n.jsx)(r.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(r.p,{children:(0,n.jsx)(r.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsx)(r.p,{children:"The alert system lets you define rules that proactively surface problems across network requests, logs, memory, and FPS — without manually scanning the panels."}),"\n",(0,n.jsx)(r.p,{children:"告警系统允许你定义规则,主动暴露网络请求、日志、内存、FPS 方面的问题,无需手动逐个面板排查。"}),"\n",(0,n.jsx)(r.h2,{id:t[1].id,children:t[1].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsxs)(r.li,{children:["Rules are evaluated by ",(0,n.jsx)(r.code,{children:"AlertService"})," whenever a relevant event occurs / 每当相关事件发生时,",(0,n.jsx)(r.code,{children:"AlertService"})," 会评估规则"]}),"\n",(0,n.jsxs)(r.li,{children:["Matched rules produce alerts, and the unread count is exposed via a ",(0,n.jsx)(r.code,{children:"ValueNotifier"})," / 命中的规则会生成告警,未读数通过 ",(0,n.jsx)(r.code,{children:"ValueNotifier"})," 暴露"]}),"\n",(0,n.jsx)(r.li,{children:"The floating button shows an unread-count badge; opening the panel clears it / 悬浮球显示未读数量角标,打开面板即清零"}),"\n",(0,n.jsxs)(r.li,{children:["An ",(0,n.jsx)(r.strong,{children:"Alerts"})," tab in the inspector panel lists all triggered alerts / 检查器面板的 ",(0,n.jsx)(r.strong,{children:"Alerts"})," 标签页列出所有已触发的告警"]}),"\n"]}),"\n",(0,n.jsx)(r.h2,{id:t[2].id,children:t[2].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(r.table,{children:[(0,n.jsx)(r.thead,{children:(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.th,{children:"Type"}),(0,n.jsx)(r.th,{children:"Trigger / 触发条件"}),(0,n.jsx)(r.th,{children:"Example / 示例"})]})}),(0,n.jsxs)(r.tbody,{children:[(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"Network"})}),(0,n.jsx)(r.td,{children:"Response status code / 响应状态码"}),(0,n.jsx)(r.td,{children:"Alert when status ≥ 400 / 状态码 ≥ 400 时告警"})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"Log"})}),(0,n.jsx)(r.td,{children:"Log level or message / 日志级别或内容"}),(0,n.jsx)(r.td,{children:"Alert on error logs / 出现 error 日志时告警"})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"Memory"})}),(0,n.jsx)(r.td,{children:"Dart heap / Native memory threshold / 内存阈值"}),(0,n.jsx)(r.td,{children:"Alert when Dart heap > 100 MB / Dart 堆 > 100 MB 时告警"})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.strong,{children:"FPS"})}),(0,n.jsx)(r.td,{children:"Frame rate / 帧率"}),(0,n.jsx)(r.td,{children:"Alert when FPS < 50 / FPS < 50 时告警"})]})]})]}),"\n",(0,n.jsx)(r.h2,{id:t[3].id,children:t[3].value}),"\n",(0,n.jsx)(r.h3,{id:t[4].id,children:t[4].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsx)(r.li,{children:"A red badge shows the current unread alert count (clamped 0–99) / 红色角标显示当前未读告警数(0–99)"}),"\n",(0,n.jsx)(r.li,{children:"The number is drawn inside the button so it is never clipped at screen edges / 数字直接绘制在按钮内部,吸附屏幕边缘时不会被裁切"}),"\n",(0,n.jsx)(r.li,{children:"Opening the inspector panel clears the unread count / 打开检查器面板即清零未读数"}),"\n"]}),"\n",(0,n.jsx)(r.h3,{id:t[5].id,children:t[5].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsx)(r.li,{children:"Lists triggered alerts with type, condition, and timestamp / 列出已触发告警的类型、条件与时间"}),"\n",(0,n.jsx)(r.li,{children:"Tap an alert to jump to the related panel (network / log / memory / FPS) / 点击告警可跳转到相关面板(网络 / 日志 / 内存 / FPS)"}),"\n"]}),"\n",(0,n.jsx)(r.h2,{id:t[6].id,children:t[6].value}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsx)(r.li,{children:"Alerts are evaluated in-process and shown in the developer console only; they do not send data off-device / 告警仅在本机开发者控制台内评估与展示,不会将数据发送到设备外"}),"\n",(0,n.jsxs)(r.li,{children:[(0,n.jsx)(r.strong,{children:"Persisted to disk (since v1.11.0)"}),": triggered alerts are flushed to the SQLite ring buffer and replay into the Alerts tab after an app restart, and are included in the session archive export / ",(0,n.jsx)(r.strong,{children:"已落盘持久化(v1.11.0 起)"}),":已触发的告警会写入 SQLite 环形缓冲,重启后回放至 Alerts 标签页,并包含在会话存档导出中"]}),"\n",(0,n.jsx)(r.li,{children:"The alert system is tree-shaken out in release builds, like the rest of the inspector / 与检查器其余部分一样,告警系统在 release 构建中被 tree-shake 移除"}),"\n"]})]})},"/Alerts",{filePath:"pages/Alerts.md",timestamp:1786656379e3,pageMap:i.v,frontMatter:{},title:"Alerts / 告警系统"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,t,r){"use strict";r.d(t,{v:function(){return n}});let n=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=6399)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/Errors-7cf1ee698701533d.js b/docs/_next/static/chunks/pages/Errors-7cf1ee698701533d.js new file mode 100644 index 0000000..6b94f99 --- /dev/null +++ b/docs/_next/static/chunks/pages/Errors-7cf1ee698701533d.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[302],{7031:function(e,r,s){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Errors",function(){return s(1824)}])},1824:function(e,r,s){"use strict";s.r(r),s.d(r,{useTOC:function(){return l}});var i=s(5893),n=s(7812),t=s(7080),d=s(8925);function l(e){return[{value:"Overview / 概述",id:"overview--概述",depth:2},{value:"What Gets Captured / 捕获来源",id:"what-gets-captured--捕获来源",depth:2},{value:"Deduplication / 去重规则",id:"deduplication--去重规则",depth:2},{value:"UI Features / UI 功能",id:"ui-features--ui-功能",depth:2},{value:"Errors Tab / Errors 标签页",id:"errors-tab--errors-标签页",depth:3},{value:"Tab Badge / 标签页红点",id:"tab-badge--标签页红点",depth:3},{value:"API / 接口",id:"api--接口",depth:2},{value:"ErrorRecord / 异常记录",id:"errorrecord--异常记录",depth:3},{value:"Session Persistence / 会话持久化",id:"session-persistence--会话持久化",depth:2},{value:"Enable / Disable / 开关",id:"enable--disable--开关",depth:2},{value:"Related / 相关",id:"related--相关",depth:2}]}r.default=(0,n.c)(function(e){let{toc:r=l(e)}=e,s={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.h1,{children:"Errors / 异常聚合查看器"}),"\n",(0,i.jsx)(s.h2,{id:r[0].id,children:r[0].value}),"\n",(0,i.jsxs)(s.blockquote,{children:["\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Available since v1.9.0"})}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"v1.9.0 起可用"})}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["The Errors tab surfaces “the same crash happening repeatedly”. When the inspector is running, ",(0,i.jsx)(s.code,{children:"ErrorService"})," hooks both ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," and ",(0,i.jsx)(s.code,{children:"PlatformDispatcher.onError"})," (keeping the default red-screen / console behavior for both) and aggregates every exception by ",(0,i.jsx)(s.strong,{children:"type + stack signature"}),". Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces."]}),"\n",(0,i.jsxs)(s.p,{children:["Errors 标签页用来快速发现”同一处崩溃反复出现”的问题。检查器运行时,",(0,i.jsx)(s.code,{children:"ErrorService"})," 会接管 ",(0,i.jsx)(s.code,{children:"FlutterError.onError"}),"(保留默认的红色报错与控制台行为),把每次异常按",(0,i.jsx)(s.strong,{children:"类型 + 堆栈签名"}),"去重聚合:同一处崩溃的多次发生会合并为一条记录,展示累计次数与首末次时间——而不是用成百上千条相同的堆栈刷屏日志。"]}),"\n",(0,i.jsx)(s.h2,{id:r[1].id,children:r[1].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Source / 来源"}),(0,i.jsx)(s.th,{children:"How / 方式"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Flutter framework errors / Flutter 框架异常"}),(0,i.jsxs)(s.td,{children:["Hooks ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," (default handler kept) / 接管 ",(0,i.jsx)(s.code,{children:"FlutterError.onError"}),"(保留默认处理)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Framework-boundary & platform-channel errors / 框架边界外与平台通道异常"}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"PlatformDispatcher.onError"})," (with save/restore) / ",(0,i.jsx)(s.code,{children:"PlatformDispatcher.onError"}),"(接管 + 还原)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Uncaught async errors / 未捕获异步异常"}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"runZonedGuarded"})," inside ",(0,i.jsx)(s.code,{children:"runAppWithInspector()"})," / ",(0,i.jsx)(s.code,{children:"runAppWithInspector()"})," 内部的 ",(0,i.jsx)(s.code,{children:"runZonedGuarded"})]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Manual reports / 手动上报"}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"ErrorService.instance.report(exception, stackTrace)"})," — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道"]})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:["Captured records are also ",(0,i.jsx)(s.strong,{children:"persisted to disk"})," (see ",(0,i.jsx)(s.a,{href:"#session-persistence--%E4%BC%9A%E8%AF%9D%E6%8C%81%E4%B9%85%E5%8C%96",children:"Session Persistence"})," below), so aggregated errors from previous sessions are replayed on the next launch."]}),"\n",(0,i.jsxs)(s.p,{children:["捕获到的记录也会",(0,i.jsx)(s.strong,{children:"落盘持久化"}),"(见下文",(0,i.jsx)(s.a,{href:"#session-persistence--%E4%BC%9A%E8%AF%9D%E6%8C%81%E4%B9%85%E5%8C%96",children:"会话持久化"}),"),下次启动时会回放上次会话的聚合异常。"]}),"\n",(0,i.jsx)(s.h2,{id:r[2].id,children:r[2].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Records are keyed by exception ",(0,i.jsx)(s.strong,{children:"type"})," plus a ",(0,i.jsx)(s.strong,{children:"stack signature"})," (first frames with line/address noise stripped) / 按异常",(0,i.jsx)(s.strong,{children:"类型"})," + ",(0,i.jsx)(s.strong,{children:"堆栈签名"}),"(取前若干帧并去掉行号/地址噪声)去重"]}),"\n",(0,i.jsxs)(s.li,{children:["A new occurrence of an existing signature bumps its ",(0,i.jsx)(s.code,{children:"count"})," and updates ",(0,i.jsx)(s.code,{children:"lastSeen"})," instead of adding a new row / 同签名的再次发生只累加 ",(0,i.jsx)(s.code,{children:"count"})," 并更新 ",(0,i.jsx)(s.code,{children:"lastSeen"}),",不新增行"]}),"\n",(0,i.jsxs)(s.li,{children:["Ring buffer capped at ",(0,i.jsx)(s.strong,{children:"200 aggregated records"})," (oldest dropped) / 环形缓冲上限 ",(0,i.jsx)(s.strong,{children:"200 条"}),"聚合记录(超出丢弃最旧)"]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:r[3].id,children:r[3].value}),"\n",(0,i.jsx)(s.h3,{id:r[4].id,children:r[4].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Lists aggregated errors newest-first, showing the exception ",(0,i.jsx)(s.strong,{children:"type"}),", ",(0,i.jsx)(s.strong,{children:"message"}),", ",(0,i.jsx)(s.code,{children:"first seen"})," / ",(0,i.jsx)(s.code,{children:"last seen"})," time, and a ",(0,i.jsx)(s.strong,{children:"\xd7N"})," badge when the same crash recurred / 按最新在前列出聚合异常,展示异常",(0,i.jsx)(s.strong,{children:"类型"}),"、",(0,i.jsx)(s.strong,{children:"消息"}),"、",(0,i.jsx)(s.code,{children:"首次/末次"}),"出现时间,同崩溃复发时显示 ",(0,i.jsx)(s.strong,{children:"\xd7N"})," 徽章"]}),"\n",(0,i.jsxs)(s.li,{children:["Tap a row to expand the ",(0,i.jsx)(s.strong,{children:"full stack sample"})," (truncated at 8000 chars); tap again to collapse / 点击行展开",(0,i.jsx)(s.strong,{children:"完整堆栈样本"}),"(8000 字符截断);再点收起"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Copy stack"})," per row / 行内",(0,i.jsx)(s.strong,{children:"复制堆栈"})]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Search"})," filters by exception type or message / ",(0,i.jsx)(s.strong,{children:"搜索"}),"按异常类型或消息过滤"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Clear"})," empties the aggregated list / ",(0,i.jsx)(s.strong,{children:"清除"}),"清空聚合列表"]}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:r[5].id,children:r[5].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.strong,{children:"Errors tab icon"})," shows a red count badge while there are aggregated errors (capped at ",(0,i.jsx)(s.code,{children:"99+"}),"), so recurring crashes are visible at a glance / ",(0,i.jsx)(s.strong,{children:"Errors 标签页图标"}),"在有聚合异常时显示红色计数(",(0,i.jsx)(s.code,{children:"99+"})," 封顶),一眼可见反复崩溃"]}),"\n",(0,i.jsxs)(s.li,{children:["Opening the Errors tab is ",(0,i.jsx)(s.strong,{children:"not"})," required to see the badge — it reflects ",(0,i.jsx)(s.code,{children:"ErrorService.instance.errorCount"})," whenever the panel is open / 只要面板打开,标签红点即反映 ",(0,i.jsx)(s.code,{children:"ErrorService.instance.errorCount"}),",无需先进入 Errors 页"]}),"\n"]}),"\n",(0,i.jsxs)(s.blockquote,{children:["\n",(0,i.jsxs)(s.p,{children:["Note: the floating ball itself stays clean — its red count (if any) is the ",(0,i.jsx)(s.strong,{children:"Alerts"})," unread badge (",(0,i.jsx)(s.code,{children:"AlertService"}),"), not error aggregation."]}),"\n",(0,i.jsxs)(s.p,{children:["注意:悬浮球本身保持纯粹——球上的红色数字(如有)来自",(0,i.jsx)(s.strong,{children:"告警"}),"未读数(",(0,i.jsx)(s.code,{children:"AlertService"}),"),与异常聚合无关。"]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:r[6].id,children:r[6].value}),"\n",(0,i.jsxs)(s.p,{children:["The full service is re-exported from the package root — no need to import ",(0,i.jsx)(s.code,{children:"lib/src/"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["完整服务已从包根导出,无需 import ",(0,i.jsx)(s.code,{children:"lib/src/"}),"。"]}),"\n",(0,i.jsx)(s.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,i.jsxs)(s.code,{children:[(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"import"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'package:zero_inspector_kit/zero_inspector_kit.dart'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:";"})]}),"\n",(0,i.jsx)(s.span,{children:" "}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Report an exception manually (gRPC/custom protocol/your own error paths) / 手动上报异常"})}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"ErrorService"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"report"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"(error, stackTrace, "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'myModule'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})]}),"\n",(0,i.jsx)(s.span,{children:" "}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Read the aggregated view (newest first) / 读取聚合视图(最新在前)"})}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" ErrorRecord"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" latest "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" ErrorService"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.errors.first;"})]}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"print"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"type"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" x"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"count"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" — first "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"firstSeen"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" / last "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"lastSeen"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})]}),"\n",(0,i.jsx)(s.span,{children:" "}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Clear all records / 清空所有记录"})}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"ErrorService"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"clear"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]})]})}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Member"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"errors"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"List"})," — aggregated records, newest first / 聚合记录,最新在前"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"errorCount"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"int"})," — number of aggregated records / 聚合记录条数"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"isEnabled"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"bool"})," — capture toggle (programmatic) / 抓取开关(代码控制)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"report(exception, stack, [context])"})}),(0,i.jsx)(s.td,{children:"Manually report an exception / 手动上报异常"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"restore(records)"})}),(0,i.jsx)(s.td,{children:"Restore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"clear()"})}),(0,i.jsx)(s.td,{children:"Clear all aggregated records / 清空全部记录"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"install()"})," / ",(0,i.jsx)(s.code,{children:"uninstall()"})]}),(0,i.jsxs)(s.td,{children:["Hook / restore ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," ",(0,i.jsx)(s.strong,{children:"and"})," ",(0,i.jsx)(s.code,{children:"PlatformDispatcher.onError"})," / 接管 / 还原 ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," 与 ",(0,i.jsx)(s.code,{children:"PlatformDispatcher.onError"})]})]})]})]}),"\n",(0,i.jsx)(s.h3,{id:r[7].id,children:r[7].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Field"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"type"})}),(0,i.jsxs)(s.td,{children:["Exception type name (e.g. ",(0,i.jsx)(s.code,{children:"_TypeError"}),") / 异常类型名"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"message"})}),(0,i.jsx)(s.td,{children:"Exception message / 异常消息"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"count"})}),(0,i.jsx)(s.td,{children:"Total occurrences / 累计出现次数"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"firstSeen"})," / ",(0,i.jsx)(s.code,{children:"lastSeen"})]}),(0,i.jsx)(s.td,{children:"First / last occurrence time / 首次 / 末次出现时间"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"sampleStack"})}),(0,i.jsx)(s.td,{children:"One full stack sample (may be truncated) / 一条完整堆栈样本(可能截断)"})]})]})]}),"\n",(0,i.jsx)(s.h2,{id:r[8].id,children:r[8].value}),"\n",(0,i.jsxs)(s.p,{children:["Errors — together with logs, network requests ",(0,i.jsx)(s.strong,{children:"and alerts"})," — are asynchronously flushed to a local SQLite ",(0,i.jsx)(s.strong,{children:"ring buffer"})," (",(0,i.jsx)(s.code,{children:"zero_inspector_kit.db"}),"). On the next launch, ",(0,i.jsx)(s.strong,{children:"logs, aggregated errors and alerts replay into their tabs"}),", so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the ",(0,i.jsx)(s.strong,{children:"storage icon"})," in the panel header to open the ",(0,i.jsx)(s.strong,{children:"Persisted data"})," manager: see the current row counts, ",(0,i.jsx)(s.strong,{children:"export the full session archive"})," as JSON, or clear the disk. See ",(0,i.jsx)(s.a,{href:"Configuration",children:"Configuration"})," (PersistenceService section) for details and the tuning parameters."]}),"\n",(0,i.jsxs)(s.p,{children:["异常与日志、网络请求、告警一起被异步写入本地 SQLite ",(0,i.jsx)(s.strong,{children:"环形缓冲"}),"(",(0,i.jsx)(s.code,{children:"zero_inspector_kit.db"}),")。",(0,i.jsx)(s.strong,{children:"下次启动时,日志、聚合异常与告警会回放入各自标签页"}),"——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的",(0,i.jsx)(s.strong,{children:"存储图标"}),"可打开 ",(0,i.jsx)(s.strong,{children:"Persisted data"})," 管理弹层:查看当前行数、",(0,i.jsx)(s.strong,{children:"导出完整会话存档"})," JSON,或清空磁盘。详见 ",(0,i.jsx)(s.a,{href:"Configuration",children:"Configuration"}),"(PersistenceService 一节)的参数说明。"]}),"\n",(0,i.jsx)(s.h2,{id:r[9].id,children:r[9].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"enableErrorCapture"})," (default ",(0,i.jsx)(s.code,{children:"true"}),") in ",(0,i.jsx)(s.code,{children:"ZeroInspectorKit.init()"})," controls error aggregation / ",(0,i.jsx)(s.code,{children:"init()"})," 的 ",(0,i.jsx)(s.code,{children:"enableErrorCapture"}),"(默认 ",(0,i.jsx)(s.code,{children:"true"}),")控制异常聚合"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"enablePersistence"})," (default ",(0,i.jsx)(s.code,{children:"true"}),") controls the disk ring buffer / ",(0,i.jsx)(s.code,{children:"enablePersistence"}),"(默认 ",(0,i.jsx)(s.code,{children:"true"}),")控制磁盘环形缓冲"]}),"\n",(0,i.jsxs)(s.li,{children:["Set both to ",(0,i.jsx)(s.code,{children:"false"})," to keep everything in memory only / 两者都设为 ",(0,i.jsx)(s.code,{children:"false"})," 时数据仅保留在内存"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,i.jsxs)(s.code,{children:[(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"ZeroInspectorKit"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"init"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" enableErrorCapture"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" true"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" enablePersistence"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" true"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:", "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// false → memory-only / 关闭后仅内存"})]}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})})]})}),"\n",(0,i.jsx)(s.h2,{id:r[10].id,children:r[10].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.a,{href:"Log-Viewer",children:"Log Viewer"})," — raw log stream including error lines / 原始日志流(含错误行)"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.a,{href:"Configuration",children:"Configuration"})," — init parameters and full service APIs / 初始化参数与完整服务接口"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.a,{href:"Usage",children:"Usage"})," — general usage guide / 使用指南"]}),"\n"]})]})},"/Errors",{filePath:"pages/Errors.md",timestamp:1788758921e3,pageMap:t.v,frontMatter:{},title:"Errors / 异常聚合查看器"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,r,s){"use strict";s.d(r,{v:function(){return i}});let i=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=7031)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/Errors-d6acd286d99365c4.js b/docs/_next/static/chunks/pages/Errors-d6acd286d99365c4.js deleted file mode 100644 index aa016f5..0000000 --- a/docs/_next/static/chunks/pages/Errors-d6acd286d99365c4.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[302],{7031:function(e,r,s){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Errors",function(){return s(1824)}])},1824:function(e,r,s){"use strict";s.r(r),s.d(r,{useTOC:function(){return l}});var i=s(5893),n=s(7812),t=s(7080),d=s(8925);function l(e){return[{value:"Overview / 概述",id:"overview--概述",depth:2},{value:"What Gets Captured / 捕获来源",id:"what-gets-captured--捕获来源",depth:2},{value:"Deduplication / 去重规则",id:"deduplication--去重规则",depth:2},{value:"UI Features / UI 功能",id:"ui-features--ui-功能",depth:2},{value:"Errors Tab / Errors 标签页",id:"errors-tab--errors-标签页",depth:3},{value:"Tab Badge / 标签页红点",id:"tab-badge--标签页红点",depth:3},{value:"API / 接口",id:"api--接口",depth:2},{value:"ErrorRecord / 异常记录",id:"errorrecord--异常记录",depth:3},{value:"Session Persistence / 会话持久化",id:"session-persistence--会话持久化",depth:2},{value:"Enable / Disable / 开关",id:"enable--disable--开关",depth:2},{value:"Related / 相关",id:"related--相关",depth:2}]}r.default=(0,n.c)(function(e){let{toc:r=l(e)}=e,s={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.h1,{children:"Errors / 异常聚合查看器"}),"\n",(0,i.jsx)(s.h2,{id:r[0].id,children:r[0].value}),"\n",(0,i.jsxs)(s.blockquote,{children:["\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Available since v1.9.0"})}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"v1.9.0 起可用"})}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["The Errors tab surfaces “the same crash happening repeatedly”. When the inspector is running, ",(0,i.jsx)(s.code,{children:"ErrorService"})," hooks ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," (keeping the default red-screen / console behavior) and aggregates every exception by ",(0,i.jsx)(s.strong,{children:"type + stack signature"}),". Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces."]}),"\n",(0,i.jsxs)(s.p,{children:["Errors 标签页用来快速发现”同一处崩溃反复出现”的问题。检查器运行时,",(0,i.jsx)(s.code,{children:"ErrorService"})," 会接管 ",(0,i.jsx)(s.code,{children:"FlutterError.onError"}),"(保留默认的红色报错与控制台行为),把每次异常按",(0,i.jsx)(s.strong,{children:"类型 + 堆栈签名"}),"去重聚合:同一处崩溃的多次发生会合并为一条记录,展示累计次数与首末次时间——而不是用成百上千条相同的堆栈刷屏日志。"]}),"\n",(0,i.jsx)(s.h2,{id:r[1].id,children:r[1].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Source / 来源"}),(0,i.jsx)(s.th,{children:"How / 方式"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Flutter framework errors / Flutter 框架异常"}),(0,i.jsxs)(s.td,{children:["Hooks ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," (default handler kept) / 接管 ",(0,i.jsx)(s.code,{children:"FlutterError.onError"}),"(保留默认处理)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Uncaught async errors / 未捕获异步异常"}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"runZonedGuarded"})," inside ",(0,i.jsx)(s.code,{children:"runAppWithInspector()"})," / ",(0,i.jsx)(s.code,{children:"runAppWithInspector()"})," 内部的 ",(0,i.jsx)(s.code,{children:"runZonedGuarded"})]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Manual reports / 手动上报"}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"ErrorService.instance.report(exception, stackTrace)"})," — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道"]})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:["Captured records are also ",(0,i.jsx)(s.strong,{children:"persisted to disk"})," (see ",(0,i.jsx)(s.a,{href:"#session-persistence--%E4%BC%9A%E8%AF%9D%E6%8C%81%E4%B9%85%E5%8C%96",children:"Session Persistence"})," below), so aggregated errors from previous sessions are replayed on the next launch."]}),"\n",(0,i.jsxs)(s.p,{children:["捕获到的记录也会",(0,i.jsx)(s.strong,{children:"落盘持久化"}),"(见下文",(0,i.jsx)(s.a,{href:"#session-persistence--%E4%BC%9A%E8%AF%9D%E6%8C%81%E4%B9%85%E5%8C%96",children:"会话持久化"}),"),下次启动时会回放上次会话的聚合异常。"]}),"\n",(0,i.jsx)(s.h2,{id:r[2].id,children:r[2].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Records are keyed by exception ",(0,i.jsx)(s.strong,{children:"type"})," plus a ",(0,i.jsx)(s.strong,{children:"stack signature"})," (first frames with line/address noise stripped) / 按异常",(0,i.jsx)(s.strong,{children:"类型"})," + ",(0,i.jsx)(s.strong,{children:"堆栈签名"}),"(取前若干帧并去掉行号/地址噪声)去重"]}),"\n",(0,i.jsxs)(s.li,{children:["A new occurrence of an existing signature bumps its ",(0,i.jsx)(s.code,{children:"count"})," and updates ",(0,i.jsx)(s.code,{children:"lastSeen"})," instead of adding a new row / 同签名的再次发生只累加 ",(0,i.jsx)(s.code,{children:"count"})," 并更新 ",(0,i.jsx)(s.code,{children:"lastSeen"}),",不新增行"]}),"\n",(0,i.jsxs)(s.li,{children:["Ring buffer capped at ",(0,i.jsx)(s.strong,{children:"200 aggregated records"})," (oldest dropped) / 环形缓冲上限 ",(0,i.jsx)(s.strong,{children:"200 条"}),"聚合记录(超出丢弃最旧)"]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:r[3].id,children:r[3].value}),"\n",(0,i.jsx)(s.h3,{id:r[4].id,children:r[4].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Lists aggregated errors newest-first, showing the exception ",(0,i.jsx)(s.strong,{children:"type"}),", ",(0,i.jsx)(s.strong,{children:"message"}),", ",(0,i.jsx)(s.code,{children:"first seen"})," / ",(0,i.jsx)(s.code,{children:"last seen"})," time, and a ",(0,i.jsx)(s.strong,{children:"\xd7N"})," badge when the same crash recurred / 按最新在前列出聚合异常,展示异常",(0,i.jsx)(s.strong,{children:"类型"}),"、",(0,i.jsx)(s.strong,{children:"消息"}),"、",(0,i.jsx)(s.code,{children:"首次/末次"}),"出现时间,同崩溃复发时显示 ",(0,i.jsx)(s.strong,{children:"\xd7N"})," 徽章"]}),"\n",(0,i.jsxs)(s.li,{children:["Tap a row to expand the ",(0,i.jsx)(s.strong,{children:"full stack sample"})," (truncated at 8000 chars); tap again to collapse / 点击行展开",(0,i.jsx)(s.strong,{children:"完整堆栈样本"}),"(8000 字符截断);再点收起"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Copy stack"})," per row / 行内",(0,i.jsx)(s.strong,{children:"复制堆栈"})]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Search"})," filters by exception type or message / ",(0,i.jsx)(s.strong,{children:"搜索"}),"按异常类型或消息过滤"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Clear"})," empties the aggregated list / ",(0,i.jsx)(s.strong,{children:"清除"}),"清空聚合列表"]}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:r[5].id,children:r[5].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.strong,{children:"Errors tab icon"})," shows a red count badge while there are aggregated errors (capped at ",(0,i.jsx)(s.code,{children:"99+"}),"), so recurring crashes are visible at a glance / ",(0,i.jsx)(s.strong,{children:"Errors 标签页图标"}),"在有聚合异常时显示红色计数(",(0,i.jsx)(s.code,{children:"99+"})," 封顶),一眼可见反复崩溃"]}),"\n",(0,i.jsxs)(s.li,{children:["Opening the Errors tab is ",(0,i.jsx)(s.strong,{children:"not"})," required to see the badge — it reflects ",(0,i.jsx)(s.code,{children:"ErrorService.instance.errorCount"})," whenever the panel is open / 只要面板打开,标签红点即反映 ",(0,i.jsx)(s.code,{children:"ErrorService.instance.errorCount"}),",无需先进入 Errors 页"]}),"\n"]}),"\n",(0,i.jsxs)(s.blockquote,{children:["\n",(0,i.jsxs)(s.p,{children:["Note: the floating ball itself stays clean — its red count (if any) is the ",(0,i.jsx)(s.strong,{children:"Alerts"})," unread badge (",(0,i.jsx)(s.code,{children:"AlertService"}),"), not error aggregation."]}),"\n",(0,i.jsxs)(s.p,{children:["注意:悬浮球本身保持纯粹——球上的红色数字(如有)来自",(0,i.jsx)(s.strong,{children:"告警"}),"未读数(",(0,i.jsx)(s.code,{children:"AlertService"}),"),与异常聚合无关。"]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:r[6].id,children:r[6].value}),"\n",(0,i.jsxs)(s.p,{children:["The full service is re-exported from the package root — no need to import ",(0,i.jsx)(s.code,{children:"lib/src/"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["完整服务已从包根导出,无需 import ",(0,i.jsx)(s.code,{children:"lib/src/"}),"。"]}),"\n",(0,i.jsx)(s.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,i.jsxs)(s.code,{children:[(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"import"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'package:zero_inspector_kit/zero_inspector_kit.dart'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:";"})]}),"\n",(0,i.jsx)(s.span,{children:" "}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Report an exception manually (gRPC/custom protocol/your own error paths) / 手动上报异常"})}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"ErrorService"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"report"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"(error, stackTrace, "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'myModule'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})]}),"\n",(0,i.jsx)(s.span,{children:" "}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Read the aggregated view (newest first) / 读取聚合视图(最新在前)"})}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" ErrorRecord"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" latest "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" ErrorService"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.errors.first;"})]}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"print"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"type"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" x"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"count"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" — first "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"firstSeen"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" / last "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"${"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"latest"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"lastSeen"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"}"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})]}),"\n",(0,i.jsx)(s.span,{children:" "}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Clear all records / 清空所有记录"})}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"ErrorService"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"clear"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]})]})}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Member"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"errors"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"List"})," — aggregated records, newest first / 聚合记录,最新在前"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"errorCount"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"int"})," — number of aggregated records / 聚合记录条数"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"isEnabled"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"bool"})," — capture toggle (programmatic) / 抓取开关(代码控制)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"report(exception, stack, [context])"})}),(0,i.jsx)(s.td,{children:"Manually report an exception / 手动上报异常"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"restore(records)"})}),(0,i.jsx)(s.td,{children:"Restore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"clear()"})}),(0,i.jsx)(s.td,{children:"Clear all aggregated records / 清空全部记录"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"install()"})," / ",(0,i.jsx)(s.code,{children:"uninstall()"})]}),(0,i.jsxs)(s.td,{children:["Hook / restore ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})," / 接管 / 还原 ",(0,i.jsx)(s.code,{children:"FlutterError.onError"})]})]})]})]}),"\n",(0,i.jsx)(s.h3,{id:r[7].id,children:r[7].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Field"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"type"})}),(0,i.jsxs)(s.td,{children:["Exception type name (e.g. ",(0,i.jsx)(s.code,{children:"_TypeError"}),") / 异常类型名"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"message"})}),(0,i.jsx)(s.td,{children:"Exception message / 异常消息"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"count"})}),(0,i.jsx)(s.td,{children:"Total occurrences / 累计出现次数"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"firstSeen"})," / ",(0,i.jsx)(s.code,{children:"lastSeen"})]}),(0,i.jsx)(s.td,{children:"First / last occurrence time / 首次 / 末次出现时间"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"sampleStack"})}),(0,i.jsx)(s.td,{children:"One full stack sample (may be truncated) / 一条完整堆栈样本(可能截断)"})]})]})]}),"\n",(0,i.jsx)(s.h2,{id:r[8].id,children:r[8].value}),"\n",(0,i.jsxs)(s.p,{children:["Errors — together with logs and network requests — are asynchronously flushed to a local SQLite ",(0,i.jsx)(s.strong,{children:"ring buffer"})," (",(0,i.jsx)(s.code,{children:"zero_inspector_kit.db"}),"). On the next launch, ",(0,i.jsx)(s.strong,{children:"logs and aggregated errors replay into their tabs"}),", so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the ",(0,i.jsx)(s.strong,{children:"storage icon"})," in the panel header to open the ",(0,i.jsx)(s.strong,{children:"Persisted data"})," manager: see the current row counts, ",(0,i.jsx)(s.strong,{children:"export the full session archive"})," as JSON, or clear the disk. See ",(0,i.jsx)(s.a,{href:"Configuration",children:"Configuration"})," (PersistenceService section) for details and the tuning parameters."]}),"\n",(0,i.jsxs)(s.p,{children:["异常与日志、网络请求一起被异步写入本地 SQLite ",(0,i.jsx)(s.strong,{children:"环形缓冲"}),"(",(0,i.jsx)(s.code,{children:"zero_inspector_kit.db"}),")。",(0,i.jsx)(s.strong,{children:"下次启动时,日志与聚合异常会回放入各自标签页"}),"——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的",(0,i.jsx)(s.strong,{children:"存储图标"}),"可打开 ",(0,i.jsx)(s.strong,{children:"Persisted data"})," 管理弹层:查看当前行数、",(0,i.jsx)(s.strong,{children:"导出完整会话存档"})," JSON,或清空磁盘。详见 ",(0,i.jsx)(s.a,{href:"Configuration",children:"Configuration"}),"(PersistenceService 一节)的参数说明。"]}),"\n",(0,i.jsx)(s.h2,{id:r[9].id,children:r[9].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"enableErrorCapture"})," (default ",(0,i.jsx)(s.code,{children:"true"}),") in ",(0,i.jsx)(s.code,{children:"ZeroInspectorKit.init()"})," controls error aggregation / ",(0,i.jsx)(s.code,{children:"init()"})," 的 ",(0,i.jsx)(s.code,{children:"enableErrorCapture"}),"(默认 ",(0,i.jsx)(s.code,{children:"true"}),")控制异常聚合"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"enablePersistence"})," (default ",(0,i.jsx)(s.code,{children:"true"}),") controls the disk ring buffer / ",(0,i.jsx)(s.code,{children:"enablePersistence"}),"(默认 ",(0,i.jsx)(s.code,{children:"true"}),")控制磁盘环形缓冲"]}),"\n",(0,i.jsxs)(s.li,{children:["Set both to ",(0,i.jsx)(s.code,{children:"false"})," to keep everything in memory only / 两者都设为 ",(0,i.jsx)(s.code,{children:"false"})," 时数据仅保留在内存"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,i.jsxs)(s.code,{children:[(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"ZeroInspectorKit"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"."}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"init"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" enableErrorCapture"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" true"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,i.jsxs)(s.span,{children:[(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" enablePersistence"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" true"}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:", "}),(0,i.jsx)(s.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// false → memory-only / 关闭后仅内存"})]}),"\n",(0,i.jsx)(s.span,{children:(0,i.jsx)(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})})]})}),"\n",(0,i.jsx)(s.h2,{id:r[10].id,children:r[10].value}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.a,{href:"Log-Viewer",children:"Log Viewer"})," — raw log stream including error lines / 原始日志流(含错误行)"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.a,{href:"Configuration",children:"Configuration"})," — init parameters and full service APIs / 初始化参数与完整服务接口"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.a,{href:"Usage",children:"Usage"})," — general usage guide / 使用指南"]}),"\n"]})]})},"/Errors",{filePath:"pages/Errors.md",timestamp:1788758921e3,pageMap:t.v,frontMatter:{},title:"Errors / 异常聚合查看器"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,r,s){"use strict";s.d(r,{v:function(){return i}});let i=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=7031)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/FPS-Viewer-93d1796fa44f218a.js b/docs/_next/static/chunks/pages/FPS-Viewer-93d1796fa44f218a.js deleted file mode 100644 index ec8b82a..0000000 --- a/docs/_next/static/chunks/pages/FPS-Viewer-93d1796fa44f218a.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[229],{7839:function(e,i,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/FPS-Viewer",function(){return n(7548)}])},7548:function(e,i,n){"use strict";n.r(i),n.d(i,{useTOC:function(){return l}});var s=n(5893),r=n(7812),t=n(7080),d=n(8925);function l(e){return[{value:"Overview / 概览",id:"overview--概览",depth:2},{value:"Master Switch / 总开关",id:"master-switch--总开关",depth:2},{value:"Features / 功能",id:"features--功能",depth:2},{value:"1. Current Stats / 当前统计",id:"1-current-stats--当前统计",depth:3},{value:"2. FPS Trend Chart / FPS 趋势图",id:"2-fps-trend-chart--fps-趋势图",depth:3},{value:"3. Janky Frame List / 卡顿帧列表",id:"3-janky-frame-list--卡顿帧列表",depth:3},{value:"4. Reset / 重置",id:"4-reset--重置",depth:3},{value:"How It Works / 工作原理",id:"how-it-works--工作原理",depth:2},{value:"Programmatic Control (Optional) / 编程式控制(可选)",id:"programmatic-control-optional--编程式控制可选",depth:2},{value:"FpsService API / FpsService 接口",id:"fpsservice-api--fpsservice-接口",depth:2},{value:"Performance Considerations / 性能说明",id:"performance-considerations--性能说明",depth:2},{value:"Platform Support / 平台支持",id:"platform-support--平台支持",depth:2},{value:"Demo / 演示",id:"demo--演示",depth:2},{value:"Related / 相关",id:"related--相关",depth:2}]}i.default=(0,r.c)(function(e){let{toc:i=l(e)}=e,n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{children:"FPS Viewer / FPS 监控面板"}),"\n",(0,s.jsx)(n.p,{children:"The FPS Viewer provides real-time frame performance analysis, including current FPS, jank rate, frame duration stats, and an FPS trend chart."}),"\n",(0,s.jsx)(n.p,{children:"FPS 监控面板提供实时帧性能分析,包括当前 FPS、卡顿率、帧耗时统计和 FPS 趋势图。"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Available since v1.2.0"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"v1.2.0 起可用"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:i[0].id,children:i[0].value}),"\n",(0,s.jsx)(n.p,{children:"The FPS Viewer is integrated into the inspector panel. Tap the floating inspector button → “FPS” tab to access."}),"\n",(0,s.jsx)(n.p,{children:"FPS 监控面板集成在检查器面板中。点击悬浮检查器按钮 → “FPS” 标签即可访问。"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"⚠️ Important"}),": FPS monitoring is ",(0,s.jsx)(n.strong,{children:"off by default"}),". You must toggle on the switch at the top of the panel to start collecting data."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"⚠️ 重要"}),":FPS 监控",(0,s.jsx)(n.strong,{children:"默认关闭"}),"。必须在面板顶部打开开关才会开始采集数据。"]}),"\n",(0,s.jsx)(n.h2,{id:i[1].id,children:i[1].value}),"\n",(0,s.jsx)(n.p,{children:"A switch at the top of the FPS panel controls whether monitoring is enabled."}),"\n",(0,s.jsx)(n.p,{children:"FPS 面板顶部有一个开关,控制是否启用监控。"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"State"}),(0,s.jsx)(n.th,{children:"Behavior"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"OFF (default)"})}),(0,s.jsx)(n.td,{children:"No frame timings callbacks, no timer, zero overhead / 无帧回调、无定时器、零开销"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"ON"})}),(0,s.jsxs)(n.td,{children:["Starts collecting frame data via ",(0,s.jsx)(n.code,{children:"WidgetsBinding.instance.addTimingsCallback"})," / 通过 ",(0,s.jsx)(n.code,{children:"addTimingsCallback"})," 开始采集帧数据"]})]})]})]}),"\n",(0,s.jsx)(n.p,{children:"When turned off, all callbacks and timers are cancelled, leaving no residual overhead."}),"\n",(0,s.jsx)(n.p,{children:"关闭时所有回调和定时器会被取消,不会残留开销。"}),"\n",(0,s.jsx)(n.h2,{id:i[2].id,children:i[2].value}),"\n",(0,s.jsx)(n.h3,{id:i[3].id,children:i[3].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Current FPS"})," — Updated every 500ms / 每 500ms 更新"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Jank Rate"})," — Percentage of janky frames (>16ms) / 卡顿帧占比(>16ms 视为卡顿)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Total Frame Count"})," — All frames captured since start / 自启动以来的总帧数"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Total Janky Count"})," — All janky frames captured / 自启动以来的总卡顿帧数"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Last Frame Janky"})," — Whether the most recent frame was janky / 最近一帧是否卡顿"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"显示当前 FPS、卡顿率、总帧数、总卡顿帧数、最近一帧是否卡顿。"}),"\n",(0,s.jsx)(n.h3,{id:i[4].id,children:i[4].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Real-time line chart with 30-second history window (60 data points \xd7 500ms)"}),"\n",(0,s.jsxs)(n.li,{children:["Y-axis dynamically scales to ",(0,s.jsx)(n.code,{children:"max(60, maxFps)"})," to avoid overflow when FPS spikes"]}),"\n",(0,s.jsx)(n.li,{children:"Janky frame region marked in red"}),"\n",(0,s.jsxs)(n.li,{children:["Time axis labels: ",(0,s.jsx)(n.code,{children:"-30s"})," / ",(0,s.jsx)(n.code,{children:"-15s"})," / ",(0,s.jsx)(n.code,{children:"Now"})]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["实时折线图,30 秒历史窗口(60 个数据点 \xd7 500ms)。Y 轴动态缩放为 ",(0,s.jsx)(n.code,{children:"max(60, maxFps)"})," 避免 FPS 飙升时溢出。卡顿区域以红色标记。"]}),"\n",(0,s.jsx)(n.h3,{id:i[5].id,children:i[5].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Lists frames exceeding 16ms duration / 列出耗时超过 16ms 的帧"}),"\n",(0,s.jsx)(n.li,{children:"Each item shows frame duration and timestamp / 每项显示帧耗时和时间戳"}),"\n",(0,s.jsx)(n.li,{children:"Helps identify specific jank spikes / 帮助定位具体卡顿点"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"列出耗时超过 16ms 的帧,每项显示帧耗时和时间戳,帮助定位具体卡顿点。"}),"\n",(0,s.jsx)(n.h3,{id:i[6].id,children:i[6].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"“Reset” button clears all statistics and historical data / “Reset” 按钮清空所有统计和历史数据"}),"\n",(0,s.jsx)(n.li,{children:"Useful for measuring a specific interaction scenario / 适合测量特定交互场景"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"“Reset” 按钮清空所有统计和历史数据,适合测量特定交互场景。"}),"\n",(0,s.jsx)(n.h2,{id:i[7].id,children:i[7].value}),"\n",(0,s.jsxs)(n.p,{children:["FPS monitoring uses Flutter’s ",(0,s.jsx)(n.code,{children:"WidgetsBinding.instance.addTimingsCallback"})," to receive frame timing information from the engine. The callback is ",(0,s.jsx)(n.strong,{children:"batched"})," — it may return multiple ",(0,s.jsx)(n.code,{children:"FrameTiming"})," objects per call, so each frame is recorded individually inside the loop to ensure accurate FPS calculation."]}),"\n",(0,s.jsxs)(n.p,{children:["FPS 监控使用 Flutter 的 ",(0,s.jsx)(n.code,{children:"WidgetsBinding.instance.addTimingsCallback"})," 接收引擎的帧时序信息。该回调是",(0,s.jsx)(n.strong,{children:"批量"}),"的——每次调用可能返回多个 ",(0,s.jsx)(n.code,{children:"FrameTiming"})," 对象,因此在循环内部为每帧单独记录时间戳,确保 FPS 计算准确。"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Jank threshold / 卡顿阈值"}),": A frame is considered janky if its duration exceeds 16ms (the 60 FPS budget of ~16.67ms per frame)."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"卡顿阈值"}),":帧耗时超过 16ms(60 FPS 每帧预算约 16.67ms)即视为卡顿。"]}),"\n",(0,s.jsx)(n.h2,{id:i[8].id,children:i[8].value}),"\n",(0,s.jsxs)(n.p,{children:["If you need to start/stop monitoring from code (e.g., for automated testing), use ",(0,s.jsx)(n.code,{children:"FpsService"}),":"]}),"\n",(0,s.jsxs)(n.p,{children:["如需从代码控制监控(如自动化测试),使用 ",(0,s.jsx)(n.code,{children:"FpsService"}),":"]}),"\n",(0,s.jsx)(n.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,s.jsxs)(n.code,{children:[(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Start / Stop FPS monitoring / 开始 / 停止 FPS 监控"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"start"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"stop"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Read current stats / 读取当前统计"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" fps "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.currentFps;"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" jankRate "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.jankRate;"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" totalFrames "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.totalFrameCount;"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" jankyFrames "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.totalJankyCount;"})]}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Clear historical data / 清空历史数据"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"clear"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Listen to updates / 监听更新"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"addListener"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"(() {"})]}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:" // Update your own UI / 更新你自己的 UI"})}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"});"})}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Historical data access / 历史数据访问"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" history "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.fpsHistory; "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// List, 60 entries / 60 条"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" records "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.frameRecords; "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// List, unmodifiable / 不可变,最多 3600 条"})]})]})}),"\n",(0,s.jsx)(n.h2,{id:i[9].id,children:i[9].value}),"\n",(0,s.jsxs)(n.p,{children:["Singleton service extending ",(0,s.jsx)(n.code,{children:"ChangeNotifier"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["继承 ",(0,s.jsx)(n.code,{children:"ChangeNotifier"})," 的单例服务。"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"start()"})}),(0,s.jsx)(n.td,{children:"Start FPS monitoring / 开始 FPS 监控"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"stop()"})}),(0,s.jsx)(n.td,{children:"Stop FPS monitoring / 停止 FPS 监控"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"clear()"})}),(0,s.jsx)(n.td,{children:"Clear all historical data and counters / 清空所有历史数据和计数器"})]})]})]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Property"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"isRunning"})}),(0,s.jsx)(n.td,{children:"bool"}),(0,s.jsx)(n.td,{children:"Whether monitoring is currently active / 是否正在监控"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"currentFps"})}),(0,s.jsx)(n.td,{children:"double"}),(0,s.jsx)(n.td,{children:"Current FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"jankRate"})}),(0,s.jsx)(n.td,{children:"double"}),(0,s.jsx)(n.td,{children:"Jank rate as percentage / 卡顿率(百分比)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"totalFrameCount"})}),(0,s.jsx)(n.td,{children:"int"}),(0,s.jsx)(n.td,{children:"Total frames captured / 总帧数"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"totalJankyCount"})}),(0,s.jsx)(n.td,{children:"int"}),(0,s.jsx)(n.td,{children:"Total janky frames captured (>16ms) / 总卡顿帧数(>16ms)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"lastFrameJanky"})}),(0,s.jsx)(n.td,{children:"bool"}),(0,s.jsx)(n.td,{children:"Whether the most recent frame was janky / 最近一帧是否卡顿"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"fpsHistory"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"List"})}),(0,s.jsx)(n.td,{children:"Recent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"frameRecords"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"List"})}),(0,s.jsx)(n.td,{children:"Recent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:i[10].id,children:i[10].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Off by default / 默认关闭"}),": Zero overhead when disabled / 关闭时零开销"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight callbacks / 轻量回调"}),": Only processes frame timing data, no heavy computation / 仅处理帧时序数据,无重计算"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Bounded history / 有界历史"}),": Trend chart keeps only 60 points, frame records capped at 3600 / 趋势图仅保留 60 个点,帧记录上限 3600 条"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Safe with other features / 与其他功能兼容"}),": Can run alongside Memory Viewer monitoring / 可与 Memory Viewer 监控同时运行"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:i[11].id,children:i[11].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Feature"}),(0,s.jsx)(n.th,{children:"Android"}),(0,s.jsx)(n.th,{children:"iOS"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"FPS Monitoring"}),(0,s.jsx)(n.td,{children:"✅"}),(0,s.jsx)(n.td,{children:"✅"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Jank Detection"}),(0,s.jsx)(n.td,{children:"✅"}),(0,s.jsx)(n.td,{children:"✅"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Trend Chart"}),(0,s.jsx)(n.td,{children:"✅"}),(0,s.jsx)(n.td,{children:"✅"})]})]})]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"FPS monitoring uses Flutter engine APIs and works on Android and iOS (the two platforms this plugin targets)."}),"\n",(0,s.jsx)(n.p,{children:"FPS 监控使用 Flutter 引擎 API,在本插件支持的两个平台 Android 与 iOS 上均可用。"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:i[12].id,children:i[12].value}),"\n",(0,s.jsx)(n.p,{children:"The Example App includes an FPS demo module with three scenarios:"}),"\n",(0,s.jsx)(n.p,{children:"Example App 包含 FPS 演示模块,提供三种场景:"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Demo"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Trigger Jank"})}),(0,s.jsx)(n.td,{children:"Blocks the main thread for 100-500ms to simulate jank / 阻塞主线程 100-500ms 模拟卡顿"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Heavy Animations"})}),(0,s.jsx)(n.td,{children:"80 simultaneously rotating+scaling widgets to intentionally trigger jank / 80 个同时旋转+缩放的 widget 故意触发卡顿"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Smooth Animation"})}),(0,s.jsx)(n.td,{children:"Single lightweight compound animation, demonstrates stable 60 FPS / 单个轻量复合动画,演示稳定 60 FPS"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:i[13].id,children:i[13].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"Usage",children:"Usage"})," — General usage guide / 通用使用指南"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"FAQ",children:"FAQ"})," — Common questions / 常见问题"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"Configuration",children:"Configuration"})," — Configuration options / 配置选项"]}),"\n"]})]})},"/FPS-Viewer",{filePath:"pages/FPS-Viewer.md",timestamp:1786753645e3,pageMap:t.v,frontMatter:{},title:"FPS Viewer / FPS 监控面板"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,i,n){"use strict";n.d(i,{v:function(){return s}});let s=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=7839)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/FPS-Viewer-b850e87e45780b99.js b/docs/_next/static/chunks/pages/FPS-Viewer-b850e87e45780b99.js new file mode 100644 index 0000000..d44360a --- /dev/null +++ b/docs/_next/static/chunks/pages/FPS-Viewer-b850e87e45780b99.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[229],{7839:function(e,i,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/FPS-Viewer",function(){return n(7548)}])},7548:function(e,i,n){"use strict";n.r(i),n.d(i,{useTOC:function(){return l}});var s=n(5893),r=n(7812),t=n(7080),d=n(8925);function l(e){return[{value:"Overview / 概览",id:"overview--概览",depth:2},{value:"Master Switch / 总开关",id:"master-switch--总开关",depth:2},{value:"Features / 功能",id:"features--功能",depth:2},{value:"1. Current Stats / 当前统计",id:"1-current-stats--当前统计",depth:3},{value:"2. FPS Trend Chart / FPS 趋势图",id:"2-fps-trend-chart--fps-趋势图",depth:3},{value:"3. Janky Frame List / 卡顿帧列表",id:"3-janky-frame-list--卡顿帧列表",depth:3},{value:"4. Reset / 重置",id:"4-reset--重置",depth:3},{value:"How It Works / 工作原理",id:"how-it-works--工作原理",depth:2},{value:"Programmatic Control (Optional) / 编程式控制(可选)",id:"programmatic-control-optional--编程式控制可选",depth:2},{value:"FpsService API / FpsService 接口",id:"fpsservice-api--fpsservice-接口",depth:2},{value:"Performance Considerations / 性能说明",id:"performance-considerations--性能说明",depth:2},{value:"Platform Support / 平台支持",id:"platform-support--平台支持",depth:2},{value:"Demo / 演示",id:"demo--演示",depth:2},{value:"Related / 相关",id:"related--相关",depth:2}]}i.default=(0,r.c)(function(e){let{toc:i=l(e)}=e,n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{children:"FPS Viewer / FPS 监控面板"}),"\n",(0,s.jsx)(n.p,{children:"The FPS Viewer provides real-time frame performance analysis, including current FPS, jank rate, frame duration stats, and an FPS trend chart."}),"\n",(0,s.jsx)(n.p,{children:"FPS 监控面板提供实时帧性能分析,包括当前 FPS、卡顿率、帧耗时统计和 FPS 趋势图。"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Available since v1.2.0"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"v1.2.0 起可用"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:i[0].id,children:i[0].value}),"\n",(0,s.jsx)(n.p,{children:"The FPS Viewer is integrated into the inspector panel. Tap the floating inspector button → “FPS” tab to access."}),"\n",(0,s.jsx)(n.p,{children:"FPS 监控面板集成在检查器面板中。点击悬浮检查器按钮 → “FPS” 标签即可访问。"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"⚠️ Important"}),": FPS monitoring is ",(0,s.jsx)(n.strong,{children:"off by default"}),". You must toggle on the switch at the top of the panel to start collecting data."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"⚠️ 重要"}),":FPS 监控",(0,s.jsx)(n.strong,{children:"默认关闭"}),"。必须在面板顶部打开开关才会开始采集数据。"]}),"\n",(0,s.jsx)(n.h2,{id:i[1].id,children:i[1].value}),"\n",(0,s.jsx)(n.p,{children:"A switch at the top of the FPS panel controls whether monitoring is enabled."}),"\n",(0,s.jsx)(n.p,{children:"FPS 面板顶部有一个开关,控制是否启用监控。"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"State"}),(0,s.jsx)(n.th,{children:"Behavior"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"OFF (default)"})}),(0,s.jsx)(n.td,{children:"No frame timings callbacks, no timer, zero overhead / 无帧回调、无定时器、零开销"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"ON"})}),(0,s.jsxs)(n.td,{children:["Starts collecting frame data via ",(0,s.jsx)(n.code,{children:"WidgetsBinding.instance.addTimingsCallback"})," / 通过 ",(0,s.jsx)(n.code,{children:"addTimingsCallback"})," 开始采集帧数据"]})]})]})]}),"\n",(0,s.jsx)(n.p,{children:"When turned off, all callbacks and timers are cancelled, leaving no residual overhead."}),"\n",(0,s.jsx)(n.p,{children:"关闭时所有回调和定时器会被取消,不会残留开销。"}),"\n",(0,s.jsx)(n.h2,{id:i[2].id,children:i[2].value}),"\n",(0,s.jsx)(n.h3,{id:i[3].id,children:i[3].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Current FPS"})," — Updated every 500ms / 每 500ms 更新"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Jank Rate"})," — Percentage of janky frames (duration exceeds the per-frame budget, derived from the display refresh rate) / 卡顿帧占比(帧耗时超过由屏幕刷新率换算的逐帧预算)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Total Frame Count"})," — All frames captured since start / 自启动以来的总帧数"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Total Janky Count"})," — All janky frames captured / 自启动以来的总卡顿帧数"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Last Frame Janky"})," — Whether the most recent frame was janky / 最近一帧是否卡顿"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"显示当前 FPS、卡顿率、总帧数、总卡顿帧数、最近一帧是否卡顿。"}),"\n",(0,s.jsx)(n.h3,{id:i[4].id,children:i[4].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Real-time line chart with 30-second history window (60 data points \xd7 500ms)"}),"\n",(0,s.jsxs)(n.li,{children:["Y-axis dynamically scales to ",(0,s.jsx)(n.code,{children:"max(60, maxFps)"})," to avoid overflow when FPS spikes"]}),"\n",(0,s.jsx)(n.li,{children:"Janky frame region marked in red"}),"\n",(0,s.jsxs)(n.li,{children:["Time axis labels: ",(0,s.jsx)(n.code,{children:"-30s"})," / ",(0,s.jsx)(n.code,{children:"-15s"})," / ",(0,s.jsx)(n.code,{children:"Now"})]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["实时折线图,30 秒历史窗口(60 个数据点 \xd7 500ms)。Y 轴动态缩放为 ",(0,s.jsx)(n.code,{children:"max(60, maxFps)"})," 避免 FPS 飙升时溢出。卡顿区域以红色标记。"]}),"\n",(0,s.jsx)(n.h3,{id:i[5].id,children:i[5].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Lists janky frames — duration exceeds the per-frame budget, derived from the display refresh rate (~16.7ms at 60Hz, ~8.3ms at 120Hz) / 列出卡顿帧——帧耗时超过由屏幕刷新率换算的逐帧预算(60Hz≈16.7ms、120Hz≈8.3ms)"}),"\n",(0,s.jsxs)(n.li,{children:["Each item shows frame duration and timestamp, plus the ",(0,s.jsx)(n.strong,{children:"build / raster split"})," (",(0,s.jsx)(n.code,{children:"buildDurationUs"})," + ",(0,s.jsx)(n.code,{children:"rasterDurationUs"}),") so you can tell whether build or GPU raster is the bottleneck / 每项显示帧耗时、时间戳,以及 ",(0,s.jsx)(n.strong,{children:"build / raster 分项"}),"(",(0,s.jsx)(n.code,{children:"buildDurationUs"})," + ",(0,s.jsx)(n.code,{children:"rasterDurationUs"}),"),便于判断是构建还是 GPU 光栅化卡顿"]}),"\n",(0,s.jsx)(n.li,{children:"Helps identify specific jank spikes / 帮助定位具体卡顿点"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"列出卡顿帧(帧耗时超过由屏幕刷新率换算的逐帧预算,60Hz≈16.7ms、120Hz≈8.3ms),每项显示帧耗时、时间戳与 build / raster 分项,帮助定位具体卡顿点。"}),"\n",(0,s.jsx)(n.h3,{id:i[6].id,children:i[6].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"“Reset” button clears all statistics and historical data / “Reset” 按钮清空所有统计和历史数据"}),"\n",(0,s.jsx)(n.li,{children:"Useful for measuring a specific interaction scenario / 适合测量特定交互场景"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"“Reset” 按钮清空所有统计和历史数据,适合测量特定交互场景。"}),"\n",(0,s.jsx)(n.h2,{id:i[7].id,children:i[7].value}),"\n",(0,s.jsxs)(n.p,{children:["FPS monitoring uses Flutter’s ",(0,s.jsx)(n.code,{children:"WidgetsBinding.instance.addTimingsCallback"})," to receive frame timing information from the engine. The callback is ",(0,s.jsx)(n.strong,{children:"batched"})," — it may return multiple ",(0,s.jsx)(n.code,{children:"FrameTiming"})," objects per call, so each frame is recorded individually inside the loop to ensure accurate FPS calculation."]}),"\n",(0,s.jsxs)(n.p,{children:["FPS 监控使用 Flutter 的 ",(0,s.jsx)(n.code,{children:"WidgetsBinding.instance.addTimingsCallback"})," 接收引擎的帧时序信息。该回调是",(0,s.jsx)(n.strong,{children:"批量"}),"的——每次调用可能返回多个 ",(0,s.jsx)(n.code,{children:"FrameTiming"})," 对象,因此在循环内部为每帧单独记录时间戳,确保 FPS 计算准确。"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Jank threshold / 卡顿阈值"}),": A frame is janky when its ",(0,s.jsx)(n.strong,{children:"total"})," duration (build + raster) exceeds the per-frame budget — ",(0,s.jsx)(n.code,{children:"1000ms \xf7 refreshRate"})," (≈16.7ms at 60Hz, ≈8.3ms at 120Hz). Since v1.11.0 the budget is ",(0,s.jsx)(n.strong,{children:"derived from the device’s refresh rate"}),", so high-refresh (120Hz) screens are held to a tight ≈8.3ms instead of being forgiven by a fixed 16ms."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"卡顿阈值"}),":帧的",(0,s.jsx)(n.strong,{children:"总"}),"耗时(build + raster)超过逐帧预算即视为卡顿——",(0,s.jsx)(n.code,{children:"1000ms \xf7 刷新率"}),"(60Hz≈16.7ms、120Hz≈8.3ms)。自 v1.11.0 起预算按",(0,s.jsx)(n.strong,{children:"设备刷新率"}),"换算,高刷(120Hz)屏会被严格约束在 ≈8.3ms,而非被固定 16ms 放过。"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Phased timing / 分阶段耗时"}),": Each ",(0,s.jsx)(n.code,{children:"FrameRecord"})," carries ",(0,s.jsx)(n.code,{children:"buildDurationUs"})," and ",(0,s.jsx)(n.code,{children:"rasterDurationUs"})," (GPU raster); the janky-frame list shows the build / raster split per row."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"分阶段耗时"}),":每条 ",(0,s.jsx)(n.code,{children:"FrameRecord"})," 都带有 ",(0,s.jsx)(n.code,{children:"buildDurationUs"})," 与 ",(0,s.jsx)(n.code,{children:"rasterDurationUs"}),"(GPU 光栅化),掉帧列表项会展示 build / raster 分项。"]}),"\n",(0,s.jsx)(n.h2,{id:i[8].id,children:i[8].value}),"\n",(0,s.jsxs)(n.p,{children:["If you need to start/stop monitoring from code (e.g., for automated testing), use ",(0,s.jsx)(n.code,{children:"FpsService"}),":"]}),"\n",(0,s.jsxs)(n.p,{children:["如需从代码控制监控(如自动化测试),使用 ",(0,s.jsx)(n.code,{children:"FpsService"}),":"]}),"\n",(0,s.jsx)(n.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,s.jsxs)(n.code,{children:[(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Start / Stop FPS monitoring / 开始 / 停止 FPS 监控"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"start"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"stop"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Read current stats / 读取当前统计"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" fps "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.currentFps;"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" jankRate "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.jankRate;"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" totalFrames "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.totalFrameCount;"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" jankyFrames "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.totalJankyCount;"})]}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Clear historical data / 清空历史数据"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"clear"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Listen to updates / 监听更新"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"addListener"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"(() {"})]}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:" // Update your own UI / 更新你自己的 UI"})}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"});"})}),"\n",(0,s.jsx)(n.span,{children:" "}),"\n",(0,s.jsx)(n.span,{children:(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Historical data access / 历史数据访问"})}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" history "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.fpsHistory; "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// List, 60 entries / 60 条"})]}),"\n",(0,s.jsxs)(n.span,{children:[(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" records "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" FpsService"}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance.frameRecords; "}),(0,s.jsx)(n.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// List, unmodifiable / 不可变,最多 3600 条"})]})]})}),"\n",(0,s.jsx)(n.h2,{id:i[9].id,children:i[9].value}),"\n",(0,s.jsxs)(n.p,{children:["Singleton service extending ",(0,s.jsx)(n.code,{children:"ChangeNotifier"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["继承 ",(0,s.jsx)(n.code,{children:"ChangeNotifier"})," 的单例服务。"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"start()"})}),(0,s.jsx)(n.td,{children:"Start FPS monitoring / 开始 FPS 监控"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"stop()"})}),(0,s.jsx)(n.td,{children:"Stop FPS monitoring / 停止 FPS 监控"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"clear()"})}),(0,s.jsx)(n.td,{children:"Clear all historical data and counters / 清空所有历史数据和计数器"})]})]})]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Property"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"isRunning"})}),(0,s.jsx)(n.td,{children:"bool"}),(0,s.jsx)(n.td,{children:"Whether monitoring is currently active / 是否正在监控"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"currentFps"})}),(0,s.jsx)(n.td,{children:"double"}),(0,s.jsx)(n.td,{children:"Current FPS (updated every 500ms) / 当前 FPS(每 500ms 更新)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"jankRate"})}),(0,s.jsx)(n.td,{children:"double"}),(0,s.jsx)(n.td,{children:"Jank rate as percentage / 卡顿率(百分比)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"totalFrameCount"})}),(0,s.jsx)(n.td,{children:"int"}),(0,s.jsx)(n.td,{children:"Total frames captured / 总帧数"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"totalJankyCount"})}),(0,s.jsx)(n.td,{children:"int"}),(0,s.jsx)(n.td,{children:"Total janky frames captured (per-frame budget) / 总卡顿帧数(按逐帧预算)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"lastFrameJanky"})}),(0,s.jsx)(n.td,{children:"bool"}),(0,s.jsx)(n.td,{children:"Whether the most recent frame was janky / 最近一帧是否卡顿"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"fpsHistory"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"List"})}),(0,s.jsx)(n.td,{children:"Recent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"frameRecords"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"List"})}),(0,s.jsx)(n.td,{children:"Recent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条)"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:i[10].id,children:i[10].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Off by default / 默认关闭"}),": Zero overhead when disabled / 关闭时零开销"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight callbacks / 轻量回调"}),": Only processes frame timing data, no heavy computation / 仅处理帧时序数据,无重计算"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Bounded history / 有界历史"}),": Trend chart keeps only 60 points, frame records capped at 3600 / 趋势图仅保留 60 个点,帧记录上限 3600 条"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Safe with other features / 与其他功能兼容"}),": Can run alongside Memory Viewer monitoring / 可与 Memory Viewer 监控同时运行"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:i[11].id,children:i[11].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Feature"}),(0,s.jsx)(n.th,{children:"Android"}),(0,s.jsx)(n.th,{children:"iOS"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"FPS Monitoring"}),(0,s.jsx)(n.td,{children:"✅"}),(0,s.jsx)(n.td,{children:"✅"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Jank Detection"}),(0,s.jsx)(n.td,{children:"✅"}),(0,s.jsx)(n.td,{children:"✅"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Trend Chart"}),(0,s.jsx)(n.td,{children:"✅"}),(0,s.jsx)(n.td,{children:"✅"})]})]})]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"FPS monitoring uses Flutter engine APIs and works on Android and iOS (the two platforms this plugin targets)."}),"\n",(0,s.jsx)(n.p,{children:"FPS 监控使用 Flutter 引擎 API,在本插件支持的两个平台 Android 与 iOS 上均可用。"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:i[12].id,children:i[12].value}),"\n",(0,s.jsx)(n.p,{children:"The Example App includes an FPS demo module with three scenarios:"}),"\n",(0,s.jsx)(n.p,{children:"Example App 包含 FPS 演示模块,提供三种场景:"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Demo"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Trigger Jank"})}),(0,s.jsx)(n.td,{children:"Blocks the main thread for 100-500ms to simulate jank / 阻塞主线程 100-500ms 模拟卡顿"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Heavy Animations"})}),(0,s.jsx)(n.td,{children:"80 simultaneously rotating+scaling widgets to intentionally trigger jank / 80 个同时旋转+缩放的 widget 故意触发卡顿"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Smooth Animation"})}),(0,s.jsx)(n.td,{children:"Single lightweight compound animation, demonstrates stable 60 FPS / 单个轻量复合动画,演示稳定 60 FPS"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:i[13].id,children:i[13].value}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"Usage",children:"Usage"})," — General usage guide / 通用使用指南"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"FAQ",children:"FAQ"})," — Common questions / 常见问题"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"Configuration",children:"Configuration"})," — Configuration options / 配置选项"]}),"\n"]})]})},"/FPS-Viewer",{filePath:"pages/FPS-Viewer.md",timestamp:1786753645e3,pageMap:t.v,frontMatter:{},title:"FPS Viewer / FPS 监控面板"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,i,n){"use strict";n.d(i,{v:function(){return s}});let s=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=7839)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/Installation-2cb5055fa0be64c8.js b/docs/_next/static/chunks/pages/Installation-95d3b8e64e9f547a.js similarity index 98% rename from docs/_next/static/chunks/pages/Installation-2cb5055fa0be64c8.js rename to docs/_next/static/chunks/pages/Installation-95d3b8e64e9f547a.js index 5960809..ae90fd5 100644 --- a/docs/_next/static/chunks/pages/Installation-2cb5055fa0be64c8.js +++ b/docs/_next/static/chunks/pages/Installation-95d3b8e64e9f547a.js @@ -1 +1 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[139],{9720:function(e,i,t){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Installation",function(){return t(1220)}])},1220:function(e,i,t){"use strict";t.r(i),t.d(i,{useTOC:function(){return l}});var r=t(5893),n=t(7812),s=t(7080),a=t(8925),d=t(5192);function l(e){return[{value:"From pub.dev (Recommended) / 从 pub.dev 安装(推荐)",id:"from-pubdev-recommended--从-pubdev-安装推荐",depth:2},{value:"From GitHub / 从 GitHub 安装",id:"from-github--从-github-安装",depth:2},{value:"Platform Setup / 平台配置",id:"platform-setup--平台配置",depth:2},{value:"Android",id:"android",depth:3},{value:"iOS",id:"ios",depth:3},{value:"Import / 导入",id:"import--导入",depth:2},{value:"Requirements / 环境要求",id:"requirements--环境要求",depth:2},{value:"Next Steps / 下一步",id:"next-steps--下一步",depth:2}]}i.default=(0,n.c)(function(e){let{toc:i=l(e)}=e,t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{children:"Installation / 安装"}),"\n",(0,r.jsx)(t.h2,{id:i[0].id,children:i[0].value}),"\n",(0,r.jsxs)(t.p,{children:["Add the following to your ",(0,r.jsx)(t.code,{children:"pubspec.yaml"}),":"]}),"\n",(0,r.jsxs)(t.p,{children:["在 ",(0,r.jsx)(t.code,{children:"pubspec.yaml"})," 中添加以下依赖:"]}),"\n",(0,r.jsx)(t.pre,{tabIndex:"0","data-language":"yaml","data-word-wrap":"","data-copy":"",children:(0,r.jsxs)(t.code,{children:[(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:"dependencies"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" zero_inspector_kit"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:": "}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"^1.10.1"})]})]})}),"\n",(0,r.jsx)(t.p,{children:"Then run:"}),"\n",(0,r.jsx)(t.p,{children:"然后运行:"}),"\n",(0,r.jsx)(t.pre,{icon:d.Fx,tabIndex:"0","data-language":"bash","data-word-wrap":"","data-copy":"",children:(0,r.jsx)(t.code,{children:(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"flutter"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" pub"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" get"})]})})}),"\n",(0,r.jsx)(t.h2,{id:i[1].id,children:i[1].value}),"\n",(0,r.jsx)(t.p,{children:"Alternatively, install from GitHub:"}),"\n",(0,r.jsx)(t.p,{children:"或者从 GitHub 安装:"}),"\n",(0,r.jsx)(t.pre,{tabIndex:"0","data-language":"yaml","data-word-wrap":"","data-copy":"",children:(0,r.jsxs)(t.code,{children:[(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:"dependencies"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" zero_inspector_kit"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" git"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" url"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:": "}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"https://github.com/zero-labsco/zero_inspector_kit.git"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" ref"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:": "}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"release/v1.10.1"})]})]})}),"\n",(0,r.jsx)(t.h2,{id:i[2].id,children:i[2].value}),"\n",(0,r.jsx)(t.h3,{id:i[3].id,children:i[3].value}),"\n",(0,r.jsx)(t.p,{children:"No additional configuration needed."}),"\n",(0,r.jsx)(t.p,{children:"无需额外配置。"}),"\n",(0,r.jsx)(t.h3,{id:i[4].id,children:i[4].value}),"\n",(0,r.jsx)(t.p,{children:"No additional configuration needed."}),"\n",(0,r.jsx)(t.p,{children:"无需额外配置。"}),"\n",(0,r.jsx)(t.h2,{id:i[5].id,children:i[5].value}),"\n",(0,r.jsx)(t.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,r.jsx)(t.code,{children:(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"import"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'package:zero_inspector_kit/zero_inspector_kit.dart'"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:";"})]})})}),"\n",(0,r.jsx)(t.h2,{id:i[6].id,children:i[6].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Requirement"}),(0,r.jsx)(t.th,{children:"Version"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"Flutter"}),(0,r.jsx)(t.td,{children:">= 3.3.0"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"Dart SDK"}),(0,r.jsx)(t.td,{children:">= 3.11.0 < 4.0.0"})]})]})]}),"\n",(0,r.jsx)(t.h2,{id:i[7].id,children:i[7].value}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.a,{href:"Getting-Started",children:"Getting Started"})," — Quick start guide / 快速开始"]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.a,{href:"Usage",children:"Usage"})," — Full usage guide / 完整使用指南"]}),"\n"]})]})},"/Installation",{filePath:"pages/Installation.md",timestamp:178793305e4,pageMap:s.v,frontMatter:{},title:"Installation / 安装"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,i,t){"use strict";t.d(i,{v:function(){return r}});let r=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=9720)}),_N_E=e.O()}]); \ No newline at end of file +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[139],{9720:function(e,i,t){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Installation",function(){return t(1220)}])},1220:function(e,i,t){"use strict";t.r(i),t.d(i,{useTOC:function(){return l}});var r=t(5893),n=t(7812),s=t(7080),a=t(8925),d=t(5192);function l(e){return[{value:"From pub.dev (Recommended) / 从 pub.dev 安装(推荐)",id:"from-pubdev-recommended--从-pubdev-安装推荐",depth:2},{value:"From GitHub / 从 GitHub 安装",id:"from-github--从-github-安装",depth:2},{value:"Platform Setup / 平台配置",id:"platform-setup--平台配置",depth:2},{value:"Android",id:"android",depth:3},{value:"iOS",id:"ios",depth:3},{value:"Import / 导入",id:"import--导入",depth:2},{value:"Requirements / 环境要求",id:"requirements--环境要求",depth:2},{value:"Next Steps / 下一步",id:"next-steps--下一步",depth:2}]}i.default=(0,n.c)(function(e){let{toc:i=l(e)}=e,t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{children:"Installation / 安装"}),"\n",(0,r.jsx)(t.h2,{id:i[0].id,children:i[0].value}),"\n",(0,r.jsxs)(t.p,{children:["Add the following to your ",(0,r.jsx)(t.code,{children:"pubspec.yaml"}),":"]}),"\n",(0,r.jsxs)(t.p,{children:["在 ",(0,r.jsx)(t.code,{children:"pubspec.yaml"})," 中添加以下依赖:"]}),"\n",(0,r.jsx)(t.pre,{tabIndex:"0","data-language":"yaml","data-word-wrap":"","data-copy":"",children:(0,r.jsxs)(t.code,{children:[(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:"dependencies"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" zero_inspector_kit"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:": "}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"^1.11.0"})]})]})}),"\n",(0,r.jsx)(t.p,{children:"Then run:"}),"\n",(0,r.jsx)(t.p,{children:"然后运行:"}),"\n",(0,r.jsx)(t.pre,{icon:d.Fx,tabIndex:"0","data-language":"bash","data-word-wrap":"","data-copy":"",children:(0,r.jsx)(t.code,{children:(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"flutter"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" pub"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" get"})]})})}),"\n",(0,r.jsx)(t.h2,{id:i[1].id,children:i[1].value}),"\n",(0,r.jsx)(t.p,{children:"Alternatively, install from GitHub:"}),"\n",(0,r.jsx)(t.p,{children:"或者从 GitHub 安装:"}),"\n",(0,r.jsx)(t.pre,{tabIndex:"0","data-language":"yaml","data-word-wrap":"","data-copy":"",children:(0,r.jsxs)(t.code,{children:[(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:"dependencies"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" zero_inspector_kit"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" git"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:":"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" url"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:": "}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"https://github.com/zero-labsco/zero_inspector_kit.git"})]}),"\n",(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#22863A","--shiki-dark":"#85E89D"},children:" ref"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:": "}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"release/v1.11.0"})]})]})}),"\n",(0,r.jsx)(t.h2,{id:i[2].id,children:i[2].value}),"\n",(0,r.jsx)(t.h3,{id:i[3].id,children:i[3].value}),"\n",(0,r.jsx)(t.p,{children:"No additional configuration needed."}),"\n",(0,r.jsx)(t.p,{children:"无需额外配置。"}),"\n",(0,r.jsx)(t.h3,{id:i[4].id,children:i[4].value}),"\n",(0,r.jsx)(t.p,{children:"No additional configuration needed."}),"\n",(0,r.jsx)(t.p,{children:"无需额外配置。"}),"\n",(0,r.jsx)(t.h2,{id:i[5].id,children:i[5].value}),"\n",(0,r.jsx)(t.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,r.jsx)(t.code,{children:(0,r.jsxs)(t.span,{children:[(0,r.jsx)(t.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"import"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'package:zero_inspector_kit/zero_inspector_kit.dart'"}),(0,r.jsx)(t.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:";"})]})})}),"\n",(0,r.jsx)(t.h2,{id:i[6].id,children:i[6].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Requirement"}),(0,r.jsx)(t.th,{children:"Version"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"Flutter"}),(0,r.jsx)(t.td,{children:">= 3.3.0"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"Dart SDK"}),(0,r.jsx)(t.td,{children:">= 3.11.0 < 4.0.0"})]})]})]}),"\n",(0,r.jsx)(t.h2,{id:i[7].id,children:i[7].value}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.a,{href:"Getting-Started",children:"Getting Started"})," — Quick start guide / 快速开始"]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.a,{href:"Usage",children:"Usage"})," — Full usage guide / 完整使用指南"]}),"\n"]})]})},"/Installation",{filePath:"pages/Installation.md",timestamp:178793305e4,pageMap:s.v,frontMatter:{},title:"Installation / 安装"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,i,t){"use strict";t.d(i,{v:function(){return r}});let r=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=9720)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/Network-Inspector-2a15a53da68a93b6.js b/docs/_next/static/chunks/pages/Network-Inspector-2a15a53da68a93b6.js deleted file mode 100644 index f923392..0000000 --- a/docs/_next/static/chunks/pages/Network-Inspector-2a15a53da68a93b6.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[310],{3814:function(e,s,i){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Network-Inspector",function(){return i(7643)}])},7643:function(e,s,i){"use strict";i.r(s),i.d(s,{useTOC:function(){return l}});var n=i(5893),r=i(7812),t=i(7080),d=i(8925);function l(e){return[{value:"Overview / 概述",id:"overview--概述",depth:2},{value:"How It Works / 工作原理",id:"how-it-works--工作原理",depth:2},{value:"WebSocket & gRPC Capture / WebSocket 与 gRPC 抓取",id:"websocket--grpc-capture--websocket-与-grpc-抓取",depth:2},{value:"Enable / 开启方式",id:"enable--开启方式",depth:3},{value:"Two Usage Modes / 两种使用方式",id:"two-usage-modes--两种使用方式",depth:3},{value:"What You See / 查看方式",id:"what-you-see--查看方式",depth:3},{value:"Captured Information / 捕获的信息",id:"captured-information--捕获的信息",depth:2},{value:"UI Features / UI 功能",id:"ui-features--ui-功能",depth:2},{value:"Request List / 请求列表",id:"request-list--请求列表",depth:3},{value:"Request Detail / 请求详情",id:"request-detail--请求详情",depth:3},{value:"Search / 搜索",id:"search--搜索",depth:3},{value:"Batch Operations / 批量操作",id:"batch-operations--批量操作",depth:3},{value:"Export & Sensitive-field Masking / 导出与敏感字段遮蔽",id:"export--sensitive-field-masking--导出与敏感字段遮蔽",depth:3},{value:"Copy as cURL / 复制为 cURL",id:"copy-as-curl--复制为-curl",depth:3},{value:"Status Code Colors / 状态码颜色",id:"status-code-colors--状态码颜色",depth:2},{value:"HTTP Method Colors / HTTP 方法颜色",id:"http-method-colors--http-方法颜色",depth:2},{value:"Usage Example / 使用示例",id:"usage-example--使用示例",depth:2},{value:"Request Interceptor / 请求拦截修改",id:"request-interceptor--请求拦截修改",depth:2},{value:"Workflow / 工作流程",id:"workflow--工作流程",depth:3},{value:"Supported Modifications / 支持的修改",id:"supported-modifications--支持的修改",depth:3},{value:"Rule Matching / 规则匹配",id:"rule-matching--规则匹配",depth:3},{value:"Why GET Requests Cannot Be Modified / 为什么 GET 请求不能修改",id:"why-get-requests-cannot-be-modified--为什么-get-请求不能修改",depth:3},{value:"Master Toggle / 拦截总开关",id:"master-toggle--拦截总开关",depth:3},{value:"Rule Management / 规则管理",id:"rule-management--规则管理",depth:3}]}s.default=(0,r.c)(function(e){let{toc:s=l(e)}=e,i={blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(i.h1,{children:"Network Inspector / 网络检查器"}),"\n",(0,n.jsx)(i.h2,{id:s[0].id,children:s[0].value}),"\n",(0,n.jsxs)(i.p,{children:["The Network Inspector automatically captures all HTTP requests made via the ",(0,n.jsx)(i.strong,{children:"http package"})," and ",(0,n.jsx)(i.strong,{children:"Dio"}),", with zero configuration needed."]}),"\n",(0,n.jsxs)(i.p,{children:["网络检查器自动捕获所有通过 ",(0,n.jsx)(i.strong,{children:"http 包"})," 和 ",(0,n.jsx)(i.strong,{children:"Dio"})," 发送的 HTTP 请求,无需任何配置。"]}),"\n",(0,n.jsx)(i.h2,{id:s[1].id,children:s[1].value}),"\n",(0,n.jsxs)(i.p,{children:["The inspector uses Flutter’s ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," to intercept all HTTP traffic at the ",(0,n.jsx)(i.code,{children:"dart:io"})," level. This means:"]}),"\n",(0,n.jsxs)(i.p,{children:["检查器通过 Flutter 的 ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," 在 ",(0,n.jsx)(i.code,{children:"dart:io"})," 层面拦截所有 HTTP 流量。这意味着:"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"http package"}),": Auto-captured ✅ / 自动捕获 ✅"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Dio"}),": Auto-captured (uses IOHttpClientAdapter → HttpClient) ✅ / 自动捕获 ✅"]}),"\n",(0,n.jsx)(i.li,{children:"No manual interceptor setup needed / 无需手动添加拦截器"}),"\n"]}),"\n",(0,n.jsx)(i.h2,{id:s[2].id,children:s[2].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"Available since v1.7.0"})," (opt-in, off by default)"]}),"\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"v1.7.0 起可用"}),"(可选开启,默认关闭)"]}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["HTTP/HTTPS traffic is captured automatically, but the ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," interceptor does ",(0,n.jsx)(i.strong,{children:"not"})," cover streaming protocols like ",(0,n.jsx)(i.strong,{children:"WebSocket"})," and ",(0,n.jsx)(i.strong,{children:"gRPC"}),". For those, enable the opt-in capture so frames and calls show up as ",(0,n.jsx)(i.code,{children:"WS"})," / ",(0,n.jsx)(i.code,{children:"gRPC"})," rows in the Network tab."]}),"\n",(0,n.jsxs)(i.p,{children:["HTTP/HTTPS 流量会自动捕获,但 ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," 拦截器",(0,n.jsx)(i.strong,{children:"无法覆盖"})," WebSocket、gRPC 这类流式协议。针对它们需开启可选的抓取,开启后收发帧/调用会以 ",(0,n.jsx)(i.code,{children:"WS"})," / ",(0,n.jsx)(i.code,{children:"gRPC"})," 行的形式出现在 Network 标签页。"]}),"\n",(0,n.jsx)(i.h3,{id:s[3].id,children:s[3].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Toggle the ",(0,n.jsx)(i.strong,{children:"WS"})," switch in the Network tab toolbar, or / 在 Network 标签页工具栏点击 ",(0,n.jsx)(i.strong,{children:"WS"})," 开关,或"]}),"\n",(0,n.jsx)(i.li,{children:"Set it programmatically: / 通过代码开启:"}),"\n"]}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// on / 开启"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"WsInspectorService"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"enable"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// off / 关闭"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"WsInspectorService"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"disable"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]})]})}),"\n",(0,n.jsxs)(i.p,{children:["Capture is ",(0,n.jsx)(i.strong,{children:"off by default"})," and only records while enabled — apps that don’t use these protocols pay nothing."]}),"\n",(0,n.jsxs)(i.p,{children:["抓取",(0,n.jsx)(i.strong,{children:"默认关闭"}),",且只在开启时记录;不使用这类协议的应用零开销。"]}),"\n",(0,n.jsx)(i.h3,{id:s[4].id,children:s[4].value}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsxs)(i.strong,{children:["1. Transparent wrapper — ",(0,n.jsx)(i.code,{children:"InspectorWebSocket"})]})}),"\n",(0,n.jsxs)(i.p,{children:["Replace ",(0,n.jsx)(i.code,{children:"WebSocket.connect"})," with ",(0,n.jsx)(i.code,{children:"InspectorWebSocket.connect"}),". Frames are auto-recorded (",(0,n.jsx)(i.code,{children:"→"})," out, ",(0,n.jsx)(i.code,{children:"←"})," in) when capture is on; when off it passes through with zero overhead."]}),"\n",(0,n.jsxs)(i.p,{children:["将 ",(0,n.jsx)(i.code,{children:"WebSocket.connect"})," 替换为 ",(0,n.jsx)(i.code,{children:"InspectorWebSocket.connect"}),"。开启抓取时会自动记录收发帧(",(0,n.jsx)(i.code,{children:"→"})," 出站、",(0,n.jsx)(i.code,{children:"←"})," 入站);关闭时零开销透传。"]}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" ws "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:" await"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" InspectorWebSocket"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"connect"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'wss://echo.websocket.events'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"ws."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"listen"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"((msg) "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"=>"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:" print"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'received: "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"$"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"msg"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"));"})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"ws."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"add"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'hello'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"); "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// recorded as an outgoing frame / 记录为出站帧"})]})]})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsxs)(i.strong,{children:["2. Manual hook — ",(0,n.jsx)(i.code,{children:"recordCall"})]})}),"\n",(0,n.jsxs)(i.p,{children:["For stacks not transparently interceptable by ",(0,n.jsx)(i.code,{children:"dart:io"})," (gRPC, ",(0,n.jsx)(i.code,{children:"web_socket_channel"}),", custom protocols), call ",(0,n.jsx)(i.code,{children:"recordCall"})," to log a request/response pair. No-ops when capture is disabled."]}),"\n",(0,n.jsxs)(i.p,{children:["对于无法被 ",(0,n.jsx)(i.code,{children:"dart:io"})," 透明拦截的栈(gRPC、",(0,n.jsx)(i.code,{children:"web_socket_channel"}),"、自定义协议),调用 ",(0,n.jsx)(i.code,{children:"recordCall"})," 记录一次请求/响应。关闭抓取时为空操作。"]}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"WsInspectorService"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"recordCall"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" name"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'user.UserService/GetUser'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" request"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" '{ \"id\": 1 }'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" response"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:' \'{ "name": "Ada" }\''}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" protocol"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'gRPC'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:", "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// appears in the method column / 显示在 method 列"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})})]})}),"\n",(0,n.jsx)(i.h3,{id:s[5].id,children:s[5].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Network tab lists a ",(0,n.jsx)(i.code,{children:"WS"})," (or ",(0,n.jsx)(i.code,{children:"gRPC"}),") entry per connection/call / Network 标签页按连接/调用列出 ",(0,n.jsx)(i.code,{children:"WS"}),"(或 ",(0,n.jsx)(i.code,{children:"gRPC"}),")记录"]}),"\n",(0,n.jsxs)(i.li,{children:["Open the detail view to see the frame log (outgoing ",(0,n.jsx)(i.code,{children:"→"})," / incoming ",(0,n.jsx)(i.code,{children:"←"}),"), accumulated in the response body / 进入详情页查看帧日志(出站 ",(0,n.jsx)(i.code,{children:"→"})," / 入站 ",(0,n.jsx)(i.code,{children:"←"}),"),累积显示在响应体中"]}),"\n",(0,n.jsxs)(i.li,{children:["A ",(0,n.jsx)(i.code,{children:"[connection closed]"})," marker is appended when the socket closes / 连接关闭后会追加 ",(0,n.jsx)(i.code,{children:"[connection closed]"})," 标记"]}),"\n"]}),"\n",(0,n.jsx)(i.h2,{id:s[6].id,children:s[6].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Field"}),(0,n.jsx)(i.th,{children:"Description"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Method"}),(0,n.jsx)(i.td,{children:"GET, POST, PUT, DELETE, PATCH"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"URL"}),(0,n.jsx)(i.td,{children:"Full request URL"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Status Code"}),(0,n.jsx)(i.td,{children:"HTTP response status code"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Duration"}),(0,n.jsx)(i.td,{children:"Request duration"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Request Headers"}),(0,n.jsx)(i.td,{children:"All request headers"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Request Body"}),(0,n.jsx)(i.td,{children:"Request payload (JSON formatted)"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Response Body"}),(0,n.jsx)(i.td,{children:"Response payload (JSON formatted)"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Host"}),(0,n.jsx)(i.td,{children:"Parsed from URL"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:s[7].id,children:s[7].value}),"\n",(0,n.jsx)(i.h3,{id:s[8].id,children:s[8].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Color-coded by HTTP method / 按 HTTP 方法着色"}),"\n",(0,n.jsx)(i.li,{children:"Status code badge / 状态码徽章"}),"\n",(0,n.jsx)(i.li,{children:"Duration display / 耗时显示"}),"\n",(0,n.jsx)(i.li,{children:"Left border color indicates status / 左侧边框颜色表示状态"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[9].id,children:s[9].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Click a request to enter detail view / 点击请求进入详情视图"}),"\n",(0,n.jsx)(i.li,{children:"Back button to return to list / 返回按钮返回列表"}),"\n",(0,n.jsx)(i.li,{children:"Request and response sections / 请求和响应分段显示"}),"\n",(0,n.jsx)(i.li,{children:"JSON formatted body / JSON 格式化显示"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[10].id,children:s[10].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Fuzzy search by URL or method / 按 URL 或方法模糊搜索"}),"\n",(0,n.jsx)(i.li,{children:"Search bar hidden in detail view / 详情视图隐藏搜索栏"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[11].id,children:s[11].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsx)(i.p,{children:"The request list supports a selection mode for operating on multiple requests at once."}),"\n",(0,n.jsx)(i.p,{children:"请求列表支持选择模式,可一次性操作多条请求。"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Tap the ",(0,n.jsx)(i.strong,{children:"selection icon"})," in the toolbar to enter selection mode / 点击工具栏的",(0,n.jsx)(i.strong,{children:"选择图标"}),"进入选择模式"]}),"\n",(0,n.jsxs)(i.li,{children:["Check/uncheck items; a top batch bar shows ",(0,n.jsx)(i.strong,{children:"Select all"})," / ",(0,n.jsx)(i.strong,{children:"Cancel"})," / 勾选/取消勾选;顶部批量条提供",(0,n.jsx)(i.strong,{children:"全选"})," / ",(0,n.jsx)(i.strong,{children:"取消"})]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Batch “Copy as cURL”"}),": copies all selected requests as cURL commands / ",(0,n.jsx)(i.strong,{children:"批量「Copy as cURL」"}),":将所有选中请求复制为 cURL 命令"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Batch delete"}),": removes selected requests from the list / ",(0,n.jsx)(i.strong,{children:"批量删除"}),":从列表中移除选中请求"]}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[12].id,children:s[12].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["The toolbar includes an ",(0,n.jsx)(i.strong,{children:"eye toggle"})," that controls whether sensitive headers are masked on export."]}),"\n",(0,n.jsxs)(i.p,{children:["工具栏包含",(0,n.jsx)(i.strong,{children:"眼睛开关"}),",控制导出时是否遮蔽敏感请求头。"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Default: off (fully visible)"})," — cURL / JSON / HAR reflect the real request verbatim / ",(0,n.jsx)(i.strong,{children:"默认关闭(完整可见)"}),":cURL / JSON / HAR 原样反映真实请求"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"On"}),": ",(0,n.jsx)(i.code,{children:"toCurl"})," / ",(0,n.jsx)(i.code,{children:"netToJson"})," / ",(0,n.jsx)(i.code,{children:"netToHar"})," / ",(0,n.jsx)(i.code,{children:"copyNet"})," / ",(0,n.jsx)(i.code,{children:"exportNetToFile"})," mask these headers / ",(0,n.jsx)(i.strong,{children:"开启后"}),":以下请求头被遮蔽:\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.code,{children:"Authorization"}),", ",(0,n.jsx)(i.code,{children:"Cookie"}),", ",(0,n.jsx)(i.code,{children:"Set-Cookie"}),", ",(0,n.jsx)(i.code,{children:"Proxy-Authorization"}),", ",(0,n.jsx)(i.code,{children:"X-Auth-Token"}),", ",(0,n.jsx)(i.code,{children:"X-CSRF-Token"}),", ",(0,n.jsx)(i.code,{children:"X-XSRF-Token"})]}),"\n"]}),"\n"]}),"\n",(0,n.jsx)(i.li,{children:"The detail page “Copy as cURL” / “Copy as JSON” follow the same toggle / 详情页「Copy as cURL」「Copy as JSON」跟随同一开关"}),"\n",(0,n.jsxs)(i.li,{children:["When masking is active, the snackbar notes ",(0,n.jsx)(i.strong,{children:"(sensitive hidden)"})," / 遮蔽开启时,snackbar 提示 ",(0,n.jsx)(i.strong,{children:"(sensitive hidden)"})]}),"\n"]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:["Tip: keep masking ",(0,n.jsx)(i.strong,{children:"on"})," before pasting cURL/JSON into terminals, chat, or issue trackers to avoid leaking credentials."]}),"\n",(0,n.jsx)(i.p,{children:"提示:将 cURL/JSON 粘贴到终端、聊天或 issue 前,建议开启遮蔽,避免凭据泄露。"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[13].id,children:s[13].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["From the request detail view, tap ",(0,n.jsx)(i.strong,{children:"Copy as cURL"})," to copy the request as a ready-to-run cURL command (method, URL, headers, body). Respects the sensitive-field masking toggle above."]}),"\n",(0,n.jsxs)(i.p,{children:["在请求详情页点击 ",(0,n.jsx)(i.strong,{children:"Copy as cURL"}),",即可将请求复制为可直接运行的 cURL 命令(方法、URL、请求头、请求体),并遵循上方的敏感字段遮蔽开关。"]}),"\n",(0,n.jsx)(i.h2,{id:s[14].id,children:s[14].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Range"}),(0,n.jsx)(i.th,{children:"Color"}),(0,n.jsx)(i.th,{children:"Description"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"2xx"}),(0,n.jsx)(i.td,{children:"Green"}),(0,n.jsx)(i.td,{children:"Success / 成功"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"3xx"}),(0,n.jsx)(i.td,{children:"Blue"}),(0,n.jsx)(i.td,{children:"Redirect / 重定向"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"4xx"}),(0,n.jsx)(i.td,{children:"Orange"}),(0,n.jsx)(i.td,{children:"Client error / 客户端错误"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"5xx"}),(0,n.jsx)(i.td,{children:"Red"}),(0,n.jsx)(i.td,{children:"Server error / 服务器错误"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:s[15].id,children:s[15].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Method"}),(0,n.jsx)(i.th,{children:"Color"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"GET"}),(0,n.jsx)(i.td,{children:"Blue / 蓝色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"POST"}),(0,n.jsx)(i.td,{children:"Green / 绿色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"PUT"}),(0,n.jsx)(i.td,{children:"Orange / 橙色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"DELETE"}),(0,n.jsx)(i.td,{children:"Red / 红色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"PATCH"}),(0,n.jsx)(i.td,{children:"Purple / 紫色"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:s[16].id,children:s[16].value}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// http package - auto-captured / http 包 - 自动捕获"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" response "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:" await"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" http."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"get"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" Uri"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"parse"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'https://api.example.com/data'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"),"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})}),"\n",(0,n.jsx)(i.span,{children:" "}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Dio - auto-captured / Dio - 自动捕获"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" response "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:" await"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" dio."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"post"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'https://api.example.com/data'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" data"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" {"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'key'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'value'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"},"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})})]})}),"\n",(0,n.jsx)(i.p,{children:"No additional setup required! All requests will appear in the Network tab."}),"\n",(0,n.jsx)(i.p,{children:"无需额外配置!所有请求都会出现在 Network 标签页中。"}),"\n",(0,n.jsx)(i.h2,{id:s[17].id,children:s[17].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"Available since v1.0.7"})," (response fields locked to read-only since v1.0.8)"]}),"\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"v1.0.7 起可用"}),"(v1.0.8 起响应字段锁定为只读)"]}),"\n"]}),"\n",(0,n.jsx)(i.p,{children:"The inspector supports intercepting and modifying network requests via rules. This is useful for testing different request parameters without modifying app code."}),"\n",(0,n.jsx)(i.p,{children:"检查器支持通过规则拦截并修改网络请求,适合在不修改应用代码的情况下测试不同的请求参数。"}),"\n",(0,n.jsx)(i.h3,{id:s[18].id,children:s[18].value}),"\n",(0,n.jsxs)(i.ol,{children:["\n",(0,n.jsx)(i.li,{children:"Send a request normally (it will be captured in the Network panel) / 正常发送请求(会被捕获到 Network 面板)"}),"\n",(0,n.jsx)(i.li,{children:"Open the request detail and tap the Interceptor icon / 打开请求详情,点击拦截器图标"}),"\n",(0,n.jsx)(i.li,{children:"Configure the modification rule (URL pattern, HTTP method, request modifications) / 配置修改规则(URL 模式、HTTP 方法、请求修改)"}),"\n",(0,n.jsx)(i.li,{children:"Save the rule — subsequent matching requests will use the modified parameters / 保存规则——后续匹配的请求将使用修改后的参数"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[19].id,children:s[19].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Field"}),(0,n.jsx)(i.th,{children:"Editable"}),(0,n.jsx)(i.th,{children:"Notes"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.strong,{children:"Request Body"})}),(0,n.jsx)(i.td,{children:"✅"}),(0,n.jsx)(i.td,{children:"Only for requests with body (POST, PUT, PATCH, etc.) / 仅适用于有 body 的请求"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.strong,{children:"Request Headers"})}),(0,n.jsx)(i.td,{children:"✅"}),(0,n.jsx)(i.td,{children:"Add / modify / remove headers / 新增 / 修改 / 删除请求头"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"URL"}),(0,n.jsx)(i.td,{children:"❌"}),(0,n.jsx)(i.td,{children:"Grayed out, read-only / 灰色不可编辑"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Response Status Code"}),(0,n.jsx)(i.td,{children:"❌"}),(0,n.jsx)(i.td,{children:"Read-only since v1.0.8 / v1.0.8 起只读"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Response Body"}),(0,n.jsx)(i.td,{children:"❌"}),(0,n.jsx)(i.td,{children:"Read-only since v1.0.8 / v1.0.8 起只读"})]})]})]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:["The interception edit panel only allows modifying the ",(0,n.jsx)(i.strong,{children:"request body"})," and ",(0,n.jsx)(i.strong,{children:"request headers"}),". Response fields (status code, response body) are grayed out and uneditable."]}),"\n",(0,n.jsxs)(i.p,{children:["拦截编辑面板仅允许修改",(0,n.jsx)(i.strong,{children:"请求体"}),"和",(0,n.jsx)(i.strong,{children:"请求头"}),"。响应字段(状态码、响应体)灰色不可编辑。"]}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[20].id,children:s[20].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"URL pattern matching"}),": exact match or regex / URL 模式匹配:精确匹配或正则匹配"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"HTTP method filtering"}),": GET, POST, PUT, DELETE, PATCH, HEAD, or Any / HTTP 方法过滤"]}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[21].id,children:s[21].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"The interceptor currently supports modifying request body and headers only / 拦截器目前仅支持修改请求体和请求头"}),"\n",(0,n.jsx)(i.li,{children:"GET requests don’t have a request body / GET 请求没有请求体"}),"\n",(0,n.jsx)(i.li,{children:"Modifying GET request parameters would require URL modification / 修改 GET 请求参数需要修改 URL"}),"\n",(0,n.jsx)(i.li,{children:"URL modification may cause unexpected issues with request routing and parameter encoding / 修改 URL 可能导致请求路由和参数编码的意外问题"}),"\n"]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:"GET request detail pages do not display interception edit buttons, making them unmodifiable."}),"\n",(0,n.jsx)(i.p,{children:"GET 请求详情页不显示拦截编辑按钮,因此无法修改。"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[22].id,children:s[22].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["The interception master toggle is displayed ",(0,n.jsx)(i.strong,{children:"only on the Network list page"}),", not in the detail page / 拦截总开关",(0,n.jsx)(i.strong,{children:"只显示在 Network 列表页"}),",不在详情页"]}),"\n",(0,n.jsx)(i.li,{children:"Rules are only applied when modification mode is enabled / 规则仅在启用修改模式时生效"}),"\n",(0,n.jsx)(i.li,{children:"When no rules are configured or rules are disabled, all requests are sent normally without any modification / 未配置规则或禁用规则时,所有请求正常发送,不做任何修改"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[23].id,children:s[23].value}),"\n",(0,n.jsx)(i.p,{children:"The network panel includes an interceptor rule editor where you can:"}),"\n",(0,n.jsx)(i.p,{children:"网络面板包含拦截规则编辑器,可以:"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Create / edit / delete rules / 创建 / 编辑 / 删除规则"}),"\n",(0,n.jsx)(i.li,{children:"Enable / disable individual rules / 启用 / 禁用单条规则"}),"\n",(0,n.jsx)(i.li,{children:"View rule status indicators in the request list / 在请求列表中查看规则状态标识"}),"\n"]})]})},"/Network-Inspector",{filePath:"pages/Network-Inspector.md",timestamp:1786656379e3,pageMap:t.v,frontMatter:{},title:"Network Inspector / 网络检查器"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,s,i){"use strict";i.d(s,{v:function(){return n}});let n=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=3814)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/_next/static/chunks/pages/Network-Inspector-e55229099cd437f6.js b/docs/_next/static/chunks/pages/Network-Inspector-e55229099cd437f6.js new file mode 100644 index 0000000..bc3a4b7 --- /dev/null +++ b/docs/_next/static/chunks/pages/Network-Inspector-e55229099cd437f6.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[310],{3814:function(e,s,i){(window.__NEXT_P=window.__NEXT_P||[]).push(["/Network-Inspector",function(){return i(7643)}])},7643:function(e,s,i){"use strict";i.r(s),i.d(s,{useTOC:function(){return l}});var n=i(5893),r=i(7812),t=i(7080),d=i(8925);function l(e){return[{value:"Overview / 概述",id:"overview--概述",depth:2},{value:"How It Works / 工作原理",id:"how-it-works--工作原理",depth:2},{value:"WebSocket & gRPC Capture / WebSocket 与 gRPC 抓取",id:"websocket--grpc-capture--websocket-与-grpc-抓取",depth:2},{value:"Enable / 开启方式",id:"enable--开启方式",depth:3},{value:"Two Usage Modes / 两种使用方式",id:"two-usage-modes--两种使用方式",depth:3},{value:"What You See / 查看方式",id:"what-you-see--查看方式",depth:3},{value:"Captured Information / 捕获的信息",id:"captured-information--捕获的信息",depth:2},{value:"UI Features / UI 功能",id:"ui-features--ui-功能",depth:2},{value:"Request List / 请求列表",id:"request-list--请求列表",depth:3},{value:"Request Detail / 请求详情",id:"request-detail--请求详情",depth:3},{value:"Search / 搜索",id:"search--搜索",depth:3},{value:"Batch Operations / 批量操作",id:"batch-operations--批量操作",depth:3},{value:"Export & Sensitive-field Masking / 导出与敏感字段遮蔽",id:"export--sensitive-field-masking--导出与敏感字段遮蔽",depth:3},{value:"Copy as cURL / 复制为 cURL",id:"copy-as-curl--复制为-curl",depth:3},{value:"Replay Editor / 重放编辑器",id:"replay-editor--重放编辑器",depth:2},{value:"Status Code Colors / 状态码颜色",id:"status-code-colors--状态码颜色",depth:2},{value:"HTTP Method Colors / HTTP 方法颜色",id:"http-method-colors--http-方法颜色",depth:2},{value:"Usage Example / 使用示例",id:"usage-example--使用示例",depth:2},{value:"Request Interceptor / 请求拦截修改",id:"request-interceptor--请求拦截修改",depth:2},{value:"Workflow / 工作流程",id:"workflow--工作流程",depth:3},{value:"Supported Modifications / 支持的修改",id:"supported-modifications--支持的修改",depth:3},{value:"Rule Matching / 规则匹配",id:"rule-matching--规则匹配",depth:3},{value:"Why GET Requests Cannot Be Modified / 为什么 GET 请求不能修改",id:"why-get-requests-cannot-be-modified--为什么-get-请求不能修改",depth:3},{value:"Master Toggle / 拦截总开关",id:"master-toggle--拦截总开关",depth:3},{value:"Rule Management / 规则管理",id:"rule-management--规则管理",depth:3}]}s.default=(0,r.c)(function(e){let{toc:s=l(e)}=e,i={blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(i.h1,{children:"Network Inspector / 网络检查器"}),"\n",(0,n.jsx)(i.h2,{id:s[0].id,children:s[0].value}),"\n",(0,n.jsxs)(i.p,{children:["The Network Inspector automatically captures all HTTP requests made via the ",(0,n.jsx)(i.strong,{children:"http package"})," and ",(0,n.jsx)(i.strong,{children:"Dio"}),", with zero configuration needed."]}),"\n",(0,n.jsxs)(i.p,{children:["网络检查器自动捕获所有通过 ",(0,n.jsx)(i.strong,{children:"http 包"})," 和 ",(0,n.jsx)(i.strong,{children:"Dio"})," 发送的 HTTP 请求,无需任何配置。"]}),"\n",(0,n.jsx)(i.h2,{id:s[1].id,children:s[1].value}),"\n",(0,n.jsxs)(i.p,{children:["The inspector uses Flutter’s ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," to intercept all HTTP traffic at the ",(0,n.jsx)(i.code,{children:"dart:io"})," level. This means:"]}),"\n",(0,n.jsxs)(i.p,{children:["检查器通过 Flutter 的 ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," 在 ",(0,n.jsx)(i.code,{children:"dart:io"})," 层面拦截所有 HTTP 流量。这意味着:"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"http package"}),": Auto-captured ✅ / 自动捕获 ✅"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Dio"}),": Auto-captured (uses IOHttpClientAdapter → HttpClient) ✅ / 自动捕获 ✅"]}),"\n",(0,n.jsx)(i.li,{children:"No manual interceptor setup needed / 无需手动添加拦截器"}),"\n"]}),"\n",(0,n.jsx)(i.h2,{id:s[2].id,children:s[2].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"Available since v1.7.0"})," (opt-in, off by default)"]}),"\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"v1.7.0 起可用"}),"(可选开启,默认关闭)"]}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["HTTP/HTTPS traffic is captured automatically, but the ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," interceptor does ",(0,n.jsx)(i.strong,{children:"not"})," cover streaming protocols like ",(0,n.jsx)(i.strong,{children:"WebSocket"})," and ",(0,n.jsx)(i.strong,{children:"gRPC"}),". For those, enable the opt-in capture so frames and calls show up as ",(0,n.jsx)(i.code,{children:"WS"})," / ",(0,n.jsx)(i.code,{children:"gRPC"})," rows in the Network tab."]}),"\n",(0,n.jsxs)(i.p,{children:["HTTP/HTTPS 流量会自动捕获,但 ",(0,n.jsx)(i.code,{children:"HttpOverrides"})," 拦截器",(0,n.jsx)(i.strong,{children:"无法覆盖"})," WebSocket、gRPC 这类流式协议。针对它们需开启可选的抓取,开启后收发帧/调用会以 ",(0,n.jsx)(i.code,{children:"WS"})," / ",(0,n.jsx)(i.code,{children:"gRPC"})," 行的形式出现在 Network 标签页。"]}),"\n",(0,n.jsx)(i.h3,{id:s[3].id,children:s[3].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Toggle the ",(0,n.jsx)(i.strong,{children:"WS"})," switch in the Network tab toolbar, or / 在 Network 标签页工具栏点击 ",(0,n.jsx)(i.strong,{children:"WS"})," 开关,或"]}),"\n",(0,n.jsx)(i.li,{children:"Set it programmatically: / 通过代码开启:"}),"\n"]}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// on / 开启"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"WsInspectorService"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"enable"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// off / 关闭"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"WsInspectorService"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"disable"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"();"})]})]})}),"\n",(0,n.jsxs)(i.p,{children:["Capture is ",(0,n.jsx)(i.strong,{children:"off by default"})," and only records while enabled — apps that don’t use these protocols pay nothing."]}),"\n",(0,n.jsxs)(i.p,{children:["抓取",(0,n.jsx)(i.strong,{children:"默认关闭"}),",且只在开启时记录;不使用这类协议的应用零开销。"]}),"\n",(0,n.jsx)(i.h3,{id:s[4].id,children:s[4].value}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsxs)(i.strong,{children:["1. Transparent wrapper — ",(0,n.jsx)(i.code,{children:"InspectorWebSocket"})]})}),"\n",(0,n.jsxs)(i.p,{children:["Replace ",(0,n.jsx)(i.code,{children:"WebSocket.connect"})," with ",(0,n.jsx)(i.code,{children:"InspectorWebSocket.connect"}),". Frames are auto-recorded (",(0,n.jsx)(i.code,{children:"→"})," out, ",(0,n.jsx)(i.code,{children:"←"})," in) when capture is on; when off it passes through with zero overhead."]}),"\n",(0,n.jsxs)(i.p,{children:["将 ",(0,n.jsx)(i.code,{children:"WebSocket.connect"})," 替换为 ",(0,n.jsx)(i.code,{children:"InspectorWebSocket.connect"}),"。开启抓取时会自动记录收发帧(",(0,n.jsx)(i.code,{children:"→"})," 出站、",(0,n.jsx)(i.code,{children:"←"})," 入站);关闭时零开销透传。"]}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" ws "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:" await"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" InspectorWebSocket"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"connect"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'wss://echo.websocket.events'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"ws."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"listen"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"((msg) "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"=>"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:" print"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'received: "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"$"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"msg"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"));"})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"ws."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"add"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'hello'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"); "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// recorded as an outgoing frame / 记录为出站帧"})]})]})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsxs)(i.strong,{children:["2. Manual hook — ",(0,n.jsx)(i.code,{children:"recordCall"})]})}),"\n",(0,n.jsxs)(i.p,{children:["For stacks not transparently interceptable by ",(0,n.jsx)(i.code,{children:"dart:io"})," (gRPC, ",(0,n.jsx)(i.code,{children:"web_socket_channel"}),", custom protocols), call ",(0,n.jsx)(i.code,{children:"recordCall"})," to log a request/response pair. No-ops when capture is disabled."]}),"\n",(0,n.jsxs)(i.p,{children:["对于无法被 ",(0,n.jsx)(i.code,{children:"dart:io"})," 透明拦截的栈(gRPC、",(0,n.jsx)(i.code,{children:"web_socket_channel"}),"、自定义协议),调用 ",(0,n.jsx)(i.code,{children:"recordCall"})," 记录一次请求/响应。关闭抓取时为空操作。"]}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:"WsInspectorService"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:".instance."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"recordCall"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" name"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'user.UserService/GetUser'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" request"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" '{ \"id\": 1 }'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" response"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:' \'{ "name": "Ada" }\''}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" protocol"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'gRPC'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:", "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// appears in the method column / 显示在 method 列"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})})]})}),"\n",(0,n.jsx)(i.h3,{id:s[5].id,children:s[5].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Network tab lists a ",(0,n.jsx)(i.code,{children:"WS"})," (or ",(0,n.jsx)(i.code,{children:"gRPC"}),") entry per connection/call / Network 标签页按连接/调用列出 ",(0,n.jsx)(i.code,{children:"WS"}),"(或 ",(0,n.jsx)(i.code,{children:"gRPC"}),")记录"]}),"\n",(0,n.jsxs)(i.li,{children:["Open the detail view to see the frame log (outgoing ",(0,n.jsx)(i.code,{children:"→"})," / incoming ",(0,n.jsx)(i.code,{children:"←"}),"), accumulated in the response body / 进入详情页查看帧日志(出站 ",(0,n.jsx)(i.code,{children:"→"})," / 入站 ",(0,n.jsx)(i.code,{children:"←"}),"),累积显示在响应体中"]}),"\n",(0,n.jsxs)(i.li,{children:["A ",(0,n.jsx)(i.code,{children:"[connection closed]"})," marker is appended when the socket closes / 连接关闭后会追加 ",(0,n.jsx)(i.code,{children:"[connection closed]"})," 标记"]}),"\n"]}),"\n",(0,n.jsx)(i.h2,{id:s[6].id,children:s[6].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Field"}),(0,n.jsx)(i.th,{children:"Description"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Method"}),(0,n.jsx)(i.td,{children:"GET, POST, PUT, DELETE, PATCH"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"URL"}),(0,n.jsx)(i.td,{children:"Full request URL"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Status Code"}),(0,n.jsx)(i.td,{children:"HTTP response status code"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Duration"}),(0,n.jsx)(i.td,{children:"Request duration"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Request Headers"}),(0,n.jsx)(i.td,{children:"All request headers"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Request Body"}),(0,n.jsx)(i.td,{children:"Request payload (JSON formatted)"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Response Body"}),(0,n.jsx)(i.td,{children:"Response payload (JSON formatted)"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Host"}),(0,n.jsx)(i.td,{children:"Parsed from URL"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:s[7].id,children:s[7].value}),"\n",(0,n.jsx)(i.h3,{id:s[8].id,children:s[8].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Color-coded by HTTP method / 按 HTTP 方法着色"}),"\n",(0,n.jsx)(i.li,{children:"Status code badge / 状态码徽章"}),"\n",(0,n.jsx)(i.li,{children:"Duration display / 耗时显示"}),"\n",(0,n.jsx)(i.li,{children:"Left border color indicates status / 左侧边框颜色表示状态"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[9].id,children:s[9].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Click a request to enter detail view / 点击请求进入详情视图"}),"\n",(0,n.jsx)(i.li,{children:"Back button to return to list / 返回按钮返回列表"}),"\n",(0,n.jsx)(i.li,{children:"Request and response sections / 请求和响应分段显示"}),"\n",(0,n.jsx)(i.li,{children:"JSON formatted body / JSON 格式化显示"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[10].id,children:s[10].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Fuzzy search by URL or method / 按 URL 或方法模糊搜索"}),"\n",(0,n.jsx)(i.li,{children:"Search bar hidden in detail view / 详情视图隐藏搜索栏"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[11].id,children:s[11].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsx)(i.p,{children:"The request list supports a selection mode for operating on multiple requests at once."}),"\n",(0,n.jsx)(i.p,{children:"请求列表支持选择模式,可一次性操作多条请求。"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["Tap the ",(0,n.jsx)(i.strong,{children:"selection icon"})," in the toolbar to enter selection mode / 点击工具栏的",(0,n.jsx)(i.strong,{children:"选择图标"}),"进入选择模式"]}),"\n",(0,n.jsxs)(i.li,{children:["Check/uncheck items; a top batch bar shows ",(0,n.jsx)(i.strong,{children:"Select all"})," / ",(0,n.jsx)(i.strong,{children:"Cancel"})," / 勾选/取消勾选;顶部批量条提供",(0,n.jsx)(i.strong,{children:"全选"})," / ",(0,n.jsx)(i.strong,{children:"取消"})]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Batch “Copy as cURL”"}),": copies all selected requests as cURL commands / ",(0,n.jsx)(i.strong,{children:"批量「Copy as cURL」"}),":将所有选中请求复制为 cURL 命令"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Batch delete"}),": removes selected requests from the list / ",(0,n.jsx)(i.strong,{children:"批量删除"}),":从列表中移除选中请求"]}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[12].id,children:s[12].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["The toolbar includes an ",(0,n.jsx)(i.strong,{children:"eye toggle"})," that controls whether sensitive headers are masked on export."]}),"\n",(0,n.jsxs)(i.p,{children:["工具栏包含",(0,n.jsx)(i.strong,{children:"眼睛开关"}),",控制导出时是否遮蔽敏感请求头。"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Default: off (fully visible)"})," — cURL / JSON / HAR reflect the real request verbatim / ",(0,n.jsx)(i.strong,{children:"默认关闭(完整可见)"}),":cURL / JSON / HAR 原样反映真实请求"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"On"}),": ",(0,n.jsx)(i.code,{children:"toCurl"})," / ",(0,n.jsx)(i.code,{children:"netToJson"})," / ",(0,n.jsx)(i.code,{children:"netToHar"})," / ",(0,n.jsx)(i.code,{children:"copyNet"})," / ",(0,n.jsx)(i.code,{children:"exportNetToFile"})," mask these headers / ",(0,n.jsx)(i.strong,{children:"开启后"}),":以下请求头被遮蔽:\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.code,{children:"Authorization"}),", ",(0,n.jsx)(i.code,{children:"Cookie"}),", ",(0,n.jsx)(i.code,{children:"Set-Cookie"}),", ",(0,n.jsx)(i.code,{children:"Proxy-Authorization"}),", ",(0,n.jsx)(i.code,{children:"X-Auth-Token"}),", ",(0,n.jsx)(i.code,{children:"X-CSRF-Token"}),", ",(0,n.jsx)(i.code,{children:"X-XSRF-Token"})]}),"\n"]}),"\n"]}),"\n",(0,n.jsx)(i.li,{children:"The detail page “Copy as cURL” / “Copy as JSON” follow the same toggle / 详情页「Copy as cURL」「Copy as JSON」跟随同一开关"}),"\n",(0,n.jsxs)(i.li,{children:["When masking is active, the snackbar notes ",(0,n.jsx)(i.strong,{children:"(sensitive hidden)"})," / 遮蔽开启时,snackbar 提示 ",(0,n.jsx)(i.strong,{children:"(sensitive hidden)"})]}),"\n"]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:["Tip: keep masking ",(0,n.jsx)(i.strong,{children:"on"})," before pasting cURL/JSON into terminals, chat, or issue trackers to avoid leaking credentials."]}),"\n",(0,n.jsx)(i.p,{children:"提示:将 cURL/JSON 粘贴到终端、聊天或 issue 前,建议开启遮蔽,避免凭据泄露。"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[13].id,children:s[13].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.3.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.3.0 起可用"})}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["From the request detail view, tap ",(0,n.jsx)(i.strong,{children:"Copy as cURL"})," to copy the request as a ready-to-run cURL command (method, URL, headers, body). Respects the sensitive-field masking toggle above."]}),"\n",(0,n.jsxs)(i.p,{children:["在请求详情页点击 ",(0,n.jsx)(i.strong,{children:"Copy as cURL"}),",即可将请求复制为可直接运行的 cURL 命令(方法、URL、请求头、请求体),并遵循上方的敏感字段遮蔽开关。"]}),"\n",(0,n.jsx)(i.h2,{id:s[14].id,children:s[14].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"Available since v1.11.0"})}),"\n",(0,n.jsx)(i.p,{children:(0,n.jsx)(i.strong,{children:"v1.11.0 起可用"})}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["Every request detail view has a ",(0,n.jsx)(i.strong,{children:"replay"})," action (↻ icon). It opens an editable sheet that lets you ",(0,n.jsx)(i.strong,{children:"re-issue the captured request in-app"})," and preview the response — handy for quickly re-running an endpoint without leaving the app."]}),"\n",(0,n.jsxs)(i.p,{children:["每个请求详情页都有",(0,n.jsx)(i.strong,{children:"重放"}),"操作(↻ 图标)。它会打开一个可编辑弹层,让你",(0,n.jsx)(i.strong,{children:"在 App 内重新发出该请求"}),"并预览响应——无需离开应用即可快速复跑某个接口。"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"Only the URL query parameters are editable"})," — add / edit / remove ",(0,n.jsx)(i.code,{children:"key=value"})," rows; the URL is rebuilt from them on send / ",(0,n.jsx)(i.strong,{children:"仅 URL 查询参数可编辑"}),"——可增删改 ",(0,n.jsx)(i.code,{children:"key=value"})," 行,发送时用其重建 URL"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"URL, request headers and request body are read-only"})," by design (the captured values are re-sent verbatim) / ",(0,n.jsx)(i.strong,{children:"URL、请求头与请求体按设计只读"}),"(以捕获到的原值原样重发)"]}),"\n",(0,n.jsxs)(i.li,{children:["On ",(0,n.jsx)(i.strong,{children:"Send"}),", the request is re-issued with the original method / headers / body and the edited query string; the sheet shows the returned status code, elapsed time, and a response preview / 点击 ",(0,n.jsx)(i.strong,{children:"Send"})," 后用原始方法 / 请求头 / 请求体加上修改后的查询串重新发出,弹层展示返回状态码、耗时与响应预览"]}),"\n"]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:["This is distinct from the interceptor (which builds a durable modification rule applied to ",(0,n.jsx)(i.em,{children:"future"})," matching requests). The replay editor makes a ",(0,n.jsx)(i.strong,{children:"one-off"})," call with editable query params — including GET requests, whose params can’t be changed by the interceptor."]}),"\n",(0,n.jsxs)(i.p,{children:["它与拦截器不同(拦截器生成作用于",(0,n.jsx)(i.em,{children:"后续"}),"匹配请求的持久规则)。重放编辑器发起的是",(0,n.jsx)(i.strong,{children:"一次性"}),"调用,仅查询参数可改——包括 GET 请求(其参数拦截器无法修改)。"]}),"\n"]}),"\n",(0,n.jsx)(i.h2,{id:s[15].id,children:s[15].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Range"}),(0,n.jsx)(i.th,{children:"Color"}),(0,n.jsx)(i.th,{children:"Description"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"2xx"}),(0,n.jsx)(i.td,{children:"Green"}),(0,n.jsx)(i.td,{children:"Success / 成功"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"3xx"}),(0,n.jsx)(i.td,{children:"Blue"}),(0,n.jsx)(i.td,{children:"Redirect / 重定向"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"4xx"}),(0,n.jsx)(i.td,{children:"Orange"}),(0,n.jsx)(i.td,{children:"Client error / 客户端错误"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"5xx"}),(0,n.jsx)(i.td,{children:"Red"}),(0,n.jsx)(i.td,{children:"Server error / 服务器错误"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:s[16].id,children:s[16].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Method"}),(0,n.jsx)(i.th,{children:"Color"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"GET"}),(0,n.jsx)(i.td,{children:"Blue / 蓝色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"POST"}),(0,n.jsx)(i.td,{children:"Green / 绿色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"PUT"}),(0,n.jsx)(i.td,{children:"Orange / 橙色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"DELETE"}),(0,n.jsx)(i.td,{children:"Red / 红色"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"PATCH"}),(0,n.jsx)(i.td,{children:"Purple / 紫色"})]})]})]}),"\n",(0,n.jsx)(i.h2,{id:s[17].id,children:s[17].value}),"\n",(0,n.jsx)(i.pre,{tabIndex:"0","data-language":"dart","data-word-wrap":"","data-copy":"",children:(0,n.jsxs)(i.code,{children:[(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// http package - auto-captured / http 包 - 自动捕获"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" response "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:" await"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" http."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"get"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79B8FF"},children:" Uri"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"parse"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'https://api.example.com/data'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"),"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})}),"\n",(0,n.jsx)(i.span,{children:" "}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#6A737D"},children:"// Dio - auto-captured / Dio - 自动捕获"})}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"final"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" response "}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:"="}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:" await"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" dio."}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#B392F0"},children:"post"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"("})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'https://api.example.com/data'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:","})]}),"\n",(0,n.jsxs)(i.span,{children:[(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" data"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:" {"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:"'key'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#F97583"},children:":"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#9ECBFF"},children:" 'value'"}),(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:"},"})]}),"\n",(0,n.jsx)(i.span,{children:(0,n.jsx)(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E1E4E8"},children:");"})})]})}),"\n",(0,n.jsx)(i.p,{children:"No additional setup required! All requests will appear in the Network tab."}),"\n",(0,n.jsx)(i.p,{children:"无需额外配置!所有请求都会出现在 Network 标签页中。"}),"\n",(0,n.jsx)(i.h2,{id:s[18].id,children:s[18].value}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"Available since v1.0.7"})," (response fields locked to read-only since v1.0.8)"]}),"\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.strong,{children:"v1.0.7 起可用"}),"(v1.0.8 起响应字段锁定为只读)"]}),"\n"]}),"\n",(0,n.jsx)(i.p,{children:"The inspector supports intercepting and modifying network requests via rules. This is useful for testing different request parameters without modifying app code."}),"\n",(0,n.jsx)(i.p,{children:"检查器支持通过规则拦截并修改网络请求,适合在不修改应用代码的情况下测试不同的请求参数。"}),"\n",(0,n.jsx)(i.h3,{id:s[19].id,children:s[19].value}),"\n",(0,n.jsxs)(i.ol,{children:["\n",(0,n.jsx)(i.li,{children:"Send a request normally (it will be captured in the Network panel) / 正常发送请求(会被捕获到 Network 面板)"}),"\n",(0,n.jsx)(i.li,{children:"Open the request detail and tap the Interceptor icon / 打开请求详情,点击拦截器图标"}),"\n",(0,n.jsx)(i.li,{children:"Configure the modification rule (URL pattern, HTTP method, request modifications) / 配置修改规则(URL 模式、HTTP 方法、请求修改)"}),"\n",(0,n.jsx)(i.li,{children:"Save the rule — subsequent matching requests will use the modified parameters / 保存规则——后续匹配的请求将使用修改后的参数"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[20].id,children:s[20].value}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Field"}),(0,n.jsx)(i.th,{children:"Editable"}),(0,n.jsx)(i.th,{children:"Notes"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.strong,{children:"Request Body"})}),(0,n.jsx)(i.td,{children:"✅"}),(0,n.jsx)(i.td,{children:"Only for requests with body (POST, PUT, PATCH, etc.) / 仅适用于有 body 的请求"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.strong,{children:"Request Headers"})}),(0,n.jsx)(i.td,{children:"✅"}),(0,n.jsx)(i.td,{children:"Add / modify / remove headers / 新增 / 修改 / 删除请求头"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"URL"}),(0,n.jsx)(i.td,{children:"❌"}),(0,n.jsx)(i.td,{children:"Grayed out, read-only / 灰色不可编辑"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Response Status Code"}),(0,n.jsx)(i.td,{children:"❌"}),(0,n.jsx)(i.td,{children:"Read-only since v1.0.8 / v1.0.8 起只读"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:"Response Body"}),(0,n.jsx)(i.td,{children:"❌"}),(0,n.jsx)(i.td,{children:"Read-only since v1.0.8 / v1.0.8 起只读"})]})]})]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsxs)(i.p,{children:["The interception edit panel only allows modifying the ",(0,n.jsx)(i.strong,{children:"request body"})," and ",(0,n.jsx)(i.strong,{children:"request headers"}),". Response fields (status code, response body) are grayed out and uneditable."]}),"\n",(0,n.jsxs)(i.p,{children:["拦截编辑面板仅允许修改",(0,n.jsx)(i.strong,{children:"请求体"}),"和",(0,n.jsx)(i.strong,{children:"请求头"}),"。响应字段(状态码、响应体)灰色不可编辑。"]}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[21].id,children:s[21].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"URL pattern matching"}),": exact match or regex / URL 模式匹配:精确匹配或正则匹配"]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:"HTTP method filtering"}),": GET, POST, PUT, DELETE, PATCH, HEAD, or Any / HTTP 方法过滤"]}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[22].id,children:s[22].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"The interceptor currently supports modifying request body and headers only / 拦截器目前仅支持修改请求体和请求头"}),"\n",(0,n.jsx)(i.li,{children:"GET requests don’t have a request body / GET 请求没有请求体"}),"\n",(0,n.jsx)(i.li,{children:"Modifying GET request parameters would require URL modification / 修改 GET 请求参数需要修改 URL"}),"\n",(0,n.jsx)(i.li,{children:"URL modification may cause unexpected issues with request routing and parameter encoding / 修改 URL 可能导致请求路由和参数编码的意外问题"}),"\n"]}),"\n",(0,n.jsxs)(i.blockquote,{children:["\n",(0,n.jsx)(i.p,{children:"GET request detail pages do not display interception edit buttons, making them unmodifiable."}),"\n",(0,n.jsx)(i.p,{children:"GET 请求详情页不显示拦截编辑按钮,因此无法修改。"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[23].id,children:s[23].value}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:["The interception master toggle is displayed ",(0,n.jsx)(i.strong,{children:"only on the Network list page"}),", not in the detail page / 拦截总开关",(0,n.jsx)(i.strong,{children:"只显示在 Network 列表页"}),",不在详情页"]}),"\n",(0,n.jsx)(i.li,{children:"Rules are only applied when modification mode is enabled / 规则仅在启用修改模式时生效"}),"\n",(0,n.jsx)(i.li,{children:"When no rules are configured or rules are disabled, all requests are sent normally without any modification / 未配置规则或禁用规则时,所有请求正常发送,不做任何修改"}),"\n"]}),"\n",(0,n.jsx)(i.h3,{id:s[24].id,children:s[24].value}),"\n",(0,n.jsx)(i.p,{children:"The network panel includes an interceptor rule editor where you can:"}),"\n",(0,n.jsx)(i.p,{children:"网络面板包含拦截规则编辑器,可以:"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsx)(i.li,{children:"Create / edit / delete rules / 创建 / 编辑 / 删除规则"}),"\n",(0,n.jsx)(i.li,{children:"Enable / disable individual rules / 启用 / 禁用单条规则"}),"\n",(0,n.jsx)(i.li,{children:"View rule status indicators in the request list / 在请求列表中查看规则状态标识"}),"\n"]})]})},"/Network-Inspector",{filePath:"pages/Network-Inspector.md",timestamp:1788107481e3,pageMap:t.v,frontMatter:{},title:"Network Inspector / 网络检查器"},"undefined"==typeof RemoteContent?l:RemoteContent.useTOC)},7080:function(e,s,i){"use strict";i.d(s,{v:function(){return n}});let n=[{data:{index:"\uD83C\uDFE0 Home","Getting-Started":"\uD83D\uDE80 Getting Started",Installation:"\uD83D\uDCE6 Installation",Usage:"\uD83D\uDCD6 Usage","--features":{type:"separator",title:"\uD83D\uDD27 Features"},"Network-Inspector":"\uD83C\uDF10 Network Inspector","Log-Viewer":"\uD83D\uDCDD Log Viewer",Errors:"\uD83D\uDEA8 Errors","Database-Viewer":"\uD83D\uDCBE Database Viewer","Route-Tracker":"\uD83E\uDDED Route Tracker","Memory-Viewer":"\uD83D\uDCCA Memory Viewer","FPS-Viewer":"\uD83C\uDFAF FPS Viewer",Alerts:"\uD83D\uDD14 Alerts","--advanced":{type:"separator",title:"\uD83D\uDEE0 Advanced"},Configuration:"⚙️ Configuration","Custom-Database-Provider":"\uD83D\uDD0C Custom Database Provider","--info":{type:"separator",title:"ℹ️ Info"},FAQ:"❓ FAQ"}},{name:"Alerts",route:"/Alerts",frontMatter:{sidebarTitle:"Alerts"}},{name:"Configuration",route:"/Configuration",frontMatter:{sidebarTitle:"Configuration"}},{name:"Custom-Database-Provider",route:"/Custom-Database-Provider",frontMatter:{sidebarTitle:"Custom Database Provider"}},{name:"Database-Viewer",route:"/Database-Viewer",frontMatter:{sidebarTitle:"Database Viewer"}},{name:"Errors",route:"/Errors",frontMatter:{sidebarTitle:"Errors"}},{name:"FAQ",route:"/FAQ",frontMatter:{sidebarTitle:"Faq"}},{name:"FPS-Viewer",route:"/FPS-Viewer",frontMatter:{sidebarTitle:"Fps Viewer"}},{name:"Getting-Started",route:"/Getting-Started",frontMatter:{sidebarTitle:"Getting Started"}},{name:"index",route:"/",frontMatter:{sidebarTitle:"Index"}},{name:"Installation",route:"/Installation",frontMatter:{sidebarTitle:"Installation"}},{name:"Log-Viewer",route:"/Log-Viewer",frontMatter:{sidebarTitle:"Log Viewer"}},{name:"Memory-Viewer",route:"/Memory-Viewer",frontMatter:{sidebarTitle:"Memory Viewer"}},{name:"Network-Inspector",route:"/Network-Inspector",frontMatter:{sidebarTitle:"Network Inspector"}},{name:"Route-Tracker",route:"/Route-Tracker",frontMatter:{sidebarTitle:"Route Tracker"}},{name:"Usage",route:"/Usage",frontMatter:{sidebarTitle:"Usage"}}]}},function(e){e.O(0,[812,888,774,179],function(){return e(e.s=3814)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 093e02d..1789f93 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,4 +1,4 @@ -
    🏠 Home

    Zero Inspector Kit

    +
    🏠 Home

    Zero Inspector Kit

    A powerful Flutter plugin for in-app developer console, providing real-time debugging tools including network request inspection, logging, error aggregation, database viewing, and route tracking.

    一个功能强大的 Flutter 插件,提供应用内开发者控制台,包括网络请求检查、日志记录、异常聚合、数据库查看和路由追踪。

    ✨ Features / 功能特性

    @@ -141,4 +141,4 @@

    This project is licensed under the GNU General Public License v3.0.

    本项目采用 GNU General Public License v3.0 许可证。

    This plugin is provided “as is”, without warranty of any kind. The author assumes no responsibility or liability for the functionality, security, or any consequences arising from the use of modified versions or derivative projects.

    -

    本插件按”原样”提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。


    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file +

    本插件按”原样”提供,不提供任何担保。作者不对修改版或衍生项目的功能、安全性及任何使用后果承担责任。


    Zero Inspector Kit · MPL-2.0
    \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock index 4571304..057da3b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -738,7 +738,7 @@ packages: path: ".." relative: true source: path - version: "1.9.0" + version: "1.11.0" sdks: dart: ">=3.11.0 <4.0.0" flutter: ">=3.38.4" diff --git a/ios/zero_inspector_kit.podspec b/ios/zero_inspector_kit.podspec index 35a7b29..258fe84 100644 --- a/ios/zero_inspector_kit.podspec +++ b/ios/zero_inspector_kit.podspec @@ -4,10 +4,10 @@ # Pod::Spec.new do |s| s.name = 'zero_inspector_kit' - s.version = '1.10.1' - s.summary = 'A Flutter plugin for in-app developer console.' + s.version = '1.11.0' + s.summary = 'An in-app developer console for Flutter: network, logs, database, memory, FPS, errors, alerts and routes.' s.description = <<-DESC -A Flutter plugin for in-app developer console with network request viewing, logging, database inspection, memory monitoring, FPS monitoring, and route tracking. +An in-app developer console for Flutter that inspects HTTP/Dio traffic, WebSocket and gRPC streams, logs, databases, memory and FPS (with build/raster split), aggregates and persists errors and alerts, tracks routes, and exports a shareable session archive - all auto-disabled in release builds. DESC s.homepage = 'https://github.com/zero-labsco/zero_inspector_kit' s.license = { :file => '../LICENSE' } diff --git a/lib/src/interceptors/dio_interceptor.dart b/lib/src/interceptors/dio_interceptor.dart index c5ebe38..3e254a4 100644 --- a/lib/src/interceptors/dio_interceptor.dart +++ b/lib/src/interceptors/dio_interceptor.dart @@ -76,7 +76,10 @@ class InspectorDioInterceptor extends InspectorDioInterceptorBase { id: requestId, method: options['method'] as String? ?? 'GET', url: options['url'] as String? ?? '', - headers: _convertHeaders(options['headers']), + // 记录给面板看的头里也剔除关联 ID,避免它被误当成业务头(导出 / 复制 cURL + // 时会带走)。The recorded headers drop the correlation ID too, so it is + // never mistaken for a business header (exported / copied as cURL). + headers: _convertHeaders(options['headers'], strip: _requestIdHeader), body: options['data'], requestTime: DateTime.now().millisecondsSinceEpoch, ); @@ -186,14 +189,26 @@ class InspectorDioInterceptor extends InspectorDioInterceptorBase { /// 转换headers为 `Map` 格式 / Convert headers to `Map` format /// Dio的headers可能包含非String类型的值(如content-length是int),需要转换 / Dio headers may contain non-String values (e.g., content-length is int), need conversion - Map? _convertHeaders(dynamic headers) { + /// + /// [strip] 需要从结果中剔除的头名(大小写不敏感),用于去掉检查器自身的关联头。 + /// [strip] header name to drop from the result (case-insensitive), used to + /// remove the inspector's own correlation header. + Map? _convertHeaders(dynamic headers, {String? strip}) { if (headers == null) return null; - if (headers is Map) return headers; - if (headers is Map) { - return headers.map( + Map converted; + if (headers is Map) { + converted = headers; + } else if (headers is Map) { + converted = headers.map( (key, value) => MapEntry(key.toString(), value.toString()), ); + } else { + return null; } - return null; + if (strip == null) return converted; + final target = strip.toLowerCase(); + return Map.fromEntries( + converted.entries.where((e) => e.key.toLowerCase() != target), + ); } } diff --git a/lib/src/interceptors/inspector_http_client.dart b/lib/src/interceptors/inspector_http_client.dart index 4c33984..250f1d1 100644 --- a/lib/src/interceptors/inspector_http_client.dart +++ b/lib/src/interceptors/inspector_http_client.dart @@ -93,6 +93,12 @@ class _InspectorHttpClient implements HttpClient { .then((request) { final dioRequestId = request.headers.value(_dioRequestIdHeader); if (dioRequestId != null) { + // 关联用 ID 到手后立刻从**真实出站请求头**里摘掉,绝不能发到服务端: + // 否则会污染 HMAC / 签名校验、触发 CORS 预检、并把调试痕迹泄漏进生产流量。 + // Once the correlation ID is read, strip it from the real outbound + // headers: leaving it would break HMAC / signature validation, + // trigger CORS preflight, and leak debug traces into production. + request.headers.removeAll(_dioRequestIdHeader); return _InspectorRequestProxy(request, dioRequestId); } // 综合请求头判定 WebSocket 升级握手(Upgrade: websocket), @@ -295,8 +301,56 @@ class _InspectorRequestProxy implements HttpClientRequest { /// the underlying request (so [close] must NOT replay the captured body) bool _streamTeeForwarded = false; + /// 是否有字节已通过 [add] / [write] 系列直接写到底层请求 + /// Whether bytes were already written straight to the underlying request via + /// the [add] / [write] family (so [close] must NOT replay them) + /// + /// 仅在请求体超过 [_maxRequestCaptureBytes] 时置位:此时已切换为直传, + /// 缓冲里只剩前 512KB 预览,重放会把大 body 截断。 + /// Only set once the body exceeds [_maxRequestCaptureBytes] and we switch to + /// pass-through; the buffer then holds just a 512KB preview and replaying it + /// would truncate the upload. + bool _wireForwarded = false; + _InspectorRequestProxy(this._request, this._requestId); + /// 追加请求体字节:先按需缓冲供检查器预览,超限后切换为直传。 + /// Append request-body bytes: buffer a bounded copy for the inspector, then + /// switch to pass-through once the capture cap is hit. + /// + /// 此前 [add] / [write] 系列无上限地全量缓冲,用它们上传大 body 会 OOM + /// (上限此前只实现在 [addStream] 上),与类注释宣称的 OOM 防护不符。 + /// Previously the [add] / [write] family buffered without any cap, so + /// uploading a large body through them could OOM (the cap only existed on + /// [addStream]), contradicting this class' documented OOM protection. + void _appendRequestBytes(List data) { + if (_isClosed) return; + if (_requestCaptureExceeded) { + // 已进入直传模式:不再缓冲,直接转发。 + // Already in pass-through mode: forward without buffering. + _request.add(data); + _wireForwarded = true; + return; + } + final remaining = _maxRequestCaptureBytes - _bodyBytes.length; + if (data.length <= remaining) { + _bodyBytes.addAll(data); + return; + } + // 首次超限:把已缓冲的预览一次性推给底层请求,其余部分直传, + // 从此不再缓冲(缓冲里保留前 512KB 供面板预览)。 + // First time over the cap: push the buffered preview to the underlying + // request, forward the rest directly, and stop buffering (the first 512KB + // stays in the buffer as the panel preview). + _bodyBytes.addAll(data.sublist(0, remaining)); + _requestCaptureExceeded = true; + _wireForwarded = true; + _request.add(_bodyBytes); + if (data.length > remaining) { + _request.add(data.sublist(remaining)); + } + } + @override Future close() async { if (_isClosed) { @@ -379,14 +433,15 @@ class _InspectorRequestProxy implements HttpClientRequest { } } catch (_) {} - // 若用户通过 [addStream] 已经把数据流直接透传到了底层请求,则 - // 不再重复发送 _bodyBytes,避免: + // 若字节已经到底层请求([addStream] 的 tee 直传,或 [add]/[write] 超限后 + // 切换的直传),则不再重放 _bodyBytes,避免: // 1) 重复发送相同字节 // 2) 因为只缓冲了前 512KB 而把大文件上传截断到 512KB - // If the user already tee'd the stream to the wire via [addStream], do - // not replay [_bodyBytes] here to avoid (1) duplicate bytes on the wire - // and (2) truncating large uploads because we only buffered a 512KB cap. - if (!_streamTeeForwarded) { + // If bytes already reached the underlying request (tee'd via [addStream], + // or pass-through after [add]/[write] exceeded the cap), do not replay + // [_bodyBytes] here to avoid (1) duplicate bytes on the wire and + // (2) truncating large uploads because we only buffered a 512KB cap. + if (!_streamTeeForwarded && !_wireForwarded) { try { await _request.addStream(Stream.value(finalBodyBytes)); } catch (_) {} @@ -417,11 +472,7 @@ class _InspectorRequestProxy implements HttpClientRequest { } @override - void add(List data) { - if (!_isClosed) { - _bodyBytes.addAll(data); - } - } + void add(List data) => _appendRequestBytes(data); @override void addError(Object error, [StackTrace? stackTrace]) => @@ -471,31 +522,28 @@ class _InspectorRequestProxy implements HttpClientRequest { @override void write(Object? obj) { if (!_isClosed) { - final bytes = utf8.encode(obj?.toString() ?? ''); - _bodyBytes.addAll(bytes); + _appendRequestBytes(utf8.encode(obj?.toString() ?? '')); } } @override void writeAll(Iterable objects, [String separator = '']) { if (!_isClosed) { - final bytes = utf8.encode(objects.join(separator)); - _bodyBytes.addAll(bytes); + _appendRequestBytes(utf8.encode(objects.join(separator))); } } @override void writeCharCode(int charCode) { if (!_isClosed) { - _bodyBytes.add(charCode); + _appendRequestBytes([charCode]); } } @override void writeln([Object? obj = '']) { if (!_isClosed) { - final bytes = utf8.encode('${obj ?? ''}\n'); - _bodyBytes.addAll(bytes); + _appendRequestBytes(utf8.encode('${obj ?? ''}\n')); } } diff --git a/lib/src/interceptors/inspector_response_proxy.dart b/lib/src/interceptors/inspector_response_proxy.dart index 5a4414c..c42a387 100644 --- a/lib/src/interceptors/inspector_response_proxy.dart +++ b/lib/src/interceptors/inspector_response_proxy.dart @@ -33,29 +33,76 @@ class _InspectorResponseProxy implements HttpClientResponse { void _captureResponse([bool isError = false]) { if (_isCaptured) return; _isCaptured = true; + final id = _requestId; + if (id == null) return; + + // 1) 先无条件落状态码。此前 body 解码与状态码写在同一次调用里, + // gzip / br / protobuf / 图片等非 UTF-8 响应会让 utf8.decode 抛异常, + // 整次 update 被跳过 —— 请求连状态码都没有,永远停在 pending。 + // Persist the status code first, unconditionally. Previously the body + // decode and the status code shared one update call, so a non-UTF-8 + // response made utf8.decode throw and skip the whole update — the + // request never even got its status code and stayed pending forever. try { - if (_requestId != null) { - final String body; - if (_captureExceeded) { - // contentLength 为 -1 表示 chunked 传输或无 Content-Length 头, - // 此时不要把 -1 当作字节数显示给用户。 - // contentLength == -1 means chunked transfer or no Content-Length - // header; don't display "-1 bytes" to the user. - final len = _response.contentLength; - final lenText = len > 0 ? '$len' : 'unknown size'; - body = '[Response body too large to capture ($lenText)]'; - } else { - body = utf8.decode(_bodyBytes); - } - InspectorService.instance.updateNetworkRequest( - _requestId, - statusCode: _response.statusCode, - responseBody: body, - ); + InspectorService.instance.updateNetworkRequest( + id, + statusCode: _response.statusCode, + ); + } catch (_) {} + + // 2) 再单独解码 body,失败降级为 hex 预览而不是丢弃整条更新。 + // Decode the body separately, degrading to a hex preview on failure + // instead of dropping the update. + try { + final String body; + if (_captureExceeded) { + // contentLength 为 -1 表示 chunked 传输或无 Content-Length 头, + // 此时不要把 -1 当作字节数显示给用户。 + // contentLength == -1 means chunked transfer or no Content-Length + // header; don't display "-1 bytes" to the user. + final len = _response.contentLength; + final lenText = len > 0 ? '$len' : 'unknown size'; + body = '[Response body too large to capture ($lenText)]'; + } else { + body = _decodeBodyOrHexPreview(_bodyBytes); } + InspectorService.instance.updateNetworkRequest(id, responseBody: body); } catch (_) {} } + /// 按 UTF-8 解码,失败时降级为"字节数 + 前若干字节 hex 预览"。 + /// Decode as UTF-8, falling back to a byte count + leading hex preview. + /// + /// 覆盖 gzip / br / protobuf / 图片等二进制响应:此前这些响应会直接抛异常, + /// 导致整条记录(含状态码)丢失。 + /// Covers gzip / brotli / protobuf / image responses, which previously threw + /// and lost the whole record (status code included). + /// 小体积二进制响应改用 base64 承载,让面板能直接预览图片等内容 + /// Small binary responses are carried as base64 so the panel can preview them + /// (images, etc.) instead of only showing a hex dump. + static const int _maxBase64PreviewBytes = 64 * 1024; // 64 KB + + static String _decodeBodyOrHexPreview(List bytes) { + if (bytes.isEmpty) return ''; + try { + return utf8.decode(bytes); + } catch (_) { + const previewBytes = 64; + final header = + '[Binary response — ${bytes.length} bytes, not valid UTF-8]'; + if (bytes.length <= _maxBase64PreviewBytes) { + // 小体积:给出完整 base64,UI 可据此渲染图片 / 还原二进制。 + // Small enough: emit full base64 so the UI can render previews. + return '$header\nbase64: ${base64Encode(bytes)}'; + } + final shown = bytes.sublist(0, previewBytes); + final hex = shown + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(' '); + return '$header\nhex: $hex …'; + } + } + RequestInterceptorRule? _getRule() { if (_requestUrl == null || _requestMethod == null) return null; return InspectorService.instance.findMatchingRule( diff --git a/lib/src/models/alert_event.dart b/lib/src/models/alert_event.dart new file mode 100644 index 0000000..4eb669b --- /dev/null +++ b/lib/src/models/alert_event.dart @@ -0,0 +1,14 @@ +/// 告警事件 / Alert event +class AlertEvent { + /// 来源(url / memory / fps / log)/ Source + final String source; + + /// 描述 / Description + final String message; + + /// 触发时间 / Timestamp + final DateTime time; + + AlertEvent({required this.source, required this.message, DateTime? time}) + : time = time ?? DateTime.now(); +} diff --git a/lib/src/models/log_entry.dart b/lib/src/models/log_entry.dart index 505e960..478ecb7 100644 --- a/lib/src/models/log_entry.dart +++ b/lib/src/models/log_entry.dart @@ -1,6 +1,30 @@ /// 日志级别枚举 / Log level enumeration enum LogLevel { verbose, debug, info, warning, error } +/// [LogLevel] 的展示文本 / Display text for [LogLevel] +/// +/// 单一实现:此前 `LogEntry.levelText` 与 `ExportService` 里各有一份同样的 +/// switch,新增级别时极易漏改其中一处。 +/// Single implementation: `LogEntry.levelText` and `ExportService` each had a +/// copy of the same switch, so adding a level risked missing one. +extension LogLevelText on LogLevel { + /// 级别缩写(V / D / I / W / E)/ Level abbreviation (V / D / I / W / E) + static String of(LogLevel level) { + switch (level) { + case LogLevel.verbose: + return 'V'; + case LogLevel.debug: + return 'D'; + case LogLevel.info: + return 'I'; + case LogLevel.warning: + return 'W'; + case LogLevel.error: + return 'E'; + } + } +} + /// 日志条目模型 / Log entry model class LogEntry { /// 日志唯一ID / Log unique ID @@ -27,20 +51,7 @@ class LogEntry { }); /// 日志级别缩写文本 / Log level abbreviation text - String get levelText { - switch (level) { - case LogLevel.verbose: - return 'V'; - case LogLevel.debug: - return 'D'; - case LogLevel.info: - return 'I'; - case LogLevel.warning: - return 'W'; - case LogLevel.error: - return 'E'; - } - } + String get levelText => LogLevelText.of(level); /// 格式化后的时间戳文本 / Formatted timestamp text String get timestampText { diff --git a/lib/src/services/alert_service.dart b/lib/src/services/alert_service.dart index d2c9a65..a940087 100644 --- a/lib/src/services/alert_service.dart +++ b/lib/src/services/alert_service.dart @@ -2,9 +2,13 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; +import '../models/alert_event.dart'; import '../models/alert_rule.dart'; import '../models/network_request.dart'; import '../models/log_entry.dart'; +import 'persistence_service.dart'; + +export '../models/alert_event.dart'; /// 告警服务 / Alert service /// @@ -32,6 +36,19 @@ class AlertService { /// per source per second; a new occurrence after the cooldown still fires. static const int _perSourceCooldownMs = 1000; + /// 节流表硬上限 / Hard cap for the throttle table + /// + /// key 是 `(source, message)`,其中 source 多为完整 URL,长跑 App 里会单调 + /// 增长且永不淘汰 —— 这是本服务唯一的确定性内存泄漏。超过上限即按插入顺序 + /// 淘汰最旧的项。 + /// Keys are `(source, message)` where source is often a full URL, so the map + /// grows monotonically and is never evicted — a deterministic leak. Past the + /// cap, the oldest entries (insertion order) are dropped. + static const int _maxThrottleEntries = 512; + + /// 节流表触发清扫的软阈值 / Soft threshold that triggers a sweep + static const int _throttleSweepThreshold = 384; + /// 已启用的规则(默认内置)/ Enabled rules (defaults built-in) final List _rules = List.of(AlertRule.defaults); @@ -80,6 +97,25 @@ class AlertService { unreadCount.value = 0; } + /// 恢复持久化的告警事件(跨重启不丢)。 + /// Restore persisted alert events (survives restarts). + /// + /// 已存在相同 (source, message) 的事件会被跳过,避免重复计数。 + /// Events whose (source, message) already exist are skipped to avoid + /// double counting. + void restore(Iterable records) { + for (final r in records) { + final exists = _events.any( + (e) => e.source == r.source && e.message == r.message, + ); + if (exists) continue; + _events.addLast(r); + } + while (_events.length > _maxEvents) { + _events.removeLast(); + } + } + /// 检测网络请求是否命中规则 / Check a network request against rules void checkNetwork(NetworkRequest r) { for (final rule in _rules) { @@ -141,26 +177,39 @@ class AlertService { return; } _lastFiredAt[key] = now; + _trimThrottleTable(now); _events.addFirst(AlertEvent(source: source, message: message)); while (_events.length > _maxEvents) { _events.removeLast(); } unreadCount.value = unreadCount.value + 1; + // 异步落盘(磁盘环形缓冲),崩溃后可在 Alerts 标签回看。 + // Async persist (disk ring buffer) so alerts survive a crash. + PersistenceService.instance.enqueueAlert( + AlertEvent(source: source, message: message), + ); } -} - -/// 告警事件 / Alert event -class AlertEvent { - /// 来源(url / memory / fps / log)/ Source - final String source; - - /// 描述 / Description - final String message; - - /// 触发时间 / Timestamp - final DateTime time; - AlertEvent({required this.source, required this.message}) - : time = DateTime.now(); + /// 裁剪节流表,防止无界增长 / Trim the throttle table so it cannot grow unbounded + /// + /// 两级策略 / Two-stage strategy: + /// 1. 过期清扫:任何 `now - ts >= _perSourceCooldownMs` 的项都不再具备节流 + /// 能力(下次同 key 触发时必然放行),属于纯垃圾,可直接丢弃。 + /// Sweep: any entry already past the cooldown can never throttle again + /// (the next fire for that key passes regardless), so it is garbage. + /// 2. 硬上限:按插入顺序淘汰最旧项,兜住"极短时间内出现大量不同 URL"的场景。 + /// Hard cap: evict oldest by insertion order as a backstop for a burst of + /// many distinct URLs within a short window. + void _trimThrottleTable(int now) { + if (_lastFiredAt.length > _throttleSweepThreshold) { + _lastFiredAt.removeWhere((_, ts) => now - ts >= _perSourceCooldownMs); + } + final overflow = _lastFiredAt.length - _maxThrottleEntries; + if (overflow <= 0) return; + final keys = _lastFiredAt.keys.take(overflow).toList(); + for (final k in keys) { + _lastFiredAt.remove(k); + } + } } diff --git a/lib/src/services/database_provider.dart b/lib/src/services/database_provider.dart index 4f793ce..03ce7d8 100644 --- a/lib/src/services/database_provider.dart +++ b/lib/src/services/database_provider.dart @@ -53,6 +53,57 @@ abstract class DatabaseProvider { }); } +/// 键值型数据源(Hive / SharedPreferences 等)的公共工具 +/// Shared helpers for key-value data sources (Hive / SharedPreferences / etc.) +/// +/// Hive 与 SharedPreferences 两个 provider 的分页、排序、过滤逻辑此前几乎 +/// 逐行重复,且各自带着同样的排序崩溃缺陷,这里收敛为共享实现。 +/// The Hive and SharedPreferences providers previously duplicated the paging, +/// sorting and filtering logic almost line for line — and both carried the same +/// sorting crash. The shared implementation lives here now. +class KeyValueQuery { + KeyValueQuery._(); + + /// 默认每页行数 / Default page size + static const int defaultLimit = 50; + + /// 安全排序:Hive 的 key 是 `dynamic`,同时存在 int 与 String 时 + /// `List.sort()` 会用 `Comparable.compare` 直接抛 TypeError。 + /// 这里同类型按自然序、不同类型按字符串序,永不抛异常。 + /// Safe sort: Hive keys are `dynamic`, and mixing int and String makes + /// `List.sort()` (which uses `Comparable.compare`) throw a TypeError. + /// Same types compare naturally, mixed types fall back to string order — + /// this never throws. + static void sortKeys(List keys) { + keys.sort((a, b) { + if (a is Comparable && + b is Comparable && + a.runtimeType == b.runtimeType) { + try { + return a.compareTo(b); + } catch (_) { + // 同类型但不可比较(罕见实现),退回字符串比较。 + // Same type but not comparable (rare); fall back to string compare. + } + } + return a.toString().compareTo(b.toString()); + }); + } + + /// 校验分页参数 / Validate paging parameters + /// + /// `LIMIT <= 0` / 负 `OFFSET` 会让 `skip/take` 行为异常(如返回全量), + /// 这里统一收敛成安全值。 + /// `LIMIT <= 0` or a negative `OFFSET` makes `skip/take` misbehave (e.g. + /// returning everything), so clamp to safe values. + static ({int limit, int offset}) clampPaging(int limit, int offset) { + return ( + limit: limit <= 0 ? defaultLimit : limit, + offset: offset < 0 ? 0 : offset, + ); + } +} + /// 数据库提供者注册表 / Database provider registry /// /// 用于管理所有已注册的数据库提供者,支持动态添加和移除 diff --git a/lib/src/services/error_service.dart b/lib/src/services/error_service.dart index 2ca7464..acb2009 100644 --- a/lib/src/services/error_service.dart +++ b/lib/src/services/error_service.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import '../models/error_record.dart'; import 'persistence_service.dart'; @@ -14,6 +15,13 @@ import 'persistence_service.dart'; /// dedups each exception by type + stack signature, and records count, /// first/last seen time and a full stack sample — powering a dedicated /// Errors Tab to surface "the same crash happening repeatedly". +/// +/// 同时接管 [PlatformDispatcher.onError]:它能捕获 [FlutterError.onError] +/// 漏掉的、发生在框架错误边界之外或平台通道回包中的异常(例如绘制/布局阶段 +/// 抛错后再次被框架捕获前的原始错误),避免这类崩溃完全不被聚合。 +/// Also hooks [PlatformDispatcher.onError], which catches errors that +/// [FlutterError.onError] misses — those thrown outside the framework error +/// boundary or from platform-channel replies — so they are aggregated too. class ErrorService extends ChangeNotifier { ErrorService._(); @@ -32,6 +40,11 @@ class ErrorService extends ChangeNotifier { /// 原生的 FlutterError 处理回调,用于保留默认行为 / Original handler FlutterExceptionHandler? _previousOnError; + /// 原生的 PlatformDispatcher.onError 回调 / Original platform dispatcher handler + /// 类型为 `bool Function(Object error, StackTrace stack)?` + /// (the type of [PlatformDispatcher.onError]). + bool Function(Object error, StackTrace stack)? _previousPlatformOnError; + /// 是否已接管 FlutterError.onError / Whether hooked bool _installed = false; @@ -47,19 +60,36 @@ class ErrorService extends ChangeNotifier { /// 聚合记录数量 / Aggregated record count int get errorCount => _errors.length; - /// 接管 FlutterError.onError(保留默认行为)/ Hook FlutterError.onError + /// 接管 FlutterError.onError 与 PlatformDispatcher.onError(均保留默认行为) + /// Hook both [FlutterError.onError] and [PlatformDispatcher.onError] (default + /// behavior preserved for both). void install() { if (_installed) return; _installed = true; _previousOnError = FlutterError.onError; FlutterError.onError = _onFlutterError; + try { + final dispatcher = WidgetsBinding.instance.platformDispatcher; + _previousPlatformOnError = dispatcher.onError; + dispatcher.onError = _onPlatformError; + } catch (_) { + // 绑定未就绪时跳过平台通道接管 / Skip if binding isn't ready + } } - /// 还原 FlutterError.onError / Restore FlutterError.onError + /// 还原 FlutterError.onError 与 PlatformDispatcher.onError + /// Restore both [FlutterError.onError] and [PlatformDispatcher.onError] void uninstall() { if (!_installed) return; FlutterError.onError = _previousOnError; _previousOnError = null; + try { + final dispatcher = WidgetsBinding.instance.platformDispatcher; + dispatcher.onError = _previousPlatformOnError; + } catch (_) { + // 绑定未就绪时忽略 / Ignore if binding isn't ready + } + _previousPlatformOnError = null; _installed = false; } @@ -81,6 +111,19 @@ class ErrorService extends ChangeNotifier { ); } + /// PlatformDispatcher.onError 回调:返回 false 表示"未处理",让框架继续走 + /// 默认上层兜底逻辑;同时聚合进 Errors Tab。 + /// PlatformDispatcher.onError callback: returning false leaves the error + /// unhandled so the framework keeps its default top-level fallback, while we + /// still aggregate it into the Errors Tab. + bool _onPlatformError(Object error, StackTrace stack) { + _previousPlatformOnError?.call(error, stack); + if (_enabled) { + _record(error, stack.toString(), 'platform-dispatcher'); + } + return false; + } + /// 手动上报一个异常(gRPC/自定义协议或 Zone 捕获)/ Manual report void report(Object exception, StackTrace? stack, [String context = '']) { if (!_enabled) return; diff --git a/lib/src/services/export_service.dart b/lib/src/services/export_service.dart index 24f70cd..dfb2065 100644 --- a/lib/src/services/export_service.dart +++ b/lib/src/services/export_service.dart @@ -8,6 +8,8 @@ import 'package:share_plus/share_plus.dart'; import '../models/log_entry.dart'; import '../models/network_request.dart'; +import '../utils/inspector_version.dart'; +import '../utils/sensitive_data.dart'; /// 导出服务 / Export service /// @@ -20,6 +22,13 @@ class ExportService { static final ExportService instance = ExportService._(); /// 导出时需遮蔽的敏感请求头(不区分大小写)/ Sensitive headers to mask on export + /// + /// 已废弃:仅保留旧的最小集合以维持公共 API 兼容。真正的脱敏统一走 + /// [SensitiveData],它额外覆盖 URL query 与请求体,并支持变体名与宿主扩展。 + /// Deprecated: kept as the legacy minimal set for public-API compatibility. + /// Masking now goes through [SensitiveData], which also covers URL query and + /// bodies, supports name variants and host-side extension. + @Deprecated('Use SensitiveData.isSensitiveHeader / SensitiveData.maskHeaders') static const Set sensitiveHeaders = { 'authorization', 'cookie', @@ -67,41 +76,58 @@ class ExportService { 'requests': requests.map((e) => _maskedJson(e, maskSensitive)).toList(), }); - /// 按 [maskSensitive] 决定是否遮蔽敏感头后再序列化 / Serialize with masking + /// 按 [maskSensitive] 决定是否遮蔽敏感数据后再序列化 / Serialize with masking + /// + /// 覆盖请求头、URL query 与请求体 —— 此前只遮蔽请求头,`?token=` 与 + /// JSON 里的 `password` 会原样导出。 + /// Covers headers, URL query and body — previously only headers were masked, + /// so `?token=` and JSON `password` fields were exported verbatim. Map _maskedJson(NetworkRequest e, bool maskSensitive) { final json = e.toJson(); - if (!maskSensitive || e.headers == null) return json; - final masked = {}; - for (final entry in e.headers!.entries) { - masked[entry.key] = sensitiveHeaders.contains(entry.key.toLowerCase()) - ? '***' - : entry.value; + if (!maskSensitive) return json; + if (e.headers != null) { + json['headers'] = SensitiveData.maskHeaders(e.headers!); + } + final url = e.url; + if (url.isNotEmpty) { + json['url'] = SensitiveData.maskUrl(url); + } + if (e.body != null) { + json['body'] = SensitiveData.maskBodyByContent(e.body.toString()); + } + if (e.responseBody != null) { + json['responseBody'] = SensitiveData.maskBodyByContent( + e.responseBody.toString(), + ); } - json['headers'] = masked; return json; } /// 网络请求 → 复制为 cURL 命令 / Network request → cURL command /// /// 生成可直接粘贴到终端执行的 curl 命令(含 method、headers、body)。 - /// [maskSensitive] 为 true 时遮蔽 Authorization/Cookie 等敏感头。 + /// [maskSensitive] 为 true 时遮蔽敏感头、URL query 与 body。 /// Produces a ready-to-run curl command. When [maskSensitive] is true, - /// sensitive headers (Authorization/Cookie/...) are masked. + /// sensitive headers, URL query params and the body are masked. String toCurl(NetworkRequest r, {bool maskSensitive = false}) { final buf = StringBuffer()..write('curl -X ${r.method} '); - // URL(含单引号时转义)/ URL (escape single quotes) + // URL(含单引号时转义,并按需遮蔽 query 中的凭据) + // URL (escape single quotes, and mask credentials in the query as needed) + final rawUrl = maskSensitive ? SensitiveData.maskUrl(r.url) : r.url; // ignore: prefer_single_quotes — 这里需要双引号以便把单引号转义为 %27 - final url = r.url.replaceAll("'", "%27"); + final url = rawUrl.replaceAll("'", "%27"); buf.writeln("'$url' \\"); for (final e in (r.headers ?? {}).entries) { - final masked = - maskSensitive && sensitiveHeaders.contains(e.key.toLowerCase()); - final name = e.key.replaceAll('"', '\\"'); - final value = (masked ? '***' : e.value).replaceAll('"', '\\"'); + final masked = maskSensitive && SensitiveData.isSensitiveHeader(e.key); + final name = _shellQuote(e.key); + final value = _shellQuote(masked ? SensitiveData.placeholder : e.value); buf.writeln(' -H "$name: $value" \\'); } if (r.body != null) { - final body = r.body.toString().replaceAll('"', '\\"'); + final rawBody = r.body.toString(); + final body = _shellQuote( + maskSensitive ? SensitiveData.maskBodyByContent(rawBody) : rawBody, + ); buf.writeln(' -d "$body" \\'); } // 去掉末尾的续行符 / Trim trailing line-continuation @@ -110,6 +136,18 @@ class ExportService { return out; } + /// 双引号内的 shell 转义 / Shell escaping inside double quotes + /// + /// 此前只转义了 `"`,含反引号或 `$(...)` 的 body 粘贴到终端会被执行。 + /// Previously only `"` was escaped, so a body containing backticks or + /// `$(...)` would be executed when pasted into a terminal. + static String _shellQuote(String s) => s + .replaceAll('\\', '\\\\') + .replaceAll('"', '\\"') + .replaceAll('`', '\\`') + .replaceAll('\$', '\\\$') + .replaceAll('!', '\\!'); + /// 网络请求 → CSV / Network to CSV /// 列:method,url,statusCode,durationMs,requestTime,hasBody,hasResponse String netToCsv(List requests) { @@ -145,14 +183,25 @@ class ExportService { .map( (e) => { 'name': e.key, - 'value': - maskSensitive && - sensitiveHeaders.contains(e.key.toLowerCase()) - ? '***' + 'value': maskSensitive && SensitiveData.isSensitiveHeader(e.key) + ? SensitiveData.placeholder : e.value, }, ) .toList(); + final url = maskSensitive ? SensitiveData.maskUrl(r.url) : r.url; + final rawBody = r.body?.toString(); + final bodyText = rawBody == null + ? null + : (maskSensitive + ? SensitiveData.maskBodyByContent(rawBody) + : rawBody); + final rawResponse = r.responseBody?.toString(); + final responseText = rawResponse == null + ? null + : (maskSensitive + ? SensitiveData.maskBodyByContent(rawResponse) + : rawResponse); final startedMs = r.requestTime; final time = r.duration ?? 0; return { @@ -162,23 +211,17 @@ class ExportService { 'time': time, 'request': { 'method': r.method, - 'url': r.url, + 'url': url, 'headers': reqHeaders, - 'postData': r.body != null - ? { - 'mimeType': 'application/octet-stream', - 'text': r.body.toString(), - } + 'postData': bodyText != null + ? {'mimeType': _guessMimeType(r.headers), 'text': bodyText} : null, }, 'response': { 'status': r.statusCode ?? 0, 'statusText': '', 'headers': >[], - 'content': { - 'size': r.responseBody?.toString().length ?? 0, - 'text': r.responseBody?.toString(), - }, + 'content': {'size': responseText?.length ?? 0, 'text': responseText}, }, 'timings': {'send': 0, 'wait': time, 'receive': 0}, }; @@ -187,12 +230,32 @@ class ExportService { return jsonEncode({ 'log': { 'version': '1.2', - 'creator': {'name': 'Zero Inspector Kit', 'version': '1.2'}, + 'creator': { + 'name': 'Zero Inspector Kit', + 'version': InspectorVersion.value, + }, 'entries': entries, }, }); } + /// 按 Content-Type 推断 HAR `postData.mimeType` + /// Infer the HAR `postData.mimeType` from Content-Type + /// + /// 此前恒为 `application/octet-stream`,导入 DevTools / Charles 后无法正常 + /// 格式化 JSON / 表单请求体。 + /// Previously always `application/octet-stream`, so imported bodies could not + /// be formatted as JSON / form data in DevTools / Charles. + static String _guessMimeType(Map? headers) { + if (headers == null) return 'application/octet-stream'; + for (final entry in headers.entries) { + if (entry.key.toLowerCase() != 'content-type') continue; + final value = entry.value.split(';').first.trim(); + if (value.isNotEmpty) return value; + } + return 'application/octet-stream'; + } + // ==================== 文件导出 / File export ==================== /// 将内容写入临时文件并返回路径。大数据量导出时优先用文件而非剪贴板(剪贴板会截断/失败)。 @@ -200,11 +263,42 @@ class ExportService { /// /// 返回的文件位于应用临时目录;可配合平台层分享或直接用文件管理器打开。 /// The returned file lives in the app temp dir; share or open via a file manager. + /// + /// [fileName] 会先消毒(去掉路径分隔符与 `..`,避免越界写文件), + /// 且当同名文件已存在时自动追加序号,避免并发导出互相覆盖。 + /// [fileName] is sanitized (path separators and `..` are stripped so a + /// crafted name cannot escape the temp dir), and a numeric suffix is appended + /// when the name is taken so concurrent exports don't overwrite each other. Future writeToFile(String content, String fileName) async { final dir = await getTemporaryDirectory(); - final file = File('${dir.path}/$fileName'); - await file.writeAsString(content); - return file.path; + final safeName = _sanitizeFileName(fileName); + var candidate = File('${dir.path}${Platform.pathSeparator}$safeName'); + if (await candidate.exists()) { + final dot = safeName.lastIndexOf('.'); + final base = dot > 0 ? safeName.substring(0, dot) : safeName; + final ext = dot > 0 ? safeName.substring(dot) : ''; + var i = 1; + do { + candidate = File('${dir.path}${Platform.pathSeparator}${base}_$i$ext'); + i++; + } while (await candidate.exists() && i < 100); + } + await candidate.writeAsString(content); + return candidate.path; + } + + /// 文件名消毒:只保留文件名部分,剔除非法字符 + /// Sanitize a file name: keep only the basename, drop illegal characters + static String _sanitizeFileName(String name) { + // 先去掉任何目录成分(`../x` 会越界),再剔除控制字符与路径分隔符。 + // Strip any directory component (`../x` would escape), then drop control + // characters and path separators. + final cleaned = name + .replaceAll(RegExp(r'[\\/]'), '_') + .replaceAll(RegExp(r'[^\w.\-]'), '_') + .replaceAll(RegExp(r'^\.+'), ''); + if (cleaned.isEmpty) return 'zero_inspector_export.txt'; + return cleaned; } /// 导出日志到文件 / Export logs to a file @@ -376,20 +470,15 @@ class ExportService { Future copyText(String content) => copy(content); /// 日志级别前缀 / Log level prefix - String _lvl(LogLevel l) { - switch (l) { - case LogLevel.verbose: - return 'V'; - case LogLevel.debug: - return 'D'; - case LogLevel.info: - return 'I'; - case LogLevel.warning: - return 'W'; - case LogLevel.error: - return 'E'; - } - } + /// + /// 此前是 [LogEntry.levelText] 之外**第二份**同样的 switch —— 新增日志级别时 + /// 极易只改一处。现在直接复用模型层的单一实现。 + /// This used to be a **second** copy of the same switch alongside + /// [LogEntry.levelText] — adding a level would likely miss one of them. + /// It now reuses the single implementation on the model. + static String _lvl(LogLevel l) => LogLevelText.of(l); + + /// CSV 单元格转义:含逗号/引号/换行时用双引号包裹并转义内部引号。 /// CSV 单元格转义:含逗号/引号/换行时用双引号包裹并转义内部引号。 /// CSV cell escaping: wrap in quotes and escape inner quotes when needed. diff --git a/lib/src/services/fps_service.dart b/lib/src/services/fps_service.dart index 72e02e6..085514f 100644 --- a/lib/src/services/fps_service.dart +++ b/lib/src/services/fps_service.dart @@ -14,14 +14,32 @@ class FrameRecord { /// 帧开始时间戳(微秒,来自 FramePhase.buildStart)/ Frame start timestamp (microseconds, from FramePhase.buildStart) final int timestamp; + /// build 阶段耗时(微秒)= buildFinish - buildStart(Widget 树构建,含 layout/paint 调度) + /// Build-phase duration (microseconds) = buildFinish - buildStart (widget tree construction). + final int buildDurationUs; + + /// raster 阶段耗时(微秒)= rasterFinish - rasterStart(GPU 光栅化/合成) + /// Raster-phase duration (microseconds) = rasterFinish - rasterStart (GPU rasterization/compositing). + final int rasterDurationUs; + /// 帧总耗时(微秒)= rasterFinish - buildStart,包含 build 和 raster 全过程 /// Frame total duration (microseconds) = rasterFinish - buildStart, includes both build and raster phases final int durationUs; - /// 是否掉帧(>16ms)/ Whether frame is janky (>16ms) - bool get isJanky => durationUs > 16000; - - const FrameRecord({required this.timestamp, required this.durationUs}); + /// 是否掉帧(默认 >16ms,可传入设备自适应阈值)/ Whether frame is janky + /// + /// 阈值默认 16ms(60fps 预算),但调用方应传入 [FpsService.jankThresholdUs] + /// 以按设备刷新率自适应(如 120Hz 设备约 8.3ms)。 + /// Defaults to 16ms (60fps budget), but callers should pass the refresh-rate + /// adaptive [FpsService.jankThresholdUs] (≈8.3ms on a 120Hz device). + bool isJanky([int? thresholdUs]) => durationUs > (thresholdUs ?? 16000); + + const FrameRecord({ + required this.timestamp, + required this.buildDurationUs, + required this.rasterDurationUs, + required this.durationUs, + }); } /// FPS 监控服务 / FPS monitoring service @@ -58,9 +76,15 @@ class FpsService extends ChangeNotifier { /// 掉帧阈值(微秒)/ Jank threshold (microseconds) /// - /// 60fps 下每帧耗时约 16.67ms,超过此值视为掉帧 - /// At 60fps each frame takes ~16.67ms, exceeding this is considered jank - static const int _jankThresholdUs = 16000; + /// 自适应:按设备标称刷新率换算每帧预算(60Hz≈16.7ms、120Hz≈8.3ms), + /// 高刷设备上同样严格的卡顿判定,避免 120Hz 屏被宽松阈值放过。 + /// Adaptive: the per-frame budget is derived from the display refresh rate + /// (≈16.7ms at 60Hz, ≈8.3ms at 120Hz) so high-refresh displays keep a tight + /// jank bar instead of being let through by a fixed 16ms cutoff. + int get _jankThresholdUs => (1000000 / _displayRefreshRate).round(); + + /// 公开的自适应掉帧阈值(微秒)/ Public adaptive jank threshold (microseconds) + int get jankThresholdUs => _jankThresholdUs; /// 低活跃判定:1 秒窗口内帧数不超过此值视为无持续动画渲染。 /// Low-activity threshold: at most this many frames per 1s window means no @@ -147,7 +171,7 @@ class FpsService extends ChangeNotifier { /// 最近一帧是否掉帧 / Whether the most recent frame is janky bool get lastFrameJanky => - _frameRecords.isNotEmpty && _frameRecords.last.isJanky; + _frameRecords.isNotEmpty && _frameRecords.last.isJanky(_jankThresholdUs); // ==================== 公开方法 / Public methods ==================== @@ -209,6 +233,12 @@ class FpsService extends ChangeNotifier { final frameStartUs = timing.timestampInMicroseconds( FramePhase.buildStart, ); + final buildFinishUs = timing.timestampInMicroseconds( + FramePhase.buildFinish, + ); + final rasterStartUs = timing.timestampInMicroseconds( + FramePhase.rasterStart, + ); // 帧总耗时 = rasterFinish - buildStart,包含 build 和 raster 全过程 // Frame total duration = rasterFinish - buildStart, includes both @@ -218,17 +248,21 @@ class FpsService extends ChangeNotifier { final rasterFinishUs = timing.timestampInMicroseconds( FramePhase.rasterFinish, ); + final buildDurationUs = buildFinishUs - frameStartUs; + final rasterDurationUs = rasterFinishUs - rasterStartUs; final durationUs = rasterFinishUs - frameStartUs; // 添加记录 / Add record final record = FrameRecord( timestamp: frameStartUs, + buildDurationUs: buildDurationUs, + rasterDurationUs: rasterDurationUs, durationUs: durationUs, ); _frameRecords.add(record); _totalFrameCount++; - // 检测掉帧 / Detect jank + // 检测掉帧 / Detect jank (adaptive threshold) if (durationUs > _jankThresholdUs) { _totalJankyCount++; } diff --git a/lib/src/services/hive_provider.dart b/lib/src/services/hive_provider.dart index 650a8bd..bd79148 100644 --- a/lib/src/services/hive_provider.dart +++ b/lib/src/services/hive_provider.dart @@ -70,7 +70,11 @@ class HiveProvider implements DatabaseProvider { bool desc = false, String? whereKeyword, }) async { - var keys = _box.keys.toList()..sort(); + var keys = _box.keys.toList(); + // 用共享的安全排序:Hive key 是 dynamic,混合 int/String 时 List.sort() + // 会抛 TypeError。Use the shared safe sort: Hive keys are dynamic and + // List.sort() throws a TypeError when int and String are mixed. + KeyValueQuery.sortKeys(keys); if (desc) keys = keys.reversed.toList(); if (whereKeyword != null && whereKeyword.isNotEmpty) { final kw = whereKeyword.toLowerCase(); @@ -79,7 +83,8 @@ class HiveProvider implements DatabaseProvider { .toList(); } final total = keys.length; - final windowed = keys.skip(offset).take(limit).map((k) { + final paging = KeyValueQuery.clampPaging(limit, offset); + final windowed = keys.skip(paging.offset).take(paging.limit).map((k) { final v = _box.get(k); return { 'key': _encodeKey(k), diff --git a/lib/src/services/inspector_service.dart b/lib/src/services/inspector_service.dart index 3ab7ab5..082a98c 100644 --- a/lib/src/services/inspector_service.dart +++ b/lib/src/services/inspector_service.dart @@ -92,7 +92,16 @@ class InspectorService { final ThrottledNotifier interceptorNotifier = ThrottledNotifier(); /// 网络请求有序 ID 列表(头部插入,最新在前)/ Ordered network request ids (head insert, newest first) - final List _networkOrder = []; + /// 网络请求 id 的有序表(最新在前)/ Ordered network request ids (newest first) + /// + /// 用 [ListQueue] 而非 [List]:此前用 `_networkOrder.insert(0, ...)` 头插, + /// 每次都要整体搬移元素(O(n)),高频请求下是纯浪费。 + /// A [ListQueue] rather than a [List]: head-insertion via `insert(0, ...)` + /// shifted every element (O(n)) on each request. + final ListQueue _networkOrder = ListQueue(); + + /// [networkRequests] 的缓存,变更时置 null 失效 / Cache for [networkRequests]; nulled on mutation + List? _networkRequestsCache; /// 网络请求索引(id -> 请求),O(1) 查找,避免 WS 高频帧的线性扫描。 /// Index (id -> request) for O(1) lookup, avoids the linear scan on WS high-freq frames. @@ -182,6 +191,11 @@ class InspectorService { if (maxGlobalBodyBytes != null && maxGlobalBodyBytes > 0) { _maxGlobalBodyBytes = maxGlobalBodyBytes; } + // 上限被调小后立即裁剪,否则旧数据会一直留在列表里。 + // Trim right away when a cap shrank, otherwise stale entries linger. + if (maxNetworkItems != null) _trimNetworkRequests(); + if (maxLogItems != null) _trimQueue(_logEntries, _maxLogItems); + if (maxRouteItems != null) _trimQueue(_routeEntries, _maxRouteItems); } /// 获取拦截总开关状态 / Get interceptor master switch state @@ -193,12 +207,26 @@ class InspectorService { interceptorNotifier.notifyThrottled(); } - /// 获取网络请求列表(按时间倒序,最新在前;每次访问重建,引用共享对象)。 - /// Get network requests newest-first; rebuilt on access (references shared objects). - UnmodifiableListView get networkRequests => - UnmodifiableListView( - _networkOrder.map((id) => _networkById[id]!).toList(), - ); + /// 获取网络请求列表(按时间倒序,最新在前)。 + /// Get network requests newest-first. + /// + /// 结果带缓存:UI 一次构建会访问该 getter 多达 7 次(其中一次仅为取 + /// `.length`),此前每次都重建整个 List。变更时缓存失效。 + /// Cached: a single UI build can hit this getter up to 7 times (one of them + /// just for `.length`), and each access used to rebuild the whole list. The + /// cache is invalidated on every mutation. + UnmodifiableListView get networkRequests { + final cached = _networkRequestsCache; + if (cached != null) return UnmodifiableListView(cached); + final built = [ + for (final id in _networkOrder) ?_networkById[id], + ]; + _networkRequestsCache = built; + return UnmodifiableListView(built); + } + + /// 使网络列表缓存失效 / Invalidate the network list cache + void _invalidateNetworkCache() => _networkRequestsCache = null; /// 获取拦截规则列表(只读视图)/ Get interceptor rule list (read-only view) UnmodifiableListView get interceptorRules => @@ -222,7 +250,12 @@ class InspectorService { /// 添加网络请求记录 / Add network request record /// [request] 网络请求对象 / Network request object void addNetworkRequest(NetworkRequest request) { - _networkOrder.insert(0, request.id); + // 同一 id 重复登记会让列表出现重复行(历史上有过 ID 碰撞的先例), + // 这里做兜底去重。Guard against duplicate ids, which would otherwise show + // up as duplicate rows (ID collisions have happened before). + _networkOrder.remove(request.id); + _networkOrder.addFirst(request.id); + _invalidateNetworkCache(); _networkById[request.id] = request; _trimNetworkRequests(); _globalBodyBytes += _bodyBytesOf(request); @@ -287,6 +320,9 @@ class InspectorService { _globalBodyBytes = _globalBodyBytes - oldSize + newSize < 0 ? 0 : _globalBodyBytes - oldSize + newSize; + // 请求对象被替换,缓存里的旧引用失效。 + // The request object is replaced, so cached references go stale. + _invalidateNetworkCache(); // O(1) 就地更新索引中的请求;不再为每次 WS 帧做 remove/addFirst 重排。 // O(1) in-place index update; no per-WS-frame remove/addFirst reorder. _networkById[id] = updated; @@ -473,6 +509,7 @@ class InspectorService { _routeEntries.clear(); _interceptorRules.clear(); _globalBodyBytes = 0; + _invalidateNetworkCache(); networkNotifier.notifyThrottled(); logNotifier.notifyThrottled(); routeNotifier.notifyThrottled(); @@ -484,13 +521,24 @@ class InspectorService { _networkOrder.clear(); _networkById.clear(); _globalBodyBytes = 0; + _invalidateNetworkCache(); networkNotifier.notifyThrottled(); } /// 按 id 删除单条网络请求(批量删除用)/ Remove a single request by id void removeNetworkRequest(String id) { _networkOrder.remove(id); - _networkById.remove(id); + final removed = _networkById.remove(id); + // 释放被删请求占用的全局 body 预算,否则预算只增不减, + // 会让代理侧提前停止缓冲 body。 + // Release the removed request's share of the global body budget; otherwise + // the budget only ever grows and the proxy stops buffering bodies early. + if (removed != null) { + _globalBodyBytes = _globalBodyBytes - _bodyBytesOf(removed) < 0 + ? 0 + : _globalBodyBytes - _bodyBytesOf(removed); + } + _invalidateNetworkCache(); networkNotifier.notifyThrottled(); } @@ -524,6 +572,7 @@ class InspectorService { /// 裁剪网络请求队列到最大条目数,并从全局 body 预算中释放被淘汰请求的占用。 /// Trim network request queue to the cap, releasing evicted requests' body budget. void _trimNetworkRequests() { + if (_networkOrder.length <= _maxNetworkItems) return; while (_networkOrder.length > _maxNetworkItems) { final removedId = _networkOrder.removeLast(); final removed = _networkById.remove(removedId); @@ -534,5 +583,6 @@ class InspectorService { : _globalBodyBytes - freed; } } + _invalidateNetworkCache(); } } diff --git a/lib/src/services/memory_inspector_service.dart b/lib/src/services/memory_inspector_service.dart index 27a07ac..5cfadc0 100644 --- a/lib/src/services/memory_inspector_service.dart +++ b/lib/src/services/memory_inspector_service.dart @@ -155,6 +155,27 @@ class MemoryInspectorService extends ChangeNotifier { /// 泄漏检测检查定时器 / Leak detection check timer Timer? _leakDetectionTimer; + /// VM Service 重连定时器 / VM Service reconnect timer + /// + /// 此前用无引用的 `Timer(...)` 创建,无法取消:用户在重连途中关闭内存监控后, + /// 重连仍会继续并在成功后 notifyListeners()。 + /// Previously created as an unreferenced `Timer(...)`, so it could not be + /// cancelled: closing the memory monitor mid-retry left the reconnect running + /// and it still notified listeners once it eventually succeeded. + Timer? _vmServiceRetryTimer; + + /// 复用的 VM Service WebSocket 长连接 / Reused persistent VM Service WebSocket + WebSocket? _vmServiceSocket; + + /// 长连接的订阅 / Subscription backing the persistent socket + StreamSubscription? _vmServiceSocketSub; + + /// 在途 RPC 调用(按 JSON-RPC id 索引)/ In-flight RPCs keyed by JSON-RPC id + final Map>> _pendingVmRpc = {}; + + /// 单调递增的 JSON-RPC 请求 id / Monotonically increasing JSON-RPC request id + int _vmRpcId = 0; + // ==================== 泄漏检测 / Leak Detection ==================== /// 所有被追踪对象的记录(按 objectId 索引)/ All tracked object records (indexed by objectId) @@ -491,6 +512,8 @@ class MemoryInspectorService extends ChangeNotifier { _storageTimer = null; _leakDetectionTimer?.cancel(); _leakDetectionTimer = null; + _vmServiceRetryTimer?.cancel(); + _vmServiceRetryTimer = null; _isMonitoring = false; _isEnabled = false; @@ -516,6 +539,9 @@ class MemoryInspectorService extends ChangeNotifier { _vmServiceConnecting = false; _vmServiceRetryCount = 0; _vmServiceInitTriggered = false; + // 关闭复用的长连接,否则停止监控后 WebSocket 仍然挂着。 + // Close the reused socket, otherwise it stays open after monitoring stops. + unawaited(_closeVmServiceSocket()); // 清空 Dart Heap 缓存数据 / Clear Dart Heap cached data _currentHeapUsage = 0; @@ -831,17 +857,29 @@ class MemoryInspectorService extends ChangeNotifier { } /// 安排 VM Service 重试连接 / Schedule VM Service retry connection + /// + /// 定时器持有引用以便 [stopMonitoring] 取消;回调内再次确认监控仍在进行, + /// 避免"监控已关闭但重连仍在网络上跑"。 + /// The timer is retained so [stopMonitoring] can cancel it, and the callback + /// re-checks that monitoring is still active so a stopped monitor never keeps + /// reconnecting in the background. void _scheduleRetry() { if (_vmServiceRetryCount >= _vmServiceMaxRetries) { _vmServiceAvailable = false; return; } + if (!_isMonitoring) return; _vmServiceRetryCount++; - Timer( + _vmServiceRetryTimer?.cancel(); + _vmServiceRetryTimer = Timer( Duration(milliseconds: _vmServiceRetryIntervalMs), - _tryConnectVmService, + () { + _vmServiceRetryTimer = null; + if (!_isMonitoring) return; + unawaited(_tryConnectVmService()); + }, ); } @@ -948,79 +986,119 @@ class MemoryInspectorService extends ChangeNotifier { String method, Map params, ) async { - if (_vmServiceWsUri == null) { - throw StateError('VM Service WebSocket URI is null'); - } - - final socket = await WebSocket.connect(_vmServiceWsUri!); - try { - // JSON-RPC 2.0 请求 / JSON-RPC 2.0 request - final request = { + final socket = await _ensureVmServiceSocket(); + final id = ++_vmRpcId; + final completer = Completer>(); + _pendingVmRpc[id] = completer; + + // JSON-RPC 2.0 请求 / JSON-RPC 2.0 request + socket.add( + jsonEncode({ 'jsonrpc': '2.0', 'method': method, 'params': params, - 'id': 1, - }; - socket.add(jsonEncode(request)); - - // 等待响应 / Wait for response - final completer = Completer>(); - late StreamSubscription sub; - sub = socket.listen( - (msg) { - if (completer.isCompleted) return; - try { - final decoded = jsonDecode(msg.toString()) as Map; - // 排除通知(没有 id 字段的是通知)/ Exclude notifications (no id field) - if (!decoded.containsKey('id') && - !decoded.containsKey('result') && - !decoded.containsKey('error')) { - return; - } - // 检查错误 / Check for error - if (decoded.containsKey('error')) { - completer.completeError( - Exception('RPC error: ${decoded['error']}'), - ); - return; - } - // 返回 result 字段 / Return result field - final result = decoded['result']; - if (result is Map) { - completer.complete(result); - } else { - completer.complete({}); - } - } catch (e) { - completer.completeError(e); - } - }, - onError: (e) { - if (!completer.isCompleted) { - completer.completeError(e); - } - }, - onDone: () { - if (!completer.isCompleted) { - completer.completeError( - StateError('WebSocket closed before response received'), - ); - } - }, - cancelOnError: true, - ); + 'id': id, + }), + ); + try { // 设置超时 / Set timeout - final result = await completer.future.timeout( + return await completer.future.timeout( const Duration(seconds: 3), onTimeout: () { throw TimeoutException('WebSocket RPC timeout: $method'); }, ); - await sub.cancel(); - return result; } finally { - await socket.close(); + // 无论成功 / 超时 / 异常都摘掉登记,避免挂起的 completer 泄漏。 + // Unregister on every path (success, timeout, error) so completed or + // abandoned completers never accumulate. + _pendingVmRpc.remove(id); + } + } + + /// 获取(必要时建立)VM Service WebSocket 长连接 + /// Get (or establish) the persistent VM Service WebSocket connection + /// + /// 此前每次 RPC 都 `WebSocket.connect` + `close`,而采集由 500ms 定时器驱动 + /// —— 等于每 500ms 做一次完整握手与拆除。 + /// Previously every RPC did `WebSocket.connect` + `close` while collection is + /// driven by a 500ms timer — i.e. a full handshake and teardown twice a second. + Future _ensureVmServiceSocket() async { + final existing = _vmServiceSocket; + if (existing != null && existing.closeCode == null) return existing; + await _closeVmServiceSocket(); + + final uri = _vmServiceWsUri; + if (uri == null) { + throw StateError('VM Service WebSocket URI is null'); + } + + final socket = await WebSocket.connect(uri); + _vmServiceSocket = socket; + _vmServiceSocketSub = socket.listen( + (msg) => _onVmServiceMessage(msg), + onError: (_) => unawaited(_closeVmServiceSocket()), + onDone: () => unawaited(_closeVmServiceSocket()), + cancelOnError: true, + ); + return socket; + } + + /// 分发 VM Service 响应:按 JSON-RPC `id` 匹配等待中的调用 + /// Dispatch VM Service responses by JSON-RPC `id` + void _onVmServiceMessage(dynamic msg) { + Map decoded; + try { + decoded = jsonDecode(msg.toString()) as Map; + } catch (_) { + return; + } + // 通知(无 id)直接忽略 / Notifications (no id) are ignored + final id = decoded['id']; + if (id is! int) return; + final completer = _pendingVmRpc.remove(id); + if (completer == null || completer.isCompleted) return; + + if (decoded.containsKey('error')) { + completer.completeError(Exception('RPC error: ${decoded['error']}')); + return; + } + final result = decoded['result']; + if (result is Map) { + completer.complete(result); + } else { + completer.complete({}); + } + } + + /// 关闭长连接并让所有在途调用失败(避免永久挂起) + /// Close the persistent socket and fail all in-flight calls (no hangs) + Future _closeVmServiceSocket() async { + final sub = _vmServiceSocketSub; + _vmServiceSocketSub = null; + if (sub != null) { + try { + await sub.cancel(); + } catch (_) {} + } + final socket = _vmServiceSocket; + _vmServiceSocket = null; + if (socket != null) { + try { + await socket.close(); + } catch (_) {} + } + if (_pendingVmRpc.isNotEmpty) { + final pending = _pendingVmRpc.values.toList(); + _pendingVmRpc.clear(); + for (final c in pending) { + if (!c.isCompleted) { + c.completeError( + StateError('WebSocket closed before response received'), + ); + } + } } } diff --git a/lib/src/services/persistence_service.dart b/lib/src/services/persistence_service.dart index 67c72b8..c0c1a2c 100644 --- a/lib/src/services/persistence_service.dart +++ b/lib/src/services/persistence_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:sqflite/sqflite.dart'; +import '../models/alert_event.dart'; import '../models/error_record.dart'; import '../models/log_entry.dart'; import '../models/network_request.dart'; @@ -34,7 +35,10 @@ class PersistenceService { static const String _dbName = 'zero_inspector_kit.db'; /// 数据库版本 / Database version - static const int _dbVersion = 1; + /// Schema 版本:2 = 补齐裁剪用的 ts / last_ts 索引;3 = 新增 alerts 表(告警持久化) + /// Schema version: 2 = ts / last_ts indexes; 3 = new `alerts` table for + /// alert persistence. + static const int _dbVersion = 3; /// 每张表的默认行数上限(环形缓冲)/ Default per-table row cap (ring buffer) static const int _defaultMaxRows = 5000; @@ -57,6 +61,7 @@ class PersistenceService { final List _pendingLogs = []; final List _pendingNetwork = []; final List _pendingErrors = []; + final List _pendingAlerts = []; /// 是否已启用(DB 可用)/ Whether enabled (DB available) bool get isEnabled => _enabled; @@ -95,6 +100,22 @@ class PersistenceService { 'ts INTEGER, type TEXT, count INTEGER, first_ts INTEGER, ' 'last_ts INTEGER, stack TEXT, message TEXT, eid TEXT)', ); + await db.execute( + 'CREATE TABLE alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'ts INTEGER, last_ts INTEGER, source TEXT, message TEXT)', + ); + await _createIndexes(db); + }, + // 版本升级时补表 + 重建索引:此前没有 onUpgrade,未来加字段必然丢数据。 + // Add the missing table and rebuild indexes on upgrade; there was no + // onUpgrade before, so adding a column would have silently lost data. + onUpgrade: (db, oldVersion, newVersion) async { + await db.execute( + 'CREATE TABLE IF NOT EXISTS alerts(' + 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'ts INTEGER, last_ts INTEGER, source TEXT, message TEXT)', + ); + await _createIndexes(db); }, ); _enabled = true; @@ -109,6 +130,27 @@ class PersistenceService { } } + /// 建索引:裁剪每 2s 跑 6 条 `NOT IN (SELECT id ... ORDER BY id DESC LIMIT n)` + /// 的全表扫描,没有 ts / last_ts 索引时代价随行数线性增长。 + /// Create the indexes: trimming runs 6 `NOT IN (SELECT id ... ORDER BY id + /// DESC LIMIT n)` statements every 2s, which degrade to full scans that grow + /// linearly with row count without ts / last_ts indexes. + static Future _createIndexes(Database db) async { + const statements = [ + 'CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts)', + 'CREATE INDEX IF NOT EXISTS idx_network_ts ON network(ts)', + 'CREATE INDEX IF NOT EXISTS idx_errors_ts ON errors(ts)', + 'CREATE INDEX IF NOT EXISTS idx_errors_last_ts ON errors(last_ts)', + 'CREATE INDEX IF NOT EXISTS idx_alerts_ts ON alerts(ts)', + 'CREATE INDEX IF NOT EXISTS idx_alerts_last_ts ON alerts(last_ts)', + ]; + for (final sql in statements) { + try { + await db.execute(sql); + } catch (_) {} + } + } + /// 入队一条日志(异步落盘)/ Enqueue a log (async flush) void enqueueLog(LogEntry e) { if (!_enabled) return; @@ -130,13 +172,22 @@ class PersistenceService { if (_pendingErrors.length >= _flushThreshold) flush(); } + /// 入队一条告警事件(异步落盘,崩溃后可在 Alerts 标签回看) + /// Enqueue an alert event (async flush; recoverable in the Alerts tab later) + void enqueueAlert(AlertEvent e) { + if (!_enabled) return; + _pendingAlerts.add(e); + if (_pendingAlerts.length >= _flushThreshold) flush(); + } + /// 把缓冲区写入磁盘并按环形缓冲裁剪 / Flush buffer to disk then trim Future flush() async { final db = _db; if (!_enabled || db == null) return; if (_pendingLogs.isEmpty && _pendingNetwork.isEmpty && - _pendingErrors.isEmpty) { + _pendingErrors.isEmpty && + _pendingAlerts.isEmpty) { return; } @@ -146,6 +197,8 @@ class PersistenceService { _pendingNetwork.clear(); final errs = List.of(_pendingErrors); _pendingErrors.clear(); + final alerts = List.of(_pendingAlerts); + _pendingAlerts.clear(); try { final batch = db.batch(); @@ -178,6 +231,15 @@ class PersistenceService { 'eid': e.id, }); } + for (final a in alerts) { + final ts = a.time.millisecondsSinceEpoch; + batch.insert('alerts', { + 'ts': ts, + 'last_ts': ts, + 'source': a.source, + 'message': a.message, + }); + } await batch.commit(noResult: true); await _trim(); } catch (_) {} @@ -192,6 +254,7 @@ class PersistenceService { await db.delete('logs', where: 'ts < ?', whereArgs: [cutoff]); await db.delete('network', where: 'ts < ?', whereArgs: [cutoff]); await db.delete('errors', where: 'last_ts < ?', whereArgs: [cutoff]); + await db.delete('alerts', where: 'last_ts < ?', whereArgs: [cutoff]); // 超出行数上限的最旧记录 / Drop oldest rows beyond the cap await db.execute( 'DELETE FROM logs WHERE id NOT IN ' @@ -205,6 +268,10 @@ class PersistenceService { 'DELETE FROM errors WHERE id NOT IN ' '(SELECT id FROM errors ORDER BY id DESC LIMIT $_maxRows)', ); + await db.execute( + 'DELETE FROM alerts WHERE id NOT IN ' + '(SELECT id FROM alerts ORDER BY id DESC LIMIT $_maxRows)', + ); } catch (_) {} } @@ -259,6 +326,24 @@ class PersistenceService { } } + /// 读取已持久化的告警事件(最新在前)/ Load persisted alerts (newest first) + Future> loadAlerts({int limit = 100}) async { + final db = _db; + if (!_enabled || db == null) return const []; + try { + final rows = await db.query('alerts', orderBy: 'id DESC', limit: limit); + return rows.map((r) { + return AlertEvent( + source: (r['source'] as String?) ?? '', + message: (r['message'] as String?) ?? '', + time: DateTime.fromMillisecondsSinceEpoch((r['ts'] as int?) ?? 0), + ); + }).toList(); + } catch (_) { + return const []; + } + } + /// 读取已持久化的网络记录(原始 JSON,最新在前) /// Load persisted network records (raw JSON, newest first) Future>> loadNetworkJson({int limit = 500}) async { @@ -287,11 +372,13 @@ class PersistenceService { int logLimit = 2000, int netLimit = 500, int errLimit = 200, + int alertLimit = 100, }) async { await flush(); final logs = await loadLogs(limit: logLimit); final errs = await loadErrors(limit: errLimit); final nets = await loadNetworkJson(limit: netLimit); + final alerts = await loadAlerts(limit: alertLimit); return jsonEncode({ 'generatedAt': DateTime.now().toIso8601String(), 'logs': logs.map((l) => l.toJson()).toList(), @@ -309,6 +396,15 @@ class PersistenceService { ) .toList(), 'network': nets, + 'alerts': alerts + .map( + (a) => { + 'source': a.source, + 'message': a.message, + 'timestamp': a.time.toIso8601String(), + }, + ) + .toList(), }); } @@ -336,12 +432,14 @@ class PersistenceService { _pendingLogs.clear(); _pendingNetwork.clear(); _pendingErrors.clear(); + _pendingAlerts.clear(); final db = _db; if (db == null) return; try { await db.delete('logs'); await db.delete('network'); await db.delete('errors'); + await db.delete('alerts'); } catch (_) {} } diff --git a/lib/src/services/shared_prefs_provider.dart b/lib/src/services/shared_prefs_provider.dart index c7895a5..7198124 100644 --- a/lib/src/services/shared_prefs_provider.dart +++ b/lib/src/services/shared_prefs_provider.dart @@ -88,14 +88,16 @@ class SharedPrefsProvider implements DatabaseProvider { bool desc = false, String? whereKeyword, }) async { - var keys = _prefs.getKeys().toList()..sort(); + var keys = _prefs.getKeys().toList(); + KeyValueQuery.sortKeys(keys); if (desc) keys = keys.reversed.toList(); if (whereKeyword != null && whereKeyword.isNotEmpty) { final kw = whereKeyword.toLowerCase(); keys = keys.where((k) => k.toLowerCase().contains(kw)).toList(); } final total = keys.length; - final windowed = keys.skip(offset).take(limit).map((k) { + final paging = KeyValueQuery.clampPaging(limit, offset); + final windowed = keys.skip(paging.offset).take(paging.limit).map((k) { final v = _prefs.get(k); return { 'key': k, diff --git a/lib/src/services/sqlite_provider.dart b/lib/src/services/sqlite_provider.dart index f01b637..d968def 100644 --- a/lib/src/services/sqlite_provider.dart +++ b/lib/src/services/sqlite_provider.dart @@ -25,6 +25,28 @@ class SqliteDatabaseProvider implements DatabaseProvider { @override String get name => 'sqlite'; + /// 受支持的数据库文件扩展名 / Supported database file extensions + static const Set _supportedExtensions = {'.db', '.sqlite'}; + + /// 插件自身的持久化数据库文件名,扫描时应排除,避免把它当成用户库打开 + /// (既会触发只读写入错误,也会把内部 logs/network/errors/alerts 表暴露给 UI)。 + /// The plugin's own persistence DB; excluded from scanning so it is not opened + /// as a user database (which would both error on a read-only write and leak + /// internal tables into the Database Viewer). + static const String _ownDbName = 'zero_inspector_kit.db'; + + /// 按扩展名判定是否为数据库文件(大小写不敏感) + /// Whether a path looks like a database file (case-insensitive) + /// + /// 此前用 `endsWith('.db')` 精确匹配,`.DB` / `.SQLite` 扫不到。 + /// Previously matched `endsWith('.db')` exactly, so `.DB` / `.SQLite` were + /// never picked up. + static bool _looksLikeDatabase(String path) { + final dot = path.lastIndexOf('.'); + if (dot < 0) return false; + return _supportedExtensions.contains(path.substring(dot).toLowerCase()); + } + @override Future> getDatabases() async { final databases = []; @@ -37,10 +59,25 @@ class SqliteDatabaseProvider implements DatabaseProvider { final directory = Directory(dirPath); if (!directory.existsSync()) continue; - final files = directory.listSync(recursive: true).whereType(); + // 异步递归列举:此前用 listSync 在主线程同步遍历整个目录树, + // 大目录下直接掉帧(且该扫描每 3s 被存储统计定时器再触发一次)。 + // Async recursive listing: listSync walked the whole tree on the main + // thread and dropped frames on large directories (and the storage-stats + // timer re-triggers this scan every 3s). + final files = await directory + .list(recursive: true) + .where((entity) => entity is File) + .cast() + .toList(); for (final file in files) { - if (file.path.endsWith('.db') || file.path.endsWith('.sqlite')) { + // 跳过插件自身的持久化数据库,避免只读打开报错并污染 UI。 + // Skip the plugin's own persistence DB to avoid a read-only write + // error and leaking internal tables into the Database Viewer. + if (file.path.split(Platform.pathSeparator).last == _ownDbName) { + continue; + } + if (_looksLikeDatabase(file.path)) { if (databases.any((d) => d.path == file.path)) continue; Database? db; @@ -190,7 +227,11 @@ class SqliteDatabaseProvider implements DatabaseProvider { final result = await db.rawQuery( 'SELECT COUNT(*) FROM ${_quoteIdent(tableName)}', ); - return result.first.values.first as int; + // 表在查询瞬间被删 / 损坏时结果为空,直接取 first 会抛 + // `Bad state: No element`。Guard against an empty result (table dropped + // or corrupted mid-query), which used to throw `Bad state: No element`. + if (result.isEmpty || result.first.values.isEmpty) return 0; + return (result.first.values.first as num?)?.toInt() ?? 0; } catch (e, st) { InspectorInternalLog.warning( 'sqlite', @@ -238,8 +279,14 @@ class SqliteDatabaseProvider implements DatabaseProvider { 0 : Sqflite.firstIntValue(await db.rawQuery(countSql, kwArgs)) ?? 0; + // LIMIT / OFFSET 必须是非负整数:SQLite 里 `LIMIT -1` 表示**不限制行数**, + // 会把整张表拉进内存。Clamp to non-negative: in SQLite `LIMIT -1` means + // "no limit" and would pull the whole table into memory. + final safeLimit = limit <= 0 ? 50 : limit; + final safeOffset = offset < 0 ? 0 : offset; final sql = - 'SELECT * FROM $quotedTable$whereClause$orderClause LIMIT $limit OFFSET $offset'; + 'SELECT * FROM $quotedTable$whereClause$orderClause ' + 'LIMIT $safeLimit OFFSET $safeOffset'; final rows = await db.rawQuery(sql, kwArgs); return QueryResult( @@ -273,7 +320,11 @@ class SqliteDatabaseProvider implements DatabaseProvider { return cached; } - final db = await openDatabase(dbPath, readOnly: true, version: 1); + // 只读检查:不传 version,避免 sqflite 尝试写入 `PRAGMA user_version` + // (在已存在 / 只读文件上会抛 "attempt to write a readonly database")。 + // Read-only inspection: no `version` so sqflite won't try to write + // `PRAGMA user_version`, which would fail on an existing / read-only file. + final db = await openDatabase(dbPath, readOnly: true); _connections[dbPath] = db; _accessOrder.add(dbPath); diff --git a/lib/src/services/ws_inspector_service.dart b/lib/src/services/ws_inspector_service.dart index 1183933..6dc110e 100644 --- a/lib/src/services/ws_inspector_service.dart +++ b/lib/src/services/ws_inspector_service.dart @@ -68,6 +68,9 @@ class WsInspectorService extends ChangeNotifier { /// 活跃会话表(sessionId -> 会话)/ Active sessions (sessionId -> session) final Map _sessions = {}; + /// 同时保留的会话数上限(按插入顺序淘汰最旧)/ Max concurrent sessions (oldest evicted) + static const int _maxSessions = 20; + /// 自增计数器,保证 sessionId 唯一 / Monotonic counter for unique session ids int _counter = 0; @@ -78,6 +81,15 @@ class WsInspectorService extends ChangeNotifier { /// 包装一个已建立的 [WebSocket],开启抓取时记录收发帧。 /// Wrap an established [WebSocket]; records frames when capture is enabled. InspectorWebSocket _wrap(WebSocket ws, String url) { + // 会话表此前只在 framesFor() 被惰性淘汰:用户从不点开 WS 详情时, + // _sessions 与每个 session.frames 全部常驻。这里在新建会话时主动淘汰最旧的。 + // Sessions were only evicted lazily in framesFor(), so if the user never + // opened a WS detail view, _sessions and every session.frames stayed + // resident. Now the oldest sessions are evicted up front on creation. + while (_sessions.length >= _maxSessions) { + final oldestId = _sessions.keys.first; + _sessions.remove(oldestId); + } final id = _newId(); _sessions[id] = _WsSession( id: id, @@ -105,7 +117,7 @@ class WsInspectorService extends ChangeNotifier { final type = _frameTypeOf(data); final text = _framePreview(data, type); final byteSize = _byteSizeOf(data); - session.frames.add( + session.addFrame( WsFrame( outgoing: outgoing, type: type, @@ -116,14 +128,16 @@ class WsInspectorService extends ChangeNotifier { ); // 累积到对应网络记录的 responseBody,供详情页/导出查看(向后兼容)。 + // 用会话内的 StringBuffer 摊还拼接,不再每帧复制整个字符串。 + // Accumulate into the network record's responseBody for the detail view & + // export (back-compat). Uses the session StringBuffer so we no longer copy + // the whole string on every frame. // Accumulate into the network record's responseBody for the detail view & export (back-compat). - final existing = InspectorService.instance.findNetworkRequest(id); - final prev = existing?.responseBody?.toString() ?? ''; final arrow = outgoing ? '→' : '←'; - final line = '$arrow $text\n'; + session.appendBody('$arrow $text\n'); InspectorService.instance.updateNetworkRequest( id, - responseBody: prev + line, + responseBody: session.bodyBuffer.toString(), ); } @@ -132,7 +146,7 @@ class WsInspectorService extends ChangeNotifier { final session = _sessions[id]; if (session == null) return; session.closed = true; - session.frames.add( + session.addFrame( WsFrame( outgoing: false, type: WsFrameType.close, @@ -141,11 +155,10 @@ class WsInspectorService extends ChangeNotifier { at: DateTime.now(), ), ); - final existing = InspectorService.instance.findNetworkRequest(id); - final prev = existing?.responseBody?.toString() ?? ''; + session.appendBody('[connection closed]\n'); InspectorService.instance.updateNetworkRequest( id, - responseBody: '$prev[connection closed]\n', + responseBody: session.bodyBuffer.toString(), ); } @@ -251,7 +264,48 @@ class _WsSession { /// 是否已关闭 / Whether the connection is closed bool closed = false; + /// 累积的帧文本(供网络详情页 / 导出查看)/ Accumulated frame text (detail view / export) + /// + /// 此前每次写帧都做 `prev + line` 的整串复制:高频 WS 下每帧复制整个 32KB + /// 预览串,是全项目最热的一条 O(n²) 路径。改为 [StringBuffer] 追加 + 超限时 + /// 一次性裁剪,把摊还代价降到 O(1)。 + /// Previously each frame did `prev + line`, copying the whole 32KB preview + /// string — the hottest O(n²) path in the project under high-frequency WS. + /// Now appends into a StringBuffer and truncates only when over the cap, + /// bringing the amortized cost down to O(1). + final StringBuffer bodyBuffer = StringBuffer(); + + /// 当前累积文本长度 / Current accumulated text length + int bodyLength = 0; + _WsSession({required this.id, required this.url, required this.startTime}); + + /// 追加一段帧文本并在超限时保留尾部 / Append frame text, keeping the tail when over the cap + void appendBody(String text) { + bodyBuffer.write(text); + bodyLength += text.length; + if (bodyLength <= _maxSessionBodyChars) return; + final full = bodyBuffer.toString(); + final keep = full.substring(full.length - _maxSessionBodyChars); + bodyBuffer + ..clear() + ..write(keep); + bodyLength = keep.length; + } + + /// 单会话累积文本上限(字符)/ Per-session accumulated text cap (chars) + static const int _maxSessionBodyChars = 32 * 1024; + + /// 单会话帧数上限(环形,超出丢弃最旧)/ Per-session frame cap (ring, oldest dropped) + static const int _maxSessionFrames = 500; + + /// 追加一帧,超出帧数上限时丢弃最旧的 / Append a frame, dropping the oldest past the cap + void addFrame(WsFrame frame) { + frames.add(frame); + if (frames.length > _maxSessionFrames) { + frames.removeAt(0); + } + } } /// 单帧记录(WebSocket 抓取) / A single captured WebSocket frame diff --git a/lib/src/ui/floating_button.dart b/lib/src/ui/floating_button.dart index 21e9605..eae67cd 100644 --- a/lib/src/ui/floating_button.dart +++ b/lib/src/ui/floating_button.dart @@ -123,7 +123,19 @@ class _FloatingInspectorButtonState extends State } /// 未读告警数变化 → 触发重绘红点 / Unread alert change → repaint red dot - void _onUnreadChanged() => setState(() {}); + /// + /// 去重:此前任何一次 notify 都重建整棵树(独立模式下连带整个 + /// InspectorPanel 子树),而红点只依赖未读数本身。 + /// Deduped: any notify used to rebuild the whole subtree (in standalone mode + /// that includes the entire InspectorPanel), while the badge only depends on + /// the unread count itself. + int? _lastUnread; + void _onUnreadChanged() { + final current = AlertService.instance.unreadCount.value; + if (current == _lastUnread) return; + _lastUnread = current; + if (mounted) setState(() {}); + } /// 球体中心内容:有未读告警时显示红色数字,否则显示默认图标。 /// Ball center: red count when unread, otherwise the default icon. diff --git a/lib/src/ui/fps_viewer.dart b/lib/src/ui/fps_viewer.dart index be63dbe..033671f 100644 --- a/lib/src/ui/fps_viewer.dart +++ b/lib/src/ui/fps_viewer.dart @@ -341,14 +341,20 @@ class _FpsViewerState extends State { ], ), const SizedBox(height: 12), + // RepaintBoundary 隔离图表重绘,避免连带同 layer 的整张卡片一起 repaint。 + // 图表每秒随采样多次重绘,是页面里最热的自绘区域。 + // Isolate chart repaints so the whole card in the same layer is not + // repainted with it — the chart redraws several times per second. SizedBox( height: 100, width: double.infinity, - child: CustomPaint( - painter: _FpsLineChartPainter( - values: history, - lineColor: InspectorColors.info, - gridColor: InspectorColors.divider, + child: RepaintBoundary( + child: CustomPaint( + painter: _FpsLineChartPainter( + values: history, + lineColor: InspectorColors.info, + gridColor: InspectorColors.divider, + ), ), ), ), @@ -382,7 +388,9 @@ class _FpsViewerState extends State { /// 构建掉帧列表卡片 / Build janky frame list card Widget _buildJankyListCard() { final service = FpsService.instance; - final jankyFrames = service.frameRecords.where((f) => f.isJanky).toList(); + final jankyFrames = service.frameRecords + .where((f) => f.isJanky(service.jankThresholdUs)) + .toList(); return Container( padding: const EdgeInsets.all(16), @@ -508,6 +516,16 @@ class _FpsViewerState extends State { fontFamily: 'monospace', ), ), + const SizedBox(width: 8), + Text( + 'build ${(frame.buildDurationUs / 1000).toStringAsFixed(1)} / ' + 'raster ${(frame.rasterDurationUs / 1000).toStringAsFixed(1)}', + style: TextStyle( + color: InspectorColors.textHint, + fontSize: 10, + fontFamily: 'monospace', + ), + ), const Spacer(), Text( _formatTime(frame.timestamp), @@ -663,6 +681,11 @@ class _FpsLineChartPainter extends CustomPainter { @override bool shouldRepaint(covariant _FpsLineChartPainter oldDelegate) { - return oldDelegate.values != values; + // 颜色也要参与比较:此前只比 values,配色变化不会触发重绘。 + // Colors must be compared too; only `values` was checked, so a theme color + // change never triggered a repaint. + return oldDelegate.values != values || + oldDelegate.lineColor != lineColor || + oldDelegate.gridColor != gridColor; } } diff --git a/lib/src/ui/inspector_panel.dart b/lib/src/ui/inspector_panel.dart index d99f56e..7c67954 100644 --- a/lib/src/ui/inspector_panel.dart +++ b/lib/src/ui/inspector_panel.dart @@ -152,8 +152,18 @@ class _InspectorPanelState extends State super.dispose(); } - /// 异常聚合变化 → 仅刷新标签栏上的错误红点(错误本身不频繁)/ Error count changed + /// 异常聚合变化 → 仅刷新标签栏上的错误红点 / Error count changed + /// + /// 与 [_onMonitorChanged] 一样做去重:此前每次 ErrorService notify 都重建 + /// 整个面板(含当前 Tab 页),而红点只依赖记录条数。 + /// Deduped like [_onMonitorChanged]: every ErrorService notify used to + /// rebuild the whole panel (including the current tab), while the badge only + /// depends on the record count. + int? _lastErrorCount; void _onErrorChanged() { + final current = ErrorService.instance.errors.length; + if (current == _lastErrorCount) return; + _lastErrorCount = current; if (mounted) setState(() {}); } diff --git a/lib/src/ui/memory_trend_chart.dart b/lib/src/ui/memory_trend_chart.dart index 6bb4dc2..7051802 100644 --- a/lib/src/ui/memory_trend_chart.dart +++ b/lib/src/ui/memory_trend_chart.dart @@ -312,17 +312,23 @@ class _MemoryTrendChartState extends State { child: SizedBox( height: 100, width: double.infinity, - child: CustomPaint( - painter: _LineChartPainter( - values: values, - maxValue: safeMax, - lineColor: widget.metric.color, - fillColor: widget.metric.color.withValues( - alpha: 0.2, + // RepaintBoundary 隔离自绘图表,避免每 500ms 的重绘 + // 扩散到同 layer 的父级卡片 / 整页。 + // Isolate the custom-painted chart so its 500ms + // repaints don't spread to the parent card / page. + child: RepaintBoundary( + child: CustomPaint( + painter: _LineChartPainter( + values: values, + maxValue: safeMax, + lineColor: widget.metric.color, + fillColor: widget.metric.color.withValues( + alpha: 0.2, + ), + backgroundColor: InspectorColors.surface, + gridColor: InspectorColors.divider, + highlightIndex: _touchedIndex, ), - backgroundColor: InspectorColors.surface, - gridColor: InspectorColors.divider, - highlightIndex: _touchedIndex, ), ), ), @@ -782,6 +788,11 @@ class _LineChartPainter extends CustomPainter { oldDelegate.maxValue != maxValue || oldDelegate.lineColor != lineColor || oldDelegate.fillColor != fillColor || + // 背景与网格色此前未参与比较,主题色变化不会重绘。 + // Background and grid colors were not compared, so a color change never + // triggered a repaint. + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.gridColor != gridColor || oldDelegate.highlightIndex != highlightIndex; } } diff --git a/lib/src/ui/network_viewer.dart b/lib/src/ui/network_viewer.dart index bb86a4d..687b994 100644 --- a/lib/src/ui/network_viewer.dart +++ b/lib/src/ui/network_viewer.dart @@ -12,10 +12,6 @@ import 'theme/inspector_theme.dart'; import 'widgets/widgets.dart'; import 'inspector_toast.dart'; -import 'package:http/http.dart' as http; - -import '../utils/network_replay.dart'; - /// 状态码分组(用于筛选维度,公开以便单测)/ Status-code groups (for the /// filter dimension; exposed publicly so it can be unit-tested). /// 定义在 [network_request.dart] 的 [StatusGroup]。 @@ -277,6 +273,13 @@ class _NetworkViewerState extends State { /// (empty = no status filter) final Set _statusFilters = {}; + /// 选中的 host 集合(空 = 不按域名筛选)/ Selected hosts (empty = no host filter) + /// + /// 此前完全没有按域名筛选,而一个 App 通常同时请求多个后端,只能靠关键词 + /// 硬搜。There was no host filter at all, even though an app typically talks + /// to several backends — keyword search was the only option. + final Set _hostFilters = {}; + /// 拦截状态筛选(null = 全部;true = 已修改;false = 未修改)。 /// Interception-status filter (null = all; true = modified; false = unmodified). bool? _modifiedFilter; @@ -328,33 +331,6 @@ class _NetworkViewerState extends State { /// 在 App 内重放该请求(用捕获的方法/URL/头/体重新发出一次)。 /// Replay the request in-app (re-issue with captured method/URL/headers/body). - Future _replayRequest(NetworkRequest r) async { - final client = http.Client(); - try { - final request = buildReplayRequest(r); - final stop = DateTime.now(); - final streamed = await client.send(request); - final response = await http.Response.fromStream(streamed); - final elapsed = DateTime.now().difference(stop); - final preview = response.body.length > 200 - ? '${response.body.substring(0, 200)}…' - : response.body; - if (mounted) { - InspectorToast.show( - context, - 'Replayed → ${response.statusCode} in ${elapsed.inMilliseconds}ms\n$preview', - duration: const Duration(seconds: 4), - ); - } - } catch (e) { - if (mounted) { - InspectorToast.show(context, 'Replay failed: $e'); - } - } finally { - client.close(); - } - } - void _saveRule(RequestInterceptorRule rule) { InspectorService.instance.addInterceptorRule(rule); setState(() { @@ -458,8 +434,11 @@ class _NetworkViewerState extends State { }); }, ), + // 用轻量计数 getter,不要为取 .length 而拷贝整个列表。 + // Use the lightweight count getter instead of materializing + // the whole list just to read its length. InspectorCountBadge( - '${InspectorService.instance.networkRequests.length}', + '${InspectorService.instance.networkRequestCount}', ), const SizedBox(width: 6), Text( @@ -541,7 +520,8 @@ class _NetworkViewerState extends State { InspectorIconButton( icon: Icons.replay_rounded, tooltip: 'Replay request', - onTap: () => _replayRequest(_selectedRequest!), + onTap: () => + showReplayEditor(context, _selectedRequest!), ), InspectorIconButton( icon: Icons.content_copy_rounded, @@ -591,6 +571,36 @@ class _NetworkViewerState extends State { } }, ), + // HAR 导出入口:`netToHar` 早已实现,但 UI 从未传过 + // format,终端用户完全拿不到 HAR。 + // HAR export entry: `netToHar` has existed for a while, + // but the UI never passed `format`, so users could not + // reach it at all. + InspectorIconButton( + icon: Icons.file_download_rounded, + tooltip: 'Export as HAR', + onTap: () async { + final messenger = Overlay.of( + context, + rootOverlay: true, + ); + final requests = + InspectorService.instance.networkRequests; + if (requests.isEmpty) return; + await ExportService.instance.exportNetAndShare( + requests, + maskSensitive: _maskSensitive, + format: 'har', + ); + if (mounted) { + InspectorToast.showOn( + messenger, + 'Sharing ${requests.length} requests as HAR' + '${_maskSensitive ? ' (sensitive hidden)' : ''}', + ); + } + }, + ), if (_selectionMode) InspectorIconButton( icon: Icons.copy_all_rounded, @@ -834,12 +844,28 @@ class _NetworkViewerState extends State { onTap: () => setState(() => _modifiedFilter = false), ), ]), + // 按域名筛选:列表里出现过的 host 各给一个 chip。 + // Host filter: one chip per host actually present in the list. + if (_availableHosts.isNotEmpty) + wrap('Host', [ + for (final host in _availableHosts) + _filterChip( + label: host, + selected: _hostFilters.contains(host), + onTap: () => setState(() { + _hostFilters.contains(host) + ? _hostFilters.remove(host) + : _hostFilters.add(host); + }), + ), + ]), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () => setState(() { _methodFilters.clear(); _statusFilters.clear(); + _hostFilters.clear(); _modifiedFilter = null; }), child: Text( @@ -897,6 +923,7 @@ class _NetworkViewerState extends State { String _filteredKeyword = ''; Set _filteredMethods = const {}; Set _filteredStatus = const {}; + Set _filteredHosts = const {}; bool? _filteredModified; List _filterRequests(List requests) { @@ -904,6 +931,7 @@ class _NetworkViewerState extends State { _filteredKeyword == _searchKeyword && _sameSet(_filteredMethods, _methodFilters) && _sameSet(_filteredStatus, _statusFilters) && + _sameSet(_filteredHosts, _hostFilters) && _filteredModified == _modifiedFilter; if (_filteredRequestsCache != null && identical(requests, _filteredRequestsSource) && @@ -937,6 +965,14 @@ class _NetworkViewerState extends State { }).toList(); } + // 按 host / 域名筛选(多选,空集合 = 不过滤) + // Filter by host (multi-select; empty set = no filter) + if (_hostFilters.isNotEmpty) { + result = result + .where((req) => _hostFilters.contains(_getHost(req.url))) + .toList(); + } + // 按拦截状态筛选 / Filter by interception status if (_modifiedFilter != null) { result = result @@ -950,6 +986,7 @@ class _NetworkViewerState extends State { _filteredKeyword = _searchKeyword; _filteredMethods = {..._methodFilters}; _filteredStatus = {..._statusFilters}; + _filteredHosts = {..._hostFilters}; _filteredModified = _modifiedFilter; return result; } @@ -1231,6 +1268,19 @@ class _NetworkViewerState extends State { } } + /// 当前列表中出现过的 host(按字母序,最多 8 个,避免 chip 刷屏) + /// Hosts present in the current list (alphabetical, capped at 8 so the chips + /// don't flood the panel) + List get _availableHosts { + final hosts = {}; + for (final r in InspectorService.instance.networkRequests) { + final host = _getHost(r.url); + if (host.isNotEmpty) hosts.add(host); + } + final sorted = hosts.toList()..sort(); + return sorted.take(8).toList(); + } + Widget _buildRequestDetail(NetworkRequest request) { final hasRule = InspectorService.instance.findMatchingRule( @@ -1334,7 +1384,7 @@ class _NetworkViewerState extends State { if (request.headers != null) _buildDetailSection('Headers', _formatJson(request.headers)), if (request.body != null) - _buildDetailSection('Body', _formatJson(request.body)), + _buildBodySection('Body', _formatJson(request.body)), const SizedBox(height: 20), _buildSectionTitle( 'Response', @@ -1347,7 +1397,7 @@ class _NetworkViewerState extends State { if (request.method == 'WS') _WsFramesView(request: request) else if (request.responseBody != null) - _buildDetailSection('Body', _formatJson(request.responseBody)), + _buildBodySection('Body', _formatJson(request.responseBody)), const SizedBox(height: 16), _buildTimelineCard(request), ], @@ -1519,7 +1569,21 @@ class _NetworkViewerState extends State { ); } - Widget _buildDetailSection(String title, String content) { + Widget _buildDetailSection(String title, String content) => + _buildBodySection(title, content, forcePlainText: true); + + /// 详情分区:JSON 走折叠树、图片走缩略图预览,其余按等宽纯文本。 + /// Detail section: JSON renders as a collapsible tree, images as a thumbnail + /// preview, everything else as monospaced plain text. + Widget _buildBodySection( + String title, + String content, { + bool forcePlainText = false, + }) { + final trimmed = content.trimLeft(); + final isJson = + !forcePlainText && (trimmed.startsWith('{') || trimmed.startsWith('[')); + return Padding( padding: const EdgeInsets.only(bottom: 10), child: Column( @@ -1534,6 +1598,9 @@ class _NetworkViewerState extends State { ), ), const SizedBox(height: 5), + // 图片优先:二进制响应现在以 base64 承载,可直接渲染。 + // Images first: binary responses now carry base64 and render directly. + if (!forcePlainText) _buildImagePreviewIfAny(content), Container( width: double.infinity, padding: const EdgeInsets.all(10), @@ -1544,21 +1611,34 @@ class _NetworkViewerState extends State { ), border: Border.all(color: InspectorColors.border, width: 0.5), ), - child: Text( - content, - style: TextStyle( - color: InspectorColors.textPrimary, - fontSize: 11.5, - fontFamily: 'monospace', - height: 1.4, - ), - ), + child: isJson + ? InspectorJsonView(text: content) + : Text( + content, + style: TextStyle( + color: InspectorColors.textPrimary, + fontSize: 11.5, + fontFamily: 'monospace', + height: 1.4, + ), + ), ), ], ), ); } + /// 图片响应的缩略图预览(非图片时不渲染)/ Thumbnail preview (nothing when not an image) + Widget _buildImagePreviewIfAny(String content) { + if (InspectorImagePreview.imageBytesOf(content) == null) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: InspectorImagePreview(body: content), + ); + } + Color _getStatusColor(int status) { if (status >= 200 && status < 300) return InspectorColors.statusSuccess; if (status >= 300 && status < 400) return InspectorColors.statusRedirect; @@ -1984,8 +2064,14 @@ class _InterceptorRulePanelState extends State { ), ), const SizedBox(height: 5), - TextField( - controller: TextEditingController(text: widget.request.url), + // 用 TextFormField(initialValue:) 而非 TextField(controller:): + // 后者在 build 里创建 controller 且从不 dispose,每次重建泄漏一个。 + // TextFormField owns and disposes its own controller. + // Use TextFormField(initialValue:) instead of + // TextField(controller:): the latter created a controller inside + // build and never disposed it, leaking one per rebuild. + TextFormField( + initialValue: widget.request.url, readOnly: true, enabled: false, style: TextStyle( diff --git a/lib/src/ui/widgets/inspector_image_preview.dart b/lib/src/ui/widgets/inspector_image_preview.dart new file mode 100644 index 0000000..b6d6e20 --- /dev/null +++ b/lib/src/ui/widgets/inspector_image_preview.dart @@ -0,0 +1,140 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; + +import '../theme/inspector_theme.dart'; + +/// 响应体图片预览 / Response-body image preview +/// +/// 此前响应体一律按文本渲染,`Content-Type: image/*` 的响应在面板里只是一串 +/// 乱码。这里对可识别的图片字节做缩略图渲染。 +/// Response bodies were always rendered as text, so an `image/*` response was +/// just a wall of mojibake. Recognizable image bytes now render as a thumbnail. +class InspectorImagePreview extends StatelessWidget { + /// 响应体文本 / Response body text + final String body; + + const InspectorImagePreview({super.key, required this.body}); + + /// 从响应体里提取图片字节;不是图片时返回 null。 + /// Extract image bytes from the body; returns null when it isn't an image. + /// + /// 支持两种承载方式 / Two carriers are supported: + /// - 检查器对非 UTF-8 响应写入的 `[Binary response …]\nbase64: <…>` + /// The `[Binary response …]\nbase64: <…>` form written by the inspector for + /// non-UTF-8 responses. + /// - `data:image/…;base64,…` 形式的 data URI / A `data:image/…;base64,…` URI + static Uint8List? imageBytesOf(String body) { + final trimmed = body.trim(); + if (trimmed.isEmpty) return null; + + String? encoded; + if (trimmed.startsWith('data:image/')) { + final comma = trimmed.indexOf(','); + if (comma < 0) return null; + encoded = trimmed.substring(comma + 1); + } else { + const marker = 'base64:'; + final idx = trimmed.indexOf(marker); + if (idx < 0) return null; + encoded = trimmed.substring(idx + marker.length).trim(); + } + + Uint8List bytes; + try { + bytes = base64Decode(encoded); + } catch (_) { + return null; + } + return _looksLikeImage(bytes) ? bytes : null; + } + + /// 按魔数嗅探常见图片格式 / Sniff common image formats by magic bytes + static bool _looksLikeImage(Uint8List bytes) { + if (bytes.length < 8) return false; + // PNG + if (bytes[0] == 0x89 && + bytes[1] == 0x50 && + bytes[2] == 0x4E && + bytes[3] == 0x47) { + return true; + } + // JPEG + if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) return true; + // GIF + if (bytes[0] == 0x47 && + bytes[1] == 0x49 && + bytes[2] == 0x46 && + bytes[3] == 0x38) { + return true; + } + // WebP: "RIFF" .... "WEBP" + if (bytes[0] == 0x52 && + bytes[1] == 0x49 && + bytes[2] == 0x46 && + bytes[3] == 0x46 && + bytes[8] == 0x57 && + bytes[9] == 0x45 && + bytes[10] == 0x42 && + bytes[11] == 0x50) { + return true; + } + // BMP + if (bytes[0] == 0x42 && bytes[1] == 0x4D) return true; + return false; + } + + @override + Widget build(BuildContext context) { + final bytes = imageBytesOf(body); + if (bytes == null) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon( + Icons.image_rounded, + size: 13, + color: InspectorColors.textHint, + ), + const SizedBox(width: 4), + Text( + 'Preview · ${(bytes.length / 1024).toStringAsFixed(1)} KB', + style: TextStyle( + color: InspectorColors.textHint, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: InspectorColors.surface, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: InspectorColors.border, width: 1), + ), + child: Center( + child: Image.memory( + bytes, + fit: BoxFit.contain, + // 超出可视高度时限制,避免大图把详情页顶飞。 + // Cap the height so a huge image can't blow up the detail page. + height: 220, + errorBuilder: (_, _, _) => Text( + 'Unable to decode this image', + style: TextStyle(color: InspectorColors.textHint, fontSize: 11), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/ui/widgets/inspector_json_view.dart b/lib/src/ui/widgets/inspector_json_view.dart new file mode 100644 index 0000000..12863a1 --- /dev/null +++ b/lib/src/ui/widgets/inspector_json_view.dart @@ -0,0 +1,442 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../theme/inspector_theme.dart'; + +/// JSON 折叠树查看器 / Collapsible JSON tree viewer +/// +/// 此前响应体只能用 `JsonEncoder.withIndent` 做纯缩进的纯文本展示:没有语法 +/// 高亮、不能折叠、无法搜索,深层嵌套的响应只能靠肉眼扫。 +/// Response bodies were previously rendered as indented plain text only: no +/// syntax highlighting, no collapsing, no search — digging into nested payloads +/// was pure eyeballing. +/// +/// 本组件提供 / This widget provides: +/// - 按类型着色的键与值 / type-colored keys and values +/// - 逐节点折叠 / 展开 / per-node collapse / expand +/// - 全部展开 / 折叠 / expand-all / collapse-all +/// - 关键字搜索(命中节点自动展开并高亮)/ keyword search (auto-expands hits) +class InspectorJsonView extends StatefulWidget { + /// 原始 JSON 文本(解析失败时按纯文本渲染) + /// Raw JSON text (falls back to plain text when it cannot be parsed) + final String text; + + /// 最大渲染节点数,超出后其余节点折叠为占位(避免超大响应卡死 UI) + /// Max rendered nodes; the rest collapse into a placeholder so huge payloads + /// can't freeze the UI. + final int maxNodes; + + const InspectorJsonView({super.key, required this.text, this.maxNodes = 500}); + + @override + State createState() => _InspectorJsonViewState(); +} + +class _InspectorJsonViewState extends State { + /// 折叠状态:节点路径 -> 是否已展开 / Collapse state: node path -> expanded + final Map _expanded = {}; + + /// 搜索输入框控制器 / Search field controller + final TextEditingController _searchController = TextEditingController(); + + /// 搜索关键字 / Search keyword + String _keyword = ''; + + /// 是否全展开 / Whether everything is expanded + bool _allExpanded = false; + + /// 已渲染节点计数(本帧)/ Rendered node counter (per build) + int _renderedNodes = 0; + + /// 解析失败时保留的错误信息 / Error message retained when parsing fails + Object? _parseError; + + /// 解析结果 / Parsed value + Object? _root; + + @override + void initState() { + super.initState(); + _parse(); + } + + @override + void didUpdateWidget(covariant InspectorJsonView oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text) { + _expanded.clear(); + _parse(); + } + } + + void _parse() { + _parseError = null; + _root = null; + try { + _root = jsonDecode(widget.text); + } catch (e) { + _parseError = e; + } + } + + @override + Widget build(BuildContext context) { + if (_parseError != null) { + // 不是合法 JSON:退回等宽纯文本,不丢内容。 + // Not valid JSON: fall back to monospaced plain text, losing nothing. + return _buildPlainText(widget.text); + } + _renderedNodes = 0; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + _buildToolbar(), + const SizedBox(height: 6), + Flexible( + child: SingleChildScrollView( + child: _buildNode( + path: r'$', + keyLabel: null, + value: _root, + depth: 0, + ), + ), + ), + ], + ); + } + + Widget _buildToolbar() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon( + Icons.data_object_rounded, + size: 13, + color: InspectorColors.textHint, + ), + const SizedBox(width: 4), + Text( + 'JSON', + style: TextStyle( + color: InspectorColors.textHint, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + _toolbarButton('Expand all', _expandAll), + const SizedBox(width: 4), + _toolbarButton('Collapse all', _collapseAll), + ], + ), + const SizedBox(height: 4), + // 搜索:命中节点会被高亮,并自动全部展开以便定位。 + // Search: matching nodes are highlighted and everything auto-expands so + // hits are reachable. + SizedBox( + height: 28, + child: TextField( + controller: _searchController, + style: TextStyle(color: InspectorColors.textPrimary, fontSize: 11), + onChanged: (v) => setState(() { + _keyword = v; + if (v.isNotEmpty) _allExpanded = true; + }), + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, + ), + hintText: 'Search keys / values', + hintStyle: TextStyle( + color: InspectorColors.textHint, + fontSize: 11, + ), + prefixIcon: Icon( + Icons.search_rounded, + size: 14, + color: InspectorColors.textHint, + ), + prefixIconConstraints: const BoxConstraints( + minWidth: 26, + minHeight: 26, + ), + filled: true, + fillColor: InspectorColors.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: InspectorColors.border, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: InspectorColors.border, width: 1), + ), + ), + ), + ), + ], + ); + } + + Widget _toolbarButton(String label, VoidCallback onTap) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + child: Text( + label, + style: TextStyle(color: InspectorColors.info, fontSize: 10), + ), + ), + ); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _expandAll() => setState(() { + _allExpanded = true; + _expanded.clear(); + }); + + void _collapseAll() => setState(() { + _allExpanded = false; + _expanded.clear(); + _keyword = ''; + _searchController.clear(); + }); + + Widget _buildPlainText(String text) { + return SelectableText( + text, + style: TextStyle( + color: InspectorColors.textPrimary, + fontSize: 11.5, + fontFamily: 'monospace', + height: 1.45, + ), + ); + } + + /// 是否为可展开的容器 / Whether the value is an expandable container + bool _isBranch(Object? value) => value is Map || value is List; + + Widget _buildNode({ + required String path, + required String? keyLabel, + required Object? value, + required int depth, + }) { + // 节点预算:超限时给出占位而不是继续展开整棵树。 + // Node budget: past the cap, render a placeholder instead of the subtree. + _renderedNodes++; + if (_renderedNodes > widget.maxNodes) { + return Padding( + padding: EdgeInsets.only(left: depth * 12.0 + 4), + child: Text( + '… (truncated, ${widget.maxNodes}+ nodes)', + style: TextStyle( + color: InspectorColors.textHint, + fontSize: 11, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + if (!_isBranch(value)) { + return _buildLeaf(path: path, keyLabel: keyLabel, value: value); + } + + final expanded = _allExpanded || (_expanded[path] ?? depth == 0); + final Iterable> entries; + final int count; + final bool isMap; + if (value case final Map map) { + isMap = true; + count = map.length; + entries = map.entries.map((e) => MapEntry(e.key.toString(), e.value)); + } else { + final list = value as List; + isMap = false; + count = list.length; + entries = list.asMap().entries.map( + (e) => MapEntry('[${e.key}]', e.value), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => setState(() { + _allExpanded = false; + _expanded[path] = !(_expanded[path] ?? depth == 0); + }), + child: Padding( + padding: EdgeInsets.only(left: depth * 12.0, top: 2, bottom: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + expanded + ? Icons.keyboard_arrow_down_rounded + : Icons.keyboard_arrow_right_rounded, + size: 14, + color: InspectorColors.textHint, + ), + if (keyLabel != null) ...[ + const SizedBox(width: 2), + _highlighted(keyLabel, InspectorColors.methodGet), + const SizedBox(width: 2), + Text( + ':', + style: TextStyle( + color: InspectorColors.textHint, + fontSize: 11.5, + ), + ), + ], + const SizedBox(width: 4), + Text( + isMap ? '{$count}' : '[$count]', + style: TextStyle( + color: InspectorColors.textHint, + fontSize: 11, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + ), + if (expanded) + ...entries.map( + (e) => _buildNode( + path: '$path.${e.key}', + keyLabel: e.key, + value: e.value, + depth: depth + 1, + ), + ), + ], + ); + } + + Widget _buildLeaf({ + required String path, + required String? keyLabel, + required Object? value, + }) { + final color = switch (value) { + null => InspectorColors.textHint, + String() => InspectorColors.success, + num() => InspectorColors.warning, + bool() => InspectorColors.methodPut, + _ => InspectorColors.textPrimary, + }; + final display = switch (value) { + null => 'null', + String() => '"$value"', + _ => value.toString(), + }; + return Padding( + padding: const EdgeInsets.only(top: 2, bottom: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 12.0 * 1 + 14), + if (keyLabel != null) ...[ + _highlighted(keyLabel, InspectorColors.methodGet), + const SizedBox(width: 2), + Text( + ':', + style: TextStyle(color: InspectorColors.textHint, fontSize: 11.5), + ), + const SizedBox(width: 4), + ], + Expanded( + child: Text( + display, + style: TextStyle( + color: color, + fontSize: 11.5, + fontFamily: 'monospace', + height: 1.4, + ), + ), + ), + ], + ), + ); + } + + /// 搜索命中时高亮关键字 / Highlight the search keyword on hits + Widget _highlighted(String text, Color baseColor) { + if (_keyword.isEmpty) { + return Text( + text, + style: TextStyle( + color: baseColor, + fontSize: 11.5, + fontFamily: 'monospace', + ), + ); + } + final lower = text.toLowerCase(); + final needle = _keyword.toLowerCase(); + final idx = lower.indexOf(needle); + if (idx < 0) { + return Text( + text, + style: TextStyle( + color: baseColor, + fontSize: 11.5, + fontFamily: 'monospace', + ), + ); + } + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: text.substring(0, idx), + style: TextStyle( + color: baseColor, + fontSize: 11.5, + fontFamily: 'monospace', + ), + ), + TextSpan( + text: text.substring(idx, idx + needle.length), + style: TextStyle( + color: InspectorColors.backgroundStart, + backgroundColor: InspectorColors.accent, + fontSize: 11.5, + fontFamily: 'monospace', + ), + ), + TextSpan( + text: text.substring(idx + needle.length), + style: TextStyle( + color: baseColor, + fontSize: 11.5, + fontFamily: 'monospace', + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/ui/widgets/inspector_replay_editor.dart b/lib/src/ui/widgets/inspector_replay_editor.dart new file mode 100644 index 0000000..0cae859 --- /dev/null +++ b/lib/src/ui/widgets/inspector_replay_editor.dart @@ -0,0 +1,336 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +import '../../models/network_request.dart'; +import '../../utils/network_replay.dart'; +import '../theme/inspector_theme.dart'; + +/// 可编辑重放编辑器 / Editable replay editor +/// +/// URL、Header、Body 均为只读(按既定设计不可修改)。仅允许编辑 URL 的 +/// 查询参数(请求参数),重放时会用修改后的参数重建 URL,其余部分原样重发。 +/// URL, headers and body are read-only by design. Only the URL query +/// parameters may be edited; on replay the URL is rebuilt from the edited +/// params while everything else is re-sent verbatim. +class InspectorReplayEditor extends StatefulWidget { + /// 原始被重放的请求 / The original captured request + final NetworkRequest request; + + const InspectorReplayEditor({super.key, required this.request}); + + @override + State createState() => _InspectorReplayEditorState(); +} + +class _InspectorReplayEditorState extends State { + /// 可编辑的查询参数行 / Editable query-parameter rows + final List<_ParamRow> _params = []; + bool _sending = false; + String? _responseText; + + @override + void initState() { + super.initState(); + // 从原始 URL 解析查询参数作为初始可编辑行 / seed from the captured URL + final original = {}; + try { + original.addAll(Uri.parse(widget.request.url).queryParameters); + } catch (_) { + // 解析失败时留空,仍可手动添加 / fall back to empty if URL is unparseable + } + if (original.isEmpty) { + _params.add(_ParamRow('', '')); + } else { + for (final entry in original.entries) { + _params.add(_ParamRow(entry.key, entry.value)); + } + } + } + + @override + void dispose() { + for (final p in _params) { + p.key.dispose(); + p.value.dispose(); + } + super.dispose(); + } + + /// 把当前参数行重新组装回 URL 的 query / Rebuild the URL query from the rows + String _rebuildUrl() { + final map = {}; + for (final p in _params) { + final k = p.key.text.trim(); + if (k.isEmpty) continue; + map[k] = p.value.text; + } + try { + final uri = Uri.parse(widget.request.url); + return uri.replace(queryParameters: map).toString(); + } catch (_) { + return widget.request.url; + } + } + + Future _send() async { + if (_sending) return; + setState(() { + _sending = true; + _responseText = null; + }); + final client = http.Client(); + try { + final req = buildReplayRequest(widget.request, url: _rebuildUrl()); + final start = DateTime.now(); + final streamed = await client.send(req); + final response = await http.Response.fromStream(streamed); + final elapsed = DateTime.now().difference(start); + final preview = response.body.length > 400 + ? '${response.body.substring(0, 400)}…' + : response.body; + if (mounted) { + setState(() { + _responseText = + 'Status: ${response.statusCode} · ${elapsed.inMilliseconds} ms\n\n' + '$preview'; + }); + } + } catch (e) { + if (mounted) { + setState(() => _responseText = 'Replay failed: $e'); + } + } finally { + client.close(); + if (mounted) setState(() => _sending = false); + } + } + + @override + Widget build(BuildContext context) { + final r = widget.request; + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + left: 14, + right: 14, + top: 12, + ), + decoration: BoxDecoration( + color: InspectorColors.backgroundStart, + borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), + border: Border(top: BorderSide(color: InspectorColors.border)), + ), + child: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + Icons.replay_rounded, + size: 16, + color: InspectorColors.accent, + ), + const SizedBox(width: 6), + Text( + 'Replay Request', + style: TextStyle( + color: InspectorColors.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Close', + style: TextStyle( + color: InspectorColors.textSecondary, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + _label('URL (read-only)'), + _readOnlyBox(r.url), + const SizedBox(height: 10), + _label('Query Parameters (editable)'), + for (final p in _params) _paramRow(p), + TextButton.icon( + onPressed: () => setState(() => _params.add(_ParamRow('', ''))), + icon: Icon( + Icons.add_rounded, + size: 14, + color: InspectorColors.info, + ), + label: Text( + 'Add parameter', + style: TextStyle(color: InspectorColors.info, fontSize: 11), + ), + ), + const SizedBox(height: 10), + _label('Headers (read-only)'), + _readOnlyBlock( + r.headers == null || r.headers!.isEmpty + ? '(none)' + : r.headers!.entries + .map((e) => '${e.key}: ${e.value}') + .join('\n'), + ), + const SizedBox(height: 10), + _label('Body (read-only)'), + _readOnlyBlock(r.body?.toString() ?? '(none)'), + const SizedBox(height: 12), + ElevatedButton( + onPressed: _sending ? null : _send, + style: ElevatedButton.styleFrom( + backgroundColor: InspectorColors.accent, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text(_sending ? 'Sending…' : 'Send'), + ), + if (_responseText != null) ...[ + const SizedBox(height: 12), + _label('Response'), + _readOnlyBlock( + _responseText!, + textColor: InspectorColors.textPrimary, + ), + ], + const SizedBox(height: 12), + ], + ), + ), + ), + ); + } + + Widget _label(String text) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + text, + style: TextStyle( + color: InspectorColors.textSecondary, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ); + + /// 单行只读展示(如 URL)/ A single-line read-only box (e.g. URL) + Widget _readOnlyBox(String text) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: InspectorColors.surface, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: InspectorColors.border, width: 0.5), + ), + child: SelectableText( + text, + style: TextStyle( + color: InspectorColors.textSecondary, + fontSize: 11.5, + fontFamily: 'monospace', + height: 1.4, + ), + ), + ); + + /// 多行只读展示(如 headers / body / response)/ + /// A multi-line read-only block (e.g. headers / body / response) + Widget _readOnlyBlock(String text, {Color? textColor}) => Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: InspectorColors.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: InspectorColors.border, width: 0.5), + ), + child: SelectableText( + text, + style: TextStyle( + color: textColor ?? InspectorColors.textSecondary, + fontSize: 11.5, + fontFamily: 'monospace', + height: 1.4, + ), + ), + ); + + Widget _paramRow(_ParamRow p) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + Expanded(flex: 2, child: _textField(p.key)), + const SizedBox(width: 6), + Expanded(flex: 3, child: _textField(p.value)), + IconButton( + icon: Icon( + Icons.remove_circle_outline_rounded, + size: 16, + color: InspectorColors.textHint, + ), + onPressed: () => setState(() { + p.key.dispose(); + p.value.dispose(); + _params.remove(p); + if (_params.isEmpty) _params.add(_ParamRow('', '')); + }), + ), + ], + ), + ); + + Widget _textField(TextEditingController c) => TextField( + controller: c, + style: TextStyle( + color: InspectorColors.textPrimary, + fontSize: 11.5, + fontFamily: 'monospace', + ), + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + filled: true, + fillColor: InspectorColors.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: InspectorColors.border, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: InspectorColors.border, width: 1), + ), + ), + ); +} + +/// 可编辑的查询参数行:持有自己的两个 [TextEditingController] +/// An editable query-parameter row: owns its two [TextEditingController]s +class _ParamRow { + final TextEditingController key; + final TextEditingController value; + _ParamRow(String k, String v) + : key = TextEditingController(text: k), + value = TextEditingController(text: v); +} + +/// 显示可编辑重放弹窗 / Show the editable replay sheet +void showReplayEditor(BuildContext context, NetworkRequest request) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => InspectorReplayEditor(request: request), + ); +} diff --git a/lib/src/ui/widgets/widgets.dart b/lib/src/ui/widgets/widgets.dart index 10ec802..af6d52a 100644 --- a/lib/src/ui/widgets/widgets.dart +++ b/lib/src/ui/widgets/widgets.dart @@ -2,3 +2,6 @@ export 'inspector_icon_button.dart'; export 'inspector_count_badge.dart'; export 'inspector_search_field.dart'; export 'inspector_state_views.dart'; +export 'inspector_json_view.dart'; +export 'inspector_image_preview.dart'; +export 'inspector_replay_editor.dart'; diff --git a/lib/src/utils/inspector_version.dart b/lib/src/utils/inspector_version.dart new file mode 100644 index 0000000..46f03a5 --- /dev/null +++ b/lib/src/utils/inspector_version.dart @@ -0,0 +1,16 @@ +/// 检查器版本号 / Inspector version +/// +/// 此前 HAR 导出把 `creator.version` 硬编码成 `'1.2'`,与 `pubspec.yaml` +/// 完全脱钩,发版时必然忘记更新。这里收敛为单一常量,并由 +/// `test/version_consistency_test.dart` 断言它与 `pubspec.yaml` 一致。 +/// HAR export used to hardcode `creator.version` as `'1.2'`, fully decoupled +/// from `pubspec.yaml` and therefore always stale at release time. It is now a +/// single constant asserted against `pubspec.yaml` by +/// `test/version_consistency_test.dart`. +class InspectorVersion { + InspectorVersion._(); + + /// 当前版本号,必须与 `pubspec.yaml` 的 `version` 字段一致 + /// Current version; must match the `version` field in `pubspec.yaml` + static const String value = '1.11.0'; +} diff --git a/lib/src/utils/network_replay.dart b/lib/src/utils/network_replay.dart index 58b886e..f1b3efe 100644 --- a/lib/src/utils/network_replay.dart +++ b/lib/src/utils/network_replay.dart @@ -2,17 +2,65 @@ import 'package:http/http.dart' as http; import '../models/network_request.dart'; +/// 重放时会被剔除的请求头(小写)/ Headers dropped on replay (lowercase) +/// +/// 两类 / Two categories: +/// - 检查器自身的关联头 [replayExcludedHeaders],此前会被原样重放出去。 +/// The inspector's own correlation header, which used to be replayed as-is. +/// - 由 `http` 包自行计算管理的逐跳头(重放旧值会导致长度不匹配 / 请求挂起)。 +/// Hop-by-hop headers managed by the `http` package itself; replaying stale +/// values causes length mismatches or hangs. +const Set replayExcludedHeaders = { + 'x-inspector-request-id', + 'host', + 'content-length', +}; + /// 根据已捕获的 [NetworkRequest] 重建一个可发送的 [http.Request], /// 用于「重放 / 重试」功能(在 App 内重新发出同一请求)。 /// Rebuild a sendable [http.Request] from a captured [NetworkRequest] for replay/retry. -http.Request buildReplayRequest(NetworkRequest r) { - final req = http.Request(r.method.toUpperCase(), Uri.parse(r.url)); - if (r.headers != null) req.headers.addAll(r.headers!); - if (r.body != null) { - if (r.body is List) { - req.bodyBytes = r.body as List; +/// +/// 可选覆盖 / Optional overrides(用于「编辑后重放」): +/// - [url]:替换目标 URL / replace the target URL +/// - [headers]:合并进请求头(同键覆盖)/ merged into the headers (same key wins) +/// - [body]:替换请求体;传 `null` 表示沿用原 body, +/// 传空字符串表示清空 body / replace the body; `null` keeps the original, +/// an empty string clears it +http.Request buildReplayRequest( + NetworkRequest r, { + String? url, + Map? headers, + Object? body, +}) { + final target = Uri.parse(url ?? r.url); + final req = http.Request(r.method.toUpperCase(), target); + + if (r.headers != null) { + for (final entry in r.headers!.entries) { + if (replayExcludedHeaders.contains(entry.key.toLowerCase())) continue; + req.headers[entry.key] = entry.value; + } + } + if (headers != null) { + for (final entry in headers.entries) { + if (replayExcludedHeaders.contains(entry.key.toLowerCase())) continue; + req.headers[entry.key] = entry.value; + } + } + + final effectiveBody = body ?? r.body; + if (effectiveBody != null) { + if (effectiveBody is List) { + req.bodyBytes = effectiveBody; } else { - req.body = r.body.toString(); + final text = effectiveBody.toString(); + if (text.isEmpty) { + // 显式清空 body(不设置 bodyBytes,避免发出 `Content-Length: 0` 的空壳)。 + // Explicitly clear the body (leave bodyBytes unset rather than sending + // an empty `Content-Length: 0` shell). + } else { + req.body = text; + } } } return req; diff --git a/lib/src/utils/sensitive_data.dart b/lib/src/utils/sensitive_data.dart new file mode 100644 index 0000000..947aca8 --- /dev/null +++ b/lib/src/utils/sensitive_data.dart @@ -0,0 +1,252 @@ +/// 敏感数据脱敏 / Sensitive data masking +/// +/// 集中处理导出 / 复制 / 分享前的脱敏,覆盖三个此前完全没覆盖的泄漏面: +/// Centralizes masking for export / copy / share, covering three surfaces that +/// were previously not masked at all: +/// +/// 1. **请求头** —— 此前是硬编码的 7 个名字且只做全等匹配,`x-api-key`、 +/// `x-auth-token-v2` 这类变体全部漏网。 +/// **Headers** — previously 7 hardcoded names matched by equality only, so +/// variants like `x-api-key` / `x-auth-token-v2` slipped through. +/// 2. **URL query** —— `?token=...` / `?sig=...` 此前原样导出。 +/// **URL query** — `?token=...` / `?sig=...` were exported verbatim. +/// 3. **请求 / 响应体** —— JSON 里的 `password` / `access_token` 此前原样导出。 +/// **Request / response body** — `password` / `access_token` inside JSON were +/// exported verbatim. +/// +/// 宿主 App 可通过 [extraSensitiveHeaderNames] / [extraSensitiveHeaderPatterns] +/// / [extraSensitiveKeys] 追加自己的敏感字段(例如内部私有头)。 +/// Host apps can register their own sensitive fields (e.g. private internal +/// headers) via [extraSensitiveHeaderNames] / [extraSensitiveHeaderPatterns] / +/// [extraSensitiveKeys]. +class SensitiveData { + SensitiveData._(); + + /// 脱敏占位符 / Mask placeholder + static const String placeholder = '***'; + + /// 精确匹配的敏感请求头(小写)/ Sensitive header names matched exactly (lowercase) + static const Set sensitiveHeaderNames = { + 'authorization', + 'proxy-authorization', + 'www-authenticate', + 'cookie', + 'set-cookie', + 'x-auth-token', + 'x-csrf-token', + 'x-xsrf-token', + 'x-api-key', + 'api-key', + 'authentication', + 'x-session-token', + 'x-refresh-token', + 'x-amz-security-token', + 'x-goog-api-key', + 'x-firebase-auth', + 'x-shopify-access-token', + // 检查器自身的关联头:既不该出现在面板里,也不该被导出 / 重放。 + // The inspector's own correlation header: it belongs neither in the panel + // nor in exports / replays. + 'x-inspector-request-id', + }; + + /// 包含匹配的敏感请求头模式(不区分大小写) + /// Sensitive header name patterns matched by containment (case-insensitive) + /// + /// 覆盖 `x-auth-token-v2`、`x-my-secret-header` 这类命名变体。 + /// Covers naming variants such as `x-auth-token-v2` / `x-my-secret-header`. + static final List sensitiveHeaderPatterns = [ + RegExp(r'auth', caseSensitive: false), + RegExp(r'token', caseSensitive: false), + RegExp(r'cookie', caseSensitive: false), + RegExp(r'secret', caseSensitive: false), + RegExp(r'passw(or)?d|passwd|pwd', caseSensitive: false), + RegExp(r'api[-_]?key', caseSensitive: false), + RegExp(r'credential', caseSensitive: false), + RegExp(r'signature|\bsig\b', caseSensitive: false), + RegExp(r'^x-inspector-', caseSensitive: false), + ]; + + /// 精确匹配的敏感键名(用于 URL query 与 JSON body) + /// Sensitive key names matched exactly (for URL query and JSON body) + static final Set sensitiveKeys = { + 'token', + 'access_token', + 'accesstoken', + 'refresh_token', + 'refreshtoken', + 'id_token', + 'idtoken', + 'auth', + 'authorization', + 'password', + 'passwd', + 'pwd', + 'secret', + 'client_secret', + 'api_key', + 'apikey', + 'key', + 'sig', + 'signature', + 'session_token', + 'sessiontoken', + 'csrf', + 'xsrf', + }; + + /// 宿主可追加的敏感请求头名 / Extra sensitive header names contributed by the host + static final Set extraSensitiveHeaderNames = {}; + + /// 宿主可追加的敏感请求头模式 / Extra sensitive header patterns from the host + static final List extraSensitiveHeaderPatterns = []; + + /// 宿主可追加的敏感键名 / Extra sensitive key names from the host + static final Set extraSensitiveKeys = {}; + + /// 判断请求头名是否敏感 / Whether a header name is sensitive + static bool isSensitiveHeader(String name) { + final lower = name.toLowerCase(); + if (sensitiveHeaderNames.contains(lower)) return true; + if (extraSensitiveHeaderNames.contains(lower)) return true; + for (final p in sensitiveHeaderPatterns) { + if (p.hasMatch(name)) return true; + } + for (final p in extraSensitiveHeaderPatterns) { + if (p.hasMatch(name)) return true; + } + return false; + } + + /// 判断数据键名(URL query 参数名 / JSON 字段名)是否敏感 + /// Whether a data key (URL query param / JSON field) is sensitive + static bool isSensitiveKey(String key) { + final lower = key.toLowerCase(); + if (sensitiveKeys.contains(lower)) return true; + if (extraSensitiveKeys.contains(lower)) return true; + return isSensitiveHeader(lower); + } + + /// 遮蔽请求头 Map 中的敏感项 / Mask sensitive entries in a header map + static Map maskHeaders(Map headers) { + return headers.map( + (key, value) => + MapEntry(key, isSensitiveHeader(key) ? placeholder : value), + ); + } + + /// 遮蔽 URL 中的敏感 query 参数值 / Mask sensitive query values in a URL + /// + /// 无法解析或没有 query 时原样返回(不因脱敏而破坏 URL)。 + /// Returns the input unchanged when it cannot be parsed or has no query, so + /// masking never corrupts a URL. + static String maskUrl(String url) { + final uri = Uri.tryParse(url); + if (uri == null) return url; + final query = uri.queryParameters; + if (query.isEmpty) return url; + var changed = false; + final masked = {}; + for (final entry in query.entries) { + if (isSensitiveKey(entry.key)) { + masked[entry.key] = placeholder; + changed = true; + } else { + masked[entry.key] = entry.value; + } + } + if (!changed) return url; + try { + return uri.replace(queryParameters: masked).toString(); + } catch (_) { + // queryParameters 替换失败(如含非常规字符)时退化为字符串级替换。 + // Fall back to string-level replacement when queryParameters rewrite + // fails (e.g. unusual characters). + var out = url; + for (final entry in query.entries) { + if (isSensitiveKey(entry.key)) { + out = out.replaceAll( + '${entry.key}=${Uri.encodeComponent(entry.value)}', + '${entry.key}=$placeholder', + ); + out = out.replaceAll( + '${entry.key}=${entry.value}', + '${entry.key}=$placeholder', + ); + } + } + return out; + } + } + + /// JSON `"key": "value"` 对(值可能是任意 JSON 字面量) + /// JSON `"key": "value"` pairs (value may be any JSON literal) + static final RegExp _jsonPair = RegExp( + r'("(?:[^"\\]|\\.)*")(\s*:\s*)("(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)', + ); + + /// `Bearer ` 形式的凭据 / `Bearer ` credentials + static final RegExp _bearer = RegExp( + r'(Bearer\s+)[A-Za-z0-9\-._~+/=]+', + caseSensitive: false, + ); + + /// JWT(三段 base64url,以 `ey` 开头)/ JWT (three base64url segments, starting with `ey`) + static final RegExp _jwt = RegExp( + r'\bey[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}', + ); + + /// 遮蔽 body 中的敏感内容 / Mask sensitive content in a body + /// + /// 先按 JSON 字段脱敏,再兜底处理 Bearer 凭据与 JWT, + /// 最后失败时原样返回(脱敏不应导致内容丢失)。 + /// Masks JSON fields first, then falls back to Bearer credentials and JWTs, + /// returning the input unchanged on failure (masking must not lose content). + static String maskBody(String body) { + if (body.isEmpty) return body; + try { + var out = body.replaceAllMapped(_jsonPair, (m) { + final rawKey = m.group(1)!; + final key = rawKey.substring(1, rawKey.length - 1); + if (!isSensitiveKey(key)) return m.group(0)!; + final value = m.group(3)!; + // 字符串值替换为带引号的占位符,其余字面量直接替换,保持 JSON 合法。 + // Quote the placeholder for string values, replace literals as-is, so + // the result stays valid JSON. + final maskedValue = value.startsWith('"') ? '"$placeholder"' : 'null'; + return '$rawKey${m.group(2)}$maskedValue'; + }); + out = out.replaceAllMapped(_bearer, (m) => '${m.group(1)}$placeholder'); + out = out.replaceAllMapped(_jwt, (_) => placeholder); + return out; + } catch (_) { + return body; + } + } + + /// 对任意文本做兜底脱敏(Bearer / JWT),用于非 JSON 内容 + /// Best-effort masking (Bearer / JWT) for non-JSON content + static String maskText(String text) { + if (text.isEmpty) return text; + try { + var out = text.replaceAllMapped( + _bearer, + (m) => '${m.group(1)}$placeholder', + ); + out = out.replaceAllMapped(_jwt, (_) => placeholder); + return out; + } catch (_) { + return text; + } + } + + /// 按内容类型选择脱敏策略:JSON 走字段级,其余走文本级。 + /// Pick the masking strategy by content: field-level for JSON, text otherwise. + static String maskBodyByContent(String body) { + final trimmed = body.trimLeft(); + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + return maskBody(body); + } + return maskText(body); + } +} diff --git a/lib/zero_inspector_kit.dart b/lib/zero_inspector_kit.dart index 814f037..b1d74ab 100644 --- a/lib/zero_inspector_kit.dart +++ b/lib/zero_inspector_kit.dart @@ -31,6 +31,9 @@ export 'src/utils/environment.dart'; export 'src/utils/inspector_internal_log.dart'; export 'src/utils/inspector_log.dart'; export 'src/utils/memory_leak_tracking.dart'; +export 'src/utils/sensitive_data.dart'; +export 'src/utils/inspector_version.dart'; +export 'src/utils/network_replay.dart'; // 暴露更多公共能力,便于外部直接引用(无需 import 内部 lib/src/ 路径)。 // Expose additional public capabilities so consumers can reference them directly @@ -63,8 +66,10 @@ import 'src/ui/inspector_panel.dart'; import 'src/models/log_entry.dart'; import 'src/services/database_provider.dart'; import 'src/services/sqlite_provider.dart'; +import 'src/services/fps_service.dart'; import 'src/services/inspector_service.dart'; import 'src/services/error_service.dart'; +import 'src/services/alert_service.dart'; import 'src/services/persistence_service.dart'; import 'src/services/memory_inspector_service.dart'; import 'src/services/shared_prefs_provider.dart'; @@ -222,6 +227,35 @@ class ZeroInspectorKit { if (logs.isNotEmpty) InspectorService.instance.restoreLogs(logs); final errors = await PersistenceService.instance.loadErrors(); if (errors.isNotEmpty) ErrorService.instance.restore(errors); + final alerts = await PersistenceService.instance.loadAlerts(); + if (alerts.isNotEmpty) AlertService.instance.restore(alerts); + } + + /// 完整释放检查器占用的资源 / Fully release the resources held by the inspector + /// + /// 各服务都是进程级单例,此前**没有任何一处**调用过它们的停止 / 释放方法, + /// 导致即使从未打开面板:持久化每 2s 的刷盘 Timer 常驻、`FlutterError.onError` + /// 被永久接管、内存 / FPS 定时器持续运行、throttle notifier 从不释放。 + /// Every service is a process-wide singleton, and **nothing** ever called their + /// stop / dispose methods. Even with the panel never opened, the persistence + /// flush Timer kept ticking every 2s, `FlutterError.onError` stayed hijacked, + /// the memory / FPS timers kept running and the throttle notifiers were never + /// released. + /// + /// 典型场景:集成测试结束、或宿主 App 想在运行时彻底关闭调试采集(如隐私 + /// 合规的"停止采集"开关)。调用后可用 [init] 重新启用。 + /// Typical use: at the end of integration tests, or when the host app wants to + /// switch off debug collection at runtime (e.g. a privacy "stop collecting" + /// toggle). Call [init] again to re-enable. + static Future dispose() async { + InspectorLogInterceptor.instance.stop(); + InspectorHttpInterceptor.instance.stop(); + ErrorService.instance.uninstall(); + FpsService.instance.stop(); + MemoryInspectorService.instance.stopMonitoring(); + InspectorService.instance.disposeService(); + await PersistenceService.instance.dispose(); + _initialized = false; } /// 等待 Flutter binding 就绪后再初始化持久化,失败按递增间隔自动重试。 diff --git a/pubspec.yaml b/pubspec.yaml index 3ebac1c..8a72612 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: zero_inspector_kit description: One-line in-app developer console for Flutter — HTTP/WebSocket/gRPC inspection, logs, database, memory leaks, FPS jank, routes, alerts, bug reports. Auto-disabled in release. -version: 1.10.1 +version: 1.11.0 topics: - logging - debug diff --git a/test/fps_service_test.dart b/test/fps_service_test.dart index d5e04b5..fabc220 100644 --- a/test/fps_service_test.dart +++ b/test/fps_service_test.dart @@ -56,17 +56,60 @@ void main() { group('FrameRecord / 帧记录', () { test('isJanky detects frames over 16ms / 超过16ms为掉帧', () { - final janky = FrameRecord(timestamp: 0, durationUs: 20000); - final smooth = FrameRecord(timestamp: 0, durationUs: 10000); - expect(janky.isJanky, isTrue); - expect(smooth.isJanky, isFalse); + final janky = FrameRecord( + timestamp: 0, + buildDurationUs: 5000, + rasterDurationUs: 15000, + durationUs: 20000, + ); + final smooth = FrameRecord( + timestamp: 0, + buildDurationUs: 4000, + rasterDurationUs: 6000, + durationUs: 10000, + ); + expect(janky.isJanky(), isTrue); + expect(smooth.isJanky(), isFalse); }); test('isJanky boundary at 16ms / 16ms 边界测试', () { - final atBoundary = FrameRecord(timestamp: 0, durationUs: 16000); - final justOver = FrameRecord(timestamp: 0, durationUs: 16001); - expect(atBoundary.isJanky, isFalse); - expect(justOver.isJanky, isTrue); + final atBoundary = FrameRecord( + timestamp: 0, + buildDurationUs: 6000, + rasterDurationUs: 10000, + durationUs: 16000, + ); + final justOver = FrameRecord( + timestamp: 0, + buildDurationUs: 6000, + rasterDurationUs: 10001, + durationUs: 16001, + ); + expect(atBoundary.isJanky(), isFalse); + expect(justOver.isJanky(), isTrue); + }); + + test('phased durations sum to total / 分项耗时之和等于总耗时', () { + final r = FrameRecord( + timestamp: 0, + buildDurationUs: 4000, + rasterDurationUs: 6000, + durationUs: 10000, + ); + expect(r.buildDurationUs + r.rasterDurationUs, equals(r.durationUs)); + }); + + test('isJanky honors adaptive threshold / 接受自适应阈值', () { + // 120Hz 设备每帧预算约 8.3ms:16ms 的帧在高刷下应判为掉帧。 + // On a 120Hz device the per-frame budget is ~8.3ms: a 16ms frame is jank. + final frame = FrameRecord( + timestamp: 0, + buildDurationUs: 8000, + rasterDurationUs: 8000, + durationUs: 16000, + ); + expect(frame.isJanky(8333), isTrue); + expect(frame.isJanky(), isFalse); // 默认 16ms 阈值不判掉帧 }); }); diff --git a/test/network_replay_test.dart b/test/network_replay_test.dart index bbf0aa2..9e20be3 100644 --- a/test/network_replay_test.dart +++ b/test/network_replay_test.dart @@ -30,4 +30,46 @@ void main() { final req = buildReplayRequest(r); expect(req.bodyBytes, [1, 2, 3]); }); + + test('buildReplayRequest honors an edited query-param URL', () { + // 重放编辑器仅允许修改查询参数:用编辑后的 URL 重建请求时, + // method / headers / body 保持原样,URL 指向新查询。 + // The replay editor only lets you edit query params: when replayed with the + // rebuilt URL, method / headers / body stay verbatim and the URL carries the + // new query. + final r = NetworkRequest( + id: '1', + method: 'get', + url: 'https://example.com/api?page=1&q=old', + headers: {'X-Test': '1'}, + requestTime: 0, + ); + const editedUrl = 'https://example.com/api?page=2&q=new&sort=asc'; + final req = buildReplayRequest(r, url: editedUrl); + expect(req.method, 'GET'); + expect(req.url.toString(), editedUrl); + expect(req.headers['X-Test'], '1'); + }); + + test('buildReplayRequest keeps original URL when none edited', () { + final r = NetworkRequest( + id: '1', + method: 'get', + url: 'https://example.com/api?a=1', + requestTime: 0, + ); + final req = buildReplayRequest(r); + expect(req.url.toString(), 'https://example.com/api?a=1'); + }); + + test('rebuilt query preserves order and updated values', () { + // 与编辑器 _rebuildUrl 等价的纯函数校验:编辑后参数顺序与值正确。 + // Pure-function check mirroring the editor's _rebuildUrl: order and values + // survive the rebuild. + final map = {'page': '2', 'q': 'new', 'sort': 'asc'}; + final rebuilt = Uri.parse( + 'https://example.com/api?page=1', + ).replace(queryParameters: map).toString(); + expect(rebuilt, 'https://example.com/api?page=2&q=new&sort=asc'); + }); } diff --git a/website/pages/Alerts.md b/website/pages/Alerts.md index 11c3f59..33c6d6c 100644 --- a/website/pages/Alerts.md +++ b/website/pages/Alerts.md @@ -42,4 +42,5 @@ The alert system lets you define rules that proactively surface problems across ## Notes / 备注 - Alerts are evaluated in-process and shown in the developer console only; they do not send data off-device / 告警仅在本机开发者控制台内评估与展示,不会将数据发送到设备外 +- **Persisted to disk (since v1.11.0)**: triggered alerts are flushed to the SQLite ring buffer and replay into the Alerts tab after an app restart, and are included in the session archive export / **已落盘持久化(v1.11.0 起)**:已触发的告警会写入 SQLite 环形缓冲,重启后回放至 Alerts 标签页,并包含在会话存档导出中 - The alert system is tree-shaken out in release builds, like the rest of the inspector / 与检查器其余部分一样,告警系统在 release 构建中被 tree-shake 移除 diff --git a/website/pages/Errors.md b/website/pages/Errors.md index cbd5ea0..3efce2f 100644 --- a/website/pages/Errors.md +++ b/website/pages/Errors.md @@ -6,7 +6,7 @@ > > **v1.9.0 起可用** -The Errors tab surfaces "the same crash happening repeatedly". When the inspector is running, `ErrorService` hooks `FlutterError.onError` (keeping the default red-screen / console behavior) and aggregates every exception by **type + stack signature**. Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces. +The Errors tab surfaces "the same crash happening repeatedly". When the inspector is running, `ErrorService` hooks both `FlutterError.onError` and `PlatformDispatcher.onError` (keeping the default red-screen / console behavior for both) and aggregates every exception by **type + stack signature**. Repeated instances of the same crash merge into one record that shows how many times it occurred and when it was first/last seen — instead of flooding the log with hundreds of identical stack traces. Errors 标签页用来快速发现"同一处崩溃反复出现"的问题。检查器运行时,`ErrorService` 会接管 `FlutterError.onError`(保留默认的红色报错与控制台行为),把每次异常按**类型 + 堆栈签名**去重聚合:同一处崩溃的多次发生会合并为一条记录,展示累计次数与首末次时间——而不是用成百上千条相同的堆栈刷屏日志。 @@ -15,6 +15,7 @@ Errors 标签页用来快速发现"同一处崩溃反复出现"的问题。检 | Source / 来源 | How / 方式 | |---------------|------------| | Flutter framework errors / Flutter 框架异常 | Hooks `FlutterError.onError` (default handler kept) / 接管 `FlutterError.onError`(保留默认处理) | +| Framework-boundary & platform-channel errors / 框架边界外与平台通道异常 | `PlatformDispatcher.onError` (with save/restore) / `PlatformDispatcher.onError`(接管 + 还原) | | Uncaught async errors / 未捕获异步异常 | `runZonedGuarded` inside `runAppWithInspector()` / `runAppWithInspector()` 内部的 `runZonedGuarded` | | Manual reports / 手动上报 | `ErrorService.instance.report(exception, stackTrace)` — for gRPC / custom protocols / your own error paths / 用于 gRPC / 自定义协议或你自己的错误通道 | @@ -75,7 +76,7 @@ ErrorService.instance.clear(); | `report(exception, stack, [context])` | Manually report an exception / 手动上报异常 | | `restore(records)` | Restore persisted records (replay on launch; existing dedup ids are skipped) / 恢复持久化记录(启动回放;已存在的去重 id 跳过) | | `clear()` | Clear all aggregated records / 清空全部记录 | -| `install()` / `uninstall()` | Hook / restore `FlutterError.onError` / 接管 / 还原 `FlutterError.onError` | +| `install()` / `uninstall()` | Hook / restore `FlutterError.onError` **and** `PlatformDispatcher.onError` / 接管 / 还原 `FlutterError.onError` 与 `PlatformDispatcher.onError` | ### ErrorRecord / 异常记录 @@ -89,9 +90,9 @@ ErrorService.instance.clear(); ## Session Persistence / 会话持久化 -Errors — together with logs and network requests — are asynchronously flushed to a local SQLite **ring buffer** (`zero_inspector_kit.db`). On the next launch, **logs and aggregated errors replay into their tabs**, so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the **storage icon** in the panel header to open the **Persisted data** manager: see the current row counts, **export the full session archive** as JSON, or clear the disk. See [Configuration](Configuration) (PersistenceService section) for details and the tuning parameters. +Errors — together with logs, network requests **and alerts** — are asynchronously flushed to a local SQLite **ring buffer** (`zero_inspector_kit.db`). On the next launch, **logs, aggregated errors and alerts replay into their tabs**, so a crash you saw yesterday is still inspectable today even though the panel was never opened; network requests stay archived on disk for later export. Use the **storage icon** in the panel header to open the **Persisted data** manager: see the current row counts, **export the full session archive** as JSON, or clear the disk. See [Configuration](Configuration) (PersistenceService section) for details and the tuning parameters. -异常与日志、网络请求一起被异步写入本地 SQLite **环形缓冲**(`zero_inspector_kit.db`)。**下次启动时,日志与聚合异常会回放入各自标签页**——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的**存储图标**可打开 **Persisted data** 管理弹层:查看当前行数、**导出完整会话存档** JSON,或清空磁盘。详见 [Configuration](Configuration)(PersistenceService 一节)的参数说明。 +异常与日志、网络请求、告警一起被异步写入本地 SQLite **环形缓冲**(`zero_inspector_kit.db`)。**下次启动时,日志、聚合异常与告警会回放入各自标签页**——即使昨天从没打开过面板,今天依然能复盘当时的崩溃现场;网络请求保留在磁盘存档,供之后导出。点击面板头部的**存储图标**可打开 **Persisted data** 管理弹层:查看当前行数、**导出完整会话存档** JSON,或清空磁盘。详见 [Configuration](Configuration)(PersistenceService 一节)的参数说明。 ## Enable / Disable / 开关 diff --git a/website/pages/FPS-Viewer.md b/website/pages/FPS-Viewer.md index 8650623..447f4fc 100644 --- a/website/pages/FPS-Viewer.md +++ b/website/pages/FPS-Viewer.md @@ -38,7 +38,7 @@ When turned off, all callbacks and timers are cancelled, leaving no residual ove ### 1. Current Stats / 当前统计 - **Current FPS** — Updated every 500ms / 每 500ms 更新 -- **Jank Rate** — Percentage of janky frames (>16ms) / 卡顿帧占比(>16ms 视为卡顿) +- **Jank Rate** — Percentage of janky frames (duration exceeds the per-frame budget, derived from the display refresh rate) / 卡顿帧占比(帧耗时超过由屏幕刷新率换算的逐帧预算) - **Total Frame Count** — All frames captured since start / 自启动以来的总帧数 - **Total Janky Count** — All janky frames captured / 自启动以来的总卡顿帧数 - **Last Frame Janky** — Whether the most recent frame was janky / 最近一帧是否卡顿 @@ -56,11 +56,11 @@ When turned off, all callbacks and timers are cancelled, leaving no residual ove ### 3. Janky Frame List / 卡顿帧列表 -- Lists frames exceeding 16ms duration / 列出耗时超过 16ms 的帧 -- Each item shows frame duration and timestamp / 每项显示帧耗时和时间戳 +- Lists janky frames — duration exceeds the per-frame budget, derived from the display refresh rate (~16.7ms at 60Hz, ~8.3ms at 120Hz) / 列出卡顿帧——帧耗时超过由屏幕刷新率换算的逐帧预算(60Hz≈16.7ms、120Hz≈8.3ms) +- Each item shows frame duration and timestamp, plus the **build / raster split** (`buildDurationUs` + `rasterDurationUs`) so you can tell whether build or GPU raster is the bottleneck / 每项显示帧耗时、时间戳,以及 **build / raster 分项**(`buildDurationUs` + `rasterDurationUs`),便于判断是构建还是 GPU 光栅化卡顿 - Helps identify specific jank spikes / 帮助定位具体卡顿点 -列出耗时超过 16ms 的帧,每项显示帧耗时和时间戳,帮助定位具体卡顿点。 +列出卡顿帧(帧耗时超过由屏幕刷新率换算的逐帧预算,60Hz≈16.7ms、120Hz≈8.3ms),每项显示帧耗时、时间戳与 build / raster 分项,帮助定位具体卡顿点。 ### 4. Reset / 重置 @@ -75,9 +75,13 @@ FPS monitoring uses Flutter's `WidgetsBinding.instance.addTimingsCallback` to re FPS 监控使用 Flutter 的 `WidgetsBinding.instance.addTimingsCallback` 接收引擎的帧时序信息。该回调是**批量**的——每次调用可能返回多个 `FrameTiming` 对象,因此在循环内部为每帧单独记录时间戳,确保 FPS 计算准确。 -**Jank threshold / 卡顿阈值**: A frame is considered janky if its duration exceeds 16ms (the 60 FPS budget of ~16.67ms per frame). +**Jank threshold / 卡顿阈值**: A frame is janky when its **total** duration (build + raster) exceeds the per-frame budget — `1000ms ÷ refreshRate` (≈16.7ms at 60Hz, ≈8.3ms at 120Hz). Since v1.11.0 the budget is **derived from the device's refresh rate**, so high-refresh (120Hz) screens are held to a tight ≈8.3ms instead of being forgiven by a fixed 16ms. -**卡顿阈值**:帧耗时超过 16ms(60 FPS 每帧预算约 16.67ms)即视为卡顿。 +**卡顿阈值**:帧的**总**耗时(build + raster)超过逐帧预算即视为卡顿——`1000ms ÷ 刷新率`(60Hz≈16.7ms、120Hz≈8.3ms)。自 v1.11.0 起预算按**设备刷新率**换算,高刷(120Hz)屏会被严格约束在 ≈8.3ms,而非被固定 16ms 放过。 + +**Phased timing / 分阶段耗时**: Each `FrameRecord` carries `buildDurationUs` and `rasterDurationUs` (GPU raster); the janky-frame list shows the build / raster split per row. + +**分阶段耗时**:每条 `FrameRecord` 都带有 `buildDurationUs` 与 `rasterDurationUs`(GPU 光栅化),掉帧列表项会展示 build / raster 分项。 ## Programmatic Control (Optional) / 编程式控制(可选) @@ -127,7 +131,7 @@ Singleton service extending `ChangeNotifier`. | `currentFps` | double | Current FPS (updated every 500ms) / 当前 FPS(每 500ms 更新) | | `jankRate` | double | Jank rate as percentage / 卡顿率(百分比) | | `totalFrameCount` | int | Total frames captured / 总帧数 | -| `totalJankyCount` | int | Total janky frames captured (>16ms) / 总卡顿帧数(>16ms) | +| `totalJankyCount` | int | Total janky frames captured (per-frame budget) / 总卡顿帧数(按逐帧预算) | | `lastFrameJanky` | bool | Whether the most recent frame was janky / 最近一帧是否卡顿 | | `fpsHistory` | `List` | Recent 60 FPS values (unmodifiable) / 最近 60 个 FPS 值(不可变) | | `frameRecords` | `List` | Recent frame records (unmodifiable, up to 3600) / 最近帧记录(不可变,最多 3600 条) | diff --git a/website/pages/Network-Inspector.md b/website/pages/Network-Inspector.md index 979eff5..7cf4860 100644 --- a/website/pages/Network-Inspector.md +++ b/website/pages/Network-Inspector.md @@ -153,6 +153,24 @@ From the request detail view, tap **Copy as cURL** to copy the request as a read 在请求详情页点击 **Copy as cURL**,即可将请求复制为可直接运行的 cURL 命令(方法、URL、请求头、请求体),并遵循上方的敏感字段遮蔽开关。 +## Replay Editor / 重放编辑器 + +> **Available since v1.11.0** +> +> **v1.11.0 起可用** + +Every request detail view has a **replay** action (↻ icon). It opens an editable sheet that lets you **re-issue the captured request in-app** and preview the response — handy for quickly re-running an endpoint without leaving the app. + +每个请求详情页都有**重放**操作(↻ 图标)。它会打开一个可编辑弹层,让你**在 App 内重新发出该请求**并预览响应——无需离开应用即可快速复跑某个接口。 + +- **Only the URL query parameters are editable** — add / edit / remove `key=value` rows; the URL is rebuilt from them on send / **仅 URL 查询参数可编辑**——可增删改 `key=value` 行,发送时用其重建 URL +- **URL, request headers and request body are read-only** by design (the captured values are re-sent verbatim) / **URL、请求头与请求体按设计只读**(以捕获到的原值原样重发) +- On **Send**, the request is re-issued with the original method / headers / body and the edited query string; the sheet shows the returned status code, elapsed time, and a response preview / 点击 **Send** 后用原始方法 / 请求头 / 请求体加上修改后的查询串重新发出,弹层展示返回状态码、耗时与响应预览 + +> This is distinct from the interceptor (which builds a durable modification rule applied to *future* matching requests). The replay editor makes a **one-off** call with editable query params — including GET requests, whose params can't be changed by the interceptor. +> +> 它与拦截器不同(拦截器生成作用于*后续*匹配请求的持久规则)。重放编辑器发起的是**一次性**调用,仅查询参数可改——包括 GET 请求(其参数拦截器无法修改)。 + ## Status Code Colors / 状态码颜色 | Range | Color | Description |