1
0

Compare commits

...

83 Commits

Author SHA1 Message Date
2f8fd8bd9c refactor: 将 memory checker 重命名为 mem
- 类型标识符 memory → mem
- 类名 MemoryChecker → MemChecker
- 内部类型名统一 Memory* → Mem*
- 内部函数名统一 *Memory* → *Mem*
- 目录重命名 memory/ → mem/(源码、测试、文档)
- 配置键 memory: → mem:
- 重新生成 probe-config.schema.json
- 保留中文"内存"用户提示

破坏性变更:无向后兼容
2026-05-27 18:19:29 +08:00
3390eb5e8d fix: 强化 CPU/memory checker 错误处理、timeout 遵守和快照校验
- Memory checker: reader 与 ctx.signal race,abort 返回 memory/timeout,reject 保持 memory/snapshot
- CPU checker: 第二次快照异常返回 cpu/snapshot,计算前校验空数组/核心数不一致/非有限值/负 delta
- CPU 计算: 零 delta 安全处理,observation 不含 NaN/Infinity
- 文档: CPU 互补描述修正,Memory timeout 约束说明
- 测试: +18 覆盖 timeout、异常和边界输入
2026-05-27 16:33:39 +08:00
145bb8fd04 feat: 新增 memory checker,支持系统级内存和交换空间检测 2026-05-27 00:05:06 +08:00
358f8d011a chore: 放宽权限配置 & 归档 openspec 变更记录 2026-05-26 22:36:33 +08:00
c2dcfab80c feat: 新增本机 CPU checker
- 新增 type: cpu checker,基于 os.cpus() 两次快照计算 CPU 使用率
- 配置项:sampleDuration(默认 1s)、includePerCore(默认 false)
- expect 字段:usagePercent、idlePercent、maxCoreUsagePercent、minCoreUsagePercent、durationMs
- idlePercent 与 usagePercent 互补恒等于 100,百分比范围 0-100
- logicalCoreCount 仅输出到 observation,不作为 expect 字段
- 不暴露 userPercent / systemPercent
- 语义校验禁止 sampleDuration >= timeout
- 支持 AbortSignal 超时取消
- 完整测试覆盖:schema、validate、normalize、resolve、calculate、execute、expect、config-loader
- 新增用户文档 docs/user/checkers/cpu.md
- 更新 checker 索引、配置类型列表、示例配置和 schema
2026-05-26 22:34:57 +08:00
f38286d74d feat: 添加 pi 配置文件
- 添加 .pi/mcp.json 配置 tdesign-mcp-server
- 添加 .pi/extensions/pi-permission-system/config.json 权限配置
- 更新 .gitignore 以跟踪 .pi/mcp.json 和 .pi/extensions
2026-05-26 15:37:27 +08:00
08b61cbf47 refactor: ProbeEngine 调度引擎重写为 per-target setTimeout 链
将 per-group setInterval + groupBy 调度模式改为 per-target setTimeout 链,
实现 catch-up 语义(超时后立即补执行)、AbortController 优雅停止、
循环内错误隔离和 overrun warn 日志。
移除 groupBy/probeGroup/timers,新增 sleep/runLoop/runOnce。
新增 croner 依赖供后续 cron 表达式支持使用。
2026-05-26 11:35:06 +08:00
c120690cf1 feat: target 时间配置校验,interval 最小 10s,timeout 不大于 interval
在配置加载阶段新增通用 target 时间字段语义校验:
- interval 解析后不得小于 10s
- timeout 解析后不得大于同一 target 的 interval
- 默认值(30s / 10s)参与校验
- 变量引用先解析再校验
- 格式错误优先于关系错误,避免级联提示
2026-05-25 17:48:51 +08:00
77c6015b3a refactor: 将 checker normalize 职责下沉到各 runner 目录
- 新增 CheckerDefinition.normalize 必需方法,typecheck 兜底遗漏实现
- 新增 expect/normalize.ts 共享 helper(compactExpect、normalizeValue、
  normalizeContent、normalizeKeyed)
- 为 HTTP、Cmd、DB、TCP、UDP、ICMP、LLM、WS、DNS 各新增独立 normalize.ts
- 简化 normalizer.ts:删除所有 checker type switch,改为 registry 委托
- 修复 DNS authoring 简写 bug:durationMs、valueCount、result 等字段
  现可通过完整加载链路
- 新增 DNS 回归测试和 registry 级合同测试
- 更新 docs/development/checker.md:补充 normalize 规范、文件结构、
  测试要求和 checklist
2026-05-25 16:16:41 +08:00
c1db793073 feat: WS checker,支持可达性检测和单次请求-响应交互验证 2026-05-25 14:13:43 +08:00
714b635aef docs: 重构文档体系
- 合并 DEVELOPMENT.md 至 docs/development/README.md
- 合并 CONTRIBUTING.md 至 docs/development/checker.md
- 合并 build-release.md 至 release.md
- 合并 testing-quality.md 内容至各专题文档
- 合并 status-model.md 至 expectations.md
- 新增 docs/user/README.md 用户入口
- 简化 docs/README.md 文档路由
- 各专题文档新增适用场景和更新触发条件
- 更新 openspec/config.yaml 文档规则
2026-05-25 10:47:52 +08:00
a6504d5a62 docs: 重构文档体系 2026-05-24 20:18:18 +08:00
483cdc596b feat: DNS checker,自研 codec/transport,支持 system/server 双模式,UDP/TCP + TC fallback 2026-05-24 17:06:22 +08:00
4f33fba793 feat: 动态粒度趋势图,支持 auto bucket 选择 + P95 延迟 + 状态条 2026-05-23 23:53:18 +08:00
6601ab458d chore: 清理过时 specs,新增 schemas 目录,更新审查提示词 2026-05-23 22:24:08 +08:00
cfca03b4d6 refactor: 规范审查与重组,合并细粒度规范,清理过时内容
- 合并 20+ 细粒度 spec 为粗粒度主题规范:dashboard、data-store、probe-engine、probe-api、probe-config 等
- 删除完全冗余规范:data-retention(被 probe-engine+data-store 覆盖)、backend-code-quality(DEVELOPMENT.md 已记录)
- 补充 http-checker 规范至完整标准(配置+执行+expect+校验+observation),匹配代码 440 行实现
- 清理 tcp/udp/llm checker 规范中已废弃 defaults 配置段的残留 Scenario
- 清理 checker-cohesion-structure 中的实现路径引用(src/server/...)
- 统一所有 spec 格式(## Purpose 开头,去除 # Capability/Title 形式)
- 更新 prompt-spec-review.md 审查提示文档
2026-05-22 18:55:18 +08:00
cf847ccd7a feat: 重构配置生命周期为 Authoring/Normalized/Resolved 三层
将变量替换和 expect 简写展开统一放入 Normalized 阶段,
运行时 AJV 使用 Normalized schema,导出 schema 面向 Authoring Config。

主要变更:
- 新增 normalizer.ts 实现 normalizeAuthoringConfig()
- 拆分 Authoring/Normalized 双 schema,checker 接口支持 authoring/normalized 片段
- config-loader 流程:normalize → Normalized AJV → semantic → resolve
- validator 兼容层自动分派 raw/normalized expect 形态
- 删除 rawExpect,store.expect 列写入 null
- Authoring schema 对 integer/boolean/enum 字段接受变量引用
- 修复 DB/HTTP validate 入口守卫和 LLM options integer 变量引用
- 优化 compact() 避免 undefined 覆盖隐患
- 移除 content.ts 恒为 true 的前置条件
- 同步 5 个主规范并归档 change
2026-05-22 14:00:47 +08:00
6e53c8130d feat: Alpine 多阶段 Docker 镜像,支持 amd64/arm64 musl 构建 2026-05-21 19:20:47 +08:00
6ca8b36542 feat: 扩展配置变量替换范围至 server/probes/targets,支持空默认值语法 2026-05-21 18:42:42 +08:00
79358ba50d refactor: 移除顶层 defaults 配置段,简化为 target 显式字段 > 代码内置默认值
- 移除 DefaultsConfig 类型、ProbeConfig.defaults 字段
- 移除 CheckerSchemas.defaults、ResolveContext.defaults、CheckerValidationInput.defaults
- 更新所有 checker schema/resolve/validate 删除 defaults 合并逻辑
- 更新 config-loader 不再读取传递 defaults
- 更新测试、README、DEVELOPMENT、probes.example.yaml
- 重新生成 probe-config.schema.json(不含 defaults)
- 同步 delta specs 到主规范
- 归档 openspec change
2026-05-21 16:53:12 +08:00
e448cb4654 feat: 重构配置布局,server.listen/storage/logging + probes.execution 分组
- 新增 server.listen (host/port)、server.storage (dataDir/retention)、
  server.logging 分组
- 新增 probes.execution (maxConcurrentChecks) 分组,替代顶层 runtime
- 旧配置入口 (runtime/logging/server.host/server.port/server.dataDir)
  启动期拒绝
- 更新 types.ts、builder.ts、config-loader.ts 适配新路径
- 更新 probe-config.schema.json、probes.example.yaml、README.md、
  DEVELOPMENT.md
- 补充 config-loader 和 variables 测试覆盖新路径和旧入口拒绝
- 同步 5 个 delta specs 到主规范 (probe-config, config-variables,
  data-retention, probe-engine, runtime-logging)
- 归档 openspec change reorganize-config-layout
2026-05-21 13:54:41 +08:00
5238dbe77d chore: 约束后端统一日志输出 2026-05-21 12:36:37 +08:00
007d74934d feat: 运行时日志系统,Pino + pino-pretty + pino-roll,console/file 双输出,敏感信息 redaction 2026-05-21 12:21:59 +08:00
0d709c7681 fix: 修复构建脚本跨平台 import specifier 路径规范化
- toImportSpecifier() 使用 replaceAll 替代 split(sep).join,明确 ESM import specifier 语义
- 增加 relativePath 可选参数支持测试注入 Windows relative 语义
- 重写 Windows 路径测试,使用 node:path.win32 显式模拟而非依赖当前平台 sep
- 更新 DEVELOPMENT.md 记录构建 code generation 约定
- 同步 static-asset-embedding 和 windows-test-compat spec 新增要求
2026-05-21 09:32:43 +08:00
b432581444 refactor: 清理测试代码 eslint-disable 指令,消除文件级和重复局部禁用 2026-05-21 00:35:08 +08:00
ccd16a583e feat: 跨平台发布打包,支持 7 个目标平台交叉编译和 tar.gz 分发
- 新增 scripts/release.ts,支持 7 个编译目标(linux/darwin/windows + musl 变体)
- 从 build.ts 提取共享构建逻辑到 build-common.ts,现有 build 行为不变
- 使用 tar-stream + node:zlib 创建 tar.gz,精确控制 Unix 权限位
- SHA256 校验和文件格式兼容 sha256sum -c
- 支持 --target 参数选择特定平台编译
- 新增 devDependency: tar-stream、@types/tar-stream
- 更新 README.md 和 DEVELOPMENT.md 文档
- 同步 openspec specs
2026-05-20 23:24:36 +08:00
8eac814cc6 feat: 版本管理,package.json 唯一版本源、/api/meta 返回版本、Dashboard Header 展示版本号 2026-05-20 19:14:37 +08:00
f3df3a203b docs: 优化审查提示词,禁止 subagent 读取文件,明确 apply 阶段不动主规范
config.yaml: subagent 限定为计算密集/多步骤任务,文件读取用 Read 工具
prompt-proposal-review.md: 收集阶段加入读取约束和分步策略,复核补全待澄清清单
prompt-apply-review.md: 禁止同步主规范,新增 Spec 覆盖完整性扫描与补充流程
2026-05-20 17:44:02 +08:00
60a54b483f refactor: expect 类型模型重构,Raw/Resolved 双层分离与断言基础设施内聚
- 重命名 ContentRules→ContentExpectations, KeyValueExpect→KeyedExpectations
- 新增 Raw/Resolved 双层模型:resolve 阶段物化为执行计划,store 持久化 Raw 快照
- HTTP body 按需读取:status/headers 失败或无 body expectation 时不读取 body
- 新增 displayValueExpectation() 解包 failure.expected 用户可读展示
- 修复 checkEarlyTimeout 独立 lte/lt 检查,修复 KeyedExpectations JSON Schema
- 新增 expect/value.ts(resolve/check/display)、keyed.ts、content.ts、headers.ts、status.ts
- 删除旧 normalize.ts/matcher.ts/validate-matcher.ts/key-value.ts
- 更新 DEVELOPMENT.md:expect 五层管线表、displayValueExpectation、1.7↔1.10 交叉引用
- 同步 13 个 main specs,归档 refactor-expect-type-model 变更(62/62 tasks)
2026-05-20 16:12:48 +08:00
6098be2d9e docs: 重构 README 文档结构,新增应用截图,按 checker 类型组织配置说明 2026-05-20 09:58:19 +08:00
b591dcca97 feat: target 软删除机制,配置移除时保留历史数据 2026-05-20 00:43:39 +08:00
9b53c746f6 refactor: ICMP checker type 从 ping 统一改为 icmp,修复前端 UI 细节
- ICMP checker 的 type/configKey/YAML 配置键/接口属性名从 ping 改为 icmp
- IcmpChecker 添加 platform 构造函数注入,修复 Windows 测试兼容性
- 前端 target 表格延迟列优化:标题简化为「延迟」,单位下移到单元格,宽度 80px
- Drawer 概览页 Descriptions 添加 tableLayout=auto 收窄 label 宽度
- 同步更新 README.md、DEVELOPMENT.md、probes.example.yaml、JSON Schema 和全部测试
2026-05-20 00:02:23 +08:00
375dd3492b feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail
- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
2026-05-19 22:49:00 +08:00
22c06820fa docs: 新增 checker-observation 变更提案,归档历史 openspec 变更记录 2026-05-19 19:22:16 +08:00
12cd05b04e feat: ValueMatcher 支持 primitive 原始值简写,等价于 { equals: value } 2026-05-19 17:07:47 +08:00
8d8549d07f refactor: 消除 es-toolkit/compat 依赖,isArray/isObject 替换为原生实现 2026-05-19 14:57:56 +08:00
7a635a0a9f refactor: 统一 expect 断言体系,引入共享 ValueMatcher/ContentRules/KeyValueExpect 模型
- 引入共享 ValueMatcher(equals/contains/regex/exists/empty/gt/gte/lt/lte)
- 引入共享 ContentRules 数组(direct/json/css/xpath 提取器)
- 引入共享 KeyValueExpect(动态键值断言,字面量等价 equals)
- maxDurationMs → durationMs: ValueMatcher(所有 checker)
- match → regex(固定无 flags)
- Ping max* → packetLossPercent/avgLatencyMs/maxLatencyMs(ValueMatcher)
- LLM finishReason/rawFinishReason → ValueMatcher
- DB 新增 result: ContentRules
- TCP banner → ContentRules 数组
- 删除旧模块:operator.ts、validate-operator.ts、duration.ts、body.ts、text.ts、output.ts
- 更新全部 checker schema/validate/expect/execute
- 更新 probe-config.schema.json、probes.example.yaml
- 更新 README.md、DEVELOPMENT.md(含 expect 字段选择规范)
- 同步 10 个 delta specs 到主 specs,归档 change
2026-05-19 14:24:27 +08:00
349896bd02 feat: 新增 LLM checker 支持大模型服务应用层拨测
基于 AI SDK v6 实现 openai/openai-responses/anthropic 三类 provider 的 http/stream 模式调用
支持 output/finishReason/usage/stream 等完整 expect 断言链路
新增 9 个源文件和 5 个测试文件共 78 个测试
更新 README/DEVELOPMENT/probes.example.yaml 和 probe-config.schema.json
2026-05-19 00:06:53 +08:00
52262a31f6 feat: 新增 UDP checker,支持自定义 payload 请求-响应探测与断言
基于 Bun connected UDP socket 实现通用 UDP 拨测能力:
- 支持 text/hex/base64 payload 编码与独立 responseEncoding 响应视图
- 支持 responded、response、responseSize、sourceHost、sourcePort、maxDurationMs 专属 expect
- 单 datagram 发送,仅断言首个 UDP 响应 datagram
- 通过 maxResponseBytes 和 flags.truncated 进行响应大小限制与截断保护
- payload 可选,省略时发送空 datagram
- 自包含模块结构(types/schema/validate/expect/encoding/execute)
- 新增 741 tests(含 unit、execute 集成、expect 和编码 roundtrip),全部通过
2026-05-18 17:23:17 +08:00
550c427814 feat: 新增 ICMP/Ping checker,支持跨平台主机存活检测与延迟监控
实现 type: ping checker,通过 Bun.spawn 调用系统 ping 命令,自行实现跨平台
输出解析器(Linux/macOS/Windows 含中文 locale),支持 alive、丢包率、延迟、
耗时等 expect 断言,复用现有 checker 架构零外部依赖。

包含完整的类型定义、TypeBox schema、语义校验、命令构建、解析、断言、执行、
注册、配置加载测试,以及 probe-config.schema.json 更新和文档更新。

审查修复:提取 buildPingCommand 为独立纯函数并补充跨平台单测,补充
maxDurationMs/maxAvgLatencyMs 类型非法和空字符串 host 边界测试用例。

变更已归档,delta specs 已同步至 main specs。
2026-05-18 10:45:17 +08:00
c51bc5a0d8 refactor: 优化 clean 脚本,目录直接删除替代 glob 逐文件清理,补充 playwright-report/test-results 2026-05-18 09:18:36 +08:00
393e8da5fd feat: 新增 ICMP/Ping checker 设计提案
- 定义 ping target 配置:host、count、packetSize
- 定义 ping expect 断言:alive、maxPacketLoss、maxAvgLatencyMs、maxMaxLatencyMs
- 设计跨平台 ping 输出解析器(Linux/macOS/Windows 含多语言支持)
- 双重超时保障:ping 命令自身超时 + AbortSignal 兜底
- 扩展 checker-runner-abstraction spec 支持 ping checker 子进程控制
- 更新 probe-config spec 支持 ping type 配置
2026-05-18 00:33:11 +08:00
0a9a9016be feat: 新增 TCP checker,支持端口可达性探测与 banner 读取
- 新增 src/server/checker/runner/tcp/ 自包含目录(types/schema/validate/execute/expect)
- 注册 TcpChecker 到 checkerRegistry,schema/engine/store/config-loader 自动委托
- 支持 expect.connected 正反向语义(默认期待可达,可配置期待不可达)
- 支持 readBanner opt-in banner 读取,受 bannerReadTimeout + maxBannerBytes 双重限制
- 复用电有 expect/operator/duration/failure 基础设施
- 新增 3 个测试文件 51 条用例(execute/validate/expect),全量 634 测试通过
- 更新 README/DEVELOPMENT/probes.example.yaml,新增 tcp-checker capability spec
2026-05-17 23:53:37 +08:00
31fd3a2a43 refactor: 统一 target name/description 可空语义,前端展示 fallback 到 id
- schema: name/description 允许省略或显式 null,TypeBox Union([Null, String])
- 类型: RawTargetConfig/ResolvedTargetBase/子类型/StoredTarget/TargetStatus name 改为 string | null
- checker resolve: name: t.name ?? null,不再 fallback 到 id
- 语义校验: 拒绝空字符串和纯空白 name
- SQLite: targets.name 列改为可空 TEXT
- 前端: 新增 getTargetDisplayName(target) 展示 name ?? id
- 测试: 覆盖 name/description null 全场景,查找改为按 id
- 文档: 更新 README/DEVELOPMENT 和 6 个 openspec specs
2026-05-17 20:12:39 +08:00
f7193e98ff feat: 新增 target description 字段,收紧 id/name 长度,调整延迟列和名称列 2026-05-17 18:42:46 +08:00
7926514986 feat: 配置变量系统与 target id/name 双字段标识
- 新增顶层 variables 段支持 string/number/boolean 字面量
- target 字符串字段支持 、、{...} 转义语法
- 变量解析优先级: variables -> process.env -> 默认值 -> 报错
- 完整引用保留原始类型,部分引用拼接为字符串
- 变量替换在 YAML 解析后、AJV 校验前执行
- 替换仅作用于 targets,跳过 id/type 字段
- target 新增必填 id 字段作为唯一标识,name 改为可选展示名称
- 数据库存储/API/前端全面迁移到 id 标识
- 统一 checker 运行时类型检查为 es-toolkit predicates
- 同步 delta specs 到主 specs,归档 config-variables 变更
2026-05-17 00:37:54 +08:00
366b3211c8 fix: 移除 README 示例配置中的 defaults.http.method 2026-05-16 21:46:20 +08:00
e924732a02 refactor: 移除 defaults.http.method 配置,简化默认值体系
- HTTP checker defaults schema 不再支持 method 字段
- resolve 逻辑从三级 fallback 简化为两级(target -> 内置默认)
- 配置文件中出现 defaults.http.method 将触发未知字段校验错误
- per-target http.method 覆盖功能保持不变
- 同步更新示例配置、README 文档和测试用例
2026-05-16 21:45:08 +08:00
04c24e6796 docs: 重写 README 为结构化项目文档,精简示例配置 2026-05-16 20:58:04 +08:00
146cef982e feat: 新增 DB checker — 支持 PostgreSQL/MySQL/SQLite 连接测试与 SQL 查询断言
- 实现 db 类型 checker,使用 Bun 内置 SQL 类
- 支持 db.url 连接字符串和可选 db.query 查询语句
- expect 支持 maxDurationMs、rowCount、rows 逐列校验
- 凭据屏蔽序列化输出
- SQLite 内存数据库测试覆盖
2026-05-16 09:00:15 +08:00
c36df94e59 chore: 归档 responsive-resizable-drawer 变更 2026-05-16 00:17:31 +08:00
f8d563c668 feat: Header 倒计时数字滚动动画 — @number-flow/react 替换静态文本 2026-05-16 00:14:35 +08:00
88f4119a4e feat: Drawer 响应式默认宽度与拖拽调整,统计卡片上下布局优化
Drawer 宽度从固定百分比改为按视口响应式默认值(6段断点),宽屏占比更小、窄屏占比更大。

启用 TDesign sizeDraggable 原生拖拽调整能力,配置 min/max 视口安全边界,不持久化拖拽宽度。

概览统计卡片改为 TDesign Statistic 上下布局(与 SummaryCards 一致),提升窄屏视觉体验。

Drawer header 间距调大,MutationObserver polyfill 补全。
2026-05-15 23:10:08 +08:00
c46ab14cce feat: Dashboard 主题模式切换 — 系统跟随/明亮/黑暗,localStorage 持久化,TDesign theme-mode 驱动
新增 useThemePreference hook 和纯工具函数,支持系统/明亮/黑暗三态主题选择、
matchMedia 系统主题跟随、localStorage 持久化和启动期主题预应用,通过 <html
theme-mode> 驱动 TDesign 主题变量切换。

Header 右侧控件重新组织为 .dashboard-header-controls 单行桌面布局,主题
RadioGroup 位于刷新频率 RadioGroup 前。

附带:build.ts import specifier 改为跨平台 sep 转换;config-loader 测试适配
Windows PATH 和 YAML 路径转义;test-utils 类型窄化修复。
2026-05-15 22:18:29 +08:00
8793fbd786 test: 重构测试体系 — 建立组件测试层、补充后端测试、清理低质量测试
- 新增 jsdom + @testing-library/react 组件测试环境
- 新增 12 个组件测试,覆盖所有前端组件
- 补充后端 middleware 和 helpers 单元测试
- 删除伪测试 use-target-detail-logic.test.ts
- 精简过度枚举的 color-threshold.test.ts
- 新增 bunfig.toml 配置测试 preload
- 更新 DEVELOPMENT.md 测试章节
- 安装 @types/jsdom 修复类型声明
2026-05-15 18:31:33 +08:00
2b08f81a0d chore: 依赖版本更新、Vite 构建配置完善、TrendChart 描述修正
- 更新 @commitlint/cli、@commitlint/config-conventional、@types/bun、typescript-eslint 至最新 patch 版本
- vite.config.ts 添加 emptyOutDir: true 消除构建 outDir 警告
- DEVELOPMENT.md TrendChart 描述从"双轴折线图(耗时/可用率)"修正为"趋势折线图(耗时+延迟范围)"
2026-05-15 15:15:57 +08:00
86b8cf1950 refactor: 前端性能优化 — 倒计时组件隔离、React memoization 链路
- 新建 RefreshCountdown 组件,内部持有 timer,消除 App 每秒重渲染
- TargetBoard 分组逻辑 useMemo 化,避免 targets 引用不变时重复计算
- TargetGroup 加 React.memo,阻断无效渲染
- TrendChart 加 React.memo + chartData useMemo,避免 recharts 不必要重绘
- OverviewTab 统计项去掉 Card 包裹,改用纯 CSS 实现视觉效果
- 同步更新 refresh-control 和 target-detail-drawer spec

性能提升:消除每秒全组件树重渲染,减少 DOM 节点数
2026-05-15 12:02:39 +08:00
d6a77b2c6e feat: 迁移前端构建从 Bun fullstack 到 Vite
前端性能问题根因在于 Bun bundler 无法有效 code split、CSS
tree-shake 和产出优化的前端资源。经多轮 Bun 原生优化尝试
均无明显效果后,决定将前端构建迁回 Vite。

主要变更:

- 前端构建:从 Bun HTML import bundling 切换为 Vite build
  (Rolldown code splitting、vendor chunk、CSS 优化)
- 开发模式:从 Bun fullstack 单进程 HMR 切换为 Vite dev
  server + Bun API server 双进程(:5173 + :3000)
- 生产构建:三步流水线(Vite build → code generation →
  Bun compile),通过 `import with { type: "file" }` 嵌入前端资源
- 静态资源服务:从 Bun HTML import manifest 切换为自定义
  serveStaticAsset 函数,支持 SPA fallback 和正确的 Cache-Control
- Server 接口:BootstrapOptions 和 StartServerOptions 增加
  staticAssets? 可选参数
- 文档更新:DEVELOPMENT.md 和 README.md 反映新的开发模式,
  主 specs 同步 delta 变更

新增能力:
- static-asset-embedding: 构建时资源扫描与 code generation、
  运行时静态资源服务
- vite-frontend-bundling: Vite 构建配置、code splitting 策略、
  CSS 处理
2026-05-15 11:26:46 +08:00
28e46b8431 feat: 优化目标详情 Drawer 性能 — TDesign 生命周期控制、Tab 感知延迟加载、滚动穿透修复 2026-05-15 00:53:41 +08:00
9904f198aa feat: Dashboard 刷新频率可配置 — RadioGroup 选择器、动态轮询间隔、手动刷新按钮
- useDashboard hook 改为接受 refetchInterval 动态参数,移除固定 8 秒常量
- Header operations 区域重构为 RadioGroup(手动/10秒/30秒/1分钟/5分钟)+ 倒计时/刷新按钮
- 新增 formatCountdown 工具函数及单元测试
- 新增 .dashboard-refresh-control 和 .dashboard-countdown CSS 类
- 同步更新 DEVELOPMENT.md、README.md、主 specs
2026-05-14 18:03:42 +08:00
c61a4a6091 refactor: 前端视觉重构 — Layout/HeadMenu 骨架、SummaryCards 合并、Card 分组、Drawer 概览重设计 2026-05-14 15:57:14 +08:00
1c5cfafda6 feat: 前端指标体系增强 — Dashboard/Metrics API、2×4 统计区、趋势图面积+异常标记、连续状态列
- 新增 GET /api/dashboard 合并原 summary+targets 首屏接口
- 新增 GET /api/targets/:id/metrics 合并原 stats+trend 概览接口
- 后端指标纯函数:可用率、百分位、故障段分析、连续状态、UTC 小时分桶
- ProbeStore 窗口取数方法替代全量历史查询
- SummaryCards 扩展为 4 卡片(新增异常事件数)+ 数据新鲜度展示
- 表格新增「连续」列(Tag 渲染 capped 状态)
- OverviewTab 重构为 2×4 Statistic 多维度布局
- TrendChart 改为延迟范围面积图 + 红色异常标记点
- 删除旧路由(summary/targets/trend)和 computeTrendStats
- 同步 delta specs 到主 specs 并归档变更
2026-05-14 12:32:41 +08:00
e983e5d75d refactor: 重命名 command checker 为 cmd checker 并适配跨平台测试
将 type/configKey 从 "command" 统一为 "cmd",源码目录 runner/command/ → runner/cmd/,
spec 目录 command-checker/ → cmd-checker/,测试全部改用 bun -e 替代 Unix 系统命令,
归档 cmd-checker-enhancement 变更并同步 delta spec 到主 spec。
2026-05-14 09:23:10 +08:00
0fa2c0c811 feat: 新增两个 OpenSpec 变更提案 — CMD Checker 增强与前端指标增强 2026-05-14 01:39:26 +08:00
6e485cc991 refactor: 迁移 Bun fullstack 架构 2026-05-14 00:23:37 +08:00
bcfac52112 refactor: HTTP checker 质量加固
- failure actual 截断格式改为 …(共 N 字符),标量不序列化直接返回
- 新增 redos.ts 实现 ReDoS 静态检测(嵌套量词/重叠交替),启动期拒绝危险正则
- JSON body rules 共享同一次 JSON.parse 结果,避免重复解析
- checkCssRule 重构为线性流程,消除 exist:true 与无 operator 的冗余分支
- extract checkEarlyTimeout 辅助函数,明确提前 duration 检查意图
- 补充 303/307/308 重定向、相对路径 Location、混合 body rules 集成测试
2026-05-13 21:35:05 +08:00
31aeee6d60 refactor: 前端架构重构 — hook拆分、组件拆分、类型筛选器动态化、Meta API
- 后端新增 GET /api/meta 端点(checkerRegistry.supportedTypes)及 MetaResponse 类型
- 前端 hook 拆分为 use-queries.ts(全局查询+useMeta)和 use-target-detail.ts(Drawer状态)
- TargetDetailDrawer 拆分为 OverviewTab + HistoryTab + history-table-columns + stats.ts
- 类型筛选器由 meta API 动态驱动,删除 target-type-display 静态映射
- 列定义改为工厂函数 createTargetTableColumns(checkerTypes),TargetGroup 新增 columns prop
- 修复 StatusDonut key、StatusBar maxSlots prop、TrendChart 移除 loading prop
- 补充 utils/time、utils/stats、动态列工厂测试,删除旧 mapping 测试
- 同步 delta specs 到主 specs,归档 frontend-architecture-refactor change
2026-05-13 20:55:42 +08:00
a62007083d chore: 归档 backend-architecture-hardening 变更并同步 delta spec 到主 spec 2026-05-13 19:50:33 +08:00
76b47006fe feat: 新增两个 OpenSpec 变更提案 — 前端架构重构与 HTTP Checker 质量加固
- frontend-architecture-refactor: 拆分 hooks/组件、类型筛选器动态化
- http-checker-quality-hardening: ReDoS 防护、failure 格式修正、测试补全
2026-05-13 18:40:08 +08:00
147a2559ae refactor: 后端架构加固 — 泛型化、批量查询、bootstrap 统一、路径修复与 pageSize 上限
- CheckerDefinition 泛型化,HTTP/Command checker 移除 resolved target 断言
- 新增 ProbeStore.getAllRecentSamples 消除 targets 路由 N+1 查询
- 统一 getAllTargetStats 与 getTargetStats 的 availability 精度
- Engine rejected 结果写入 internal error 记录,提升可观测性
- 新增 bootstrap.ts 统一 dev/production 启动序列
- dataDir 相对路径改为基于配置文件目录解析
- validatePagination 增加 pageSize 上限 200 校验
- 修复 ErrorBoundary override 标记
- 更新 README/DEVELOPMENT 文档,新增完整测试覆盖
2026-05-13 18:15:46 +08:00
6ea185315f docs: 修正 DEVELOPMENT.md 与实际代码的差异并精简 tcp 示例 2026-05-13 17:27:33 +08:00
ecd47748d2 docs: 修正 README 配置说明与实际代码的差异 2026-05-13 17:27:23 +08:00
bcfb907bd3 feat: 基础设施加固 — 修复构建、数据保留、错误边界、bundle 拆分
- 修复 build script 引用已删除的 registerCheckers,恢复生产构建
- 生产入口添加 SIGINT/SIGTERM 优雅关闭(与 dev.ts 一致)
- 新增 runtime.retention 配置(默认 7d),ProbeStore.prune() 定时清理过期数据
- parseDuration 扩展支持 h/d 单位
- 新增前端 ErrorBoundary 组件,防止渲染错误白屏
- Vite codeSplitting.groups 拆分 vendor chunks(业务代码 1180KB → 47KB)
- 同步 delta specs 到主规范
2026-05-13 16:48:56 +08:00
26f0bfe104 docs: 归档 checker 内聚化重构变更,同步 delta specs 到主规范
- 归档 refactor-checker-coherence 变更至 archive/2026-05-13-refactor-checker-cohesion/
- 新增主规范 checker-cohesion-structure(12 条需求)
- 更新主规范 checker-runner-abstraction(新增 base interface 类型相关场景)
2026-05-13 15:02:59 +08:00
bb6b2bc20b refactor: checker 模块内聚化 — 每个 checker 自包含于独立目录
将 checker 架构重构为完全内聚模式:每个 checker 目录包含自身的
types、schema、validate、execute、expect 和 index,新增 checker
只需创建一个目录并在 runner/index.ts 添加一行注册。

主要变更:
- runner/shared/ 拆分:断言基础设施迁入 checker/expect/,
  body.ts 迁入 http/,text.ts 迁入 command/
- config-contract/ 重命名为 schema/,schema.ts → builder.ts
- size.ts + parseDuration 合并为 utils.ts
- 顶层 types.ts 改为 base interface + index signature,
  checker 专属类型下沉到各自 types.ts
- runner/index.ts 改为显式数组注册模式
- 更新 DEVELOPMENT.md 项目结构和开发新 Checker 指南
2026-05-13 14:38:21 +08:00
c396c29402 docs: 添加 checker 内聚化重构方案及归档历史变更
新增 refactor-checker-cohesion 变更提案,包含 proposal、design、
specs 和 tasks,定义 checker 目录内聚结构规范。同时归档已完成的
历史变更记录。
2026-05-13 13:30:05 +08:00
aade0bbff7 refactor: 清理 checker 遗留边界 2026-05-13 12:53:03 +08:00
7b20b59b79 feat: 重构配置校验为 TypeBox + Ajv + semantic validator,严格禁止未知字段
- 新增 config-contract 模块(TypeBox fragments、Ajv 契约校验、ConfigValidationIssue)
- CheckerDefinition 扩展为含 configKey、schemas、validate 的完整插件接口
- HTTP/Command 各自维护 contract.ts + validate.ts,校验从 resolve 中分离
- resolve 不再承担校验,只做默认值合并和路径/单位解析
- config-loader 流程: unknown → RawProbeConfig → ValidatedProbeConfig → ResolvedConfig
- 导出 probe-config.schema.json,新增 schema/schema:check 脚本
- 更新 DEVELOPMENT.md 新增 1.7 开发新 Checker 完整指引
- 同步更新 4 个 main specs(probe-config、command-checker、expect-body-checkers、checker-runner-abstraction)
2026-05-13 12:19:36 +08:00
bce0f8e7a8 feat: 增强 HTTP checker 鲁棒性 — 严格配置校验、完整耗时、流式body、重定向与编码完善
启动期校验: 新增 validate.ts 对 HTTP config/expect/body rule/operator 全方位严格校验
执行语义: body 改为 Web Stream 流式超限中止,durationMs 覆盖完整执行
错误归属: status/header 失败不读 body,phase 分层 request/body,early duration skip body
重定向: 跟随前释放 body,POST/303 改 GET 清理 header,跨 origin 剥离敏感 header
编码: 支持 quoted charset,未知编码返回结构化解码错误
文档: README match→regex+durationMs,DEVELOPMENT 执行流程与错误归属
测试: +63 测试覆盖全部新增场景,325 pass 0 fail
规格: 同步 probe-config/probe-engine/expect-body-checkers 3 个 delta spec
2026-05-13 08:00:05 +08:00
2fd0f206be feat: HTTP 探针增强 — ignoreSSL、精确重定向控制、状态码范围匹配、编码自动检测 2026-05-13 00:02:04 +08:00
87d946a441 fix: 修复 Windows 平台测试兼容性问题
- 新增 tests/helpers.ts 的 rmRetry 工具函数,解决 SQLite 文件句柄未及时释放导致 afterAll 清理时 EBUSY 错误
- 修改通配符测试用例,使用 bun -e 替代 echo 命令,确保跨平台行为一致
2026-05-12 22:12:32 +08:00
ad87be6956 refactor: 精简 package.json scripts,引入 eslint-plugin-prettier 统一格式检查
删除 start/build:web/format:check,简化 check 为 typecheck+lint+test

引入 eslint-plugin-prettier 将 Prettier 集成至 ESLint,统一质量检查入口

简化 lint-staged 配置,扩展 clean 清理范围至 dist/
2026-05-12 21:43:20 +08:00
9a71b7967c chore: 配置跨平台行尾规范与 VSCode 编辑器统一设置 2026-05-12 20:30:48 +08:00
357 changed files with 44401 additions and 6757 deletions

8
.claude/settings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"tdesign-mcp-server": {
"command": "bunx",
"args": ["tdesign-mcp-server@latest"]
}
}
}

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
.build
.claude
.codex
.env
.env.*
.git
.opencode
.agents
.DS_Store
coverage
data
dist
node_modules
*.log
*.bun-build

32
.gitattributes vendored Normal file
View File

@@ -0,0 +1,32 @@
# 跨平台行尾规范
# 所有文本文件统一用 LFUnix 风格),避免 CRLF/LF 混用
* text=auto eol=lf
# 二进制文件不转换行尾
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.pdf binary
*.zip binary
*.gz binary
*.tar binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.mp4 binary
*.mov binary
*.mp3 binary
# Shell 脚本必须 LF
*.sh text eol=lf
# Windows 批处理必须 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
# 锁定文件(如 package-lock.json保持 LF
*.lock text eol=lf

5
.gitignore vendored
View File

@@ -403,11 +403,15 @@ cython_debug/
!.claude/settings.json
.opencode
.codex
.pi/*
!.pi/mcp.json
!.pi/extensions
openspec/changes/archive
temp
.agents
skills-lock.json
.worktrees
data/
!scripts/build/
backend/bin
backend/server
@@ -421,3 +425,4 @@ backend/cmd/desktop/rsrc_windows_*.syso
# Bun
.build/
*.bun-build
dist/release/

View File

@@ -1,4 +1,4 @@
{
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{ts,tsx}": ["eslint --fix"],
"*.{md,json,yaml,yml}": ["prettier --write"]
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "https://raw.githubusercontent.com/gotgenes/pi-permission-system/main/schemas/permissions.schema.json",
"permission": {
"*": "allow",
"write": "allow",
"edit": "allow",
"bash": {
"*": "allow",
"npm *": "deny",
"npx *": "deny",
"pnpm *": "deny",
"pnpx *": "deny"
},
"external_directory": {
"*": "ask",
"/tmp/*": "allow"
}
}
}

8
.pi/mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"tdesign-mcp-server": {
"command": "bunx",
"args": ["tdesign-mcp-server@latest"]
}
}
}

View File

@@ -10,3 +10,4 @@ bun.lock
.agents/
skills-lock.json
data/
probe-config.schema.json

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"files.eol": "\n",
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}

View File

@@ -1,707 +0,0 @@
# DiAL 开发文档
本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。
用户使用说明请参阅 [README.md](README.md)。
## 目录
- [项目结构](#项目结构)
- [一、后端开发指引](#一后端开发指引)
- [二、前端开发指引](#二前端开发指引)
- [三、项目运行、集成与打包](#三项目运行集成与打包)
- [代码质量](#代码质量)
- [已知限制](#已知限制)
---
## 项目结构
```text
src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚)
config.ts CLI 参数解析
dev.ts 生产/开发启动入口
server.ts HTTP server 启动工厂
helpers.ts 共享响应格式化工具jsonResponse、createHeaders 等)
middleware.ts API 参数校验中间件guardGetHead、validateTargetId 等)
static.ts 静态资源服务与 SPA fallback
routes/ API 路由 handler按端点拆分
health.ts GET /health
summary.ts GET /api/summary
targets.ts GET /api/targets
history.ts GET /api/targets/:id/history
trend.ts GET /api/targets/:id/trend
checker/
types.ts 类型定义
config-loader.ts YAML 配置解析与校验
store.ts SQLite 数据存储
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
size.ts 大小单位解析
runner/ Checker 统一抽象与注册机制
types.ts Checker 接口、CheckerContext、ResolveContext
registry.ts CheckerRegistry 注册中心
index.ts 注册入口registerCheckers
shared/ 共享 expect 断言函数(跨 checker 复用)
failure.ts 失败信息类型
operator.ts 操作符系统applyOperator、evaluateJsonPath
duration.ts 耗时断言
text.ts 文本规则断言
body.ts Body 规则断言JSONPath/XPath/CSS/contains/regex
http/ HTTP Checker 子包
runner.ts HttpCheckerresolve/execute/serialize
expect.ts HTTP 专用断言status/headers
command/ Command Checker 子包
runner.ts CommandCheckerresolve/execute/serialize
expect.ts Command 专用断言exitCode
shared/
api.ts 前后端共享 TypeScript 类型
web/ Vite + React 前端 Dashboard
components/ UI 组件表格、分组、Drawer、状态条等
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数)
hooks/ TanStack Query 数据层useTargetDetail 集成轮询/条件查询)
utils/ 前端工具函数
scripts/ 开发、构建和 smoke test 脚本
tests/ Bun test 测试
openspec/ OpenSpec 变更与规格文档
```
## 前后端边界
前端只通过 HTTP 调用后端API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。
---
## 一、后端开发指引
### 1.1 架构概览
```
启动流程:
dev.ts → readRuntimeConfig(cli args) → loadConfig(yaml)
→ ProbeStore(db) → ProbeEngine(store, targets) → startServer(store)
运行时:
定时器(tick) → ProbeEngine.probeGroup()
→ HTTP: fetcher.ts / Command: command-runner.ts
→ runner/*/expect.ts 校验 → store.insertCheckResult()
HTTP 请求:
Request → app.ts(路由分发) → routes/*.ts(handler)
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
```
### 1.2 库使用优先级
后端代码开发遵循严格的库选择顺序:
| 优先级 | 来源 | 典型用途 |
| ------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Bun 内置 API | `Bun.serve``bun:sqlite``Bun.spawn``Bun.file``Bun.YAML` |
| 2 | es-toolkit | 类型判断(`isPlainObject`/`isNil`/`isEmptyObject`)、深度比较(`isEqual`)、错误判断(`isError`)、并发控制(`Semaphore`)、集合操作(`groupBy` |
| 3 | 标准 Web API | `Object.fromEntries``Headers``fetch``AbortController` |
| 4 | 主流三方库 | cheerioHTML 解析、xpath + @xmldom/xmldomXML 解析) |
| 5 | 自行实现 | 仅在以上都无法满足时(如 `parseDuration``parseSize``evaluateJsonPath` 等专项逻辑) |
**原则**:新增依赖前先检查上述每一层级是否已有可用方案。禁止随意引入新依赖。
### 1.3 API 路由开发
路由文件位于 `src/server/routes/`每个端点一个文件。handler 函数签名统一为:
```typescript
export function handleXxx(params, store: ProbeStore, method: string, mode: RuntimeMode): Response;
```
**请求处理流程**
1. `app.ts``createFetchHandler` 作为总入口,根据 URL pattern 匹配路由
2. API 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验
4. 校验函数返回 `Response` 表示校验失败(直接返回),返回数据对象表示通过
5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
**新增路由步骤**
1.`src/server/routes/` 下创建 `<name>.ts`
2. 实现 handler 函数并 export
3.`app.ts``createFetchHandler` 中注册路径匹配和调用
4.`tests/server/app.test.ts` 中添加对应测试
### 1.4 共享工具
- **`helpers.ts`**:跨路由共用的响应工具函数(`jsonResponse``createHeaders``createApiError``mapCheckResult``formatDuration``createHealthResponse`
- **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
### 1.5 类型定义规范
- **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用
- 前端不得 `import src/server/` 下的任何文件
- **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string`
- **后端内部扩展**`checker/types.ts``CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetName` 等内部字段
- 存储层类型(`StoredTarget``StoredCheckResult`)独立定义,与 API 类型分离
- 配置类型(`ProbeConfig``TargetConfig`)支持 discriminated union通过 `type` 字段区分 http/command
### 1.6 数据存储规范
基于 `bun:sqlite`WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
**Statement 使用规范**
| 场景 | 方式 | 原因 |
| -------------- | -------------------------------------- | ---------------------------------------- |
| 单次读/写 | `this.db.query(sql).get()/all()/run()` | bun:sqlite 内置 statement 缓存,自动复用 |
| 事务内多次复用 | `this.db.prepare(sql)` 缓存为局部变量 | 事务闭包中需要持有引用 |
**查询优化**
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合GROUP BY、子查询 JOIN+ 内存组装
- 新增批量查询方法时必须编写对应单元测试
- `getSummary()``GET /api/targets` 的响应组装已通过 `getLatestChecksMap` + `getAllTargetStats` 实现批量查询
**Schema**
- `targets`nameUNIQUE、type、target展示摘要、configJSON、interval_ms、timeout_ms、expectJSON、grp
- `check_results`target_idFK CASCADE、timestamp、matched0/1、duration_ms、status_detail、failureJSON
- 复合索引:`(target_id, timestamp)`
### 1.7 拨测引擎
- **调度**`ProbeEngine``es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
- **并发控制**`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks``acquire()` 阻塞等待
- **Runner 选择**`engine.runCheck()``target.type` 分发到 `runHttpCheck``runCommandCheck`
- **超时控制**HTTP 用 `AbortController`Command 用 `setTimeout` + `proc.kill()`
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射
- **生命周期**`start()`/`stop()` 管理定时器,`stop()` 清理所有 `setInterval`
### 1.8 expect 断言系统
两层模型:**观测值收集** → **规则校验**
**HTTP 校验流程**
```
runHttpCheck → 收集观测(statusCode/headers/body/durationMs)
→ checkHttpExpect → status → duration → headers → body(可选)
→ 首个失败即停止,返回 CheckFailure
```
**Command 校验流程**
```
runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
→ checkCommandExpect → exitCode → duration → stdout → stderr
→ 首个失败即停止
```
**Body 规则类型**
- `contains`:文本包含匹配
- `regex`:正则表达式匹配
- `json`JSONPath 提取 + 操作符比较(使用 `es-toolkit/isPlainObject` 区分纯值和操作符)
- `css`cheerio CSS 选择器 + 操作符比较
- `xpath`XPath 节点提取 + 操作符比较
**操作符**`equals`(深度比较,`es-toolkit/isEqual`)、`contains``match`(正则)、`empty``isNil`+`isEmptyObject`)、`exists``gte`/`lte`/`gt`/`lt`
### 1.9 错误模式
- **API 错误**`{ error: "描述", status: <code> }`,状态码 400/404/405/503
- **CheckFailure**`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
- **错误处理**expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
### 1.10 测试规范
- 测试文件与源文件对应:`tests/server/checker/runner/shared/body.test.ts``src/server/checker/runner/shared/body.ts`
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
- 测试后清理:`afterAll``store.close()` + `rm(tempDir, { recursive: true })`
---
## 二、前端开发指引
### 2.1 技术栈概览
| 层面 | 技术 | 用途 |
| ------ | ----------------------------------- | ---------------------------- |
| 框架 | React 19 | UI 组件开发 |
| 构建 | Vite 8 | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图与状态环状图 |
| 路由 | 无(单页面 Dashboard | 仅需 Drawer/Tab 做页面内导航 |
**不引入的依赖**React Router单页面场景不需要、状态管理库TanStack Query 即服务端状态层,组件内用 `useState` 足够)
### 2.2 组件树与数据流
```
main.tsx
└── QueryClientProviderTanStack Query 全局挂载)
└── App根组件
├── SummaryCards总览统计卡片
│ └── useSummary() ─── GET /api/summary8s 轮询)
└── TargetBoard目标列表
├── useTargets() ─── GET /api/targets8s 轮询)
└── TargetGroup[](按 group 字段分组)
└── PrimaryTable ← TARGET_TABLE_COLUMNS列定义排序/筛选/渲染)
└── TargetDetailDrawer目标详情抽屉
└── useTargetDetail() ── 按需发起 trend + history 查询
├── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions
└── Tab: 记录 → PrimaryTable分页历史记录
```
**数据层架构**
```
hooks/useTargetDetail.ts唯一的数据层入口
├── queryKeys结构化 query key确保缓存粒度精确
├── useSummary() → /api/summary8s 自动轮询)
├── useTargets() → /api/targets8s 自动轮询)
└── useTargetDetail()(组合 hook管理 Drawer 全部状态)
├── 内部复用 useTargets() 的缓存来查找 selectedTarget
├── useQuery(/api/targets/:id/trend)条件查询enabled 仅当 Drawer 打开且时间范围有效)
└── useQuery(/api/targets/:id/history)(条件查询:含分页)
```
### 2.3 TanStack Query 数据层
#### Query Key 规范
```typescript
const queryKeys = {
summary: () => ["summary"] as const,
targets: () => ["targets"] as const,
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
};
```
- Key 使用 **structured array**(非字符串),以便精确匹配和按 prefix 失效
- 使用 `as const` 保持字面量类型
- 排序scope → id → 参数(粒度从粗到细)
#### 查询配置规范
```typescript
// 全局面板级查询(需要持续刷新)
useQuery({
queryKey: queryKeys.summary(),
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
refetchInterval: 8000, // 自动轮询间隔
refetchIntervalInBackground: false, // 切后台不轮询
});
// 详情级查询(按需加载)
useQuery({
queryKey: selectedTargetId ? queryKeys.trend(id, from, to) : ["trend", "disabled"],
queryFn: () => fetchJson(`/api/targets/${id}/trend?...`),
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, // 条件查询
});
```
#### fetch 封装
```typescript
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
```
- 统一使用 `fetch`(不引入 axios与后端共享 Web API 生态
- 错误抛异常,由 TanStack Query 的 `error` 状态承接
#### QueryClient 全局配置
```typescript
new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 失败重试 1 次
refetchOnWindowFocus: true, // 窗口聚焦时刷新
staleTime: 5000, // 5s 内视为 fresh避免重复请求
},
},
});
```
### 2.4 组件开发规范
#### 文件命名与导入
- 每个 React 组件一个 `.tsx` 文件,文件名使用 PascalCase`StatusDot.tsx`
- 组件 props 定义为 `interface XxxProps`,紧邻组件函数声明
- 类型从 `../../shared/api` 导入,使用 `type` 导入(`import type { ... }`
```typescript
import type { TargetStatus } from "../../shared/api";
import { StatusDot } from "./StatusDot";
interface TargetGroupProps {
name: string;
targets: TargetStatus[];
onTargetClick: (target: TargetStatus) => void;
}
export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) {
// ...
}
```
#### 组件拆分原则
- **展示组件**`components/`):纯渲染逻辑,通过 props 接收数据,通过回调返回事件
- **容器逻辑**放在 hooks 中,组件只做数据消费
- **常量数据**(列定义、排序器、筛选器)放在 `constants/`,不放在组件内部
- **工具函数**(时间处理等)放在 `utils/`,保持纯函数无副作用
#### 现有组件清单
| 组件 | 文件 | 用途 |
| -------------------- | ----------------------------------- | ---------------------------------- |
| `App` | `app.tsx` | 根组件,编排全局状态与布局 |
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) |
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 |
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable |
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图UP/DOWN 分布) |
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) |
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) |
### 2.5 新增功能开发步骤
以"新增一个详情页面 Tab"为例:
1. **确认数据需求**:是已有 API 数据还是需要新端点?
- 如有新端点,先在 `src/server/routes/` 添加,参考 [1.3 新增路由步骤](#13-api-路由开发)
- 如有新字段,更新 `src/shared/api.ts` 类型定义
2. **实现 hooks**:在 `src/web/hooks/useTargetDetail.ts` 中新增 `useQuery`(写好 `queryKey``enabled` 条件)
3. **编写组件**:在 `src/web/components/` 创建组件文件
-`TargetDetailDrawer.tsx` 中新增 `<Tabs.TabPanel>` 引用
4. **编写常量**:如有列定义/排序器/筛选器,放在 `src/web/constants/`
5. **编写测试**:在 `tests/web/` 下添加对应的单元测试
### 2.6 样式开发规范
前端基于 TDesign React 构建 UI样式开发遵循以下优先级从高到低
1. **使用 TDesign 组件**:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography
2. **使用 TDesign 组件 props**:通过组件的 props 参数控制外观(如 `theme``variant``size`
3. **使用 TDesign CSS tokens**:颜色、间距、字体等使用 `--td-*` CSS 变量(如 `--td-success-color``--td-comp-margin-xxl`
4. **在 styles.css 中定义 CSS 类**:无法通过上述方式满足的样式需求,集中定义在 `styles.css`
5. **自行开发组件**:仅在 TDesign 无法满足需求时自行开发
**红线**
- **严禁在组件中使用 `style` 属性内联调整样式**
- **严禁通过 CSS 覆盖 TDesign 组件内部类名**(如 `.t-tab-panel`),如需定制使用组件的 `className` prop
- **严禁使用 `!important`**
- 颜色统一使用 TDesign CSS tokens`--td-success-color``--td-error-color``--td-warning-color` 等),不使用硬编码色值
**styles.css 组织**
- 自定义 CSS 变量(如可用率渐变色 `--avail-0` ~ `--avail-9`)定义在 `:root`
- 布局类(`.dashboard``.dashboard-header`)定义全局页面结构
- 组件修饰类(`.status-dot--up``.latency-ok`)为自定义视觉组件提供样式变体
- TDesign 表格行高亮(`.row-down`)通过 `rowClassName` prop 应用
### 2.7 前端测试规范
- 测试目录:`tests/web/`,结构对应 `src/web/`
- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值等)
- 使用 `bun:test` 框架
---
## 三、项目运行、集成与打包
### 3.1 开发期运行
#### 同时启动前后端
```bash
bun run dev probes.yaml
```
`scripts/dev.ts` 通过 `Bun.spawn` 同时启动两个子进程:
```
bun run dev probes.yaml
├── bun run dev:server probes.yaml → Bun HTTP 后端(默认 3000 端口)
└── bun run dev:web → Vite 前端开发服务器5173 端口)
```
- 任一子进程退出会导致整体退出
- `SIGINT`/`SIGTERM` 信号会同时终止两个子进程
- `BACKEND_PORT` 环境变量可覆盖后端端口
#### 分别启动
```bash
# 启动后端(含 watch 模式自动重启)
bun run dev:server probes.yaml
# 另开终端启动前端
bun run dev:web
```
### 3.2 前后端集成方式
#### 开发期代理
Vite 配置了开发代理(`vite.config.ts`
```typescript
server: {
proxy: {
"/api": {
target: `http://127.0.0.1:${backendPort}`,
changeOrigin: true,
},
},
}
```
前端访问 `/api/*`Vite 开发服务器自动转发到后端 `http://127.0.0.1:${backendPort}`,无需 CORS 配置。
前端开发地址为 `http://127.0.0.1:5173`(严格端口 `strictPort: true`)。
后端在开发模式下不提供静态资源服务,访问 `http://127.0.0.1:3000` 会提示"请通过 Vite 前端地址访问"。
#### 生产期集成
生产可执行文件是单体应用:前端静态资源嵌入 binary后端同时提供 API 和静态文件服务。
```
./dist/dial-server probes.yaml
启动后:
访问 http://127.0.0.1:3000/ → 返回前端 SPAindex.html
访问 http://127.0.0.1:3000/api/* → 返回后端 API
访问 /assets/* → 返回带不可变缓存的静态资源
```
SPA fallback 逻辑(`src/server/static.ts`
- `/` → index.html
- 匹配 `/assets/*` → 返回对应文件(未匹配则 404
- 其他路径(如 `/dashboard`)→ fallback 到 index.htmlSPA 路由)
### 3.3 构建打包
#### 构建命令
```bash
bun run build
```
#### 构建流程详解
`scripts/build.ts` 执行以下步骤:
```
1. vite build
├── 入口src/web/index.html
└── 输出dist/web/index.html + assets/
2. 生成 .build/static-assets.ts临时文件
├── import Vite 产物为 Bun.file
└── 导出 staticAssets: StaticAssets 对象
3. 生成 .build/server-entry.ts临时文件
└── import 后端入口模块 + staticAssets作为 Bun.build 入口
4. Bun.build({ compile, minify, sourcemap: "linked" })
└── 输出dist/dial-server单文件可执行 binary
```
#### 产物
| 产物 | 用途 |
| ------------------ | -------------------------- |
| `dist/dial-server` | 生产可执行文件 |
| `dist/web/` | Vite 构建产物(中间产物) |
| `.build/` | 临时生成文件(构建后清理) |
#### 构建参数
| 环境变量 | 说明 |
| --------------------------- | -------------------------------------- |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(如 `bun-linux-x64` |
#### 运行可执行文件
```bash
./dist/dial-server probes.yaml
```
#### 清理
```bash
bun run clean
# 清理 .build/ 缓存和 *.bun-build 临时文件
```
### 3.4 开发工作流
#### 日常开发循环
```bash
bun run dev probes.yaml # 启动开发环境
# 修改代码 → Vite HMR前端/ bun --watch后端自动重启
bun run check # 提交前运行完整质量检查
```
#### 完整验证流程
```bash
bun run verify
# = bun run check + bun run build + bun run test:smoke
```
`verify` 适合 CI 或正式提交前会完整验证类型检查、lint、格式、单元测试、构建、smoke test。
### 3.5 Smoke Test
```bash
bun run test:smoke
```
`scripts/smoke.ts` 构建后验证流程:
1. 动态分配空闲端口
2. 用临时配置文件启动 `dist/dial-server`
3. 等待健康检查通过
4. 验证所有 API 端点返回正确数据
5. 验证静态资源服务(含 SPA fallback 和 404 处理)
6. 验证安全 headers
7. 测试结束清理临时目录和进程
### 3.6 脚本说明
| 脚本 | 文件 | 说明 |
| -------------------- | ------------------ | ------------------------------ |
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
### 3.7 环境变量
| 变量 | 用途 | 默认值 |
| --------------------------- | ---------------------------------------------------- | -------- |
| `PORT`/`BACKEND_PORT` | 后端监听端口(开发期 Vite 代理目标、生产期监听端口) | `3000` |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 |
### 3.8 项目配置文件
| 文件 | 用途 |
| --------------------- | ---------------------------------------------- |
| `package.json` | 项目信息、脚本、依赖声明 |
| `tsconfig.json` | TypeScript 配置ESNext 模块、严格模式) |
| `vite.config.ts` | Vite 开发代理与构建配置 |
| `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) |
| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120` |
| `.prettierignore` | Prettier 排除路径 |
| `probes.example.yaml` | 配置文件示例 |
| `opencode.json` | OpenCode 工具配置TDesign MCP server |
### 3.9 依赖管理
- **包管理器**:仅使用 `bun`,禁止使用 npm、pnpm、yarn
- **安装依赖**`bun install`
- **运行工具**:使用 `bunx`,禁止使用 `npx``pnpx`
- **锁文件**`bun.lock`
### 3.10 目录约定
| 目录 | 约定 |
| ------------- | -------------------------------------------- |
| `src/server/` | 后端代码,不能 import `src/web/` |
| `src/web/` | 前端代码,不能 import `src/server/` |
| `src/shared/` | 前后端共享类型,双向可引用 |
| `scripts/` | 独立运行脚本,可 import 项目源码 |
| `tests/` | 测试目录,结构镜像 src 目录 |
| `dist/` | 构建产物gitignore |
| `.build/` | 构建临时文件gitignore |
| `openspec/` | OpenSpec 变更管理与规格文档 |
| `data/` | 默认数据目录gitignore运行期生成 SQLite |
---
## 代码质量
项目使用多层代码质量保障体系ESLint 类型感知规则 + Perfectionist 导入排序 + Prettier 格式化 + TypeScript 严格模式 + Git hooks 自动化。
```bash
bun run lint # ESLint 检查(含类型感知规则、导入排序、导入验证)
bun run format:check # Prettier 格式检查
bun run format # Prettier 自动格式化
bun run typecheck # TypeScript 类型检查(含 noUnusedLocals、noPropertyAccessFromIndexSignature
bun test # 运行所有测试
bun run check # 一键运行 typecheck + lint + format:check + test
```
`check` 是日常开发推荐的质量检查命令。
### ESLint 规则
配置文件:`eslint.config.js`
| 配置来源 | 用途 |
| ------------------------------------------------- | -------------------------------------------------- |
| `@eslint/js` recommended | JavaScript 基础规则 |
| `typescript-eslint` recommended-type-checked | TypeScript 类型感知规则no-floating-promises 等) |
| `typescript-eslint` stylistic-type-checked | TypeScript 风格规则(命名规范、语法选择等) |
| `eslint-plugin-perfectionist` recommended-natural | 导入语句和命名导出自动排序 |
| `eslint-plugin-import` | 导入路径验证、循环依赖检测、重复导入合并 |
### Prettier 配置
配置文件:`.prettierrc.json`
显式声明所有格式化参数(`printWidth: 120``semi: true``singleQuote: false``trailingComma: "all"``endOfLine: "lf"` 等),确保不同开发环境产出完全一致的格式化结果。
### TypeScript 严格标志
| 标志 | 值 | 说明 |
| ------------------------------------ | ----- | -------------------------------------------------------------------------- |
| `strict` | true | 全局严格模式 |
| `noUnusedLocals` | true | 未使用局部变量视为错误 |
| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)` |
| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 |
| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 |
### Git Hooks
通过 husky 在 commit 阶段自动执行检查:
| Hook | 行为 |
| ------------ | -------------------------------------------------------------- |
| `pre-commit` | lint-staged 对变更文件运行 `eslint --fix` + `prettier --write` |
| `commit-msg` | commitlint 校验提交信息格式 `类型: 简短描述` |
提交类型限定:`feat``fix``refactor``docs``style``test``chore`
`bun install` 时自动初始化 husky hooks无需手动配置。
## 测试
```bash
bun run check # 日常开发(类型检查 + lint + 格式 + 单元测试)
bun run verify # 完整验证check + 构建 + smoke test
```
## 已知限制
当前不做告警通知、数据自动清理、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。

45
Dockerfile Normal file
View File

@@ -0,0 +1,45 @@
# syntax=docker/dockerfile:1.7
ARG BUN_IMAGE=oven/bun:1-alpine
ARG ALPINE_IMAGE=alpine:3.22
FROM ${BUN_IMAGE} AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
FROM deps AS build
WORKDIR /app
COPY . .
ARG TARGETARCH
RUN set -eux; \
case "${TARGETARCH:-$(uname -m)}" in \
amd64|x86_64) export BUN_TARGET=bun-linux-x64-musl ;; \
arm64|aarch64) export BUN_TARGET=bun-linux-arm64-musl ;; \
*) echo "不支持的架构: ${TARGETARCH:-$(uname -m)}" >&2; exit 1 ;; \
esac; \
bun run build
FROM ${ALPINE_IMAGE} AS runtime
RUN apk add --no-cache ca-certificates iputils-ping libgcc libstdc++ tzdata \
&& addgroup -S dial \
&& adduser -S -G dial -h /nonexistent -s /sbin/nologin dial \
&& mkdir -p /etc/dial /data/dial \
&& chown -R dial:dial /data/dial
COPY --from=build --chmod=0755 /app/dist/dial-server /usr/local/bin/dial-server
COPY --chmod=0644 docker/probes.yaml /etc/dial/probes.yaml
USER dial
WORKDIR /data/dial
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q -O - "http://127.0.0.1:3000/health" >/dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/dial-server"]
CMD ["/etc/dial/probes.yaml"]

233
README.md
View File

@@ -1,200 +1,99 @@
# DiAL
<h1 align="center">DiAL</h1>
基于 Bun + TypeScript 的多类型拨测监控工具。通过 YAML 配置文件定义 HTTP 和命令行拨测目标,后端按配置定时并发拨测,结果持久化到本地 SQLite前端 Dashboard 展示各目标实时状态、可用率、耗时趋势等。
<p align="center">
<strong>轻量级多类型拨测监控工具</strong>
</p>
<p align="center">
基于 Bun + TypeScript 构建 · YAML 配置驱动 · 内置 Dashboard
</p>
---
DiAL 是一个自托管的拨测监控工具,支持 **HTTP**、**命令行**、**数据库**、**TCP**、**UDP**、**DNS**、**ICMP** 和 **LLM** 多种拨测类型。通过 YAML 配置文件定义拨测目标,后端定时并发执行拨测并将结果持久化到本地 SQLite前端 Dashboard 展示各目标的实时状态、可用率和耗时趋势。
## 功能亮点
- 多类型拨测HTTP、Cmd、DB、TCP、UDP、DNS、ICMP、LLM
- 丰富校验规则状态码、响应头、JSONPath、CSS 选择器、XPath、正则匹配、数值比较等
- 结构化观测数据HTTP body 预览、TCP/UDP 响应摘要、ICMP 丢包率、CMD 输出、LLM token 用量等
- 内置 Dashboard实时状态、可用率统计、趋势图、最近状态条、手动/自动刷新、版本号展示
- 多主题支持:系统、明亮、黑暗三种主题模式
- 自托管部署:本地 SQLite 存储,无需额外数据库服务
## 应用截图
| | 亮色 | 暗色 |
| ------ | --------------------------------------------------- | ------------------------------------------------- |
| 主页 | ![index-light](assets/screenshot/light_index.png) | ![index-dark](assets/screenshot/dark_index.png) |
| 详情页 | ![detail-light](assets/screenshot/light_detail.png) | ![detail-dark](assets/screenshot/dark_detail.png) |
## 快速开始
**前置条件:** [Bun](https://bun.sh/) >= 1.0
ICMP checker 依赖系统 `ping` 命令。精简容器镜像需额外安装,例如 Alpine 可安装 `iputils-ping`
```bash
git clone https://github.com/your-org/DiAL.git
cd DiAL
bun install
cp probes.example.yaml probes.yaml
bun run dev probes.yaml
```
`bun run dev` 会同时启动 Bun 后端和 Vite 前端。开发期请打开 Vite 前端地址 `http://127.0.0.1:5173`
`bun run dev` 会同时启动 Vite 开发服务器(`http://127.0.0.1:5173`)和 API 服务器(`http://127.0.0.1:3000`),访问前端地址即可使用 Dashboard
也可以分别运行:
```bash
bun run dev:server probes.yaml
bun run dev:web
```
## 配置文件
程序通过 YAML 配置文件定义所有运行参数:
## 最小配置示例
```yaml
server:
host: "127.0.0.1"
port: 3000
dataDir: "/tmp/probes_data"
runtime:
maxConcurrentChecks: 20
defaults:
interval: "5s"
timeout: "10s"
http:
method: GET
maxBodyBytes: "100MB"
command:
maxOutputBytes: "100MB"
# yaml-language-server: $schema=./probe-config.schema.json
targets:
- name: "Baidu"
- id: "baidu-home"
name: "Baidu"
type: http
http:
url: "https://www.baidu.com"
expect:
status: [200]
maxDurationMs: 10000
- name: "JSON API 示例"
type: http
http:
url: "https://httpbin.org/json"
expect:
status: [200]
headers:
Content-Type:
contains: "application/json"
body:
- contains: "slideshow"
- json:
path: "$.slideshow.title"
equals: "Sample Slide Show"
- name: "HTML 页面示例"
type: http
http:
url: "https://httpbin.org/html"
expect:
status: [200]
body:
- contains: "Moby-Dick"
- xpath:
path: "/html/body/h1/text()"
equals: "Herman Melville - Moby-Dick"
- name: "Nginx 进程检查"
type: command
command:
exec: "pgrep"
args: ["nginx"]
expect:
exitCode: [0]
stdout:
- match: "\\d+"
durationMs:
lte: 5000
```
### 配置说明
完整配置、checker、expect 和部署说明参见 [用户文档](docs/user/README.md)、[配置文件](docs/user/configuration.md)、[Checker 参考](docs/user/checkers/README.md) 和 [校验规则](docs/user/expectations.md)。
- **server**: 服务配置(均可省略,使用默认值)
- `host`: 监听地址,默认 `127.0.0.1`
- `port`: 监听端口,默认 `3000`
- `dataDir`: 数据目录,默认 `./data`
- **runtime**: 运行时配置
- `maxConcurrentChecks`: 最大并发拨测数,默认 `20`
- **defaults**: 全局默认值(均可省略)
- `interval`: 拨测间隔,默认 `30s`
- `timeout`: 超时时间,默认 `10s`
- `http`: HTTP 类型默认值
- `method`: HTTP 方法,默认 `GET`
- `maxBodyBytes`: 响应体最大字节数,默认 `100MB`
- `command`: Command 类型默认值
- `maxOutputBytes`: 输出最大字节数,默认 `100MB`
- **targets**: 拨测目标列表(必填)
- `name`: 目标名称(必填,唯一)
- `type`: 目标类型,`http``command`(必填)
- `group`: 分组名称(可选,默认 `"default"`
- `http`: HTTP 拨测配置type 为 http 时必填)
- `url`: 目标 URL
- `method``headers``body`: 请求参数
- `command`: 命令行拨测配置type 为 command 时必填)
- `exec`: 可执行文件名或路径
- `args`: 命令行参数列表
- `env`: 环境变量覆盖(可选,继承进程环境变量并合并覆盖)
- `cwd`: 工作目录(可选,相对于配置文件所在目录解析,默认 `.`
- `interval``timeout`: 覆盖全局默认值
- `expect`: 期望校验
- `status`: 可接受的状态码列表HTTP
- `exitCode`: 可接受的退出码列表Command
- `headers`: 响应头校验HTTP支持 `equals``contains` 等操作符)
- `maxDurationMs`: 最大耗时阈值(毫秒)
- `body`: HTTP 响应体校验(数组,可组合使用)
- `contains`: 响应体包含的文本
- `match`: 响应体匹配的正则表达式
- `json`: JSONPath 提取值比较(`path` + 比较操作符)
- `css`: CSS 选择器提取 HTML 元素比较
- `xpath`: XPath 提取 XML/HTML 节点比较
- `stdout` / `stderr`: Command 输出校验(数组,同 body 格式)
- 比较操作符:`equals`(默认)、`contains``match`(正则)、`empty``exists``gte``lte``gt``lt`
大小说明:`maxBodyBytes``maxOutputBytes` 支持单位 `KB``MB``GB`,也可直接使用数字(字节数)。
时长格式支持:`30s``5m``500ms`
## API 端点
| 端点 | 说明 |
| ----------------------------------------------------------------- | --------------------------------------- |
| `GET /health` | 健康检查 |
| `GET /api/summary` | 总览统计total/up/down/lastCheckTime |
| `GET /api/targets` | 目标列表及最新状态、分组和采样数据 |
| `GET /api/targets/:id/history?from=ISO&to=ISO&page=1&pageSize=20` | 指定目标的拨测记录(时间范围 + 分页) |
| `GET /api/targets/:id/trend?from=ISO&to=ISO` | 指定目标的按小时聚合趋势 |
### 响应字段
**SummaryResponse**: `total``up``down``lastCheckTime`
**TargetStatus**: `id``name``type`http/command`target`URL 或命令摘要)、`group``interval``latestCheck``stats``recentSamples`
**RecentSample**: `timestamp``durationMs``up`
**CheckResult**: `timestamp``matched``durationMs``statusDetail``failure`
**CheckFailure**: `kind`error/mismatch`phase``path``expected``actual``message`
**TargetStats**: `totalChecks``availability`
**TrendPoint**: `hour``avgDurationMs``availability``totalChecks`
**HistoryResponse**: `items`CheckResult[])、`total``page``pageSize`
### 错误响应
API 错误返回 `ApiErrorResponse` 格式:
```json
{ "error": "描述信息", "status": 400 }
```
| 状态码 | 触发场景 |
| ------ | ----------------------------------------------------------------------- |
| 400 | 参数格式错误(无效 ID、from/to 缺失或格式错误、page/pageSize 非正整数) |
| 404 | 目标不存在、API 路由未匹配 |
| 405 | 非 GET 方法请求 API 路由 |
## 运行参数
CLI 只接受一个参数YAML 配置文件路径。
## 生产运行
```bash
bun run build
./dist/dial-server ./probes.yaml
```
## 目标状态判定
Docker、跨平台发布包和运行时注意事项参见 [部署文档](docs/user/deployment.md)。
单层判定模型,适用于 HTTP 和 Command 两种类型:
## 文档导航
- **matched**: 是否符合 expect 规则(无 expect 时默认为 true
- **UP** = matched
- **DOWN** = NOT matched
| 入口 | 内容 |
| -------------------------------------------- | ------------------------------------------- |
| [文档总览](docs/README.md) | 全部文档入口和文档归属矩阵 |
| [用户文档](docs/user/README.md) | 配置、部署、expect、排障和 checker 使用入口 |
| [配置文件](docs/user/configuration.md) | YAML 结构、变量、server、targets 通用字段 |
| [Checker 参考](docs/user/checkers/README.md) | 所有 checker 的配置、expect 和示例 |
| [校验规则](docs/user/expectations.md) | expect 规则、状态判定、failure、observation |
| [部署文档](docs/user/deployment.md) | 构建、Docker、发布包和容器运行边界 |
| [故障排查](docs/user/troubleshooting.md) | 常见运行问题和排查入口 |
| [开发文档](docs/development/README.md) | 开发入口、常用命令、质量门禁和专题索引 |
执行失败(网络错误、超时、进程崩溃)和 expect 不匹配都统一为 `matched=false`,通过 `failure.kind` 区分(`"error"` vs `"mismatch"`)。
## 开发
---
```bash
bun run check # schema:check + typecheck + lint + test
bun run verify # check + build
```
> 开发相关文档(项目结构、构建、测试、代码规范等)请参阅 [DEVELOPMENT.md](DEVELOPMENT.md)。
开发入口参见 [开发文档](docs/development/README.md)。新增或修改 checker 前请先阅读 [Checker 开发](docs/development/checker.md)。
## License
Apache-2.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

436
bun.lock
View File

@@ -5,42 +5,78 @@
"": {
"name": "gateway-checker",
"dependencies": {
"@ai-sdk/anthropic": "^3",
"@ai-sdk/openai": "^3",
"@number-flow/react": "^0.6.0",
"@sinclair/typebox": "^0.34.49",
"@tanstack/react-query": "^5.100.10",
"@xmldom/xmldom": "^0.9.10",
"ai": "^6",
"ajv": "^8.20.0",
"cheerio": "^1.2.0",
"croner": "^10.0.1",
"es-toolkit": "^1.46.1",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"pino-roll": "^4.0.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"recharts": "^3.8.1",
"systeminformation": "^5.31.6",
"tdesign-icons-react": "^0.6.4",
"tdesign-react": "^1.16.9",
"xpath": "^0.0.34",
},
"devDependencies": {
"@commitlint/cli": "^21.0.0",
"@commitlint/config-conventional": "^21.0.0",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@eslint/js": "^10.0.1",
"@tanstack/react-query-devtools": "^5.100.10",
"@types/bun": "^1.3.13",
"@testing-library/react": "^16.3.2",
"@types/bun": "^1.3.14",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@types/tar-stream": "^3.1.4",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-perfectionist": "^5.9.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.4",
"prettier": "^3.8.3",
"tar-stream": "^3.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.11",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.13",
},
},
},
"packages": {
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.78.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.115", "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-3.0.115.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xonmGfN9pt54WdKqMzWe68BRYS3rsYvraBzioyA0gfNcecHs8Ir5qk/X8grJSyZ95hghjWiOphrK6bAc11E6SA=="],
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.64", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.64.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-epO4iS6QwktaY2PF6uBcPnDTJ3BxPOfsGS7/OEtBe3GtNj7C8h8gMDVtIe5K8W16HNDbn0tbR4dcQfpfs+XVFg=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-3.0.10.tgz", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
"@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.3", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.3.tgz", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
@@ -75,42 +111,56 @@
"@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@commitlint/cli": ["@commitlint/cli@21.0.0", "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.0.tgz", { "dependencies": { "@commitlint/format": "^21.0.0", "@commitlint/lint": "^21.0.0", "@commitlint/load": "^21.0.0", "@commitlint/read": "^21.0.0", "@commitlint/types": "^21.0.0", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-p3y2oC0G2R45zaadMwBxCiSesS8digi5RDplP3Zrfpzm7xIgrgAj0W4fGzONjpHyg8obDVJDU45g5txzeMcblg=="],
"@bramus/specificity": ["@bramus/specificity@2.4.2", "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.0", "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-QJX/rPK4Yu3f5J4OCIBy5aXq2e0EEdwSDFZ3NQvFAXTm3gs12ipyZ+yjhZxm3hHn6DB8wuv3zhFTL1I2tYzUBA=="],
"@commitlint/cli": ["@commitlint/cli@21.0.1", "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.1.tgz", { "dependencies": { "@commitlint/format": "^21.0.1", "@commitlint/lint": "^21.0.1", "@commitlint/load": "^21.0.1", "@commitlint/read": "^21.0.1", "@commitlint/types": "^21.0.1", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-8vq10krmbJwBkvzXKhbs4o4JQEVscd3pqOlWuDUaDBwbeL694/P33UC29tZQFTAgPU9fVJ2+f2m3zw16yKWxHg=="],
"@commitlint/config-validator": ["@commitlint/config-validator@21.0.0", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "ajv": "^8.11.0" } }, "sha512-v0UplTYryNUB463X5WrelzKq5/qyYm9/iUNk38S7ZLnd56Uuk2T9awhYKGlgD2/4L5YuN2gsKkyy4EHpRPPz2Q=="],
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.1", "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-gRorrkfWOh/+V5X8GYWWbQvrzPczopGMS4CCNrQdHkK4xWElv82BDvIsDhJZWTlI7TazOlYea6VATufCsFs+sw=="],
"@commitlint/ensure": ["@commitlint/ensure@21.0.0", "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "es-toolkit": "^1.46.0" } }, "sha512-n+OYs0Ws9GKC2WlmAeLNoPz9CUg6n/ZyYMkFF8rJ0aMn2kDTDTG0VqK/2Dco0EB4fhuF3JPIllJmU9/LKTl4aw=="],
"@commitlint/config-validator": ["@commitlint/config-validator@21.0.1", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "ajv": "^8.11.0" } }, "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA=="],
"@commitlint/execute-rule": ["@commitlint/execute-rule@21.0.0", "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.0.tgz", {}, "sha512-3OhTq2gQX1tEheMsbDNqxfcNHsAM6g9cub9plf05I9jCxtbNfn8Y+mhClKyUwhX4dbtmC4OLZ9i+HNmoL1aksA=="],
"@commitlint/ensure": ["@commitlint/ensure@21.0.1", "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "es-toolkit": "^1.46.0" } }, "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ=="],
"@commitlint/format": ["@commitlint/format@21.0.0", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "picocolors": "^1.1.1" } }, "sha512-RTfGSrueEgofs1piqwi42U05d85wfxiMH2ncMCZnltx1XqPR3N2S48oACBtTy4xRAhWlf5XlHkK2RaDzEQu3dA=="],
"@commitlint/execute-rule": ["@commitlint/execute-rule@21.0.1", "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", {}, "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA=="],
"@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.0", "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "semver": "^7.6.0" } }, "sha512-K3SaaOTVY9VKhge7vl0R3ng7GENRzJQ9MPV43Tu53kAwEgSx/E0HF4US3AcVqdvlvsDUbF2yXvED95dhela83w=="],
"@commitlint/format": ["@commitlint/format@21.0.1", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "picocolors": "^1.1.1" } }, "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw=="],
"@commitlint/lint": ["@commitlint/lint@21.0.0", "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.0.tgz", { "dependencies": { "@commitlint/is-ignored": "^21.0.0", "@commitlint/parse": "^21.0.0", "@commitlint/rules": "^21.0.0", "@commitlint/types": "^21.0.0" } }, "sha512-dlUJA0Ka14R1YaR46JVRWE3m/8dOQAgE/D0heUfzYua5Jogtq/zzu2ITAIaB/u25DaKjtEO6kuvASzsFDyrPMw=="],
"@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.1", "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "semver": "^7.6.0" } }, "sha512-iNDP8SFdw8JEkM0CHZ2XFnhTN4Zg5jKUY2d8kBOSFrI2aA+3YJI7fcqVpfgbpJ9xtxFVYpi+DBATU5AvhoTq8g=="],
"@commitlint/load": ["@commitlint/load@21.0.0", "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.0.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.0", "@commitlint/execute-rule": "^21.0.0", "@commitlint/resolve-extends": "^21.0.0", "@commitlint/types": "^21.0.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-l0nBfO/20PKcJXHZqDIgh7kw/TWVVwn8zZJOkVGBK/ig/h328jBu9jK7OiDl2oZr5mLphmKGjYDR2ffEyb2lIA=="],
"@commitlint/lint": ["@commitlint/lint@21.0.1", "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.1.tgz", { "dependencies": { "@commitlint/is-ignored": "^21.0.1", "@commitlint/parse": "^21.0.1", "@commitlint/rules": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-gF+iYtUw1gBG3HUH9z3VxwUjGg2R2G5j+nmvPs8aIeYkiB7TtneBu3wO85I0bUl93bYNsvsCNI9Nte2fmDUMww=="],
"@commitlint/message": ["@commitlint/message@21.0.0", "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.0.tgz", {}, "sha512-+daU92JaOHhI2En9KcH+2mvZGJ6D4YSxb/32QDwqkOwSj1Vanjio8PbAqX7dneACdg6B7RgQ7i3mpyYZAws4nw=="],
"@commitlint/load": ["@commitlint/load@21.0.1", "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/execute-rule": "^21.0.1", "@commitlint/resolve-extends": "^21.0.1", "@commitlint/types": "^21.0.1", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-Btg1q1mKmiihN4W3x0EsPDrJMOQfMa9NIqlzlJyXAfxvsOGdGXOW5p3R3RcSxDCaY7JabY9flIl+Om1af3PSrw=="],
"@commitlint/parse": ["@commitlint/parse@21.0.0", "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-1dbvFBcQK79aTbpc2QCrgEDc6/MMkQ0Mdz4gGmYkN4AHMnAK9HesSewTHqGTrW5mALrMlYSgcWyvKjloY2w19A=="],
"@commitlint/message": ["@commitlint/message@21.0.1", "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.1.tgz", {}, "sha512-R3dVQeJQ0B6yqrZEjkUHD4r7UJYLV9Lvk2xs3PTOmtWk2G3mI6Xgc+YdRxL1PwcDfBiUjv2SkIkW4AUc976w1w=="],
"@commitlint/read": ["@commitlint/read@21.0.0", "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.0.tgz", { "dependencies": { "@commitlint/top-level": "^21.0.0", "@commitlint/types": "^21.0.0", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-8VKLKLl2vBSKoTMm1LwcySsyxrBeotnqcT5qJi9pPuPfqSapdAD870Ckgh79c41UFywL6kMqtiyY+kxtfcqZGg=="],
"@commitlint/parse": ["@commitlint/parse@21.0.1", "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-oh/nCSOqdoeQNA1tO8aAmxkq5EBo8/NzcFQRvv66AWc9HpED28sL2iSicCKU6hPintWuscL6BJEWi77Wq1LPMQ=="],
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.0", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.0.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.0", "@commitlint/types": "^21.0.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-hrJYSZRpmecmSoxYrpuJ/1Q4J9JHt4AVVtr5/Ac6upLO/jJ1DnIm2AjD+38gru3KGOec4aHCVqETuWWLJhydWw=="],
"@commitlint/read": ["@commitlint/read@21.0.1", "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.1.tgz", { "dependencies": { "@commitlint/top-level": "^21.0.1", "@commitlint/types": "^21.0.1", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-pMEu4lbpC8W0ZgKJj2U6WaobXIZWdFlULpIEewYhkPXx+WZcnoO53YrVPc7QErQuNolq2Me8dP58Wu7YAVXVOA=="],
"@commitlint/rules": ["@commitlint/rules@21.0.0", "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.0.tgz", { "dependencies": { "@commitlint/ensure": "^21.0.0", "@commitlint/message": "^21.0.0", "@commitlint/to-lines": "^21.0.0", "@commitlint/types": "^21.0.0" } }, "sha512-NgQhX1qENA+rbrMw5KKyvVZpZG4D/0wgK8Z4INtcwKbfKtVDFMbn0oNc/Rs8wdyBPBj7ue8Lo/GllUL2Mqjwkg=="],
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.1", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/types": "^21.0.1", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg=="],
"@commitlint/to-lines": ["@commitlint/to-lines@21.0.0", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.0.tgz", {}, "sha512-qMwvrJK/x3dPcXsIAtQAMKV5Q0wTioyqyHKR06vVN4wmBF4cCrrLq5x81FDeY3Ba+GWgDt0/P3Zw/IHGM8lwgg=="],
"@commitlint/rules": ["@commitlint/rules@21.0.1", "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.1.tgz", { "dependencies": { "@commitlint/ensure": "^21.0.1", "@commitlint/message": "^21.0.1", "@commitlint/to-lines": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-VMooYpz4nJg7xlaUso6CCOWEz8D/ChkvsvZUMARcoJ1ZpfKPyFCGrHNha2tbsETNAb6ErgiRuCr2DvghrvPDYQ=="],
"@commitlint/top-level": ["@commitlint/top-level@21.0.0", "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.0.tgz", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-8jPqyWZueuN4hU6/ArKVsZ6i8xWtjIrbzHEOaLaTGUfjhhbZNBfXef/DGjzxy55hAv3yFNxHLINfI1bCJ0/MzA=="],
"@commitlint/to-lines": ["@commitlint/to-lines@21.0.1", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.1.tgz", {}, "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw=="],
"@commitlint/types": ["@commitlint/types@21.0.0", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.0.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-6nEz+M7I90iix4sviA8NLwskOuyt0M98KUU2aYgiKbn46jMSxUm1l2ACtzRd9ec+y38aKyJhW4Fp6NW0z35kJQ=="],
"@commitlint/top-level": ["@commitlint/top-level@21.0.1", "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.1.tgz", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-4esUYqzY7K0FCgcJ/1xWEZekV7Ch4yZT1+xjEb7KzqbJ05XEkxHVsTfC8ADKNNtlCE2pj98KEbPGZWw9WwEnVw=="],
"@commitlint/types": ["@commitlint/types@21.0.1", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.1.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg=="],
"@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.7.0", "https://registry.npmmirror.com/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.4.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
"@csstools/css-calc": ["@csstools/css-calc@3.2.1", "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.1.tgz", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.1", "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.4", "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -133,6 +183,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
"@exodus/bytes": ["@exodus/bytes@1.15.0", "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.0.tgz", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
@@ -155,43 +207,51 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@oxc-project/types": ["@oxc-project/types@0.128.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.128.0.tgz", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="],
"@number-flow/react": ["@number-flow/react@0.6.0", "https://registry.npmmirror.com/@number-flow/react/-/react-0.6.0.tgz", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-77Yfc9+zkV2UDSP8phhZzxJGuwxi/Tt1TikmipL+1r3e9GFKEYDZ1XwInj67NoSt3OnOB0KLvvcl3lfPZgBHVQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@oxc-project/types": ["@oxc-project/types@0.130.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.130.0.tgz", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "https://registry.npmmirror.com/@pinojs/redact/-/redact-0.4.0.tgz", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@pkgr/core": ["@pkgr/core@0.2.9", "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
"@popperjs/core": ["@popperjs/core@2.11.8", "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "arm" }, "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "x64" }, "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", { "os": "none", "cpu": "arm64" }, "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", { "os": "win32", "cpu": "x64" }, "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
@@ -199,6 +259,8 @@
"@simple-libs/stream-utils": ["@simple-libs/stream-utils@1.2.0", "https://registry.npmmirror.com/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", {}, "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.34.49.tgz", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -211,9 +273,15 @@
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.10", "https://registry.npmmirror.com/@tanstack/react-query-devtools/-/react-query-devtools-5.100.10.tgz", { "dependencies": { "@tanstack/query-devtools": "5.100.10" }, "peerDependencies": { "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, "sha512-zes0+o9ef5rAZXJ9f/SeaLs2nufJaeVkZkl/Or9NGrWVF41kL9Od9ED9nCwtQlgiF2VGtrzhEw5AU/igAO+aAg=="],
"@testing-library/dom": ["@testing-library/dom@10.4.1", "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
"@testing-library/react": ["@testing-library/react@16.3.2", "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/bun": ["@types/bun@1.3.13", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.13.tgz", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/bun": ["@types/bun@1.3.14", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.14.tgz", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -237,6 +305,8 @@
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/jsdom": ["@types/jsdom@28.0.3", "https://registry.npmmirror.com/@types/jsdom/-/jsdom-28.0.3.tgz", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^8.0.0", "undici-types": "^7.21.0" } }, "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
@@ -249,29 +319,33 @@
"@types/sortablejs": ["@types/sortablejs@1.15.9", "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="],
"@types/tar-stream": ["@types/tar-stream@3.1.4", "https://registry.npmmirror.com/@types/tar-stream/-/tar-stream-3.1.4.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg=="],
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "https://registry.npmmirror.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/validator": ["@types/validator@13.15.10", "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.2.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.3.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.2.tgz", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.2.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -311,7 +385,9 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.2.0.tgz", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
@@ -319,16 +395,20 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"ai": ["ai@6.0.184", "https://registry.npmmirror.com/ai/-/ai-6.0.184.tgz", { "dependencies": { "@ai-sdk/gateway": "3.0.115", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j//zHkKvj5ra27l8izHco8cj1g1Pr7vx1ZK+hrzrkHvndgIRmdfZKOb6+RAPpvbk42qGIsuYvlYbGlVAu3erNQ=="],
"ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"ansi-styles": ["ansi-styles@5.2.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-query": ["aria-query@5.3.0", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"array-ify": ["array-ify@1.0.0", "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="],
@@ -345,19 +425,37 @@
"async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"b4a": ["b4a@1.8.1", "https://registry.npmmirror.com/b4a/-/b4a-1.8.1.tgz", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="],
"balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"bare-events": ["bare-events@2.8.3", "https://registry.npmmirror.com/bare-events/-/bare-events-2.8.3.tgz", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw=="],
"bare-fs": ["bare-fs@4.7.1", "https://registry.npmmirror.com/bare-fs/-/bare-fs-4.7.1.tgz", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="],
"bare-os": ["bare-os@3.9.1", "https://registry.npmmirror.com/bare-os/-/bare-os-3.9.1.tgz", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="],
"bare-path": ["bare-path@3.0.0", "https://registry.npmmirror.com/bare-path/-/bare-path-3.0.0.tgz", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
"bare-stream": ["bare-stream@2.13.1", "https://registry.npmmirror.com/bare-stream/-/bare-stream-2.13.1.tgz", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow=="],
"bare-url": ["bare-url@2.4.3", "https://registry.npmmirror.com/bare-url/-/bare-url-2.4.3.tgz", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.28", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA=="],
"bidi-js": ["bidi-js@1.0.3", "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"boolbase": ["boolbase@1.0.0", "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@5.0.6", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"bun-types": ["bun-types@1.3.13", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"bun-types": ["bun-types@1.3.14", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"call-bind": ["call-bind@1.0.9", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
@@ -383,6 +481,8 @@
"clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"colorette": ["colorette@2.0.20", "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"compare-func": ["compare-func@2.0.0", "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
"concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@@ -399,10 +499,14 @@
"cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="],
"croner": ["croner@10.0.1", "https://registry.npmmirror.com/croner/-/croner-10.0.1.tgz", {}, "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g=="],
"cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-select": ["css-select@5.2.2", "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-tree": ["css-tree@3.2.1", "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"css-what": ["css-what@6.2.2", "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -429,16 +533,24 @@
"d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"data-urls": ["data-urls@7.0.0", "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"date-fns": ["date-fns@4.2.1", "https://registry.npmmirror.com/date-fns/-/date-fns-4.2.1.tgz", {}, "sha512-37RhSdxaG1suen6VDCza6rNrQfooyQh57HFVPwQGEq2QWliVLzPQZ8Oa017weOu+HZCnzI7N3Pf/wyoBKfEqrA=="],
"dateformat": ["dateformat@4.6.3", "https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"dayjs": ["dayjs@1.11.10", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz", {}, "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="],
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -447,10 +559,14 @@
"define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dom-serializer": ["dom-serializer@2.0.0", "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -471,7 +587,9 @@
"encoding-sniffer": ["encoding-sniffer@0.2.1", "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
"entities": ["entities@4.5.0", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"end-of-stream": ["end-of-stream@1.4.5", "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"entities": ["entities@8.0.0", "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
"env-paths": ["env-paths@2.2.1", "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
@@ -501,6 +619,8 @@
"eslint": ["eslint@10.3.0", "https://registry.npmmirror.com/eslint/-/eslint-10.3.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-import-context": ["eslint-import-context@0.1.9", "https://registry.npmmirror.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
@@ -513,6 +633,8 @@
"eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@5.9.0", "https://registry.npmmirror.com/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.9.0.tgz", { "dependencies": { "@typescript-eslint/utils": "^8.58.2", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" } }, "sha512-8TWzg02zmnBdZwCkWLi8jhzqXI+fE7Z/RwV8SL6xD45tJ8Bp3wGuYL2XtQgfe/Wd0eBqOUX+s6ey73IyszvKTA=="],
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="],
@@ -521,6 +643,8 @@
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"esm-env": ["esm-env@1.2.2", "https://registry.npmmirror.com/esm-env/-/esm-env-1.2.2.tgz", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"espree": ["espree@11.2.0", "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["esquery@1.7.0", "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
@@ -533,12 +657,24 @@
"eventemitter3": ["eventemitter3@5.0.4", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events-universal": ["events-universal@1.0.1", "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"eventsource-parser": ["eventsource-parser@3.0.8", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.8.tgz", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"fast-copy": ["fast-copy@4.0.3", "https://registry.npmmirror.com/fast-copy/-/fast-copy-4.0.3.tgz", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-diff": ["fast-diff@1.3.0", "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
"fast-fifo": ["fast-fifo@1.3.2", "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -599,12 +735,16 @@
"hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"help-me": ["help-me@5.0.0", "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
"hermes-estree": ["hermes-estree@0.25.1", "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"htmlparser2": ["htmlparser2@10.1.0", "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
"husky": ["husky@9.1.7", "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
@@ -665,6 +805,8 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-regex": ["is-regex@1.2.1", "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
@@ -689,17 +831,23 @@
"jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"joycon": ["joycon@3.1.1", "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"jsdom": ["jsdom@29.1.1", "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="],
"jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-schema": ["json-schema@0.4.0", "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
@@ -747,10 +895,14 @@
"loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru-cache": ["lru-cache@11.3.6", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
"lz-string": ["lz-string@1.5.0", "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdn-data": ["mdn-data@2.27.1", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"meow": ["meow@13.2.0", "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
"mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
@@ -777,6 +929,8 @@
"nth-check": ["nth-check@2.1.1", "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"number-flow": ["number-flow@0.6.0", "https://registry.npmmirror.com/number-flow/-/number-flow-0.6.0.tgz", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ=="],
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@@ -793,6 +947,10 @@
"object.values": ["object.values@1.2.1", "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@7.0.0", "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"optionator": ["optionator@0.9.4", "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
@@ -807,7 +965,7 @@
"parse-json": ["parse-json@5.2.0", "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5": ["parse5@8.0.1", "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
@@ -825,6 +983,16 @@
"picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pino": ["pino@10.3.1", "https://registry.npmmirror.com/pino/-/pino-10.3.1.tgz", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="],
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"pino-pretty": ["pino-pretty@13.1.3", "https://registry.npmmirror.com/pino-pretty/-/pino-pretty-13.1.3.tgz", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="],
"pino-roll": ["pino-roll@4.0.0", "https://registry.npmmirror.com/pino-roll/-/pino-roll-4.0.0.tgz", { "dependencies": { "date-fns": "^4.1.0", "sonic-boom": "^4.0.1" } }, "sha512-axI1aQaIxXdw1F4OFFli1EDxIrdYNGLowkw/ZoZogX8oCSLHUghzwVVXUS8U+xD/Savwa5IXpiXmsSGKFX/7Sg=="],
"pino-std-serializers": ["pino-std-serializers@7.1.0", "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
@@ -833,10 +1001,20 @@
"prettier": ["prettier@3.8.3", "https://registry.npmmirror.com/prettier/-/prettier-3.8.3.tgz", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="],
"pretty-format": ["pretty-format@27.5.1", "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"process-warning": ["process-warning@5.0.0", "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"pump": ["pump@3.0.4", "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"raf": ["raf@3.4.1", "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="],
"react": ["react@19.2.6", "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
@@ -845,12 +1023,14 @@
"react-fast-compare": ["react-fast-compare@3.2.2", "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
"react-is": ["react-is@19.2.6", "https://registry.npmmirror.com/react-is/-/react-is-19.2.6.tgz", {}, "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw=="],
"react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-redux": ["react-redux@9.2.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
"react-transition-group": ["react-transition-group@4.4.5", "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"recharts": ["recharts@3.8.1", "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
"redux": ["redux@5.0.1", "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
@@ -877,7 +1057,7 @@
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rolldown": ["rolldown@1.0.0-rc.18", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.18.tgz", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="],
"rolldown": ["rolldown@1.0.1", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.1.tgz", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="],
"safe-array-concat": ["safe-array-concat@1.1.4", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
@@ -885,10 +1065,16 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"saxes": ["saxes@6.0.0", "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"secure-json-parse": ["secure-json-parse@4.1.0", "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
"semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-function-length": ["set-function-length@1.2.2", "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
@@ -913,14 +1099,20 @@
"slice-ansi": ["slice-ansi@8.0.0", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"sonic-boom": ["sonic-boom@4.2.1", "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.1.tgz", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
"sortablejs": ["sortablejs@1.15.7", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="],
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"split2": ["split2@4.2.0", "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"stable-hash-x": ["stable-hash-x@0.2.0", "https://registry.npmmirror.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"streamx": ["streamx@2.25.0", "https://registry.npmmirror.com/streamx/-/streamx-2.25.0.tgz", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
"string-argv": ["string-argv@0.3.2", "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-width": ["string-width@7.2.0", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -935,18 +1127,42 @@
"strip-bom": ["strip-bom@3.0.0", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"symbol-tree": ["symbol-tree@3.2.4", "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"synckit": ["synckit@0.11.12", "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
"systeminformation": ["systeminformation@5.31.6", "https://registry.npmmirror.com/systeminformation/-/systeminformation-5.31.6.tgz", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA=="],
"tar-stream": ["tar-stream@3.2.0", "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.2.0.tgz", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
"tdesign-icons-react": ["tdesign-icons-react@0.6.4", "https://registry.npmmirror.com/tdesign-icons-react/-/tdesign-icons-react-0.6.4.tgz", { "dependencies": { "@babel/runtime": "^7.16.5", "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-USAoi9vBWcwcJT45VqR3dRqX1MeAsn/RhHVx4bLwplhrlvE80ZQ1N9V+6F3HqE1Qe9mMDbtRM8Ul80+lALScww=="],
"tdesign-react": ["tdesign-react@1.16.9", "https://registry.npmmirror.com/tdesign-react/-/tdesign-react-1.16.9.tgz", { "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", "@types/sortablejs": "^1.10.7", "@types/validator": "^13.1.3", "classnames": "~2.5.1", "dayjs": "1.11.10", "hoist-non-react-statics": "~3.3.2", "lodash-es": "^4.17.21", "mitt": "^3.0.0", "raf": "~3.4.1", "react-fast-compare": "^3.2.2", "react-is": "^18.2.0", "react-transition-group": "~4.4.1", "sortablejs": "^1.15.0", "tdesign-icons-react": "^0.6.4", "tslib": "~2.3.1", "validator": "~13.15.0" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-C3uZRTkJ1iQ62BrMkuvqvBK+4HEuhl82rABxa6kAHGHL3eBI4DPfzAJGF0T3b+DKCBeJxb0x10elumT6NkQEaw=="],
"teex": ["teex@1.0.1", "https://registry.npmmirror.com/teex/-/teex-1.0.1.tgz", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
"text-decoder": ["text-decoder@1.2.7", "https://registry.npmmirror.com/text-decoder/-/text-decoder-1.2.7.tgz", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
"thread-stream": ["thread-stream@4.2.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-4.2.0.tgz", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyexec": ["tinyexec@1.1.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.2.tgz", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
"tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tldts": ["tldts@7.0.30", "https://registry.npmmirror.com/tldts/-/tldts-7.0.30.tgz", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="],
"tldts-core": ["tldts-core@7.0.30", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.30.tgz", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
"tough-cookie": ["tough-cookie@6.0.1", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
"tr46": ["tr46@6.0.0", "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
@@ -965,13 +1181,13 @@
"typescript": ["typescript@6.0.3", "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"typescript-eslint": ["typescript-eslint@8.59.2", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
"typescript-eslint": ["typescript-eslint@8.59.3", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.3.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici": ["undici@7.25.0", "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
"undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"undici-types": ["undici-types@7.25.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.25.0.tgz", {}, "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
@@ -985,11 +1201,17 @@
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@8.0.11", "https://registry.npmmirror.com/vite/-/vite-8.0.11.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="],
"vite": ["vite@8.0.13", "https://registry.npmmirror.com/vite/-/vite-8.0.13.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.1", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
"whatwg-url": ["whatwg-url@16.0.1", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1005,6 +1227,12 @@
"wrap-ansi": ["wrap-ansi@10.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="],
"wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlchars": ["xmlchars@2.2.0", "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"xpath": ["xpath@0.0.34", "https://registry.npmmirror.com/xpath/-/xpath-0.0.34.tgz", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="],
"y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -1025,7 +1253,7 @@
"@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"@commitlint/config-validator/ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@commitlint/is-ignored/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
@@ -1045,14 +1273,46 @@
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/node/undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"cheerio/parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"cli-truncate/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"cliui/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"dom-serializer/entities": ["entities@4.5.0", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"eslint/ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-module-utils/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -1073,22 +1333,62 @@
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5-parser-stream/parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"pretty-format/react-is": ["react-is@17.0.2", "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"prop-types/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"tdesign-react/@babel/runtime": ["@babel/runtime@7.26.10", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.10.tgz", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
"tdesign-react/react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"thread-stream/real-require": ["real-require@1.0.0", "https://registry.npmmirror.com/real-require/-/real-require-1.0.0.tgz", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"wrap-ansi/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"cheerio/parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"parse5-htmlparser2-tree-adapter/parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"parse5-parser-stream/parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}

3
bunfig.toml Normal file
View File

@@ -0,0 +1,3 @@
[test]
preload = ["./tests/setup.ts"]
exclude = ["./tests/e2e/**"]

17
docker/probes.yaml Normal file
View File

@@ -0,0 +1,17 @@
# yaml-language-server: $schema=../probe-config.schema.json
server:
listen:
host: "${DIAL_HOST|0.0.0.0}"
port: "${DIAL_PORT|3000}"
storage:
dataDir: "${DIAL_DATA_DIR|/data/dial}"
targets:
- id: "self-health"
name: "DiAL 自检"
type: http
http:
url: "http://127.0.0.1:${DIAL_PORT|3000}/health"
expect:
status: [200]

127
docs/README.md Normal file
View File

@@ -0,0 +1,127 @@
# DiAL 文档
本文档是 DiAL 的文档路由入口。AI 工具和开发者应先阅读本文件判断本次任务需要读取和更新哪些专题文档,再按任务类型读取最小必要上下文。
## 目录索引
```text
docs/
README.md
development/
README.md
architecture.md
backend.md
frontend.md
release.md
checker.md
user/
README.md
configuration.md
deployment.md
expectations.md
troubleshooting.md
checkers/
README.md
http.md
cmd.md
db.md
tcp.md
udp.md
icmp.md
dns.md
llm.md
```
`docs/prompts/` 是提示词资产目录,不属于常规开发流程和用户使用文档。代码、配置、部署或 checker 变更不需要更新该目录,除非任务明确要求维护提示词资产。
## 入口文档
| 入口 | 定位 |
| ------------------------------------------- | ------------------------------------------ |
| [项目 README](../README.md) | 项目整体介绍、快速开始、核心能力、文档引导 |
| [开发文档](development/README.md) | 开发入口、全局规则、常用命令、质量门禁 |
| [用户文档](user/README.md) | 用户使用入口、配置、部署、expect、排障 |
| [Checker 用户参考](user/checkers/README.md) | 各 checker 的配置项、expect 字段和示例 |
## 按任务阅读路径
| 任务 | 必读文档 |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| 修改项目介绍或快速开始 | [项目 README](../README.md)、本文档 |
| 修改开发流程、质量门禁或工程规则 | [开发文档](development/README.md)、本文档、[OpenSpec 配置](../openspec/config.yaml) |
| 修改架构边界或启动流程 | [开发文档](development/README.md)、[架构与边界](development/architecture.md) |
| 修改后端 API、store、engine、logger | [开发文档](development/README.md)、[后端开发](development/backend.md) |
| 修改前端 | [开发文档](development/README.md)、[前端开发](development/frontend.md) |
| 新增或修改 checker | [Checker 开发](development/checker.md)、[Checker 用户参考](user/checkers/README.md)、相近 checker 文档 |
| 修改配置 schema | [配置文件](user/configuration.md)、[后端开发](development/backend.md)、相关 checker 文档 |
| 修改 expect 或状态模型 | [校验规则](user/expectations.md)、[后端开发](development/backend.md)、[Checker 开发](development/checker.md) |
| 修改构建、Docker、release | [构建与发布](development/release.md)、[部署文档](user/deployment.md) |
| 修改故障处理或运行依赖 | [故障排查](user/troubleshooting.md)、相关用户文档 |
| 修改文档规则或文档目录结构 | 本文档、[OpenSpec 配置](../openspec/config.yaml) |
## 文档归属矩阵
| 变更类型 | 默认更新位置 |
| -------------------------------------------------------------- | -------------------------------------------------------------- |
| 项目定位、核心能力、快速开始、顶层文档导航 | `README.md` |
| 文档路由、文档更新规则、文档归属矩阵 | `docs/README.md``openspec/config.yaml` |
| 开发入口、常用命令、质量门禁、全局工程规则、OpenSpec 约定 | `docs/development/README.md` |
| 架构边界、启动流程、运行时流程、前后端边界 | `docs/development/architecture.md` |
| 后端 API、共享类型、store、engine、logger、expect 基础设施 | `docs/development/backend.md` |
| 前端技术栈、组件、样式、数据层、前端测试 | `docs/development/frontend.md` |
| checker 开发机制、schema/validate/resolve/execute/expect 约定 | `docs/development/checker.md` |
| 构建、发布、Dockerfile、脚本、前后端静态资源集成 | `docs/development/release.md` |
| YAML 顶层结构、server、variables、targets 通用字段 | `docs/user/configuration.md` |
| checker 配置、expect 字段、示例、用户可见 checker 行为 | `docs/user/checkers/<type>.md``docs/user/checkers/README.md` |
| ValueMatcher、ContentExpectations、KeyedExpectations、状态模型 | `docs/user/expectations.md` |
| 构建产物运行、Docker 参数、发布包、运行时依赖 | `docs/user/deployment.md` |
| 常见运行问题、依赖命令、容器权限、配置校验问题 | `docs/user/troubleshooting.md` |
## development 文档如何更新
开发文档解释“如何实现和维护”。代码变更影响开发者理解、开发流程、测试方式或架构边界时,必须更新 `docs/development/` 对应文档。
- 全局规则、常用命令、质量门禁、目录边界、OpenSpec 约定更新到 `docs/development/README.md`
- 架构图、启动链路、运行时流程、前后端边界更新到 `docs/development/architecture.md`
- 后端 API、配置加载、store、engine、logger、expect 基础设施和后端测试规范更新到 `docs/development/backend.md`
- 前端技术栈、组件边界、数据流、样式规则和前端测试规范更新到 `docs/development/frontend.md`
- checker 开发机制、文件结构、schema、validate、resolve、execute、expect、测试 checklist 更新到 `docs/development/checker.md`
- 构建、Docker、release、脚本和发布验证更新到 `docs/development/release.md`
- 不新增“杂项”开发文档;优先把内容放入上述最贴近的专题,确需新增专题时先更新本文档和 `openspec/config.yaml`
## user 文档如何更新
用户文档解释“如何使用”和“用户能观察到什么”。变更影响用户配置、运行、部署、checker 行为、expect 规则、状态结果或排障方式时,必须更新 `docs/user/` 对应文档。
- 配置事实来源是 TypeBox schema、`probe-config.schema.json`、语义校验器和测试;`docs/user/configuration.md` 负责解释顶层结构和通用字段。
- checker 专属字段和示例只在 `docs/user/checkers/<type>.md` 完整展开,`docs/user/checkers/README.md` 只维护类型索引和选择建议。
- expect 断言模型、UP/DOWN、`failure``observation`、快速失败顺序更新到 `docs/user/expectations.md`
- Docker、生产运行、发布包和运行时依赖更新到 `docs/user/deployment.md`
- 常见错误和排查路径更新到 `docs/user/troubleshooting.md`
- 用户文档避免解释内部实现细节,需要实现细节时链接到 `docs/development/`
## 文档影响分析
每次代码变更都必须执行文档影响分析。
```text
代码或配置变更
-> 用户能感知吗?更新 docs/user/ 或 README.md
-> 开发者需要知道吗?更新 docs/development/
-> 文档规则变化吗?更新 docs/README.md 和 openspec/config.yaml
-> 都不是?收尾说明写明无需更新文档及原因
```
同一事实只在最贴近读者的文档中完整展开,其他文档使用链接引用。根目录 README 保持轻量不承载完整配置参考、checker 表或实现教程。
## 收尾说明示例
```text
文档影响分析:本次修改了 HTTP checker 的配置字段,已更新 docs/user/checkers/http.md、docs/user/configuration.md 和 probe-config.schema.json。
```
无需更新文档时:
```text
文档影响分析:本次仅调整内部测试 helper未改变用户可见行为、配置、架构边界或开发流程因此无需更新文档。
```

115
docs/development/README.md Normal file
View File

@@ -0,0 +1,115 @@
# 开发文档
本文档是 DiAL 的开发入口。AI 工具和开发者应先阅读 [`../README.md`](../README.md) 判断文档归属,再阅读本文和最小必要专题。
适用场景修改源码、测试、构建脚本、开发流程、架构边界、checker 开发机制或项目工程规则。
## 专题索引
| 文档 | 内容 |
| ---------------------------------- | ------------------------------------------------------------------------------------------------- |
| [architecture.md](architecture.md) | 项目结构、启动流程、运行时流程、HTTP 请求流程、前后端边界 |
| [backend.md](backend.md) | 后端库优先级、API 路由、共享 helpers、类型规范、配置契约、store、engine、logger、expect、错误模型 |
| [frontend.md](frontend.md) | React、TDesign、TanStack Query、组件、样式和前端测试规范 |
| [checker.md](checker.md) | 新增或修改 checker 的实现机制、测试要求、文档同步和 checklist |
| [release.md](release.md) | 开发服务、前后端集成、构建、Docker、release、脚本、环境变量 |
| [../README.md](../README.md) | 文档路由、文档归属矩阵、development/user 文档更新规则 |
## 常用命令
| 命令 | 说明 |
| -------------------------------- | ---------------------------------------- |
| `bun install` | 安装依赖 |
| `bun run dev probes.yaml` | 启动双进程开发环境 |
| `bun run dev:server probes.yaml` | 仅启动后端 API server |
| `bun run dev:web` | 仅启动 Vite dev server |
| `bun run schema` | 生成 `probe-config.schema.json` |
| `bun run schema:check` | 检查导出 schema 是否同步 |
| `bun run typecheck` | TypeScript 类型检查 |
| `bun run lint` | ESLint 和 Prettier 格式检查 |
| `bun run format` | Prettier 自动格式化 |
| `bun test` | 运行全部测试 |
| `bun run check` | `schema:check + typecheck + lint + test` |
| `bun run build` | 构建生产可执行文件 |
| `bun run verify` | `check + build` 完整验证 |
| `bun run release` | 跨平台发布打包 |
| `bun run clean` | 清理构建缓存与产物 |
## 质量门禁
代码变更必须按影响范围执行验证。
| 变更类型 | 必跑命令 |
| -------------------------------- | --------------------------------------------------------- |
| 常规代码变更 | `bun run check` |
| 构建、部署、发布、前后端集成变更 | `bun run verify` |
| 配置 schema 变化 | `bun run schema``bun run schema:check``bun run check` |
| checker 新增或修改 | `bun run schema``bun run schema:check``bun run check` |
| 仅文档变更 | 检查链接、索引和文档归属一致性 |
正式提交或影响构建产物时优先运行 `bun run verify`。如果因环境限制无法执行完整验证,必须在收尾说明中记录未执行项和原因。
## 全局工程规则
- 使用中文编写注释、文档和项目内交流内容。
- 仅使用 `bun` 作为包管理器,禁止使用 npm、pnpm、yarn。
- 运行工具使用 `bunx`,禁止使用 npx、pnpx。
- 新增代码优先复用已有组件、工具和依赖库,不引入新依赖;确需新增依赖时先说明原因。
- 后端优先使用 Bun 内置 API其次是 es-toolkit、标准 Web API、主流三方库最后才自行实现。
- 前端样式优先使用 TDesign 组件、组件 props、TDesign CSS tokens、`styles.css` CSS 类,最后才自行开发组件。
- 前端禁止组件内联 `style`、覆盖 TDesign 内部类名、使用 `!important`、硬编码色值。
- 当前项目未上线,不需要为旧行为做向前兼容,除非用户明确要求。
## 包管理、依赖与提交
- 仅使用 `bun` 安装依赖和运行项目脚本,锁文件为 `bun.lock`
- 新增依赖前先确认 Bun 内置 API、es-toolkit、标准 Web API、现有三方库和项目公共工具是否已满足需求。
- Git 提交信息使用中文,格式为 `类型: 简短描述`
- 提交类型限定为 `feat``fix``refactor``docs``style``test``chore`
- 多行提交描述时,标题和正文之间空一行。
## 目录边界
| 目录 | 约定 |
| ------------------- | ---------------------------------------------------------- |
| `src/server/` | Bun 后端代码,不能 import `src/web/`HTML import 集成除外 |
| `src/web/` | React Dashboard不能 import `src/server/` 运行时实现 |
| `src/shared/` | 前后端共享 TypeScript 类型 |
| `scripts/` | 独立运行脚本,可 import 项目源码 |
| `tests/` | 测试目录,结构镜像 `src/` |
| `docs/user/` | 用户使用、配置、部署、checker 和排障文档 |
| `docs/development/` | 架构、后端、前端、发布和 checker 开发文档 |
| `openspec/` | OpenSpec 变更管理与规格文档 |
## 文档影响分析
每次代码变更都必须执行文档影响分析。
| 如果变更影响 | 更新 |
| --------------------------------------------------- | ------------------------------------------ |
| 用户可见行为、配置、checker、expect、部署、状态模型 | `docs/user/` 对应文档 |
| 开发流程、架构、测试、构建发布、checker 开发机制 | `docs/development/` 对应文档 |
| 项目定位、快速开始、核心能力列表、文档导航 | `README.md` |
| 文档同步规则或文档归属矩阵 | `docs/README.md``openspec/config.yaml` |
如果无需更新文档,必须在收尾说明中说明原因。详细规则见 [文档总览](../README.md)。
## OpenSpec 协作规则
- 本项目 OpenSpec 使用 `fast-drive` schema变更文档只包含 `design.md``tasks.md`,不创建 `proposal.md``specs/*.md`
- `design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth。
- `tasks.md` 必须从 `design.md` 派生,一行一个 checkbox 任务。
- 实现阶段按 `tasks.md` 顺序执行,完成后立即标记任务状态。
## 事实来源
| 主题 | 事实来源 |
| -------------- | ---------------------------------------------------------- |
| 代码结构和实现 | `src/``scripts/``tests/` |
| 配置 schema | TypeBox fragments、`probe-config.schema.json`、schema 测试 |
| 项目全局规则 | `openspec/config.yaml`、本文档、本目录专题文档 |
| checker 流程 | [checker.md](checker.md) |
## 更新触发条件
修改常用命令、质量门禁、全局工程规则、目录边界、OpenSpec 协作方式或开发文档索引时,必须更新本文档。

View File

@@ -0,0 +1,114 @@
# 架构与边界
本文档说明 DiAL 的项目结构、启动链路、运行时流程、HTTP 请求流程和前后端边界。
适用场景修改目录边界、启动流程、运行时调度、HTTP server、前后端集成方式或主要模块职责。
## 项目结构
```text
src/
server/
bootstrap.ts
config.ts
dev.ts
logger.ts
main.ts
server.ts
helpers.ts
middleware.ts
version.ts
routes/
checker/
config-loader.ts
variables.ts
schema/
store.ts
engine.ts
expect/
runner/
shared/
api.ts
web/
app.tsx
main.tsx
styles.css
components/
constants/
hooks/
utils/
scripts/
tests/
docs/
openspec/
probe-config.schema.json
```
## 启动流程
```text
dev.ts / main.ts
-> readRuntimeConfig(cli args)
-> bootstrap({ configPath, mode })
-> loadConfig(yaml)
-> createRuntimeLogger(logging)
-> ProbeStore(db)
-> store.syncTargets(targets)
-> ProbeEngine(...).start()
-> startServer({ config, mode, store, logger })
-> 注册 SIGINT/SIGTERM shutdown
```
`loadConfig()` 的处理顺序YAML 解析 -> Authoring normalize变量替换 + expect 简写展开)-> Normalized 契约校验 -> 语义校验 -> resolve。
## 运行时流程
```text
定时器 tick
-> ProbeEngine.probeGroup()
-> checkerRegistry.get(target.type).execute()
-> runner/*/expect.ts 校验
-> engine.writeResult()
-> store.insertCheckResult()
```
数据清理由 engine 定时调用 `store.prune(retentionMs)`,每小时执行一次。
## HTTP 请求流程
```text
Request
-> Bun.serve routes 声明式匹配
-> routes/*.ts handler
-> middleware.ts 参数校验
-> helpers.ts 响应格式化
-> Response
```
生产模式下,非 API 路径由 fetch fallback 处理静态资源和 SPA fallback。开发模式下Vite proxy 将 `/api``/health` 请求转发到 Bun API server。
## 前后端边界
- 前端只通过 HTTP 调用后端API 路径为 `/api/*`
- 共享类型放在 `src/shared/`
- 前端不得 import `src/server/` 的运行时实现。
- 后端不得依赖 `src/web/` 运行时代码HTML import 集成除外。
## 主要模块职责
| 模块 | 职责 |
| ------------------------------------- | ------------------------------------------- |
| `src/server/bootstrap.ts` | 统一启动引导和 shutdown 编排 |
| `src/server/server.ts` | Bun HTTP server 和 routes 注册 |
| `src/server/routes/` | API handler按端点拆分 |
| `src/server/checker/config-loader.ts` | YAML 解析、契约校验、语义校验、resolve 调度 |
| `src/server/checker/store.ts` | SQLite 数据存储 |
| `src/server/checker/engine.ts` | 定时调度、并发控制、结果写入、数据清理 |
| `src/server/checker/runner/` | 各 checker 自包含实现 |
| `src/server/checker/expect/` | 跨 checker 复用的断言基础设施 |
| `src/web/` | React Dashboard |
| `src/shared/api.ts` | 前后端共享 API 类型 |
## 更新触发条件
修改项目结构、启动流程、运行时流程、HTTP 请求流程、前后端边界或主要模块职责时,必须更新本文档。

142
docs/development/backend.md Normal file
View File

@@ -0,0 +1,142 @@
# 后端开发
本文档说明 DiAL 后端的 API、配置加载、存储、拨测引擎、日志、expect 和错误模型开发约定。
适用场景:修改 `src/server/``src/shared/api.ts`、后端测试、配置契约、API 响应、store、engine、logger 或 expect 基础设施。
## 库使用优先级
| 优先级 | 来源 | 典型用途 |
| ------ | ------------ | -------------------------------------------------------------- |
| 1 | Bun 内置 API | `Bun.serve``bun:sqlite``Bun.spawn``Bun.file``Bun.YAML` |
| 2 | es-toolkit | 类型判断、深度比较、错误判断、并发控制、集合操作 |
| 3 | 标准 Web API | `Object.fromEntries``Headers``fetch``AbortController` |
| 4 | 主流三方库 | cheerio、xpath、@xmldom/xmldom |
| 5 | 自行实现 | 仅在以上都无法满足时 |
新增依赖前必须先检查上述每一层是否已有可用方案。
## API 路由开发
路由文件位于 `src/server/routes/`,每个端点一个文件。路由通过 `server.ts``Bun.serve({ routes })` 声明式注册,使用 per-method handler 对象。
新增路由步骤:
1.`src/server/routes/` 下创建 `<name>.ts`
2. 实现 handler 函数并 export。
3.`server.ts``routes` 对象中注册路径和 method handler。
4.`tests/server/app.test.ts` 中添加集成测试。
请求参数校验使用 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination``validateDashboardWindow``validateRecentLimit``validateMetricsBucket`
## 共享 helpers
| 函数 | 用途 |
| ------------------------------- | ------------------------------------ |
| `createApiError(error, status)` | 构造 API 错误体 |
| `createHeaders(mode, init)` | 创建响应 Headers生产模式附加安全头 |
| `createHealthResponse()` | 构造健康检查响应 |
| `formatDuration(ms)` | 毫秒转为可读时长字符串 |
| `jsonResponse(body, options)` | JSON 响应构造 |
| `mapCheckResult(row, type)` | 数据库行转 API CheckResult |
## 类型规范
- 共享类型以 `src/shared/api.ts` 为唯一源头。
- 严格联合类型优先于宽类型。
- 存储层类型与 API 类型分离。
- checker 具体类型在各自目录定义,中间层通过 base interface 和 registry 完成类型擦除。
- 纯类型导入使用 `import type`
## 配置契约与校验
配置加载流程固定为:`unknown -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`
| 层级 | 职责 |
| ---------- | ------------------------------------------------ |
| Authoring | 用户 YAML 可书写形态,允许变量引用和 expect 简写 |
| Normalized | 变量替换和 expect 简写展开后的契约校验形态 |
| Validated | 通过契约校验和语义校验的形态 |
| Resolved | checker `resolve()` 后的运行期配置 |
Ajv 保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。默认对象策略是 `additionalProperties: false`,只有明确的动态键值表可以开放任意键名。
新增或修改配置字段时必须同步更新 TypeBox schema fragments、`probe-config.schema.json`、语义 validator、测试和对应用户文档并运行 `bun run schema:check`
## 数据存储
存储层基于 `bun:sqlite`WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
| 方法 | 用途 |
| ------------------------------------------ | ---------------------------------- |
| `syncTargets(targets)` | 启动期同步 targets |
| `insertCheckResult()` | 写入单条检查结果 |
| `getTargets()` | 查询全部 targets |
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果 |
| `getAllTargetWindowStats(from, to)` | 批量获取窗口基础计数 |
| `getDashboardIncidentStates(from, to)` | 获取 Dashboard 窗口状态序列 |
| `getAllRecentSamples(limit)` | 批量获取最近采样 |
| `getTargetCheckpoints(targetId, from, to)` | 获取单目标窗口检查点序列 |
| `getTargetDurations(targetId, from, to)` | 获取单目标成功耗时数组 |
| `getHistory()` | 分页查询历史记录 |
| `prune(retentionMs)` | 清理过期数据 |
数据库只负责存储、筛选、排序、分页、LIMIT 和基础聚合。指标语义在后端应用层实现。
## 拨测引擎
- 按 interval 分组,每组独立定时触发。
- 使用 `es-toolkit/Semaphore` 限制全局最大并发数。
- 通过 `checkerRegistry.get(target.type)` 选择 runner。
- 每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abort。
- 状态变化通过注入的 `Logger` 输出结构化日志。
## 日志模块
后端运行时代码统一通过 `Logger` 接口输出日志,禁止直接使用 `console.*`。配置加载失败前使用 `ConsoleFallbackLogger`
| 实现 | 用途 |
| ----------------------- | --------------------------------------------- |
| `PinoLoggerWrapper` | 生产运行时,封装 Pino、pino-pretty、pino-roll |
| `NoopLogger` | 静默丢弃日志 |
| `MemoryLogger` | 测试替身 |
| `ConsoleFallbackLogger` | 配置加载失败前的降级日志 |
敏感信息会自动 redact `authorization``cookie``set-cookie``authToken``key``password``token``apiKey` 及其嵌套路径。
## expect 系统
共享断言基础设施位于 `src/server/checker/expect/`。新增或修改 checker 的 expect 字段时,按以下原则选择模型:
| 模型 | 用途 | 典型字段 |
| --------------------- | ---------------------------- | ------------------------------------------------------------------- |
| enum / boolean | 状态类结果,结果集合小且稳定 | HTTP status、Cmd exitCode、TCP connected、UDP responded、ICMP alive |
| `ValueMatcher` | 数字指标和字符串元数据 | durationMs、rowCount、finishReason、usage |
| `ContentExpectations` | 返回内容或半结构化内容 | body、stdout、stderr、banner、response、output、result |
| `KeyedExpectations` | 动态键值断言 | headers、DB rows 列值 |
详细 checker 开发流程见 [Checker 开发](checker.md)。
## 错误模型
- API 错误:`{ error: "描述", status: <code> }`
- CheckFailure`{ kind: "error" | "mismatch", phase, path, expected?, actual?, message }`
expect 校验失败记录首个失败原因;网络、超时、进程崩溃统一为 `kind: "error"`
## 后端测试与验证
| 变更类型 | 测试重点 |
| ---------------------- | ---------------------------------------- |
| API 路由 | `tests/server/app.test.ts` 集成行为 |
| 配置 schema 或语义校验 | schema 导出、合法配置、非法配置 |
| store | SQLite 写入、查询、分页、聚合和清理 |
| engine | 调度、并发、超时、结果写入和状态变化日志 |
| expect 基础设施 | matcher 语义、快速失败、错误信息 |
| checker runner | 见 [Checker 开发](checker.md#测试要求) |
后端运行时代码统一通过注入的 Logger 输出日志,禁止直接使用 `console.*`。新增或修改后端逻辑通常需要运行 `bun run check`;影响构建产物或前后端集成时运行 `bun run verify`
## 更新触发条件
修改后端 API、共享类型、配置契约、store、engine、logger、expect 基础设施、错误模型或后端测试规范时,必须更新本文档。

221
docs/development/checker.md Normal file
View File

@@ -0,0 +1,221 @@
# Checker 开发
Checker 是 DiAL 的核心扩展单元。每个 checker 是 `src/server/checker/runner/<type>/` 下的自包含目录包含类型、schema、语义校验、执行逻辑、序列化和断言。
适用场景:新增 checker、修改 checker 配置或 expect、调整 checker 注册机制、改动 checker 测试或用户文档同步规则。
新增或修改 checker 前必须阅读 [开发入口](README.md)、[配置文件](../user/configuration.md)、[校验规则](../user/expectations.md) 和 [Checker 用户文档](../user/checkers/README.md)。还应阅读现有同类 checker 的实现和测试,例如 `src/server/checker/runner/http/``tests/server/checker/runner/http/`
## 设计原则
- 每个 checker 必须自包含在 `src/server/checker/runner/<type>/`
- checker 专属类型、schema、validate、execute、expect、normalize 和协议辅助逻辑放在同一目录。
- 注册只修改 `src/server/checker/runner/index.ts`,中间层不新增 type switch。
- schema 层只描述契约,语义规则放入 `validate.ts`
- `resolve()` 只做默认值填充、路径解析和单位转换,不执行校验。
- `execute()` 必须支持 `CheckerContext.signal` 超时取消。
- expect 字段必须选择合适断言模型,不为了统一而滥用 ValueMatcher。
- failure phase 命名遵循去单位后缀规则,例如 `durationMs` 对应 `duration`
## 架构目标
```text
checkerRegistry
├── runner/index.ts
├── schema/builder.ts
├── schema/validate.ts
├── config-loader.ts
├── engine.ts
└── store.ts
```
注册后,中间层通过 registry 自动委托 schema 生成、契约校验、配置 normalize、配置 resolve、执行和序列化。新增 checker 不应在中间层新增 `switch/case` 或类型分支。
## 标准文件结构
| 文件 | 职责 |
| -------------- | ------------------------------------------------------- |
| `index.ts` | 模块入口re-export Checker 类 |
| `types.ts` | Checker 专属类型 |
| `schema.ts` | TypeBox 契约 schema包含 config 和 expect |
| `validate.ts` | 启动期语义校验 |
| `normalize.ts` | Checker 专属 authoring expect 归一化 |
| `execute.ts` | Checker 类,实现 normalize、resolve、execute、serialize |
| `expect.ts` | Checker 专用断言函数 |
| 其他文件 | 协议解析、编码、provider 适配、平台命令封装等专属逻辑 |
## 类型定义
`types.ts` 中定义:
- `RawXxxTargetConfig`
- `RawXxxExpectConfig`
- `ResolvedXxxExpectConfig`
- `ResolvedXxxTarget extends ResolvedTargetBase`
不需要修改顶层 `checker/types.ts`。base interface 使用 index signature 支持扩展。
## Schema
checker 必须提供 `CheckerSchemas`,包含 Authoring 和 Normalized 两套 config/expect 片段。Authoring 描述用户 YAML 可写 DSLNormalized 描述 normalizer 输出。
常用 fragments
| Fragment | 用途 |
| ----------------------------------- | ------------------------- |
| `durationSchema` | 时长字符串 |
| `sizeSchema` | 大小单位 |
| `statusCodePatternSchema` | HTTP 状态码或范围 |
| `stringMapSchema` | headers、env 等字符串映射 |
| `createValueMatcherSchema()` | ValueMatcher |
| `createContentExpectationsSchema()` | ContentExpectations |
| `createKeyedExpectationsSchema()` | KeyedExpectations |
默认对象策略为 `additionalProperties: false`。只有明确的动态键值表可以开放任意键名。
## 语义校验
`validate.ts` 中实现 JSON Schema 无法表达的规则,统一返回 `ConfigValidationIssue[]`,不要直接拼接最终错误字符串。
共享校验工具包括:
| 函数 | 用途 |
| -------------------------------- | ---------------------------- |
| `validateRawValueExpectation` | 校验 Raw ValueExpectation |
| `validateRawContentExpectations` | 校验 ContentExpectations |
| `validateRawKeyedExpectations` | 校验 KeyedExpectations |
| `validateJsonPath` | 校验项目支持的 JSONPath 子集 |
| `isJsonValue` | 判断合法 JSON value |
## normalize 规范
`normalize()``CheckerDefinition` 中定义为必需方法,负责将 authoring expect DSL 转换为 normalized 形态。输入为变量已解析后的 target输出为适配 normalized schema 的 target。该方法在 `resolve()` 和 normalized contract 校验之前执行。
`normalize.ts` 中实现 `normalizeTargetExpect` 函数,`execute.ts` 中的 `normalize` 方法委托到该函数。
共享 normalize helper 位于 `src/server/checker/expect/normalize.ts`
| 函数 | 用途 |
| ------------------ | -------------------------------------------------------- |
| `compactExpect` | 合并两个 expect record过滤 undefined 字段 |
| `normalizeValue` | ValueMatcher 原始值简写展开为 `{equals: value}` |
| `normalizeContent` | ContentExpectations 简写展开为 normalized 形态 |
| `normalizeKeyed` | KeyedExpectations 对象形态展开为 `[{key, matcher}]` 数组 |
```typescript
import { compactExpect, normalizeContent, normalizeKeyed, normalizeValue } from "../../expect/normalize";
export function normalizeTargetExpect(target: RawTargetConfig): RawTargetConfig {
if (target.expect === undefined || !isPlainObject(target.expect)) return target;
const raw = target.expect as Record<string, unknown>;
return {
...target,
expect: compactExpect(raw, {
/* checker 专属字段映射 */
}),
};
}
```
expect 字段的归一化规则ValueMatcher 字段调用 `normalizeValue()`ContentExpectations 字段调用 `normalizeContent()`KeyedExpectations 字段调用 `normalizeKeyed()`boolean/enum/array 等非断言模型字段直接透传。
## resolve 规范
`resolve()` 只做内置默认值填充、路径解析、单位转换,不执行校验。输入已经通过 Normalized schema 和语义校验expect 已是 normalized 形态。
```typescript
const expect = target.expect as ResolvedXxxExpectConfig | undefined;
const resolvedExpect: ResolvedXxxExpectConfig = expect
? { ...expect, status: expect.status ?? [200] }
: { status: [200] };
```
返回值使用 `satisfies ResolvedXxxTarget` 确保类型正确。
## execute 规范
- 始终记录 `timestamp``start = performance.now()`
- 通过 `ctx.signal` 支持超时取消。
- 首个 expect 失败即停止,返回带 `failure` 的结果。
- 成功时 `failure: null, matched: true`
- 异常时使用 `errorFailure()`
- 不匹配时使用 `mismatchFailure()`
- `expected` 参数应传用户可读值,必要时使用 `displayValueExpectation()`
## expect 字段选择
| 场景 | 模型 |
| ------------------------------------ | ------------------- |
| 状态类结果且集合小而稳定 | enum 或 boolean |
| 单值数字指标或字符串元数据 | ValueMatcher |
| 文本、JSON、HTML、XML 或半结构化内容 | ContentExpectations |
| 动态键值表 | KeyedExpectations |
不要为了统一而把状态类字段改成 ValueMatcher。一个 expect 字段只能对应一种断言模型。
## 注册
1. 创建 `src/server/checker/runner/<type>/index.ts`
2.`src/server/checker/runner/index.ts` 添加导入。
3. 在 registry 初始化数组中添加 checker 实例。
注册后schema builder、validate、config-loader、engine、store 会自动按 registry 分发。
## 测试要求
测试文件放在 `tests/server/checker/runner/<type>/`,结构镜像源文件。
| 测试类别 | 覆盖内容 |
| -------------- | ---------------------------------------------------- |
| 契约测试 | TypeBox schema 与 JSON Schema 导出一致性 |
| 语义校验测试 | 合法和非法配置 |
| normalize 测试 | authoring expect 简写展开和 normalized contract 通过 |
| resolve 测试 | 默认值合并、路径解析、单位转换 |
| execute 测试 | 成功、失败、超时、expect 组合 |
| 注册测试 | registry 注册行为 |
| 配置加载测试 | 含新 checker 的 YAML 完整加载流程 |
## 文档和 schema 更新
新增或修改 checker 时通常需要更新:
- `probes.example.yaml`
- `probe-config.schema.json`,通过 `bun run schema` 生成
- `docs/user/checkers/<type>.md`
- `docs/user/checkers/README.md`
- `docs/user/expectations.md`,仅当断言模型、状态模型或通用规则变化
- `docs/user/configuration.md`,仅当 target 通用字段或配置加载形态变化
- `docs/development/checker.md`,仅当 checker 开发机制、测试要求或 checklist 变化
- `docs/README.md``openspec/config.yaml`,仅当文档同步规则变化
## 验证命令
新增或修改 checker 后通常需要运行:
```bash
bun run schema
bun run schema:check
bun run check
```
影响构建、Docker 或发布包时追加运行 `bun run verify`
## 完成检查清单
```text
□ checker 类型、schema、validate、normalize、resolve、execute、serialize 已实现
□ checker 已在 runner/index.ts 注册
□ 配置契约、语义校验和 JSON Schema 导出已同步
□ probes.example.yaml 已添加或更新示例
□ tests/server/checker/runner/<type>/ 已覆盖契约、校验、normalize、resolve、execute、注册和配置加载
□ docs/user/checkers/<type>.md 已添加或更新
□ docs/user/checkers/README.md 已添加或更新
□ 文档影响分析已完成,必要文档已同步
□ bun run schema 和 bun run schema:check 已通过
□ bun run check 已通过
□ bun run verify 已通过或记录未执行原因
```
## 更新触发条件
修改 checker 开发机制、目录结构、schema/validate/normalize/resolve/execute/expect 约定、测试要求、验证命令或文档同步 checklist 时,必须更新本文档。

View File

@@ -0,0 +1,130 @@
# 前端开发
本文档说明 DiAL Dashboard 的 React、TDesign、TanStack Query、组件、样式和前端测试约定。
适用场景:修改 `src/web/`、前端共享类型使用方式、Dashboard 数据流、组件结构、样式规则或前端测试。
## 技术栈
| 层面 | 技术 | 用途 |
| ------ | ------------------------------------- | ---------------------------------------------- |
| 框架 | React 19 | UI 组件开发 |
| 构建 | Bun HTML import + Vite dev server | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query + React Query Devtools | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势图 |
| 动画 | @number-flow/react | 倒计时数字滚动过渡 |
| 路由 | 无 | 单页面 Dashboard通过 Drawer/Tab 做页面内导航 |
不引入 React Router 或额外状态管理库。TanStack Query 承担服务端状态,组件内状态使用 `useState`
## 组件树与数据流
```text
main.tsx
└── StrictMode
└── ErrorBoundary
└── QueryClientProvider
├── App
│ ├── useThemePreference()
│ ├── useDashboard(refreshInterval)
│ ├── SummaryCards
│ └── TargetBoard
│ └── TargetGroup[]
│ └── PrimaryTable
│ └── TargetDetailDrawer
│ └── useTargetDetail()
│ ├── OverviewTab
│ └── HistoryTab
└── ReactQueryDevtools
```
## TanStack Query 规范
Query key 使用 structured array排序为 scope -> id -> 参数。
```typescript
const queryKeys = {
dashboard: () => ["dashboard", "24h", 30] as const,
meta: () => ["meta"] as const,
metrics: (targetId: number, from: string, to: string, bucket: "auto" | MetricsBucket) =>
["metrics", targetId, from, to, bucket] as const,
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
};
```
全局面板级查询可持续刷新,详情级查询必须按 Drawer 状态和 Tab 状态条件启用。
## fetch 封装
统一使用 `fetch`,不引入 axios。错误抛异常由 TanStack Query 的 `error` 状态承接。
```typescript
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
```
## 组件开发规范
- 每个 React 组件一个 `.tsx` 文件,文件名使用 PascalCase。
- 组件 props 定义为 `interface XxxProps`,紧邻组件函数声明。
- 类型从 `../../shared/api` 导入,使用 `import type`
- 展示组件放在 `components/`,通过 props 接收数据,通过回调返回事件。
- 容器逻辑放在 hooks 中,组件只做数据消费。
- 列定义、排序器、筛选器、颜色阈值等常量放在 `constants/`
- 时间处理等纯函数放在 `utils/`
## 现有组件
| 组件 | 用途 |
| -------------------- | ----------------------------------------------------------------- |
| `App` | 根组件Layout + HeadMenu 骨架、主题模式、刷新控制、Skeleton 加载 |
| `ErrorBoundary` | React 错误边界 |
| `SummaryCards` | 总览统计卡片 |
| `TargetBoard` | 按分组渲染目标表格列表 |
| `TargetGroup` | 单个分组 Card + PrimaryTable |
| `TargetDetailDrawer` | 目标详情抽屉 |
| `OverviewTab` | 目标详情概览 |
| `HistoryTab` | 目标历史记录表格和分页 |
| `TrendChart` | 趋势折线图 |
| `StatusDot` | 圆形状态指示点 |
| `StatusBar` | 最近采样状态条 |
| `RefreshCountdown` | Header 刷新倒计时和手动刷新按钮 |
## 样式规范
前端基于 TDesign React 构建 UI样式开发优先级
1. TDesign 组件
2. TDesign 组件 props
3. TDesign CSS tokens`--td-*`
4. `styles.css` CSS 类
5. 自行开发组件
红线:
- 严禁在组件中使用 `style` 属性内联调整样式。
- 严禁通过 CSS 覆盖 TDesign 组件内部类名。
- 严禁使用 `!important`
- 颜色统一使用 TDesign CSS tokens不使用硬编码色值。
## 前端测试与验证
- 测试目录为 `tests/web/`,结构对应 `src/web/`
- 单元测试重点覆盖 `constants/``utils/` 和 hooks 中的纯逻辑。
- 组件测试使用 jsdom 和 `@testing-library/react`
- 测试用户行为而非实现细节。
- 只 mock 系统边界,例如 `fetch`
- 使用真实的 QueryClientProvider 包裹依赖 TanStack Query 的组件。
- 异步错误断言使用 helper 或显式 try/catch避免依赖 Bun `expect(...).rejects``await-thenable` 规则的类型不匹配。
- 组件测试环境由 `tests/setup.ts``bunfig.toml` preload 提供,包含 ResizeObserver、IntersectionObserver、matchMedia、attachEvent 和 Recharts mock。
前端逻辑变更通常需要运行 `bun run check`。影响生产静态资源、前后端集成或构建流程时运行 `bun run verify`
## 更新触发条件
修改前端技术栈、组件边界、数据流、样式规则、测试环境或前端验证方式时,必须更新本文档。

127
docs/development/release.md Normal file
View File

@@ -0,0 +1,127 @@
# 构建与发布
本文档说明开发服务、前后端集成、生产构建、Docker 镜像、跨平台 release 和相关脚本维护方式。
适用场景:修改 `scripts/`、构建流程、Dockerfile、静态资源集成、release 打包、运行时环境变量或部署产物。
## 开发期运行
```bash
bun run dev probes.yaml
```
`scripts/dev.ts` 同时启动两个进程:
| 进程 | 用途 |
| --------------- | ------------------------------------------------- |
| Bun API server | 后端 API 服务,`--watch` 监听后端文件变更自动重启 |
| Vite dev server | 前端 SPA、HMR、模块热替换 |
也可以单独启动:
```bash
bun run dev:server probes.yaml
bun run dev:web
```
## 前后端集成
开发模式下Vite 通过 proxy 将 `/api/*``/health` 转发到 Bun。
生产模式下,前端通过 Vite 构建为静态资源,通过 `import with { type: "file" }` 嵌入 Bun 可执行文件。非 API 路径由 fetch fallback 处理:有文件扩展名的返回静态资源或 404无扩展名的返回 SPA index.html。
## 构建
```bash
bun run build
```
构建流程:
```text
1. Vite build -> dist/web/
2. Code generation -> .build/static-assets.ts + .build/server-entry.ts
3. Bun compile -> dist/dial-server
```
构建参数:
| 环境变量 | 说明 |
| -------------- | ---------------- |
| `BUN_TARGET` | 交叉编译目标平台 |
| `BUILD_TARGET` | 交叉编译目标平台 |
## Docker 镜像
Docker 镜像使用 Alpine 多阶段构建,保持与生产单可执行文件交付模型一致。
```text
oven/bun:1-alpine -> bun install --frozen-lockfile
-> BUN_TARGET=bun-linux-*-musl bun run build
-> dist/dial-server
alpine -> 仅复制 /usr/local/bin/dial-server
-> 安装 ca-certificates、iputils-ping、libgcc、libstdc++、tzdata
-> 使用非 root dial 用户运行
```
Dockerfile 通过 `TARGETARCH` 选择 Bun compile target。
| `TARGETARCH` | `BUN_TARGET` |
| ------------ | ---------------------- |
| `amd64` | `bun-linux-x64-musl` |
| `arm64` | `bun-linux-arm64-musl` |
## Release
```bash
bun run release
bun run release --target linux-x64
bun run release --target linux-x64,windows-x64,darwin-arm64
```
release 流程:
```text
1. Vite build -> dist/web/
2. Code generation -> .build/
3. 多目标 Bun compile -> dist/release/binaries/
4. tar.gz 打包 -> dist/release/packages/
```
支持的平台见 [用户部署文档](../user/deployment.md#跨平台发布包)。
## 脚本说明
| 脚本 | 文件 | 说明 |
| ---------------------- | ----------------------------------- | ------------------------------ |
| `bun run dev` | `scripts/dev.ts` | 双进程开发服务 |
| `bun run dev:server` | `src/server/dev.ts` | 仅启动后端 API server |
| `bun run dev:web` | Vite CLI | 仅启动 Vite dev server |
| `bun run build` | `scripts/build.ts` | Vite -> codegen -> Bun compile |
| `bun run release` | `scripts/release.ts` | 多目标交叉编译和打包 |
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成配置 JSON Schema |
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 JSON Schema 同步 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
## 维护约定
- `scripts/build-common.ts` 中的 import specifier 输出必须使用 `/` 分隔符。
- 跨平台路径测试不得用当前平台 `path.sep` 伪装其他平台,应使用 `node:path.win32` 或等价注入方式模拟。
- 如本地 Docker 环境不支持 buildx 或多架构模拟,需在变更记录中说明未执行原因。
## 发布验证
| 变更类型 | 验证方式 |
| ---------------- | --------------------------------------- |
| 构建脚本 | `bun run verify` |
| release 脚本 | `bun run release` 或指定受影响 target |
| Dockerfile | 本地 `docker build`,无法执行时说明原因 |
| 静态资源集成 | `bun run build`,必要时启动产物手动验证 |
| 配置 schema 同步 | `bun run schema:check` |
影响用户部署方式、Docker 运行参数、发布包内容或运行时依赖时,必须同步更新 [用户部署文档](../user/deployment.md)。
## 更新触发条件
修改开发服务、前后端集成、构建产物、Docker 镜像、release target、脚本参数或发布验证方式时必须更新本文档。

View File

@@ -7,7 +7,6 @@
| 文件 | 用途 |
| ------------------------------------------------------ | ------------------------------------------------------------------------ |
| [prompt-smart-merge.md](prompt-smart-merge.md) | 批量合并 `dev*` 分支到目标分支,含规则探测、依赖分析、冲突处理、安全回退 |
| [prompt-spec-review.md](prompt-spec-review.md) | 审查和整理 `openspec/specs/` 下的稳定规范,提升可检索性和一致性 |
| [prompt-proposal-review.md](prompt-proposal-review.md) | 审查 proposal/design/tasks/specs 与讨论、代码现状、OpenSpec 规范的一致性 |
| [prompt-apply-review.md](prompt-apply-review.md) | 审查 apply 后代码、测试、变更文档的一致性,并补齐遗漏或回写文档 |
@@ -85,7 +84,7 @@
- 是否以代码、文档、讨论或用户确认为准
- 何时必须使用提问工具确认
- 删除、重写前是否必须备份
- 改动后是否必须同步 README、测试、变更文档
- 改动后是否必须同步相关用户文档、开发文档、测试、变更文档
### 4. 计划与执行分离
@@ -124,7 +123,7 @@
- 作用域边界:改什么,不改什么
- 真相来源优先级:代码 / README / spec / 讨论 / 用户确认
- 风险动作边界删除、重写、提交、推送、回退、stash、merge 等
- 同步要求:测试、README、变更文档、现有 spec 是否要同步
- 同步要求:测试、用户文档、开发文档、变更文档、现有 spec 是否要同步
- 降级规则:信息不足时如何处理
避免:
@@ -142,7 +141,7 @@
推荐做法:
- 先读仓库规则来源,如 `README.md`、配置、架构文档、近期提交、任务入口
- 先读仓库规则来源,如 `README.md``DEVELOPMENT.md``CONTRIBUTING.md``docs/README.md`配置、架构文档、近期提交、任务入口
- 先读直接相关 artifacts再扩展到相关代码和测试
- 需要探测时,要求 AI 先探测再决定,不把仓库结构写死在提示词里

View File

@@ -1,25 +1,57 @@
审查 OpenSpec apply 完成后以及后续手动修补后的实际实现,判断代码、测试、变更文档是否一致,识别偏离、漏记和可优化点,并将确认后的实际变更同步回变更文档,按以下流程执行。
审查 OpenSpec apply 完成后以及后续手动修补后的实际变更,判断实际产物、验证结果和变更文档是否与 `design.md` source of truth 一致,识别偏离、漏记和可优化点,并将确认后的实际变更同步回变更文档,按以下流程执行。
## 约束
- 先审查再修复;未经用户确认,不修改代码或变更文档
- 默认按 `spec-driven` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
- 优先使用当前会话中的实现说明、测试结论、手动修补记录和已生成的变更文档;仅在无法明确 change、`schemaName`、改动范围或修补来源时,再用提问工具或 OpenSpec 命令补充定位
- 不要因为代码已经存在就自动以代码为准;先判断差异属于"文档要求未实现"、"测试后新增修补"还是"意外偏离/回归"
- 每批代码或文档修改执行前用提问工具获得用户确认
- 先审查再修复;未经用户确认,不修改实际产物或变更文档
- 默认按 `fast-drive` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
- `fast-drive` workflow 下,核心 artifacts 是 `design.md``tasks.md`;不要要求存在 `proposal.md``specs/*.md`
- `fast-drive` workflow 下,`design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth`tasks.md` 是 apply 进度和验证闭环的 tracking 文件
- 禁止同步或修改 `openspec/specs/` 下的主规范;若实际 schema 包含 `specs/*.md`,也只允许修改本次 change 目录下实际存在的 spec artifacts主规范同步属于 archive 阶段,不在此提示词范围内
- 优先使用当前会话中的执行说明、验证结论、手动修补记录和已生成的变更文档;仅在无法明确 change、`schemaName`、改动范围或修补来源时,再用提问工具或 OpenSpec 命令补充定位
- 不要因为实际产物已经存在就自动以实际产物为准先判断差异属于“design 要求未完成”、“验证后新增修补”、“合理落地细化”还是“意外偏离/回归”
- 每批实际产物或文档修改执行前用提问工具获得用户确认
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
- 若修改代码涉及新逻辑、模块结构、API、实体或用户可见行为,同步更新测试、相关变更文档和 README
- 若修改实际产物涉及新行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,同步更新验证材料、相关变更文档和必要的文档/沟通材料
## 1. 收集
并行读取:
读取约束
- 本次 change 的实际 artifacts`spec-driven` 下通常包括 `proposal.md``design.md``tasks.md``specs/*.md`
- 当前会话中与本次变更相关的实现说明、apply 过程中的偏离、测试失败、手动修补原因、待确认事项
- 与本次变更相关的代码和测试文件;优先依据 `git diff --name-only``git diff --name-only --cached``tasks.md`、Impact、失败测试栈定位若工作区已干净再结合文档和代码模块反推
- 最近一次相关测试命令、测试结果、失败信息和修补后的验证结果
- `openspec/config.yaml`
- 与本次改动相关的 README、架构文档以及现有 `openspec/specs/**/spec.md` 中与本次变更相关的规范,相关性来源包括:`proposal.md``Capabilities` / `Modified Capabilities`、手动修补涉及的受影响能力、`design.md` / Impact 中提到的模块、相关代码对应的现有能力
- 直接使用 Read 工具并行读取文件,禁止使用 subagent/Task 工具做文件读取和内容转发
- 不原样输出文件内容,仅在步骤 2 输出审查结论
分步收集:
a) 先并行读取核心入口和配置,确定范围:
- 本次 change 的 `design.md`
- 本次 change 的 `tasks.md`
- workflow context/configuration例如存在时读取 `openspec/config.yaml`
- 若可定位到 schema读取对应 schema`fast-drive` 下优先读取 `openspec/schemas/fast-drive/schema.yaml`
b) 从 `design.md` 提取审查基准:
- `Context`
- `Discussion Notes`
- `Requirements`
- `Goals / Non-Goals`
- `Execution Guardrails`
- `Affected Areas`
- `Decisions`
- `Execution Plan`
- `Verification Plan`
- `Risks / Trade-offs`
- `Open Questions`
c) 从 `tasks.md` 提取任务状态、已完成项、未完成项、验证任务和文档/沟通任务;重点记录所有已标记完成的 `- [x]` 或等价完成状态。
d) 获取实际改动范围:若在版本控制工作区中,优先使用 `git diff --name-only``git diff --name-only --cached`;若工作区已干净或不适用版本控制,再结合 `design.md``tasks.md`、验证记录和执行记录反推。
e) 并行读取实际改动范围、`Affected Areas``Execution Plan``Verification Plan` 涉及的实际产物、参考材料、验证材料、流程说明、配置、文档或沟通材料。
f) 收集当前会话中与本次变更相关的执行说明、apply 过程中的偏离、验证失败、手动修补原因、验证命令或检查结果、待确认事项。
g) 若实际 schema 不是 `fast-drive`,只读取实际存在的 artifacts若存在 `proposal.md``specs/*.md`,再按该 schema 的要求补充读取和审查。
若当前上下文无法明确 change 或文档路径:
@@ -28,63 +60,75 @@
若已明确 change但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
若缺少测试结果或手动修补记录,明确说明本次无法可靠判断部分差异的来源,仅能基于代码与文档现状审查。
若缺少验证结果或手动修补记录,明确说明本次无法可靠判断部分差异的来源,仅能基于实际产物与文档现状审查。
## 2. 分析
按以下优先级检查:
| 优先级 | 维度 | 检查点 |
| ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P0 | 实际实现与测试结论 | 当前代码的真实行为是什么apply 后是否有手动改动或测试后修补;测试是否证明这些实现有效;若缺少测试结果,标记相关结论为"未验证";检查是否存在回归、未覆盖场景或被掩盖的问题 |
| P1 | 文档同步性 | 对实际存在的 artifacts 检查已落地的实现、测试后新增修补、边界处理、异常路径、验证结论是否已同步回变更文档若影响模块结构、API、实体或用户可见行为再检查 README 是否同步 |
| P2 | 文档要求覆盖 | 对实际存在的 artifacts 检查文档中承诺的目标、方案、Requirement、Scenario 是否都已实现;在 `spec-driven` 下重点检查 `proposal.md``design.md``specs/*.md``tasks.md` |
| P3 | 实现质量 | 代码结构、复用、命名、复杂度、错误处理、测试质量、与项目现有模式的一致性是否存在明显问题或可优化点 |
| 优先级 | 维度 | 检查点 |
| ------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P0 | 实际变更与验证结论 | 当前实际产物的真实状态是什么apply 后是否有手动改动或验证后修补;验证是否证明这些变更有效;若缺少验证结果,标记相关结论为未验证;检查是否存在回归、未覆盖场景或被掩盖的问题 |
| P1 | `design.md` 一致性 | 实际变更是否符合 `Requirements``Goals / Non-Goals``Execution Guardrails``Decisions``Execution Plan``Verification Plan``Open Questions` 是否已明确区分 blocking / non-blocking 或写出 `None`;是否违反被明确否决的方案、用户偏好或约束 |
| P2 | `tasks.md` 真实性 | 已完成任务是否真的完成并完成必要验证未完成任务是否仍然必要apply 或手动修补是否引入了需要补充的新任务、验证任务或文档/沟通任务 |
| P3 | 文档回写完整性 | 已落地的实际变更、验证后新增修补、边界处理、异常路径、验证结论、实际触达产物是否已同步回 `design.md``tasks.md`;若影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,再检查必要的文档/沟通材料是否同步 |
| P4 | 质量与可维护性 | 实际产物的结构、清晰度、一致性、可维护性、风险处理、移交质量、验证质量、与现有模式的一致性是否存在明显问题或可优化点 |
| P5 | Schema 兼容性 | 对实际存在的 artifacts 检查是否符合其 schema若不是 `fast-drive`,仅按实际 artifacts 检查,不凭空要求 `fast-drive` 专属结构;最终 artifacts 是否仍保留模板注释、空表格行或占位任务文本 |
分析时区分类差异:
分析时区分类差异:
- 文档要求已明确,但代码未实现或实现不完整 → 需补充代码或测试
- 代码因测试暴露问题、手动修补或合理落地细化而新增/变更 → 需回写文档
- 代码与文档不一致,且无法判断应以哪边为准 → 列入待确认清单
- `design.md` 要求已明确,但实际变更未完成或完成不充分 → 需补充实际工作或验证
- 实际变更因验证暴露问题、手动修补或合理落地细化而新增/变更 → 需回写 `design.md` 和/或 `tasks.md`
- 实际变更与 `design.md` 不一致,且无法判断应以哪边为准 → 列入待确认清单
- `tasks.md` 状态与实际完成情况或验证结果不一致 → 修正任务状态或补充任务
不要把以下情况直接视为合理修补:
- 通过 `skip``only`、弱化断言、绕过错误处理来让测试通过
- 为了贴合现有代码而降低已确认的 Requirement 或行为约束
- 未经过讨论和验证就扩大功能范围
- 通过跳过、弱化或绕过验证来声称变更完成
- 为了贴合当前实际产物而降低已确认的 requirement、acceptance criteria 或 guardrail
- 未经过讨论和验证就扩大功能、流程、内容或责任范围
- 违反 `Execution Guardrails`、被拒绝方案或 `Open Questions` 中尚未解决的 blocker
重点识别:
- 文档要求但未落地的功能、场景、异常处理或验证步骤
- apply 完成后新增的代码修补、边界处理、接口调整、行为变化未同步到文档
- `tasks.md` 标记完成,但代码、测试或文档未闭环
- `Modified Capabilities` 应更新但未更新的现有 spec
- 代码存在明显的重复、复杂度过高、命名不清、错误处理薄弱、测试质量不足等问题
- `design.md` 要求但未落地的结果、流程、内容、场景、异常处理、文档/沟通更新或验证步骤
- 实际变更偏离 `Goals / Non-Goals``Execution Guardrails``Decisions``Execution Plan` 的地方
- apply 完成后新增的修补、边界处理、接口调整、行为变化、流程变化或内容变化未同步到 `design.md`
- `Affected Areas` 与实际改动范围不一致,导致新会话无法复盘真实影响面
- `Verification Plan` 中要求的验证、质量检查、审阅、批准、沟通检查或 manual checks 未执行或未记录
- `tasks.md` 标记完成,但实际产物、验证、文档或沟通未闭环
- `design.md``tasks.md` 仍保留 `<!-- ... -->` 模板注释、空表格行、`Replace with...``TBD``TODO` 等未解决占位内容
- 必要的文档/沟通材料未同步影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果的变更
- 实际产物存在明显的重复、复杂度过高、表达不清、责任不明、风险处理薄弱、验证质量不足等问题
- `fast-drive` change 中仍错误依赖 `proposal.md``specs/*.md``Capabilities``Modified Capabilities` 的内容
输出审查结果:
1. **问题总览表**:问题类型 × 涉及文件数
2. **实际改动与修补清单**:本次实现中已落地的主要功能、后续修补和验证结论;若缺少测试结果,对未验证部分单独标记
3. **未覆盖清单**:文档要求但未在代码中实现或未充分验证的内容
4. **需回写文档清单**代码和测试中已确认、但文档未体现的实现、修补或约束变化
5. **方向待确认清单**代码与文档不一致,且无法判断应以哪边为准的事项
2. **实际变更与修补清单**:本次已落地的主要变更、后续修补和验证结论;若缺少验证结果,对未验证部分单独标记
3. **Design 偏离清单**:实际变更未完成、完成不充分或偏离 `design.md` 的内容
4. **需回写文档清单**实际产物和验证中已确认、但 `design.md``tasks.md` 或相关文档/沟通材料未体现的变更、修补或约束变化
5. **方向待确认清单**实际变更与 `design.md` 不一致,且无法判断应以哪边为准的事项
6. **任务状态问题清单**:未真正完成、状态错误或需补充的新任务
7. **测试问题清单**:缺失覆盖、掩盖错误、验证不足或修补后未回归验证的测试问题
8. **代码质量/优化清单**:可优化的实问题和建议
9. **逐项分析**:每个问题说明位置、问题、影响、建议和建议修复方向
7. **验证问题清单**:缺失覆盖、掩盖错误、验证不足或修补后未回归验证的问题
8. **质量/优化清单**:可优化的实际产物问题和建议
9. **Schema 差异清单**:实际 schema 与默认 `fast-drive` 不同时,列出因此跳过或改按实际 artifacts 审查的内容
10. **逐项分析**:每个问题说明位置、问题、影响、建议和建议修复方向
若所有清单均为空,输出"审查通过,未发现问题",跳至步骤 5。
若所有清单均为空,输出审查通过,未发现问题,跳至步骤 5。
## 3. 计划(用户确认)
先针对"方向待确认清单"用提问工具逐项向用户确认。
先针对方向待确认清单用提问工具逐项向用户确认。
再整理完整修复方案,按类别列出:
- 代码或测试补充:补实现、补异常处理、补回归测试、修复掩盖错误的测试
- 文档回写:同步 `proposal.md``design.md``tasks.md``specs/*.md`、README 中遗漏或过时的内容
- 实际工作或验证补充:补完成、补异常处理、补回归验证、修复被弱化或绕过的验证
- Design 回写:同步 `design.md` 中遗漏或过时的 requirements、guardrails、affected areas、decisions、execution plan、verification plan、risks 或 open questions
- 任务状态修正:修正已完成/未完成状态,补充 apply 后新增但已完成的修补任务或验证任务
- 代码质量优化:在不改变目标行为的前提下优化结构、复用、命名或可维护性
- 文档/沟通同步:同步行为、流程、接口、内容、数据、配置、责任边界或用户可见结果变化
- 质量优化:在不改变目标结果的前提下优化结构、表达、一致性、可维护性或移交质量
- Schema 兼容处理:若实际 schema 不是 `fast-drive`,按实际存在的 artifacts 说明额外文档同步项
对每个拟修改的文件说明:
@@ -98,33 +142,38 @@
## 4. 执行
逐项执行已确认的代码、测试和文档修复。
逐项执行已确认的实际产物、验证和文档修复。
若涉及删除或重写:
- 先创建备份文件 `{file}.bak.{timestamp}`
- 再执行修改
若修改了代码或测试
若修改了实际产物或验证材料
- 同步更新相关变更文档;若影响模块结构、API、实体或用户可见行为,再同步 README
- 运行相关测试;若修补影响范围较大,再补充执行受影响的回归测试
- 同步更新相关变更文档;若影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,再同步必要的文档/沟通材料
- 运行或执行相关验证;若修补影响范围较大,再补充执行受影响的回归验证
若修改了文档:
- 确认实际存在的变更文档之间保持一致;在 `spec-driven` 下重点检查 `proposal.md``design.md``tasks.md``specs/*.md`
- 若 apply 后新增修补改变了能力边界或行为约束,同步更新 `Capabilities` / `Modified Capabilities`
- `fast-drive` workflow 下,确认 `design.md` 仍是 source of truth`tasks.md` 仍从 `design.md` 派生
- 确认 `design.md` 的 requirements、guardrails、affected areas、decisions、execution plan、verification plan、risks 和 open questions 与实际变更一致
- 确认 `tasks.md` 每个完成任务都有对应实际产物和必要验证,新增修补已补充任务或记录在合适任务中
- 禁止将本次 change 内容同步到 `openspec/specs/`,该操作属于 archive 阶段
-`fast-drive` workflow 下不创建 `proposal.md``specs/*.md`;若实际 schema 不是 `fast-drive`,则按实际 schema 的 required artifacts 创建或更新本次 change 目录下的 artifacts
执行后重新读取所有被修改的代码、测试和文档,并复核:
执行后重新读取所有被修改的实际产物、验证材料和文档,并复核:
- "未覆盖清单" 是否已清空或已标注保留原因
- "需回写文档清单" 是否已清空
- "方向待确认清单" 是否已清空或已记录用户决策
- "任务状态问题清单""测试问题清单" 是否已清空或已标注残留原因
- "代码质量/优化清单" 中哪些已处理,哪些有意延期
- “Design 偏离清单 是否已清空或已标注保留原因
- 需回写文档清单 是否已清空
- 方向待确认清单 是否已清空或已记录用户决策
- 任务状态问题清单“验证问题清单 是否已清空或已标注残留原因
- 质量/优化清单 中哪些已处理,哪些有意延期
- 必要的文档/沟通材料是否已按影响范围同步
- 所有模板注释、空表格行和占位文本是否已清空或替换为有效内容
## 5. 收尾
列出所有修改的文件、备份文件、测试命令与结果、文档同步摘要和剩余风险。
列出所有修改的文件、备份文件、验证命令或检查结果、文档同步摘要和剩余风险。
若本次因缺少测试结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
若本次因缺少验证结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。

View File

@@ -1,22 +1,48 @@
审查本次 OpenSpec 变更文档是否与前序讨论、当前代码现状和 OpenSpec 文档规范一致,识别遗漏、冲突和不合理假设,给出可执行的补充建议,按以下流程执行。
审查本次 OpenSpec 变更文档是否与前序讨论、当前实际状态和实际 OpenSpec workflow 一致,重点检查 `fast-drive` workflow 下的 `design.md` 是否足以在上下文压缩或新会话中指导后续 `apply`,并识别遗漏、冲突和不合理假设,给出可执行的补充建议,按以下流程执行。
## 约束
- 仅修改本次变更文档,不修改源码
- 默认按 `spec-driven` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
- 优先使用当前会话中的讨论和已生成的变更文档;仅在无法明确 change、`schemaName` 或文档范围时,再用提问工具或 OpenSpec 命令补充定位
- 仅修改本次变更文档,不修改实际产物
- 默认按 `fast-drive` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
- `fast-drive` workflow 下,核心 artifacts 是 `design.md``tasks.md`;不要要求存在 `proposal.md``specs/*.md`
-`fast-drive` workflow 下,`design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth`tasks.md` 必须从 `design.md` 派生
- 优先使用当前会话中的讨论、explore/propose 阶段结论和已生成的变更文档;仅在无法明确 change、`schemaName` 或文档范围时,再用提问工具或 OpenSpec 命令补充定位
- 每批文档修改建议执行前用提问工具获得用户确认
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
## 1. 收集
并行读取:
读取约束
- 本次 change 的实际 artifacts`spec-driven` 下通常包括 `proposal.md``design.md``tasks.md``specs/*.md`
- 当前会话中与本次变更相关的讨论、澄清、边界约束、非目标、待确认事项
- 与本次变更直接相关的源码、测试、README、架构文档
- `openspec/config.yaml`
- 现有 `openspec/specs/**/spec.md` 中与本次变更相关的规范,相关性来源包括:`proposal.md``Capabilities` / `Modified Capabilities`、讨论中提到的受影响能力、`design.md` / Impact 中提到的模块、相关代码对应的现有能力
- 直接使用 Read 工具并行读取文件,禁止使用 subagent/Task 工具做文件读取和内容转发
- 不原样输出文件内容,仅在步骤 2 输出审查结论
分步收集:
a) 先并行读取核心入口和配置,确定范围:
- 本次 change 的 `design.md`
- 本次 change 的 `tasks.md`
- workflow context/configuration例如存在时读取 `openspec/config.yaml`
- 若可定位到 schema读取对应 schema`fast-drive` 下优先读取 `openspec/schemas/fast-drive/schema.yaml`
b) 从 `design.md` 提取审查基准:
- `Context`
- `Discussion Notes`
- `Requirements`
- `Goals / Non-Goals`
- `Execution Guardrails`
- `Affected Areas`
- `Decisions`
- `Execution Plan`
- `Verification Plan`
- `Risks / Trade-offs`
- `Open Questions`
c) 基于 `Affected Areas``Execution Plan``Verification Plan`、讨论中提到的受影响范围,并行读取相关实际产物、参考材料、验证材料、流程说明、配置、文档或沟通材料,确认文档是否贴合当前实际状态。
d) 若实际 schema 不是 `fast-drive`,只读取实际存在的 artifacts若存在 `proposal.md``specs/*.md`,再按该 schema 的要求补充读取和审查。
若当前上下文无法明确 change 或文档路径:
@@ -25,48 +51,55 @@
若已明确 change但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
若缺少讨论记录,明确说明本次降级为"文档 + 代码现状审查",不做讨论一致性结论。
若缺少讨论记录,明确说明本次降级为文档 + 当前实际状态审查,不做讨论一致性结论。
## 2. 分析
按以下优先级检查:
| 优先级 | 维度 | 检查点 |
| ------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P0 | 讨论一致性 | 仅在存在讨论记录时检查:文档是否完整覆盖已确认的目标、范围、非目标、约束、边界条件、风险、决策点、待办事项;若无讨论记录,标记为"跳过" |
| P1 | 代码现实性 | 文档对当前模块、接口、数据结构、命名、依赖、目录结构、复用路径的描述是否准确;是否把"计划变更"误写成"当前现状";是否遗漏真实受影响的现有能力 |
| P2 | 文档内部一致性 | 对实际存在的 artifacts 检查是否互相支撑;在 `spec-driven` 下重点检查 `proposal.md``design.md``tasks.md``specs/*.md``Capabilities` / `Modified Capabilities` 是否完整;每个 capability 是否有对应 spec`tasks.md` 是否覆盖 `design.md``specs/*.md` |
| P3 | OpenSpec 合规性 | 对实际存在的 artifacts 检查是否遵循 OpenSpec 格式和术语;`specs/*.md` 是否只描述行为与约束、不混入实现细节;`tasks.md` 是否一行一个任务;是否混入 git 操作任务 |
| 优先级 | 维度 | 检查点 |
| ------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P0 | 讨论承接性 | 仅在存在讨论记录时检查:`design.md` 是否完整记录已确认的目标、非目标、用户偏好、约束、边界条件、风险、关键决策、被否决方案和待澄清事项;若无讨论记录,标记为跳过 |
| P1 | `design.md` 自包含性 | `design.md` 是否足以让看不到前序对话的执行者继续工作;是否包含完整 required sections`Open Questions` 是否明确区分 blocking / non-blocking 或写出 `None`;是否存在依赖未记录聊天上下文的隐含要求 |
| P2 | 当前状态真实性 | `design.md` 对当前实际产物、流程、接口、内容、数据、配置、依赖、责任边界、参考材料和验证入口的描述是否准确;是否把“计划变更”误写成“当前现状”;`Affected Areas` 是否遗漏真实受影响区域 |
| P3 | `tasks.md` 派生性 | `tasks.md` 是否从 `design.md` 派生;是否覆盖 requirements、guardrails、decisions、execution plan 和 verification plan是否依赖 `proposal.md``specs/*.md` 中未写入 `design.md` 的内容 |
| P4 | OpenSpec 合规性 | 对实际存在的 artifacts 检查是否遵循对应 schema 和 OpenSpec 术语;`tasks.md` 是否一行一个 `- [ ]` checkbox 任务、按 `##` numbered headings 分组、无无关的仓库/版本控制/发布操作任务;`design.md` 是否避免 task checkbox最终 artifacts 是否仍保留模板注释、空表格行或占位任务文本 |
分析时区分两类情况:
- 文档对当前代码现状的描述错误
- 文档描述的是预期变更,本来就应当与当前代码不同
- 文档对当前实际状态的描述错误
- 文档描述的是预期变更,本来就应当与当前状态不同
重点识别:
- 讨论中已确定但文档未记录的内容
- 文档基于错误现状做出的设计或任务拆分
- 文档之间相互冲突的目标、方案、约束、任务
- `proposal -> specs -> design -> tasks` 链路中的断点
- `Modified Capabilities` 应更新但未更新的现有 spec
- 讨论中已确定但 `design.md` 未记录的内容
- `design.md` 中缺失或含糊的 requirements、acceptance criteria、guardrails、decisions、verification expectations
- `Open Questions` 未明确区分 blocking / non-blocking、与 `tasks.md` 冲突,或包含 apply 前必须解决的 blocker
- `tasks.md` 未覆盖 `design.md` 的要求、约束、执行计划、验证计划或文档/沟通更新要求
- `tasks.md` 标记了无法验证、跨行、过大、顺序错误或包含无关仓库/版本控制/发布操作的任务
- 文档仍保留 `<!-- ... -->` 模板注释、空表格行、`Replace with...``TBD``TODO` 等未解决占位内容
- 文档基于错误当前状态做出的设计或任务拆分
- 文档之间相互冲突的目标、方案、约束、任务和验证要求
- `fast-drive` change 中仍错误依赖 `proposal.md``specs/*.md``Capabilities``Modified Capabilities` 的内容
输出审查结果:
1. **问题总览表**:问题类型 × 涉及文档数
2. **讨论遗漏清单**:讨论已确定但文档未体现的内容;若缺少讨论记录,标记为"未审查"
3. **现实性问题清单**与当前代码现状不符的描述、假设或影响分析
4. **文档冲突清单**proposal、design、tasks、specs 之间的不一致
5. **OpenSpec 规范问题清单**:格式、术语、结构问题
6. **待澄清清单**仅靠讨论和代码仍无法判断的事项
7. **逐项分析**:每个问题说明位置、问题、影响、建议
8. **补充建议方案**:按文件列出建议补充/修正的内容、原因和可选方案
2. **讨论遗漏清单**:讨论已确定但 `design.md` 未体现的内容;若缺少讨论记录,标记为未审查
3. **Design 自包含性问题清单**缺失、含糊或无法指导新会话 apply 的内容
4. **当前状态问题清单**与当前实际状态不符的描述、假设或影响分析
5. **Tasks 派生与覆盖问题清单**`tasks.md` 未从 `design.md` 正确派生或覆盖不足的内容
6. **文档冲突清单**`design.md``tasks.md` 和实际存在的其他 artifacts 之间的不一致
7. **OpenSpec 规范问题清单**:格式、术语、结构问题
8. **待澄清清单**:仅靠讨论和当前状态仍无法判断的事项
9. **逐项分析**:每个问题说明位置、问题、影响、建议
10. **补充建议方案**:按文件列出建议补充/修正的内容、原因和可选方案
若所有清单均为空,输出"审查通过,未发现问题",跳至步骤 5。
若所有清单均为空,输出审查通过,未发现问题,跳至步骤 5。
## 3. 计划(用户确认)
先针对"待澄清清单"用提问工具逐项向用户确认。
先针对待澄清清单用提问工具逐项向用户确认。
再整理完整修复方案,按文件列出:
@@ -79,7 +112,9 @@
## 4. 执行
逐项修改已确认的变更文档,不修改源码
逐项修改已确认的变更文档,不修改实际产物
`fast-drive` workflow 下,通常只修改本次 change 的 `design.md``tasks.md`;若实际 schema 存在其他 artifacts仅在确有必要且用户确认后修改实际存在的 artifacts。
若涉及删除或重写:
@@ -88,9 +123,14 @@
执行后重新读取所有被修改的文档,并复核:
- "讨论遗漏清单" 是否已清空或已标注保留原因
- "现实性问题清单" 是否已清空或已标注为预期变更
- "文档冲突清单" 和 "OpenSpec 规范问题清单" 是否已清空
- 讨论遗漏清单 是否已清空或已标注保留原因
- “Design 自包含性问题清单 是否已清空
- “当前状态问题清单 是否已清空或已标注为预期变更
- “Tasks 派生与覆盖问题清单” 是否已清空
- “文档冲突清单” 是否已清空
- “OpenSpec 规范问题清单” 是否已清空
- “待澄清清单” 是否已清空或已记录用户决策
- 所有模板注释、空表格行和占位文本是否已清空或替换为有效内容
## 5. 收尾

View File

@@ -1,142 +0,0 @@
请审查并整理 `openspec/specs/` 下的稳定规范,使其成为可搜索、边界清晰、无冗余、与当前业务一致的能力索引,按以下流程执行。
## 约束
- `openspec/specs/` 描述长期稳定的业务能力、规则和外部行为,不记录变更过程、迁移说明、实现路径、内部类型名、组件 props、样式数值、层级分层等实现细节
- 用户可感知或对外暴露的契约可以保留:公开 API 路径、请求/响应字段、协议名、错误码、数据约束、交互结果
- `Requirement``Scenario` 应描述业务能力、外部行为或稳定约束,不以“使用某层/某组件/某库实现”作为标题或核心表述
- 不把当前代码自动视为唯一真相若代码、README、现有 spec 冲突且无法判断应以哪边为准,列入待确认清单,不直接改写规范
- 仅删除内容已被其他规范完整覆盖且无独立检索价值的规范;非冗余内容仅迁移、合并、拆分或重命名
- 每批重构执行前用提问工具获得用户确认;删除或重写前先备份原文件为 `{file}.bak.{timestamp}`
- 命名、Purpose、Requirement 标题都必须保留用户下一次最可能搜索的业务关键词
## 1. 收集
并行读取:
- `openspec/config.yaml`
- `README.md`以及与模块结构、API、架构相关的 README 或文档
- `openspec/specs/*/spec.md`
默认不读取 `openspec/changes/**`、历史 proposal/design/tasks 作为稳定规范整理依据;仅在用户明确要求“连同历史变更一起校对”时再纳入。
先建立索引,不直接开始改写:
| 索引 | 内容 |
| -------------- | ----------------------------------------------------------------------------- |
| `spec_index[]` | 每个 spec 的目录名、Purpose、Requirement 摘要、关键词、外部契约、疑似重叠对象 |
| `domain_map[]` | 从 README、API、模块文档中提炼的核心业务域、横切能力和术语 |
| `term_map[]` | 同义词、旧名、缩写和推荐标准术语 |
| `suspects[]` | 需要进一步对照代码或测试确认的 spec |
仅对 `suspects[]` 做定向读取:
- 读取与该 spec 对应的源码、测试、README 或架构文档
- 不对 `backend/``frontend/` 做无差别逐文件扫描
判定依据优先级:
- 当前稳定 spec 与 README 共同支持的事实,可直接视为高置信度
- 仅代码可见但 README 和 spec 未体现的内容,先判断它是稳定外部行为还是临时实现细节
- 代码、README、现有 spec 互相冲突且无法自动定夺时,进入 `待确认清单`
## 2. 审查
按 spec、Requirement、Scenario 三层检查:
| 维度 | 检查点 |
| --------- | --------------------------------------------------------------------------------- |
| 过时 | 描述的能力、术语、外部契约是否仍成立;是否存在 `TBD``TODO`、占位说明 |
| 冲突 | 不同规范是否对同一行为给出不同约束、命名或边界 |
| 重复/重叠 | 是否在文件级、Requirement 级、Scenario 级重复描述同一能力 |
| 错位 | 内容是否放错能力域;横切规则是否混入实体规范;平台实现是否混入通用能力规范 |
| 粒度 | 是否过大导致难检索,或过碎导致回答一个问题必须同时打开多个 spec |
| 术语 | 同一概念是否混用多个名字;旧名、别名、缩写是否需要归一并保留检索入口 |
| 命名/检索 | 目录名、Purpose、Requirement 标题是否准确;是否能被 README、API、业务术语直接命中 |
| 规范性 | 是否使用 SHALL/WHEN/THEN是否混入变更记录、迁移说明、内部实现或 UI/代码细节 |
| 完整性 | Purpose 是否明确;是否存在空目录、非 spec 噪音文件、无清晰归属的孤立规范 |
重构判定规则:
- 若两个 spec 回答的是同一个核心问题,或其中一个只是另一个的子集,优先合并
- 若一个 spec 混合多个独立检索意图,或同时包含横切规则与业务流程,优先拆分
- 若内容正确但目录名、Purpose 或 Requirement 标题不利于检索,优先重命名或改写标题
- 若多个术语指向同一概念,统一到一个标准术语,并在 Purpose 或 Requirement 中保留必要别名以支持搜索
- 若某段内容只是内部实现细节,且不影响外部行为理解,删除该段而不是为其单独保留 spec
- 若某个具体值同时属于外部契约与内部实现,按“是否对调用方可见、是否影响兼容性”判断是否保留
### 命名约定
命名优先复用仓库已存在的稳定术语,如 `provider``model``stats``protocol``proxy``logging``validation``migration``frontend``desktop``mysql`
| 类型 | 模式 | 示例 |
| ------------ | ---------------------------------------------------------- | -------------------------------------------------- |
| 实体生命周期 | `{entity}-management` | `provider-management``model-management` |
| 横切能力 | `{concern}``{concern}-{qualifier}` | `error-handling``structured-logging` |
| 协议/适配 | `{protocol}-{capability}``protocol-adapter-{protocol}` | `openai-protocol-proxy``protocol-adapter-openai` |
| 运行面/入口 | `{surface}``{surface}-{capability}` | `frontend``desktop-app` |
| 基础设施 | `{resource}-{operation}` | `database-migration``mysql-driver` |
命名原则:
- 1-4 个词,保持单一主题
- 优先使用业务名词,不使用 `basic``general``misc``info``data` 等泛化词
- 不使用 `crud``list``table``display` 等实现模式词,除非它本身就是外部契约的一部分
- 同一主题的命名模式保持一致,不同时混用多套前后缀
## 3. 报告
输出分析结果:
1. **问题总览表**:问题类型 × 涉及规范数
2. **规范关系表**:每个 spec 的主主题、重叠对象、冲突对象、建议动作
3. **术语归一表**:旧术语 / 别名 / 缩写 → 推荐标准术语
4. **逐项分析**:每个有问题的规范说明位置、问题、影响、建议和目标规范
5. **待确认清单**代码、README、现有 spec 冲突且无法自动定夺的事项
6. **重构方案**:按优先级分批
7. **重构后目录结构**:预期的新 `openspec/specs/` 目录树
优先级建议:
- P0删除空目录、非 spec 噪音文件、占位内容
- P1删除完全冗余规范将其内容映射到主规范
- P2合并重复/子集规范;拆分错位或过大规范
- P3重命名目录、改写 Purpose 和 Requirement 标题以提升检索性
- P4修正过时描述清理实现细节、迁移说明和变更记录
若所有问题清单为空,输出“审查通过,未发现问题”,跳至步骤 5。
## 4. 计划(用户确认)
先针对 `待确认清单` 用提问工具逐项向用户确认。
再按批次展示完整重构计划,每批必须包含:
- 操作类型:删除、重命名、迁移、合并、拆分、改写
- 路径变化:源路径 → 目标路径
- 内容映射:源 spec 的 Requirement / Scenario 将迁移到哪里
- 术语处理:哪些旧词保留为检索入口,哪些词统一替换
- 执行原因:为什么这样做更利于检索、去重和边界清晰
- 验证方式:如何确认没有丢失约束或引入新的冲突
用提问工具获得当前批次确认后再执行。
## 5. 执行
按 P0 → P4 逐批执行已确认的重构。
执行要求:
- 合并或拆分时先写目标 spec再删除或重命名源 spec
- 删除前确认其 Requirement 和 Scenario 已被完整保留、迁移或判定为纯冗余
- 每批执行后重新读取受影响的 spec并复核结构和内容
每批执行后至少验证:
- 目录结构完整,`openspec/specs/*/spec.md` 可正常读取
- 不存在未承接的 Requirement 或 Scenario
- Purpose、Requirement 标题、目录名可以直接表达主能力
- 不再包含 `TBD`、变更记录、迁移说明、内部实现细节或噪音文件
- 若本批涉及代码对照项,相关外部契约描述与当前仓库现状一致,或已列入残留待确认
收尾时输出:修改文件清单、备份文件清单、最终目录树、残留待确认事项和整理摘要。

35
docs/user/README.md Normal file
View File

@@ -0,0 +1,35 @@
# 用户文档
本文档是 DiAL 的用户使用入口说明如何阅读配置、部署、expect 规则、故障排查和各 checker 参考。
适用场景:编写 YAML 配置、部署 DiAL、理解拨测结果、排查运行问题、查询某个 checker 的字段和示例。
## 文档索引
| 文档 | 内容 |
| ---------------------------------------- | ------------------------------------------------- |
| [configuration.md](configuration.md) | YAML 顶层结构、变量、server、targets 通用字段 |
| [deployment.md](deployment.md) | 生产构建、Docker、ICMP 权限、发布包运行方式 |
| [expectations.md](expectations.md) | expect 规则、状态判定、failure、observation |
| [troubleshooting.md](troubleshooting.md) | 配置校验、变量、ICMP、CMD、Docker、证书和正则问题 |
| [checkers/README.md](checkers/README.md) | 各 checker 的配置项、expect 字段和示例 |
## 按任务阅读
| 任务 | 建议阅读 |
| --------------------- | ---------------------------------------------------------------------- |
| 首次运行 | [项目快速开始](../../README.md#快速开始)、[配置文件](configuration.md) |
| 编写配置 | [配置文件](configuration.md)、[Checker 参考](checkers/README.md) |
| 编写 expect | [校验规则](expectations.md)、对应 checker 文档 |
| 容器或生产部署 | [部署](deployment.md)、[故障排查](troubleshooting.md) |
| 排查启动或运行问题 | [故障排查](troubleshooting.md)、相关 checker 文档 |
| 查询 checker 专属字段 | [Checker 参考](checkers/README.md) |
## 用户文档更新规则
- 配置结构、变量、server、probes、targets 通用字段变化时,更新 [configuration.md](configuration.md)。
- checker 配置项、expect 字段、示例或运行行为变化时,更新 `checkers/<type>.md` 和 [checkers/README.md](checkers/README.md)。
- expect 模型、状态判定、failure、observation 或快速失败顺序变化时,更新 [expectations.md](expectations.md)。
- 构建产物运行方式、Docker 参数、镜像内置依赖、发布包结构变化时,更新 [deployment.md](deployment.md)。
- 常见错误、运行依赖、权限、证书或配置校验排查方式变化时,更新 [troubleshooting.md](troubleshooting.md)。
- 用户文档只解释“如何使用”和“用户能观察到什么”,实现细节放入 [`../development/`](../development/README.md)。

View File

@@ -0,0 +1,49 @@
# Checker 参考
Checker 是 DiAL 的拨测执行单元。每个 target 通过 `type` 选择一个 checker并配置对应的专属字段和 `expect` 规则。
适用场景:查询 checker 类型选择、专属配置、expect 字段、示例和各 checker 文档入口。
## 支持的类型
| 类型 | 用途 | 文档 |
| -------- | -------------------------------------- | ------------------- |
| `http` | HTTP/HTTPS 应用层健康检查 | [HTTP](http.md) |
| `cmd` | 执行本地命令或脚本 | [Cmd](cmd.md) |
| `db` | PostgreSQL/MySQL/SQLite 连接和查询检查 | [DB](db.md) |
| `tcp` | TCP 端口可达性和 banner 探测 | [TCP](tcp.md) |
| `udp` | UDP payload 请求-响应检查 | [UDP](udp.md) |
| `icmp` | 基于系统 `ping` 的存活、延迟、丢包检查 | [ICMP](icmp.md) |
| `dns` | 本机解析或指定 DNS server 协议级检查 | [DNS](dns.md) |
| `llm` | 大模型服务应用层健康检查 | [LLM](llm.md) |
| `ws` | WebSocket 可达性和消息交互检查 | [WS](ws.md) |
| `cpu` | 本机 CPU 使用率健康检查 | [CPU](cpu.md) |
| `memory` | 本机系统内存使用状况检查 | [Memory](memory.md) |
## 选择建议
| 目标 | 推荐 checker |
| ---------------------------------- | ------------ |
| Web API、网页、HTTP 状态码或响应体 | `http` |
| 本机脚本、外部命令、CLI 工具 | `cmd` |
| 数据库连接或查询结果 | `db` |
| 端口是否可连接、服务 banner | `tcp` |
| UDP 服务响应或简单心跳 | `udp` |
| 主机可达性、延迟、丢包率 | `icmp` |
| 域名解析值、DNS RCODE、TTL、flags | `dns` |
| LLM API 是否可用、输出是否符合预期 | `llm` |
| WebSocket 可达性或消息交互验证 | `ws` |
| 本机 CPU 使用率健康检查 | `cpu` |
| 本机系统内存使用状况检查 | `memory` |
## 通用字段
所有 checker 都共享 target 通用字段,见 [配置文件](../configuration.md#targets-通用字段)。
## 通用断言模型
各 checker 的 `expect` 字段复用 `ValueMatcher``ContentExpectations``KeyedExpectations`。详情见 [校验规则](../expectations.md)。
## 更新触发条件
新增、移除或修改 checker 类型、用途、选择建议、通用字段或通用断言模型时必须更新本文档。checker 专属字段变化还必须同步更新对应 `checkers/<type>.md`

38
docs/user/checkers/cmd.md Normal file
View File

@@ -0,0 +1,38 @@
# Cmd Checker
`type: cmd` 用于执行本地命令或脚本并校验退出码、stdout、stderr 和耗时。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ---------- | ------------------------------------ | ---- | ------ |
| `cmd.exec` | 可执行文件名或路径 | 是 | 无 |
| `cmd.args` | 命令行参数列表 | 否 | `[]` |
| `cmd.env` | 环境变量覆盖,继承进程环境变量并合并 | 否 | 无 |
| `cmd.cwd` | 工作目录,相对于配置文件所在目录 | 否 | 无 |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ------------ | --------------------------------------------- | ---- | ------ |
| `exitCode` | 可接受的退出码列表 | 否 | `[0]` |
| `stdout` | 标准输出校验,使用 `ContentExpectations` 数组 | 否 | 无 |
| `stderr` | 标准错误校验,使用 `ContentExpectations` 数组 | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "bun-script"
name: "Bun 脚本检查"
type: cmd
cmd:
exec: "bun"
args: ["-e", "console.log('ok')"]
expect:
exitCode: [0]
stdout:
- contains: "ok"
```
Docker 官方镜像不内置常见外部命令。容器内使用 CMD checker 时,按需通过派生镜像安装依赖命令。

74
docs/user/checkers/cpu.md Normal file
View File

@@ -0,0 +1,74 @@
# CPU Checker
`type: cpu` 用于检查本机 CPU 使用率,基于两次系统快照计算总体和每核心的忙碌比例。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| -------------------- | -------------------------------- | ---- | ------- |
| `cpu.sampleDuration` | CPU 采样窗口,支持时长格式 | 否 | `1s` |
| `cpu.includePerCore` | 是否在结果中输出每核心使用率数组 | 否 | `false` |
`sampleDuration` 必须小于 target 的 `timeout`
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| --------------------- | ----------------------------------------------------------------------------------------------- | ---- | ------ |
| `usagePercent` | 总体 CPU 使用率,范围 `0-100`,使用 `ValueMatcher` | 否 | 无 |
| `idlePercent` | 总体 CPU 空闲率,与 `usagePercent` 互补,两者之和恒为 100`idlePercent + usagePercent = 100` | 否 | 无 |
| `maxCoreUsagePercent` | 单核心最高使用率,使用 `ValueMatcher` | 否 | 无 |
| `minCoreUsagePercent` | 单核心最低使用率,使用 `ValueMatcher` | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
所有百分比字段范围为 `0-100`,表示所有可见逻辑 CPU 的总体比例,不是"核心数 × 100"。
## 示例
```yaml
- id: "local-cpu"
name: "本机 CPU"
type: cpu
interval: "30s"
timeout: "5s"
cpu:
sampleDuration: "1s"
expect:
usagePercent:
lte: 85
maxCoreUsagePercent:
lte: 95
```
输出每核心使用率:
```yaml
- id: "local-cpu-detail"
name: "本机 CPU 详细"
type: cpu
cpu:
sampleDuration: "2s"
includePerCore: true
expect:
usagePercent:
lte: 80
```
## 语义说明
CPU checker 采集的是 DiAL 进程运行环境通过系统 API`os.cpus()`)可见的 CPU 视图。在容器中,它可能不等于严格的 cgroup quota 使用率。
`usagePercent``idlePercent` 互补,恒等于 100。`sampleDuration` 决定了两次快照之间的等待时间,窗口越长结果越稳定,但会增加 checker 执行耗时。
## 不支持的功能
- CPU 温度、电源状态、频率
- `userPercent` / `systemPercent`(用户态/系统态占比)
- `loadAverage`(系统负载均值)
- 进程级 CPU 使用率
- Linux cgroup 精确 CPU 计算
- `logicalCoreCount` 作为 expect 字段(仅在 observation 中输出)
## 更新触发条件
修改 CPU checker 配置、expect 字段、行为或语义时,必须更新本文档。

38
docs/user/checkers/db.md Normal file
View File

@@ -0,0 +1,38 @@
# DB Checker
`type: db` 用于数据库连接和查询结果检查,支持 PostgreSQL、MySQL 和 SQLite。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ---------- | ------------------------------------------------------------- | ---- | ------ |
| `db.url` | 数据库连接字符串,支持 `postgres://``mysql://``sqlite://` | 是 | 无 |
| `db.query` | SQL 查询语句,不配置时仅测试连接 | 否 | 无 |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ------------ | ----------------------------------------------------------------------- | ---- | ------ |
| `rowCount` | 查询返回行数校验,使用 `ValueMatcher` | 否 | 无 |
| `rows` | 查询结果逐行校验,数组内每行为列名到 `KeyedExpectations` 的映射 | 否 | 无 |
| `result` | 完整查询结果 `{ rows, rowCount }` 校验,使用 `ContentExpectations` 数组 | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "sqlite-query"
name: "SQLite 数据库检查"
type: db
db:
url: "sqlite:///path/to/db.sqlite"
query: "SELECT COUNT(*) as cnt FROM users WHERE status = 'active'"
expect:
durationMs:
lte: 5000
rowCount:
gte: 1
rows:
- cnt:
gte: 0
```

104
docs/user/checkers/dns.md Normal file
View File

@@ -0,0 +1,104 @@
# DNS Checker
`type: dns` 支持两种解析模式:本机解析器检查和指定 DNS server 协议级检查。
## resolver 模式
| 模式 | 说明 |
| -------- | ----------------------------------------------------------------------------- |
| `system` | 使用本机 DNS 解析器检查域名是否能解析到预期地址 |
| `server` | 直接向指定 DNS server 发起 UDP/TCP 深度拨测,检查 RCODE、TTL、flags、记录值等 |
## `dns.resolver: system` 配置项
| 字段 | 说明 | 必填 | 默认值 |
| -------------- | ----------------------------- | ---- | -------- |
| `dns.resolver` | 解析模式 | 是 | `system` |
| `dns.name` | 待解析域名 | 是 | 无 |
| `dns.family` | 地址族:`any``ipv4``ipv6` | 否 | `any` |
### system 模式 expect
| 字段 | 说明 | 断言模型 |
| ------------ | -------------------- | --------------------------------- |
| `values` | 解析结果地址集合断言 | DNS 集合include/exclude/exact |
| `valueCount` | 解析结果数量 | ValueMatcher |
| `durationMs` | 解析耗时 | ValueMatcher |
```yaml
- id: "dns-system-api"
name: "本机 DNS 解析"
type: dns
dns:
resolver: system
name: "api.example.com"
family: any
expect:
values:
exact:
- "203.0.113.10"
durationMs:
lte: 500
```
## `dns.resolver: server` 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ---------------------- | --------------------------------- | ---- | -------- |
| `dns.resolver` | 解析模式 | 是 | `server` |
| `dns.server` | DNS server 地址 | 是 | 无 |
| `dns.name` | 查询域名 | 是 | 无 |
| `dns.port` | DNS server 端口 | 否 | `53` |
| `dns.protocol` | 传输协议:`udp``tcp` | 否 | `udp` |
| `dns.recordType` | DNS 记录类型 | 否 | `A` |
| `dns.recursionDesired` | 是否设置 RD flag | 否 | `true` |
| `dns.tcpFallback` | UDP 响应 TC=1 时是否 TCP fallback | 否 | `true` |
| `dns.maxResponseBytes` | 响应最大字节数 | 否 | `4KB` |
`recordType` 可选值:`A``AAAA``CNAME``NS``MX``TXT``SOA``SRV``CAA``PTR`
### server 模式 expect
| 字段 | 说明 | 断言模型 |
| -------------------- | ---------------------------------- | --------------------------------- |
| `responded` | 是否收到 DNS response | boolean |
| `rcode` | 期望 RCODE 列表,如 `NOERROR` | string[] |
| `values` | 目标类型记录值集合断言 | DNS 集合include/exclude/exact |
| `valueCount` | 目标类型记录数量 | ValueMatcher |
| `answerCount` | answer section 总记录数 | ValueMatcher |
| `ttlMin` | answer 中最小 TTL | ValueMatcher |
| `ttlMax` | answer 中最大 TTL | ValueMatcher |
| `authoritative` | AA flag | boolean |
| `recursionAvailable` | RA flag | boolean |
| `truncated` | TC flag | boolean |
| `authenticatedData` | AD flag | boolean |
| `result` | 完整结构化响应的 JSONPath 兜底断言 | ContentExpectations |
| `durationMs` | 完整查询耗时 | ValueMatcher |
```yaml
- id: "dns-server-api"
name: "Cloudflare DNS A 记录"
type: dns
dns:
resolver: server
server: "1.1.1.1"
name: "api.example.com"
recordType: A
expect:
rcode: ["NOERROR"]
values:
include:
- "203.0.113.10"
ttlMin:
gte: 60
durationMs:
lte: 200
```
## 注意事项
- 未配置 expect 时,`system` 模式默认要求解析成功且 `valueCount > 0``server` 模式默认要求 `NOERROR + valueCount > 0`
- 显式配置非 `NOERROR` rcode`NXDOMAIN`)时,不自动要求 `valueCount > 0`
- `values.exact` 忽略返回顺序。
- 对 A/AAAA 查询CNAME 链不计入 `values`,单独放入 `cnameChain`
- `values` 按记录类型规范化为字符串,例如 MX 为 `"10 mail.example.com"`SRV 为 `"10 60 443 server.example.com"`

View File

@@ -0,0 +1,48 @@
# HTTP Checker
`type: http` 用于 HTTP/HTTPS 应用层健康检查。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ------------------- | ------------------- | ---- | ------- |
| `http.url` | 目标 URL | 是 | 无 |
| `http.method` | HTTP 方法 | 否 | `GET` |
| `http.headers` | 请求头 | 否 | 无 |
| `http.body` | 请求体 | 否 | 无 |
| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ------------ | -------------------------------------------------- | ---- | ------- |
| `status` | 可接受的状态码列表,支持精确码和范围(如 `"2xx"` | 否 | `[200]` |
| `headers` | 响应头校验,使用 `KeyedExpectations` | 否 | 无 |
| `body` | 响应体校验,使用 `ContentExpectations` 数组 | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "json-api"
name: "JSON API 示例"
type: http
http:
url: "https://httpbin.org/json"
headers:
Authorization: "Bearer token"
expect:
status: [200]
headers:
Content-Type:
contains: "application/json"
body:
- json:
path: "$.slideshow.title"
equals: "Sample Slide Show"
durationMs:
lte: 10000
```
HTTP checker 的 `durationMs` 覆盖完整执行,包括重定向、按需响应体读取、解码和 expect 校验。未配置 body expectation、status 失败或 headers 失败时不会读取 body。

View File

@@ -0,0 +1,45 @@
# ICMP Checker
`type: icmp` 使用系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS 和 Windows 输出解析。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ----------------- | ------------------------- | ---- | ------ |
| `icmp.host` | 目标主机地址 | 是 | 无 |
| `icmp.count` | ICMP 包数量,范围 `1-100` | 否 | `3` |
| `icmp.packetSize` | ICMP 包大小bytes | 否 | `56` |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ------------------- | --------------------------------------------------- | ---- | ------ |
| `alive` | 期望主机可达性 | 否 | `true` |
| `packetLossPercent` | 丢包率百分比校验,范围 `0-100`,使用 `ValueMatcher` | 否 | 无 |
| `avgLatencyMs` | 平均延迟校验,使用 `ValueMatcher` | 否 | 无 |
| `maxLatencyMs` | 最大单次延迟校验,使用 `ValueMatcher` | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "gateway-icmp"
name: "网关 ICMP 可达"
type: icmp
icmp:
host: "10.0.0.1"
count: 3
packetSize: 56
expect:
alive: true
packetLossPercent:
lte: 10
avgLatencyMs:
lte: 100
maxLatencyMs:
lte: 300
durationMs:
lte: 5000
```
容器中运行 ICMP checker 通常需要 `--cap-add=NET_RAW`,详情见 [部署文档](../deployment.md#icmp-权限)。

53
docs/user/checkers/llm.md Normal file
View File

@@ -0,0 +1,53 @@
# LLM Checker
`type: llm` 用于大模型服务应用层健康检查。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| --------------------- | ----------------------------------------------------- | ---- | ------- |
| `llm.provider` | 模型提供方:`openai``openai-responses``anthropic` | 是 | 无 |
| `llm.url` | API base URL | 是 | 无 |
| `llm.model` | 模型名称 | 是 | 无 |
| `llm.prompt` | 单轮 prompt | 是 | 无 |
| `llm.mode` | 调用模式:`http``stream` | 否 | `http` |
| `llm.key` | API key支持 `${VAR}` 变量替换 | 否 | `""` |
| `llm.authToken` | Bearer token`anthropic` provider`key` 互斥 | 否 | 无 |
| `llm.headers` | 附加请求头 | 否 | 无 |
| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` |
| `llm.options` | 生成选项 | 否 | 无 |
| `llm.providerOptions` | Provider 专属选项 | 否 | 无 |
`llm.options` 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP``topK``presencePenalty``frequencyPenalty``stopSequences``seed`
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ----------------- | --------------------------------------------------------------------------- | ---- | ------- |
| `status` | 可接受的状态码列表,支持精确码和范围(如 `"2xx"` | 否 | `[200]` |
| `headers` | 响应头校验,使用 `KeyedExpectations` | 否 | 无 |
| `output` | 模型输出校验,使用 `ContentExpectations` 数组 | 否 | 无 |
| `finishReason` | finish reason 校验,使用 `ValueMatcher` | 否 | 无 |
| `rawFinishReason` | 原始 finish reason 校验,使用 `ValueMatcher` | 否 | 无 |
| `usage` | Token usage 校验,支持 `inputTokens``outputTokens``totalTokens` matcher | 否 | 无 |
| `stream` | 流式断言,支持 `completed``firstTokenMs` matcher`mode: stream` | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "llm-openai-probe"
name: "OpenAI 健康检查"
type: llm
llm:
provider: openai
url: "https://api.openai.com/v1"
model: "gpt-4o-mini"
prompt: "Say OK"
key: "${OPENAI_API_KEY}"
expect:
status: [200]
finishReason: "stop"
output:
- contains: "OK"
```

119
docs/user/checkers/mem.md Normal file
View File

@@ -0,0 +1,119 @@
# Mem Checker
`type: mem` 用于检查本机系统级内存使用状况,包括物理内存和交换空间的使用率及字节数。
## 配置项
Mem checker 配置为空对象,无需额外参数:
```yaml
mem: {}
```
## expect 校验项
### 百分比字段
| 字段 | 说明 | 必填 | 默认值 |
| ------------------ | -------------------------------------------------------------------------- | ---- | ------ |
| `usagePercent` | 真实内存使用率 = `activeBytes / totalBytes × 100`,不含 buffers/cache 假象 | 否 | 无 |
| `usedPercent` | 原始已用百分比 = `usedBytes / totalBytes × 100`,包含 buffers/cache | 否 | 无 |
| `freePercent` | 空闲百分比 = `freeBytes / totalBytes × 100` | 否 | 无 |
| `activePercent` | 活跃内存百分比 = `activeBytes / totalBytes × 100` | 否 | 无 |
| `availablePercent` | 可用内存百分比 = `availableBytes / totalBytes × 100` | 否 | 无 |
| `swapUsagePercent` | 交换空间使用率,当系统无交换分区时为 `null` | 否 | 无 |
所有百分比字段范围为 `0-100`,使用 `ValueMatcher`
### 字节字段
| 字段 | 说明 | 必填 | 默认值 |
| ---------------- | ----------------------------------------- | ---- | ------ |
| `activeBytes` | 活跃内存字节数 | 否 | 无 |
| `usedBytes` | 已用内存字节数(含 buffers/cache | 否 | 无 |
| `freeBytes` | 空闲内存字节数 | 否 | 无 |
| `availableBytes` | 可用内存字节数 | 否 | 无 |
| `totalBytes` | 物理内存总字节数 | 否 | 无 |
| `swapUsedBytes` | 交换空间已用字节数,无交换分区时为 `null` | 否 | 无 |
| `swapFreeBytes` | 交换空间空闲字节数,无交换分区时为 `null` | 否 | 无 |
| `swapTotalBytes` | 交换空间总字节数,无交换分区时为 `0` | 否 | 无 |
| `buffcacheBytes` | 缓冲缓存字节数,部分平台可能为 `null` | 否 | 无 |
字节字段支持数字(字节数)或大小字符串(如 `"512MB"``"1GB"`),使用 `ValueMatcher`
### 通用字段
| 字段 | 说明 | 必填 | 默认值 |
| ------------ | ------------------------------------- | ---- | ------ |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
检查内存使用率不超过 85%
```yaml
- id: "local-memory"
name: "本机内存"
type: mem
interval: "30s"
timeout: "5s"
mem: {}
expect:
usagePercent:
lte: 85
```
检查可用内存不低于 4GB
```yaml
- id: "local-memory-available"
name: "可用内存检查"
type: mem
mem: {}
expect:
availableBytes:
gte: "4GB"
```
同时检查内存和交换空间:
```yaml
- id: "local-memory-swap"
name: "内存和交换空间"
type: mem
mem: {}
expect:
usagePercent:
lte: 80
swapUsagePercent:
lte: 50
```
## 语义说明
Mem checker 通过 `systeminformation` 库读取系统内存数据,在 Linux、macOS 和 Windows 上均可运行。
- **`usagePercent`** 使用 `activeBytes / totalBytes` 计算,反映真实的内存压力,不受 Linux buffers/cache 缓存影响。推荐使用此字段进行内存健康检查。
- **`usedPercent`** 使用 `usedBytes / totalBytes` 计算,包含 buffers/cache。在 Linux 上此值通常高于 `usagePercent`
- **Swap 字段**:当系统未配置交换分区时,`swapTotalBytes``0``swapUsagePercent``null`(非 `0`)。
- **`buffcacheBytes`**:反映 Linux 的 buffers + cache 用量,在其他平台上可能为 `null`
Mem checker 是即时读取(非采样),无需 `sampleDuration`,执行速度远快于 CPU checker。虽然读取本身很快但仍受 target `timeout` 约束——若底层系统调用悬挂或阻塞超过 `timeout`checker 会返回 `mem/timeout` failure。
## 跨平台注意事项
- Windows 环境依赖 PowerShell 5+ 获取部分内存指标
- `buffcacheBytes` 在非 Linux 平台上可能返回 `null`
- 容器环境中内存数据可能不反映 cgroup 内存限制
## 不支持的功能
- 进程级内存使用(如 RSS、VSZ
- cgroup/container 内存限制精度
- 内存趋势采样和历史记录
- 内存条物理布局信息
- 详细内存分类slab、reclaimable、dirty 等)作为 expect 字段
## 更新触发条件
修改 Mem checker 配置、expect 字段、行为或语义时,必须更新本文档。

35
docs/user/checkers/tcp.md Normal file
View File

@@ -0,0 +1,35 @@
# TCP Checker
`type: tcp` 用于 TCP 端口可达性和可选 banner 探测。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ----------------------- | --------------------------------------------- | ---- | ------- |
| `tcp.host` | 目标主机地址 | 是 | 无 |
| `tcp.port` | 目标端口,范围 `1-65535` | 是 | 无 |
| `tcp.readBanner` | 是否读取服务端 banner | 否 | `false` |
| `tcp.bannerReadTimeout` | banner 读取超时,毫秒 | 否 | `2000` |
| `tcp.maxBannerBytes` | banner 最大字节数,支持 `KB``MB``GB` 单位 | 否 | `4KB` |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ------------ | ------------------------------------------------------------------------- | ---- | ------ |
| `connected` | 期望连接结果,`true` 可达或 `false` 期望不可达 | 否 | `true` |
| `banner` | Banner 内容校验,使用 `ContentExpectations` 数组,需开启 `tcp.readBanner` | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "redis-port"
name: "Redis 端口可达"
type: tcp
tcp:
host: "127.0.0.1"
port: 6379
expect:
durationMs:
lte: 3000
```

43
docs/user/checkers/udp.md Normal file
View File

@@ -0,0 +1,43 @@
# UDP Checker
`type: udp` 用于 UDP payload 请求-响应检查。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| ---------------------- | ------------------------------------------ | ---- | ------ |
| `udp.host` | 目标主机地址 | 是 | 无 |
| `udp.port` | 目标端口,范围 `1-65535` | 是 | 无 |
| `udp.payload` | 发送数据 | 否 | `""` |
| `udp.encoding` | payload 编码:`text``hex``base64` | 否 | `text` |
| `udp.responseEncoding` | 响应解码:`text``hex``base64` | 否 | `text` |
| `udp.maxResponseBytes` | 响应最大字节数,支持 `KB``MB``GB` 单位 | 否 | `4KB` |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| -------------- | --------------------------------------------- | ---- | ------ |
| `responded` | 期望是否收到响应 | 否 | `true` |
| `response` | 响应内容校验,使用 `ContentExpectations` 数组 | 否 | 无 |
| `responseSize` | 响应字节数校验,使用 `ValueMatcher` | 否 | 无 |
| `sourceHost` | 响应来源地址校验,使用 `ValueMatcher` | 否 | 无 |
| `sourcePort` | 响应来源端口校验,使用 `ValueMatcher` | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 示例
```yaml
- id: "udp-heartbeat"
name: "UDP 心跳检测"
type: udp
udp:
host: "127.0.0.1"
port: 9000
payload: "PING"
expect:
responded: true
response:
- contains: "PONG"
durationMs:
lte: 100
```

81
docs/user/checkers/ws.md Normal file
View File

@@ -0,0 +1,81 @@
# WS Checker
`type: ws` 用于 WebSocket 服务可达性检查和消息交互验证。
## 配置项
| 字段 | 说明 | 必填 | 默认值 |
| -------------------- | ---------------------------------------------- | ---- | ------- |
| `ws.url` | 目标 URL必须以 `ws://``wss://` 开头 | 是 | 无 |
| `ws.headers` | 握手 HTTP 头 | 否 | `{}` |
| `ws.subprotocols` | 子协议协商 | 否 | `[]` |
| `ws.ignoreSSL` | 忽略 TLS 证书校验 | 否 | `false` |
| `ws.send` | 发送的 text 消息,配置后进入请求-响应模式 | 否 | 无 |
| `ws.receiveTimeout` | 等待响应超时,毫秒 | 否 | `5000` |
| `ws.maxMessageBytes` | 单条消息最大字节数,支持 `KB``MB``GB` 单位 | 否 | `4KB` |
## expect 校验项
| 字段 | 说明 | 必填 | 默认值 |
| ------------------ | --------------------------------------------------------------------- | ---- | ------ |
| `connected` | 期望连接结果,`true` 可达或 `false` 期望不可达 | 否 | `true` |
| `handshakeHeaders` | 握手响应头校验,使用 `KeyedExpectations` | 否 | 无 |
| `message` | 收到的消息内容校验,使用 `ContentExpectations` 数组,需配置 `ws.send` | 否 | 无 |
| `connectTimeMs` | 连接建立耗时校验,使用 `ValueMatcher` | 否 | 无 |
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
## 两种模式
不配置 `ws.send` 时只做可达性检查(连接后立即关闭),配置 `ws.send` 后进入请求-响应模式(发送消息并等待首条响应)。
## 示例
可达性检查:
```yaml
- id: "ws-reachability"
name: "WebSocket 服务可达"
type: ws
ws:
url: "wss://api.example.com/ws"
expect:
durationMs:
lte: 3000
```
带鉴权的请求-响应:
```yaml
- id: "ws-echo"
name: "WebSocket Echo 检查"
type: ws
ws:
url: "wss://echo.example.com/ws"
headers:
Authorization: "Bearer ${TOKEN}"
subprotocols: ["json"]
send: '{"action":"ping"}'
receiveTimeout: 3000
expect:
handshakeHeaders:
Sec-WebSocket-Protocol:
equals: "json"
message:
- json:
path: "$.action"
equals: "pong"
durationMs:
lte: 5000
```
期望不可达:
```yaml
- id: "ws-internal-down"
name: "内部服务已下线"
type: ws
ws:
url: "ws://internal.monitor:9443/ws"
expect:
connected: false
```

135
docs/user/configuration.md Normal file
View File

@@ -0,0 +1,135 @@
# 配置文件
DiAL 通过 YAML 配置文件定义运行参数和拨测目标。完整可运行示例参见 [`../../probes.example.yaml`](../../probes.example.yaml)。配置 JSON Schema 位于 [`../../probe-config.schema.json`](../../probe-config.schema.json)。
## 配置结构
```yaml
# yaml-language-server: $schema=./probe-config.schema.json
server:
listen:
host: "127.0.0.1"
port: "${server_port}"
storage:
dataDir: "/tmp/probes_data"
retention: "${retention}"
logging:
level: "${log_level|info}"
file:
path: "<dataDir>/logs/dial.log"
probes:
execution:
maxConcurrentChecks: "${max_checks}"
variables:
server_port: 3000
retention: "7d"
max_checks: 20
default_interval: "30s"
default_timeout: "10s"
targets:
- id: "baidu-home"
name: "Baidu"
type: http
interval: "${default_interval}"
timeout: "${default_timeout}"
http:
url: "https://www.baidu.com"
expect:
status: [200]
```
## server.listen
| 字段 | 说明 | 必填 | 默认值 |
| ------ | -------- | ---- | ----------- |
| `host` | 监听地址 | 否 | `127.0.0.1` |
| `port` | 监听端口 | 否 | `3000` |
## server.storage
| 字段 | 说明 | 必填 | 默认值 |
| ----------- | ---------------------------------------------------- | ---- | -------- |
| `dataDir` | 数据目录,相对路径基于配置文件所在目录解析 | 否 | `./data` |
| `retention` | 历史数据保留时长,支持 `ms``s``m``h``d` 单位 | 否 | `7d` |
## probes.execution
| 字段 | 说明 | 必填 | 默认值 |
| --------------------- | -------------- | ---- | ------ |
| `maxConcurrentChecks` | 最大并发拨测数 | 否 | `20` |
## server.logging
| 字段 | 说明 | 必填 | 默认值 |
| ---------------------------------------- | ---------------------------------------------- | ---- | ------------------------- |
| `server.logging.level` | 全局日志等级console 和 file 未指定时继承此值 | 否 | `info` |
| `server.logging.console.level` | 控制台日志等级 | 否 | 继承 `level` |
| `server.logging.file.level` | 文件日志等级 | 否 | 继承 `level` |
| `server.logging.file.path` | 日志文件路径,相对路径基于配置文件目录解析 | 否 | `<dataDir>/logs/dial.log` |
| `server.logging.file.rotation.size` | 按大小滚动,支持 `KB``MB``GB` 单位 | 否 | `50MB` |
| `server.logging.file.rotation.frequency` | 按时间滚动:`hourly``daily``weekly` | 否 | `daily` |
| `server.logging.file.rotation.maxFiles` | 保留的归档文件数量,不含活跃日志 | 否 | `14` |
日志等级支持:`trace``debug``info``warn``error``fatal`
控制台始终输出 pretty 格式,文件始终输出 JSONL 格式并支持滚动。`rotation.size``rotation.frequency` 任一条件触发即滚动。
## 内置默认值
| 字段 | 默认值 | 约束 |
| ---------- | ------ | ----------------------- |
| `interval` | `30s` | 最小 `10s` |
| `timeout` | `10s` | 必须小于等于 `interval` |
各 checker 专属默认值见 [Checker 参考](checkers/README.md)。
## variables
`variables` 是顶层动态键值表key 必须符合 `[a-zA-Z_][a-zA-Z0-9_]*`value 仅支持 string、number、boolean。`server``probes``targets` 中的字符串值可引用变量。
| 语法 | 说明 |
| ----------------- | ------------------------------------------ |
| `${key}` | 引用 variables 或环境变量 |
| `${key\|default}` | variables 和环境变量都不存在时使用默认值 |
| `${key\|}` | variables 和环境变量都不存在时使用空字符串 |
| `$${key}` | 转义输出字面量 `${key}` |
解析优先级为 `variables -> process.env -> 默认值`。三者均不存在时配置校验失败。字段值完整等于单个变量引用时会保留 number、boolean、string 类型;部分拼接时统一转为字符串。
变量替换作用于 `server``probes``targets`,不作用于 `variables` 段自身,且不会替换 `targets[].id``targets[].type` 字段;对象 key 不参与替换。
## 配置加载形态
配置加载内部区分三层形态:
| 形态 | 说明 |
| ----------------- | ------------------------------------------------------------------------------------- |
| Authoring Config | 用户 YAML 可书写形态,允许变量引用和 expect 简写 |
| Normalized Config | `normalizeAuthoringConfig()` 完成变量替换、expect 简写展开并移除 `variables` 后的形态 |
| ResolvedConfig | checker `resolve()` 补默认值并解析 duration、size、路径和运行期环境后的形态 |
根目录 `probe-config.schema.json` 面向 Authoring Config因此 VSCode 校验会接受 `server.listen.port: "${server_port|3000}"``http.maxRedirects: "${MAX|5}"``expect.durationMs: 5000` 这类写法。
## targets 通用字段
| 字段 | 说明 | 必填 | 默认值 |
| ------------- | ------------------------------------------------------------------------------------ | ---- | --------- |
| `id` | 目标唯一标识,最长 30 字符,支持字母数字、下划线、连字符,不参与变量替换 | 是 | 无 |
| `name` | 展示名称,最长 30 字符,支持变量替换,可省略或显式 null前端展示时 null 回退到 `id` | 否 | 无 |
| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null允许空字符串 | 否 | 无 |
| `type` | 目标类型:`http``cmd``db``tcp``udp``dns``icmp``llm``ws``cpu` | 是 | 无 |
| `group` | 分组名称 | 否 | `default` |
| `interval` | 拨测间隔,最小 `10s` | 否 | `30s` |
| `timeout` | 超时时间,必须小于等于 `interval` | 否 | `10s` |
## Checker 专属配置
每个 target 必须根据 `type` 配置对应的 checker 专属字段。详情见 [Checker 参考](checkers/README.md)。
## 校验规则
`expect` 字段按 checker 类型不同而变化。通用断言模型见 [校验规则](expectations.md)。

109
docs/user/deployment.md Normal file
View File

@@ -0,0 +1,109 @@
# 部署
本文档说明如何构建、运行、容器化和发布 DiAL。开发环境运行见 [README 快速开始](../../README.md#快速开始)。
## 生产构建和运行
```bash
bun run build
./dist/dial-server ./probes.yaml
```
构建产物为独立可执行文件,只需要一个 YAML 配置文件即可运行。
启动后:
| 地址 | 行为 |
| ------------------------------ | ------------------ |
| `http://127.0.0.1:3000/` | 返回前端 Dashboard |
| `http://127.0.0.1:3000/api/*` | 返回后端 API |
| `http://127.0.0.1:3000/health` | 返回健康检查 |
## Docker 部署
DiAL 提供基于 Alpine 的多阶段镜像。构建阶段使用 Bun 生成 musl 目标单可执行文件,运行阶段只包含 `dial-server`、基础证书、`ping`、Bun musl executable 必需运行库、时区数据和容器运行目录。
```bash
docker build -t dial:alpine .
docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine
```
容器默认读取 `/etc/dial/probes.yaml`,推荐将数据卷挂载到 `/data/dial`
使用自定义配置文件:
```bash
docker run --rm -p 3000:3000 \
-v "$PWD/docker/probes.yaml:/etc/dial/probes.yaml:ro" \
-v dial-data:/data/dial \
dial:alpine
```
容器专用示例配置位于 [`../../docker/probes.yaml`](../../docker/probes.yaml),默认监听 `0.0.0.0:3000`,并将 SQLite 数据和日志写入 `/data/dial`
## ICMP 权限
如需在容器中运行 ICMP checker除镜像内置 `iputils-ping` 外,还需要授予 `NET_RAW` capability
```bash
docker run --rm --cap-add=NET_RAW -p 3000:3000 -v dial-data:/data/dial dial:alpine
```
## CMD checker 额外命令
官方镜像不内置 `bun``node``curl``dig``psql``mysql``redis-cli` 等 CMD checker 可能需要的额外命令。需要这些命令时请使用派生镜像自行安装:
```dockerfile
FROM dial:alpine
USER root
RUN apk add --no-cache curl bind-tools postgresql-client
USER dial
```
## 多架构镜像
```bash
docker buildx build --platform linux/amd64,linux/arm64 -t dial:alpine .
```
Dockerfile 通过 Docker 提供的 `TARGETARCH` 选择 Bun compile target。
| `TARGETARCH` | `BUN_TARGET` |
| ------------ | ---------------------- |
| `amd64` | `bun-linux-x64-musl` |
| `arm64` | `bun-linux-arm64-musl` |
## 跨平台发布包
```bash
bun run release
bun run release --target linux-x64
bun run release --target linux-x64,windows-x64,darwin-arm64
```
支持的目标平台:
| CLI 参数 | Bun CompileTarget |
| ------------------ | ---------------------- |
| `linux-x64` | `bun-linux-x64` |
| `linux-arm64` | `bun-linux-arm64` |
| `linux-x64-musl` | `bun-linux-x64-musl` |
| `linux-arm64-musl` | `bun-linux-arm64-musl` |
| `windows-x64` | `bun-windows-x64` |
| `darwin-x64` | `bun-darwin-x64` |
| `darwin-arm64` | `bun-darwin-arm64` |
产出物结构:
```text
dist/release/
├── binaries/
│ ├── dial-server-0.1.0-linux-x64
│ └── dial-server-0.1.0-windows-x64.exe
└── packages/
├── dial-server_0.1.0_linux_x64.tar.gz
└── dial-server_0.1.0_linux_x64.tar.gz.sha256
```
压缩包内含可执行文件、`probes.example.yaml``LICENSE`,解压后可直接使用。

161
docs/user/expectations.md Normal file
View File

@@ -0,0 +1,161 @@
# 校验规则
本文档说明 `expect` 规则、状态判定、failure、observation 和各 checker 的快速失败顺序。
适用场景:编写 `expect`、理解 UP/DOWN、排查 mismatch/error、查看返回结果中的 `failure``observation`
`expect` 描述拨测结果必须满足的条件。不同 checker 暴露不同字段,但共享三类基础断言模型:`ValueMatcher``ContentExpectations``KeyedExpectations`
## 状态判定
DiAL 使用单层状态模型。
| 状态 | 含义 |
| ------ | ---------------------------------------- |
| `UP` | 拨测结果符合 `expect` 规则 |
| `DOWN` | 拨测结果不符合 `expect` 规则,或执行失败 |
执行失败(网络错误、超时、进程崩溃)和 expect 不匹配都统一为 `DOWN`,通过 `failure.kind` 区分原因。
| `failure.kind` | 含义 |
| -------------- | ---------------------------------------- |
| `error` | 网络、超时、进程、协议解析或内部执行错误 |
| `mismatch` | 拨测完成,但结果不满足 expect |
## API 结果字段
API 返回的检查结果包含 `detail``observation`
| 字段 | 说明 |
| ------------- | ------------------------------------------------------------ |
| `detail` | 后端按 checker 类型从结构化 observation 动态生成的人可读摘要 |
| `observation` | 保存该次检查的结构化观测数据 |
| `failure` | 保存首个错误或不匹配原因 |
| `matched` | 是否符合 expect |
| `durationMs` | 本次检查耗时 |
| `timestamp` | 本次检查时间 |
`detail` 不写入 SQLite。存储层仅持久化 `observation` JSON、`failure` JSON、匹配状态、耗时和时间戳。
## observation 示例
不同 checker 的 observation 字段不同,常见信息包括:
| Checker | observation 内容示例 |
| ------- | ------------------------------------------------------------------ |
| HTTP | 状态码、响应头、按需读取的 body 预览 |
| Cmd | exit code、stdout/stderr 预览 |
| TCP | 连接结果、banner 摘要 |
| UDP | 响应内容、来源地址、响应大小 |
| ICMP | 存活结果、丢包率、平均延迟、最大延迟 |
| DNS | RCODE、记录值、TTL、flags、CNAME 链 |
| LLM | HTTP 状态、模型输出、finish reason、token usage、流式首 token 时间 |
| WS | 连接结果、连接耗时、握手头、消息内容、消息大小 |
Dashboard 基于存储的检查结果计算实时状态、可用率、耗时趋势、P95、状态条和故障段等指标。指标语义由后端应用层实现SQLite 主要负责存储、筛选、排序、分页和基础聚合。
## ContentExpectations
`body``stdout``stderr``banner``response``output``result``message` 等返回内容字段均使用数组。
| 规则 | 说明 |
| ---------- | ------------------------------------------------------ |
| `contains` | 内容包含指定文本 |
| `regex` | 正则匹配,启动期会拒绝存在 ReDoS 风险的模式 |
| `json` | JSONPath 提取值比较,`path` 必填 |
| `css` | CSS 选择器提取 HTML 元素,`selector` 必填,`attr` 可选 |
| `xpath` | XPath 提取 XML/HTML 节点,`path` 必填 |
示例:
```yaml
expect:
body:
- contains: "ok"
- json:
path: "$.status"
equals: "ready"
```
ContentExpectations 数组按顺序快速失败。数组项可以是直接 matcher也可以是 `json``css``xpath` 提取器规则。一条规则不能混用直接 matcher 和 extractor多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`
## ValueMatcher
`ValueMatcher` 用于单个标量值、数字指标和字符串元数据。
| 字段 | 说明 |
| ---------- | ------------------------------- |
| `equals` | 精确匹配,支持 JSON 深度相等 |
| `contains` | 字符串包含 |
| `regex` | 正则匹配,固定使用无 flags 正则 |
| `empty` | 判断是否为空 |
| `exists` | 判断是否存在 |
| `gte` | 大于等于 |
| `lte` | 小于等于 |
| `gt` | 大于 |
| `lt` | 小于 |
一个 matcher 对象内多个字段为 AND 语义。`exists: false` 不能和其他 matcher 组合。
ValueMatcher expect 字段可直接写 string、number、boolean 或 null等价于 `{ equals: value }`。数组和对象必须显式写成 `{ equals: ... }`
```yaml
expect:
durationMs:
lte: 5000
finishReason: "stop"
```
## KeyedExpectations
`headers`、DB `rows[]` 中的列值等动态键值对象使用 `KeyedExpectations`。每个键的值支持 `ValueMatcher` 的全部字段,字面量值自动等价于 `{ equals: value }`
```yaml
expect:
headers:
Content-Type:
contains: "application/json"
```
## 大小和时长格式
| 类型 | 示例 |
| ---- | -------------------------------- |
| 大小 | `4KB``10MB``1GB`、直接数字 |
| 时长 | `500ms``30s``5m``2h``7d` |
`maxBodyBytes``maxOutputBytes``maxResponseBytes``maxBannerBytes` 等大小字段支持 `KB``MB``GB` 单位。
## 快速失败顺序
| Checker | 顺序 |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP | `status -> headers -> body -> durationMs` |
| Cmd | `exitCode -> durationMs -> stdout -> stderr` |
| DB | `durationMs -> rowCount -> rows -> result` |
| TCP | `connected -> banner -> durationMs` |
| UDP | `responded -> responseSize -> response -> sourceHost -> sourcePort -> durationMs` |
| ICMP | `alive -> packetLossPercent -> avgLatencyMs -> maxLatencyMs -> durationMs` |
| DNS system | `values -> valueCount -> durationMs` |
| DNS server | `responded -> rcode -> values -> valueCount -> answerCount -> ttlMin -> ttlMax -> authoritative -> recursionAvailable -> truncated -> authenticatedData -> result -> durationMs` |
| LLM http | `status -> headers -> output -> finishReason -> rawFinishReason -> usage -> durationMs` |
| LLM stream | `status -> headers -> stream.completed -> stream.firstTokenMs -> output -> finishReason -> rawFinishReason -> usage -> durationMs` |
| WS | `connected -> handshakeHeaders -> message -> connectTimeMs -> durationMs` |
## JSON Schema
仓库根目录导出 `probe-config.schema.json`。在 YAML 文件顶部添加以下注释可在编辑器中获得提示和校验:
```yaml
# yaml-language-server: $schema=./probe-config.schema.json
```
## 已移除字段
旧字段 `maxDurationMs``maxPacketLoss``maxAvgLatencyMs``maxMaxLatencyMs` 和旧正则字段 `match` 已移除,请分别改用 `durationMs`、ICMP matcher 字段和 `regex`
非法配置会阻止启动并输出错误信息。除动态键值表(`headers``env``variables`)外,未知字段会导致启动失败,请使用 YAML 注释表达说明。
## 更新触发条件
修改 expect 断言模型、状态判定、failure 字段、observation 字段、快速失败顺序或已移除字段说明时,必须更新本文档。

View File

@@ -0,0 +1,73 @@
# 故障排查
本文档记录常见运行问题和排查入口。
## 配置校验失败
DiAL 启动时会校验 YAML 配置。除动态键值表(`headers``env``variables`)外,未知字段会导致启动失败。
排查顺序:
1. 在 YAML 顶部添加 `# yaml-language-server: $schema=./probe-config.schema.json`
2. 对照 [配置文件](configuration.md) 检查顶层结构和通用字段。
3. 对照 [Checker 参考](checkers/README.md) 检查 checker 专属字段。
4. 对照 [校验规则](expectations.md) 检查 expect 写法。
## 变量无法解析
变量解析优先级为 `variables -> process.env -> 默认值`。如果三者均不存在,配置校验会失败。
常见修复:
| 问题 | 修复 |
| -------------- | ----------------------------------- |
| 环境变量未设置 | 设置环境变量或在 `variables` 中声明 |
| 希望允许空值 | 使用 `${key\|}` |
| 希望提供默认值 | 使用 `${key\|default}` |
| 希望输出字面量 | 使用 `$${key}` |
## ICMP checker 无法运行
ICMP checker 依赖系统 `ping` 命令。
| 环境 | 处理 |
| ------------------- | -------------------------------------- |
| Alpine 或精简镜像 | 安装 `iputils-ping` |
| Docker 容器 | 运行容器时增加 `--cap-add=NET_RAW` |
| Windows/macOS/Linux | 确认系统 `ping` 可执行且输出格式受支持 |
Docker 示例:
```bash
docker run --rm --cap-add=NET_RAW -p 3000:3000 -v dial-data:/data/dial dial:alpine
```
## CMD checker 找不到命令
官方 Docker 镜像不内置 `bun``node``curl``dig``psql``mysql``redis-cli` 等额外命令。需要这些命令时请使用派生镜像安装。
```dockerfile
FROM dial:alpine
USER root
RUN apk add --no-cache curl bind-tools postgresql-client
USER dial
```
## Docker 数据或日志丢失
推荐将数据卷挂载到 `/data/dial`,并在配置中使用该目录作为 storage dataDir。
```bash
docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine
```
容器示例配置位于 [`../../docker/probes.yaml`](../../docker/probes.yaml)。
## HTTP 或 LLM 证书问题
HTTP 和 LLM checker 支持 `ignoreSSL`。该选项适合内网、自签名证书或测试环境;生产环境应优先修复证书链。
## 正则规则被拒绝
`regex` 启动期会执行 ReDoS 风险检测。被拒绝时应改写为更明确、回溯风险更低的表达式。

View File

@@ -1,10 +1,14 @@
import js from "@eslint/js";
import importPlugin from "eslint-plugin-import";
import perfectionist from "eslint-plugin-perfectionist";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
const noDirectConsoleMessage =
"后端运行时代码禁止直接使用 console.*;请通过注入的 Logger 实例输出日志,配置加载失败前使用 createConsoleFallback()。";
export default tseslint.config(
{
ignores: [
@@ -43,6 +47,8 @@ export default tseslint.config(
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
"@typescript-eslint/no-empty-function": ["error", { allow: ["private-constructors", "protected-constructors"] }],
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/only-throw-error": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
@@ -50,6 +56,19 @@ export default tseslint.config(
"no-undef": "off",
},
},
{
files: ["src/server/**/*.ts"],
ignores: ["src/server/logger.ts"],
rules: {
"no-restricted-syntax": [
"error",
{
message: noDirectConsoleMessage,
selector: "MemberExpression[object.name='console']",
},
],
},
},
{
files: ["eslint.config.js"],
rules: {
@@ -88,4 +107,5 @@ export default tseslint.config(
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},
eslintPluginPrettierRecommended,
);

View File

@@ -6,5 +6,16 @@
"type": "local",
"command": ["bunx", "tdesign-mcp-server@latest"]
}
},
"permission": {
"bash": {
"npm *": "deny",
"npx *": "deny",
"pnpm *": "deny",
"pnpx *": "deny"
},
"external_directory": {
"/tmp/**": "allow"
}
}
}

View File

@@ -1,31 +1,39 @@
schema: spec-driven
schema: fast-drive
context: |
- 使用中文(注释、文档、交流),面向中文开发者
- openspec文档的关键字按openspec规范使用不要翻译为中文
- **优先阅读README.md和DEVELOPMENT.md**获取项目概览与开发规范所有代码风格、命名、注解、依赖、API等规范以DEVELOPMENT.md为准
- 涉及模块结构、API、实体等变更时同步更新README.md
- 本项目openspec使用fast-drive自定义schema变更文档只包含design.md和tasks.md无proposal.md和specs
- **优先阅读docs/README.md**判断文档归属和本次任务需要读取的专题文档
- README.md用于项目概览、快速开始和顶层文档引导docs/user/README.md用于用户使用入口docs/development/README.md用于开发入口、全局规则和质量门禁
- 所有代码风格、命名、注解、依赖、API等开发规范以docs/development/README.md和docs/development/下对应专题文档为准
- 新增或修改checker时必须阅读docs/development/checker.md、docs/user/checkers/README.md和相近checker用户文档
- 每次代码变更都必须执行文档影响分析判断是否影响用户可见行为、配置格式、checker行为、expect规则、API、部署方式、开发流程、架构边界、测试规范或构建发布流程
- 若影响用户使用方式、配置格式、checker行为、expect规则、部署方式或运行行为必须同步更新docs/user/下对应文档README.md仅在项目定位、快速开始、核心能力列表或文档导航变化时更新
- 若影响开发流程、架构边界、质量门禁、测试规范、构建发布流程或checker开发机制必须同步更新docs/development/README.md或docs/development/下对应专题文档
- 若影响文档同步规则或文档归属矩阵必须同步更新docs/README.md和openspec/config.yaml
- 若无需更新文档,必须在收尾说明中说明原因
- 新增代码优先复用已有组件、工具、依赖库,不引入新依赖
- 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试
- 这是基于bun实现的前端后一体化项目使用bun作为唯一包管理器严禁使用pnpm、npm使用bunx运行工具严禁使用npx、pnpx
- src/server目录下是基于bun实现的后端代码
- src/web目录下是基于vite、react、TDesign实现的前端代码
- 后端库使用优先级Bun 内置 API > es-toolkit > 主流三方库 > 项目公共工具 > 自行实现
- 前端样式开发优先级TDesign组件 > 组件props > TDesign CSS tokens(--td-*) > styles.css CSS类 > 自行开发组件
- 后端库使用优先级Bun 内置 API > es-toolkit > 标准 Web API > 主流三方库 > 项目公共工具 > 自行实现
- src/web目录下是基于Bun HTML import、React、TDesign实现的前端代码
- 前端样式开发优先级TDesign组件 > 组件props > TDesign CSS tokens(--td-*) > styles.css CSS类 > 自行开发组件
- 前端严禁组件内联style属性、CSS覆盖TD内部类名、使用!important、硬编码色值
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
- 禁止创建git操作task
- 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行
- 使用subagents处理计算密集或多步骤的并行任务如代码实现、测试执行文件读取直接使用Read工具并行调用禁止用subagent转发文件内容
- 优先使用提问工具对用户进行提问
- (当前项目未上线,不需要考虑向前兼容)
rules:
proposal:
- 仔细审查每一个过往spec判断是否存在Modified Capabilities
design:
- 先前的讨论技术方案要尽可能体现在设计文档中,便于指导实现阶段不偏离已定的技术路线
tasks:
- 一行一个任务,严禁任务内容跨行
- 如果是代码存在更新必须
- 执行完整的测试、代码检查、格式检查等质量保障手段
- 更新 README.md 和/或 DEVELOPMENT.md
- 执行文档影响分析,并按影响范围更新对应文档;若无需更新文档,必须在任务或收尾说明中明确写出原因
- 新增或修改checker时必须更新docs/user/checkers/下对应用户文档并在checker开发机制变化时更新docs/development/checker.md
- 新增或修改配置字段时必须更新probe-config.schema.json、probes.example.yaml、docs/user/configuration.md或对应checker文档

View File

@@ -0,0 +1,181 @@
name: fast-drive
version: 1
description: Fast OpenSpec workflow - design -> tasks -> apply
artifacts:
- id: design
generates: design.md
description: Self-contained solution brief and execution plan
template: design.md
instruction: |
Create design.md as the self-contained source of truth for what will
change, why it is changing, and how the work will be executed.
This workflow does not use proposal or specs artifacts. design.md MUST
preserve the important outcomes from prior exploration and user
discussion so a later apply phase can proceed correctly even after
context compression or in a new session.
Write for someone who cannot see the earlier conversation. Keep simple
changes concise, but include enough detail to make execution
unambiguous. Add more detail when any apply:
- Cross-cutting change across multiple systems, teams, workstreams, or
artifacts
- New dependency, integration, vendor, tool, policy, or external input
- Significant information model, process model, data model, or ownership
changes
- Security, privacy, compliance, performance, operational, or migration
complexity
- Ambiguity that benefits from decisions before execution
- Prior discussion settled non-obvious requirements, constraints, or
rejected alternatives
Required sections:
- **Context**: Problem, current state, relevant references, and the user
request that triggered this change
- **Discussion Notes**: Key points from exploration or prior discussion
that must not be lost. Include agreed conclusions, user preferences,
constraints, and important rejected ideas.
- **Requirements**: Expected outcomes, behavior/process/interface/content
changes, continuity expectations, and acceptance criteria.
- **Goals / Non-Goals**: What this change will achieve and what is
explicitly out of scope.
- **Execution Guardrails**: Must-follow constraints, forbidden approaches,
preserved behavior/processes, dependency limits, and project- or
workflow-specific boundaries.
- **Affected Areas**: Concrete artifacts, references, stakeholders,
systems, workstreams, documents, configurations, assets, or handoffs that
are relevant to the change.
- **Decisions**: Key choices with rationale (why X over Y?). For each
important decision, include alternatives considered and why they were not
chosen.
- **Execution Plan**: Main workstreams or artifacts to change, integration
or handoff points, sequencing, and any rollout notes.
- **Verification Plan**: Validation checks, reviews, approvals,
acceptance checks, documentation checks, communication checks, and manual
checks needed to prove the change is complete.
- **Risks / Trade-offs**: Known limitations and things that could go
wrong.
Format: [Risk] -> Mitigation
- **Open Questions**: Outstanding decisions, assumptions, or unknowns to
resolve before execution. Separate blocking questions that must pause
apply from non-blocking follow-ups. Use "None" if there are no open
questions.
Optional sections when relevant:
- **Migration / Rollout Plan**: Rollout steps, communication, ownership,
rollback, or continuity strategy.
Focus on preserving requirements, rationale, constraints, and approach.
Avoid line-by-line or step-by-step details unless a detail is a deliberate
decision from the discussion.
Prefer durable summaries over chat transcripts. Use concrete artifact
names, data/information shapes, examples, stakeholders, ownership, and
edge cases when they affect execution.
Do not use task checkboxes in design.md; checkboxes belong only in
tasks.md.
Final design.md must not contain unresolved template comments, empty
table rows, or placeholder text.
If information is missing, state assumptions and open questions instead
of inventing hidden requirements. Do not rely on unstated chat context.
requires: []
- id: tasks
generates: tasks.md
description: Trackable execution checklist derived from design.md
template: tasks.md
instruction: |
Create tasks.md by breaking design.md into executable work.
**IMPORTANT: Follow the template below exactly.** The apply phase parses
checkbox format to track progress. Tasks not using `- [ ]` will not be
tracked.
Guidelines:
- Derive tasks from design.md. Do not depend on proposal.md or specs
artifacts; any relevant prior discussion must already be captured in
design.md.
- Group related tasks under `##` numbered headings
- Each task MUST be a single-line checkbox: `- [ ] X.Y Task description`
- Tasks should be small enough to complete in one session
- Order tasks by dependency (what must be done first?)
- Start with context review tasks when execution depends on guardrails,
affected areas, or open questions
- Include validation tasks for checks, reviews, approvals, acceptance,
documentation, communication, and manual checks when required
- Do not include repository, version-control, or release operation tasks
unless they are explicitly part of the change scope
- Final tasks.md must not contain unresolved template comments, empty
table rows, or placeholder task text
Example:
```
## 1. Context Review
- [ ] 1.1 Read design.md and identify scope, requirements, decisions, guardrails, and open questions
- [ ] 1.2 Review relevant artifacts and references listed in Affected Areas
## 2. Execution
- [ ] 2.1 Execute first concrete work item from design.md
- [ ] 2.2 Execute next concrete work item from design.md
## 3. Validation
- [ ] 3.1 Run required validation from Verification Plan
- [ ] 3.2 Perform quality checks required by the project or workflow
- [ ] 3.3 Perform required manual review or acceptance checks from Verification Plan
## 4. Documentation / Communication
- [ ] 4.1 Update relevant documentation, runbooks, communication materials, or project references if behavior, process, interface, configuration, or usage changed
```
Reference design.md for scope, requirements, decisions, execution
direction, and verification expectations.
Each task should be verifiable: it must be clear when the task is done.
requires:
- design
apply:
requires:
- design
- tasks
tracks: tasks.md
instruction: |
Read design.md first, then tasks.md.
Also follow workflow context/configuration, such as openspec/config.yaml when available, and any relevant project or workflow documentation referenced by design.md.
Treat design.md as the source of truth for scope, requirements, decisions, guardrails, execution direction, and verification expectations.
Work through pending tasks in dependency order and mark complete as you go.
Mark a task complete only after its execution and required verification are done.
Pause if tasks conflict with design.md, if design.md has blocking open questions, or if clarification is needed.

View File

@@ -0,0 +1,77 @@
## Context
<!-- Problem, current state, relevant references, and triggering user request -->
## Discussion Notes
<!-- Key conclusions from exploration or prior discussion that apply must preserve -->
- Agreed conclusions:
- User preferences:
- Constraints:
- Rejected ideas:
## Requirements
<!-- Expected outcomes, behavior/process/interface/content changes, continuity expectations, and acceptance criteria -->
| Requirement | Acceptance Criteria |
| ----------- | ------------------- |
| | |
## Goals / Non-Goals
**Goals:**
<!-- What this design aims to achieve -->
**Non-Goals:**
<!-- What is explicitly out of scope -->
## Execution Guardrails
<!-- Must-follow constraints, forbidden approaches, preserved behavior/processes, dependency limits, and project- or workflow-specific boundaries -->
- Dependencies:
- Constraints:
- Quality Bar:
- Stakeholders:
- Documentation / Communication:
- Compatibility / Continuity:
## Affected Areas
<!-- Concrete artifacts, references, stakeholders, systems, workstreams, documents, configurations, assets, or handoffs relevant to this change -->
| Area | Artifacts / References | Expected Change | Notes |
| ---- | ---------------------- | --------------- | ----- |
| <!-- Area --> | <!-- Artifacts / References --> | <!-- Expected Change --> | <!-- Notes --> |
## Decisions
<!-- Key decisions, rationale, and alternatives considered -->
| Decision | Rationale | Alternatives Rejected |
| -------- | --------- | --------------------- |
| | | |
## Execution Plan
<!-- Main workstreams or artifacts to change, integration or handoff points, sequencing, and rollout notes -->
## Verification Plan
<!-- Validation checks, reviews, approvals, acceptance checks, documentation checks, communication checks, and manual checks needed -->
| Requirement / Risk | Verification |
| ------------------ | ------------ |
| | |
## Risks / Trade-offs
<!-- Format: [Risk] -> Mitigation -->
## Open Questions
| Status | Question | Decision Needed |
| ------ | -------- | --------------- |
| None | No open questions. | None |

View File

@@ -0,0 +1,19 @@
## 1. Context Review
- [ ] 1.1 Read design.md and identify scope, requirements, decisions, guardrails, and open questions
- [ ] 1.2 Review relevant artifacts and references listed in Affected Areas
## 2. Execution
- [ ] 2.1 Execute first concrete work item from design.md
- [ ] 2.2 Execute next concrete work item from design.md
## 3. Validation
- [ ] 3.1 Run required validation from Verification Plan
- [ ] 3.2 Perform quality checks required by the project or workflow
- [ ] 3.3 Perform required manual review or acceptance checks from Verification Plan
## 4. Documentation / Communication
- [ ] 4.1 Update relevant documentation, runbooks, communication materials, or project references if behavior, process, interface, configuration, or usage changed

View File

@@ -1,78 +0,0 @@
## Purpose
定义后端 API 路由的组织规范:按端点拆分为独立 handler、共享响应工具集中管理、参数校验逻辑抽取为中间件、静态资源服务独立维护。
## Requirements
### Requirement: 路由按职责拆分
系统 SHALL 将 HTTP 路由处理逻辑按 API 端点拆分为独立模块,每个模块导出一个 handler 函数供 app.ts 统一注册。
#### Scenario: health 端点独立路由
- **WHEN** 客户端请求 `GET /health`
- **THEN** `routes/health.ts` 导出的 handler 负责处理,返回 HealthResponse JSON
#### Scenario: summary 端点独立路由
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** `routes/summary.ts` 导出的 handler 负责处理,委托 store 查询并返回 SummaryResponse JSON
#### Scenario: targets 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** `routes/targets.ts` 导出的 handler 负责处理,委托 store 查询并返回 TargetStatus[] JSON
#### Scenario: history 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/history?from=ISO&to=ISO`
- **THEN** `routes/history.ts` 导出的 handler 负责处理包含参数校验、store 查询和 HistoryResponse 返回
#### Scenario: trend 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/trend?from=ISO&to=ISO`
- **THEN** `routes/trend.ts` 导出的 handler 负责处理包含参数校验、store 查询和 TrendPoint[] 返回
### Requirement: 共享辅助函数集中管理
系统 SHALL 将跨路由共享的响应格式化函数抽取到 helpers.ts 模块,单一职责、集中管理。
#### Scenario: createApiError 集中定义
- **WHEN** 任意路由需要返回 API 错误响应
- **THEN** 从 `helpers.ts` 导入 `createApiError` 函数,提供错误信息和状态码
#### Scenario: jsonResponse 集中定义
- **WHEN** 任意路由需要返回 JSON 响应
- **THEN** 从 `helpers.ts` 导入 `jsonResponse` 函数,处理 HEAD 方法、Content-Type 和安全头
#### Scenario: mapCheckResult 集中定义
- **WHEN** 需要将 StoredCheckResult 映射为 API CheckResult
- **THEN** 从 `helpers.ts` 导入 `mapCheckResult` 函数,处理 failure JSON 解析和格式转换
### Requirement: 参数校验逻辑抽取为中间件
系统 SHALL 将重复的参数校验逻辑target ID 解析、时间范围校验、分页参数校验、方法检查)抽取到 middleware.ts 模块。
#### Scenario: 方法检查中间件
- **WHEN** 请求方法不是 GET 或 HEAD
- **THEN** `guardGetHead(request, mode)` SHALL 返回 405 Response否则返回 null 表示放行
#### Scenario: Target ID 校验
- **WHEN** URL 中的 id 参数不是正整数
- **THEN** `validateTargetId(idStr)` SHALL 返回 400 ApiError
#### Scenario: 时间范围参数校验
- **WHEN** from 或 to 参数缺失或格式无效
- **THEN** `validateTimeRange(from, to)` SHALL 返回 400 ApiError
#### Scenario: 分页参数校验
- **WHEN** page 或 pageSize 参数不是正整数
- **THEN** `validatePagination(page, pageSize)` SHALL 返回 400 ApiError
### Requirement: 静态资源服务独立管理
系统 SHALL 将静态资源服务、SPA fallback 和 Content-Type 映射逻辑抽取到 static.ts 模块。
#### Scenario: 根路径返回 index.html
- **WHEN** 客户端请求 `/`
- **THEN** `static.ts` 的 handler 返回 index.html设置正确的 Content-Type 和 Cache-Control
#### Scenario: 资源文件返回正确 Content-Type
- **WHEN** 客户端请求 `/assets/main.js`
- **THEN** `static.ts` 的 handler 根据文件扩展名返回正确的 Content-Type`.js``text/javascript`
#### Scenario: SPA fallback
- **WHEN** 客户端请求非 API、非资源的路径`/dashboard`
- **THEN** `static.ts` 的 handler 返回 index.html 实现 SPA 的客户端路由

View File

@@ -1,102 +0,0 @@
## Purpose
定义后端代码中 es-toolkit 和 Bun 内置 API 的使用规范:类型判断、空值检测、深度比较、错误判断、并发控制、集合分组和 Web API 标准方法,替代手写实现落实库使用优先级规则。
## Requirements
### Requirement: 使用 es-toolkit 进行类型判断
系统 SHALL 使用 es-toolkit 的 `isPlainObject` 替代手写的对象类型判断函数,用于 expect 校验中区分纯值(原始值)和操作符对象。
#### Scenario: 识别纯对象为操作符
- **WHEN** body 校验规则中 expected 配置为 `{ equals: "value" }`(纯对象操作符)
- **THEN** `isPlainObject(expected)` SHALL 返回 true系统按操作符语义处理
#### Scenario: 排除非纯对象作为操作符
- **WHEN** body 校验规则中 expected 为原始值如 `"value"` 或数字 `200`
- **THEN** `isPlainObject(expected)` SHALL 返回 false系统按 equals 默认操作符处理
### Requirement: 使用 es-toolkit 进行空值检测
系统 SHALL 使用 es-toolkit 的 `isNil` 替代手写的 `actual === null || actual === undefined` 检测,用于 expect 中 `empty` 操作符的空值判断。
#### Scenario: null 值判定为空
- **WHEN** 校验值为 null
- **THEN** `isNil(null)` SHALL 返回 true
#### Scenario: undefined 值判定为空
- **WHEN** 校验值为 undefined
- **THEN** `isNil(undefined)` SHALL 返回 true
#### Scenario: 非空值判定为非空
- **WHEN** 校验值为 0、"false"、空数组 `[]` 等非 nil 值
- **THEN** `isNil(value)` SHALL 返回 false
### Requirement: 使用 es-toolkit 进行空对象检测
系统 SHALL 使用 es-toolkit 的 `isEmptyObject` 替代手写的 `typeof actual === "object" && Object.keys(actual).length === 0` 检测,用于 expect 中 `empty` 操作符的空对象判断。
#### Scenario: 空对象判定为空
- **WHEN** 校验值为 `{}`
- **THEN** `isEmptyObject({})` SHALL 返回 true
#### Scenario: 非空对象判定为非空
- **WHEN** 校验值为 `{ key: "val" }`
- **THEN** `isEmptyObject({ key: "val" })` SHALL 返回 false
#### Scenario: null 不是空对象
- **WHEN** 校验值为 null
- **THEN** `isEmptyObject(null)` SHALL 返回 false空值由 isNil 前置处理)
### Requirement: 使用 es-toolkit 进行深度相等比较
系统 SHALL 使用 es-toolkit 的 `isEqual` 替代 `!==` 浅比较,用于 expect 中 `equals` 操作符的值比较,支持对象和数组的深度比较。
#### Scenario: 原始值浅比较
- **WHEN** expected 和 actual 均为原始值字符串、数字、布尔值、null
- **THEN** `isEqual(actual, expected)` 的行为 SHALL 与 `actual === expected` 一致
#### Scenario: 对象深度比较
- **WHEN** expected 和 actual 均为对象(如从 JSONPath 提取的结构化数据)
- **THEN** `isEqual(actual, expected)` SHALL 递归比较所有属性值,而非引用比较
### Requirement: 使用 es-toolkit 进行错误类型判断
系统 SHALL 使用 es-toolkit 的 `isError` 替代 `error instanceof Error`,用于 HTTP runner 和 command runner 中的错误类型判断。
#### Scenario: Error 实例识别
- **WHEN** 错误对象为 `new Error("msg")`
- **THEN** `isError(error)` SHALL 返回 true
#### Scenario: Error 子类识别
- **WHEN** 错误对象为继承 Error 的自定义类型
- **THEN** `isError(error)` SHALL 返回 true
#### Scenario: 非 Error 对象识别
- **WHEN** 错误对象为字符串或普通对象
- **THEN** `isError(error)` SHALL 返回 false
### Requirement: 使用 es-toolkit Semaphore 实现并发控制
系统 SHALL 使用 es-toolkit 的 `Semaphore` 类替代手写的信号量实现(计数器 + Promise 队列),用于 ProbeEngine 中的组内并发拨测控制。
#### Scenario: 获取并发槽位
- **WHEN** 当前并发数未达上限
- **THEN** `semaphore.acquire()` SHALL 立即返回,不阻塞
#### Scenario: 等待并发槽位
- **WHEN** 当前并发数已达上限 maxConcurrentChecks
- **THEN** `semaphore.acquire()` SHALL 阻塞等待,直到其他任务调用 `semaphore.release()`
#### Scenario: 释放并发槽位
- **WHEN** 调用 `semaphore.release()`
- **THEN** 系统 SHALL 唤醒一个等待中的 acquire() 调用
### Requirement: 使用 es-toolkit groupBy 实现 target 分组
系统 SHALL 使用 es-toolkit 的 `groupBy` 函数替代手写的 Map 循环分组,用于 ProbeEngine 中按 interval 分组拨测目标。
#### Scenario: 按 interval 分组
- **WHEN** 输入包含不同 intervalMs 值的多个 target
- **THEN** `groupBy(targets, t => t.intervalMs)` SHALL 返回 key 为 intervalMs 值的分组对象,值为对应 target 数组
### Requirement: 使用 Bun 内置 API 进行 Headers 转换
系统 SHALL 使用 `Object.fromEntries(headers)` 标准 Web API 替代手写的 `headersToRecord` 函数,用于将 Fetch API 的 Headers 对象转换为键值对。
#### Scenario: 转换响应头
- **WHEN** HTTP runner 获取到 response headers
- **THEN** `Object.fromEntries(response.headers)` SHALL 返回以 header 名称为 key、header 值为 value 的对象

View File

@@ -1,73 +0,0 @@
## Purpose
定义 ProbeStore 的批量查询方法getLatestChecksMap、getAllTargetStats以及 getSummary 和 createTargetsResponse 的 N+1 查询优化规范。同时约定单次查询操作使用 db.query() 利用内置缓存。
## Requirements
### Requirement: 批量查询最新检查结果
系统 SHALL 提供 `getLatestChecksMap` 方法,通过单次 SQL 查询获取所有 target 的最新一次 check 结果,返回 Map 结构供调用方按 target_id 索引。
#### Scenario: 获取所有目标的最新检查
- **WHEN** 调用 `getLatestChecksMap()`
- **THEN** 系统 SHALL 执行子查询找到每个 target_id 的 MAX(timestamp),再 JOIN 回 check_results 获取完整行,返回 `Map<number, StoredCheckResult | null>`
#### Scenario: 目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
### Requirement: 批量查询目标统计
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计totalChecks 和 availability
#### Scenario: 获取所有目标的聚合统计
- **WHEN** 调用 `getAllTargetStats()`
- **THEN** 系统 SHALL 执行 `SELECT target_id, COUNT(*), SUM(CASE WHEN matched=1 THEN 1 ELSE 0 END) FROM check_results GROUP BY target_id`,在内存中计算 availability 并返回 `Map<number, { totalChecks, availability }>`
#### Scenario: 目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
#### Scenario: availability 精度
- **WHEN** 计算 availabilityupCount / totalChecks * 100
- **THEN** 结果 SHALL 四舍五入保留两位小数
### Requirement: summary 查询使用批量方法
`getSummary` 方法 SHALL 使用 `getLatestChecksMap` 一次性获取所有 target 的最新检查结果,而非对每个 target 逐条查询。
#### Scenario: 统计总览使用批量查询
- **WHEN** 调用 `store.getSummary()`
- **THEN** 系统 SHALL 调用 `getLatestChecksMap()` 一次获取所有最新结果,在内存中遍历统计 up/down 数量,而非循环 N 次调用 `getLatestCheck()`
### Requirement: targets 列表使用批量方法
`createTargetsResponse`app.ts 中生成 TargetStatus[] 的逻辑SHALL 使用 `getLatestChecksMap``getAllTargetStats` 替代逐目标查询 latest checkout、stats 和 samples。
#### Scenario: 目标列表使用批量查询
- **WHEN** 处理 `GET /api/targets` 请求
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()``getAllTargetStats()` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
### Requirement: prepared statement 使用 query() 缓存
ProbeStore 中不涉及事务内复用的单次读/写操作 SHALL 使用 `this.db.query()` 而非 `this.db.prepare()`,利用 bun:sqlite 内置的 statement 缓存机制。
#### Scenario: insertCheckResult 使用 query
- **WHEN** 写入一条检查结果
- **THEN** `insertCheckResult` SHALL 使用 `this.db.query("INSERT INTO ...").run(...)` 而非 `this.db.prepare("INSERT INTO ...").run(...)`
#### Scenario: getHistory 查询使用 query
- **WHEN** 查询历史记录(包括 COUNT 和分页查询)
- **THEN** `getHistory` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getTargetStats 查询使用 query
- **WHEN** 查询单目标统计
- **THEN** `getTargetStats` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getTrend 查询使用 query
- **WHEN** 查询趋势数据
- **THEN** `getTrend` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getRecentSamples 查询使用 query
- **WHEN** 查询采样数据
- **THEN** `getRecentSamples` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: syncTargets 事务保持 prepare例外
- **WHEN** 同步 targets 配置(事务内多次复用 insertStmt/updateStmt/deleteStmt
- **THEN** `syncTargets` 方法 SHALL 保持使用 `this.db.prepare()`,因需要在事务闭包内持有引用

View File

@@ -1,113 +0,0 @@
## Purpose
定义 Checker 接口规范、注册机制、CheckerContext 上下文注入,以及共享 expect 断言函数的职责边界。此 capability 是 checker 系统的架构基础,不定义任何具体 checker 类型的业务行为。
## Requirements
### Requirement: Checker 接口定义
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义 `Checker` 接口,包含 `type``resolve``execute``serialize` 四个成员。`CheckerContext` SHALL 包含引擎注入的 `AbortSignal`
#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`resolve(target, context)`(解析配置并校验)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON
#### Scenario: CheckerContext 注入 signal
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
### Requirement: CheckerRegistry 注册中心
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)``get(type)``supportedTypes`。重复注册同一 type SHALL 抛出错误。
#### Scenario: 注册并获取 Checker
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例
#### Scenario: 获取未注册的 type
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
- **THEN** 系统 SHALL 抛出错误,提示不支持的 probe type
#### Scenario: 重复注册
- **WHEN** 同一 type 值被重复 `register()`
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
#### Scenario: 查询支持的 type 列表
- **WHEN** 注册了 "http" 和 "command" 两个 checker 后查询 `registry.supportedTypes`
- **THEN** 返回的数组 SHALL 包含 `["http", "command"]`(按注册顺序)
### Requirement: 引擎通过 registry 调度 checker
系统 SHALL 在 `ProbeEngine.runCheck()` 中通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
#### Scenario: 引擎使用 registry 调度
- **WHEN** engine 需要执行一个 type 为 "http" 的 target
- **THEN** engine SHALL 从 `checkerRegistry` 中获取对应 checker 并调用其 `execute()` 方法,不再使用 `switch/case`
#### Scenario: 引擎注入超时 signal
- **WHEN** engine 调度一次 checker 执行
- **THEN** engine SHALL 创建 `AbortController`,设置超时定时器,将 `controller.signal` 注入 `CheckerContext`,执行完成后清理定时器
### Requirement: 配置解析通过 registry 委托 checker
系统 SHALL 在 `config-loader.ts``resolveTarget()` 中通过 `checkerRegistry.get(target.type).resolve(target, context)` 委托解析,替代原有的 `if/else` 分支。`validateConfig()` SHALL 仅校验通用字段name 非空、name 不重复、group 类型),不再包含 type 专属字段校验。
#### Scenario: 配置解析委托 checker
- **WHEN** config-loader 解析一个 type 为 "command" 的 target
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("command").resolve()` 进行解析、校验和默认值填充
#### Scenario: 通用字段校验保留在 config-loader
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
- **THEN** config-loader 的 `validateConfig()` SHALL 仍负责校验这些通用字段
#### Scenario: type 专属校验下沉到 checker
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
- **THEN** HttpChecker 的 `resolve()` SHALL 抛出校验错误,提示缺少必填字段
### Requirement: 存储序列化通过 registry 获取展示格式
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。
#### Scenario: 序列化委托 checker
- **WHEN** store 同步 targets 表
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
### Requirement: 共享 expect 断言函数
系统 SHALL 在 `src/server/checker/runner/shared/` 中提供可被多个 checker 复用的 expect 函数。checker 专用的 expect 函数 SHALL 保留在各自子包内。
#### Scenario: 共享 duration 断言
- **WHEN** 任何 checker 需要校验执行耗时
- **THEN** SHALL 调用 `runner/shared/duration.ts` 中的 `checkDuration(durationMs, maxDurationMs?)`,返回统一的 `ExpectResult`
#### Scenario: 共享 text 规则断言
- **WHEN** 任何 checker 需要对文本输出执行有序规则校验
- **THEN** SHALL 调用 `runner/shared/text.ts` 中的 `checkTextRules(text, rules, phase)`,返回统一的 `ExpectResult`
#### Scenario: 共享 body 规则断言
- **WHEN** 任何 checker 需要对文本体执行 contains/regex/json/css/xpath 规则校验
- **THEN** SHALL 调用 `runner/shared/body.ts` 中的 `checkBodyExpect(body, rules)`,返回统一的 `ExpectResult`
#### Scenario: HTTP 专用 expect
- **WHEN** HTTP checker 需要校验响应状态码和响应头
- **THEN** SHALL 调用 `runner/http/expect.ts` 中的 `checkStatus()``checkHeaders()`
#### Scenario: Command 专用 expect
- **WHEN** Command checker 需要校验退出码
- **THEN** SHALL 调用 `runner/command/expect.ts` 中的 `checkExitCode()`
### Requirement: 超时控制由引擎注入 signal
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController``setTimeout` 用于超时控制。仅 command checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
#### Scenario: HTTP checker 使用 signal
- **WHEN** HttpChecker 执行 HTTP 请求
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()``signal` 选项,不自行创建 `AbortController`
#### Scenario: Command checker 响应 signal
- **WHEN** CommandChecker 执行命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
### Requirement: CheckFailure.phase 使用 string 类型
`shared/api.ts``CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`
#### Scenario: phase 支持 checker 专用值
- **WHEN** command checker 在执行失败spawn error时生成 failure
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
#### Scenario: 前端展示 phase 不依赖硬编码类型
- **WHEN** 前端收到任意 phase 字符串值
- **THEN** 前端 SHALL 直接展示而不做类型判断

View File

@@ -1,117 +0,0 @@
## Purpose
定义项目代码质量门禁、格式化检查、快速检查和完整验证命令的行为要求,确保开发者可以通过文档化命令稳定验证源码质量、基础测试和生产 executable 行为。
## Requirements
### Requirement: ESLint 代码质量门禁
项目 SHALL 提供 ESLint 代码质量门禁,用于审查 TypeScript、React 前端、脚本和测试代码中的质量问题。ESLint 配置 SHALL 包括 `@eslint/js` recommended 规则、`typescript-eslint` recommended-type-checked 和 stylistic-type-checked 规则、`eslint-plugin-perfectionist` 导入排序规则、`eslint-plugin-import` 导入验证规则,以及精选的单项类型安全和风格规则。
#### Scenario: 运行 lint 检查
- **WHEN** 开发者运行文档化的 lint 命令
- **THEN** 系统 SHALL 使用 ESLint 检查项目源码、脚本和测试代码,并在发现违规时以非零状态退出
#### Scenario: 检查 React Hooks 规则
- **WHEN** 前端 React 代码违反 Hooks 调用规则
- **THEN** lint 命令 MUST 失败并报告对应违规
#### Scenario: 保护前后端边界
- **WHEN** `src/web` 前端代码导入 `src/server` 后端运行时实现
- **THEN** lint 命令 MUST 失败并报告前后端边界违规
#### Scenario: 检测类型安全违规
- **WHEN** 代码中存在浮动的 Promise 未 await、any 类型泄漏到明确类型位置、模板字符串中包含非字符串化对象等类型安全隐患
- **THEN** lint 命令 MUST 失败并报告对应 `@typescript-eslint` 规则违规
#### Scenario: 检测导入路径错误
- **WHEN** 代码中导入路径指向不存在的文件或已废弃的路径
- **THEN** lint 命令 MUST 失败并报告 `import/no-unresolved``import/no-deprecated` 错误
#### Scenario: 排除第三方模板目录
- **WHEN** ESLint 运行检查
- **THEN** 系统 MUST 排除 `.agents/` 等第三方模板目录,不检查其中的代码
#### Scenario: 排除生成产物和锁文件
- **WHEN** ESLint 运行检查
- **THEN** 系统 MUST 排除 `dist/``.build/``node_modules/``openspec/``.opencode/``.claude/``.codex/``*.bun-build``bun.lock``data/` 等非源码目录
### Requirement: Prettier 代码格式门禁
项目 SHALL 提供 Prettier 格式化和格式检查命令用于统一代码风格。Prettier 配置 SHALL 显式声明 `printWidth``semi``singleQuote``trailingComma``bracketSpacing``arrowParens``endOfLine``tabWidth``useTabs` 全部格式化参数。
#### Scenario: 检查代码格式
- **WHEN** 开发者运行文档化的格式检查命令
- **THEN** 系统 SHALL 使用 Prettier 检查受管理文件,并在发现未格式化文件时以非零状态退出
#### Scenario: 自动格式化代码
- **WHEN** 开发者运行文档化的格式化命令
- **THEN** 系统 SHALL 使用 Prettier 重写受管理文件的格式
#### Scenario: 排除 OpenSpec 文档和生成产物
- **WHEN** Prettier 格式化或格式检查运行
- **THEN** 系统 MUST 排除 `openspec/``dist/``.build/``node_modules/``bun.lock``skills-lock.json``.agents/``data/``*.bun-build``.opencode/``.claude/``.codex/` 和临时构建产物
#### Scenario: 格式化配置一致性
- **WHEN** 不同开发者在不同操作系统上运行 `prettier --write`
- **THEN** 由于所有格式化参数均显式定义,产物 SHALL 完全一致
### Requirement: TypeScript 未使用变量检测
项目 SHALL 启用 TypeScript `noUnusedLocals` 编译选项,将未使用的局部变量检测为编译错误。
#### Scenario: 存在未使用的局部变量
- **WHEN** TypeScript 代码中存在声明但未被引用的局部变量
- **THEN** `tsc --noEmit` MUST 以非零状态退出并报告未使用变量
### Requirement: TypeScript 索引签名属性访问检测
项目 SHALL 启用 TypeScript `noPropertyAccessFromIndexSignature` 编译选项,禁止通过点号访问未显式声明的属性。
#### Scenario: 通过点号访问 Record 动态属性
- **WHEN** 代码通过 `.property` 点号语法访问 `Record<string, T>` 类型或索引签名类型的属性
- **THEN** `tsc --noEmit` MUST 以非零状态退出,强制使用 `["property"]` 括号语法显式访问
### Requirement: ESLint 导入自动排序
项目 SHALL 通过 `eslint-plugin-perfectionist` 对导入语句进行自动排序,确保导入顺序一致性。
#### Scenario: 导入语句无序排列
- **WHEN** 文件中导入语句未按要求排序
- **THEN** `eslint --fix` SHALL 自动重排 import 声明和 named imports 内部顺序
#### Scenario: type import 与 value import 混合
- **WHEN** 文件中同时存在 `import type``import` 语句
- **THEN** perfectionist SHALL 正确识别并分别排序,不将 type 和 value 导入混淆
### Requirement: ESLint 导入路径验证
项目 SHALL 通过 `eslint-plugin-import` 验证导入路径的有效性和一致性。
#### Scenario: 导入不存在的模块路径
- **WHEN** 代码中导入了不存在或路径错误的模块
- **THEN** lint 命令 MUST 失败并报告 `import/no-unresolved` 错误
#### Scenario: 存在重复导入
- **WHEN** 同一个模块在同一文件中被多次导入
- **THEN** `eslint --fix` SHALL 自动合并重复导入为目标模块的单条导入
#### Scenario: 存在循环依赖
- **WHEN** 模块 A 导入模块 B同时模块 B 导入模块 A
- **THEN** lint 命令 MUST 报告 `import/no-cycle` 警告
### Requirement: 快速检查命令
项目 SHALL 提供快速 `check` 命令,用于日常开发期间验证代码质量和基础行为。
#### Scenario: 运行快速检查
- **WHEN** 开发者运行 `bun run check`
- **THEN** 系统 SHALL 依次执行类型检查、lint、格式检查和单元测试
#### Scenario: 快速检查失败
- **WHEN** `check` 中任一子检查失败
- **THEN** `check` MUST 以非零状态退出且不静默忽略失败
### Requirement: 完整验证命令
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产 executable 行为。
#### Scenario: 运行完整验证
- **WHEN** 开发者运行 `bun run verify`
- **THEN** 系统 SHALL 先运行 `check`,再运行生产构建和 executable smoke test
#### Scenario: 完整验证失败
- **WHEN** `verify` 中任一阶段失败
- **THEN** `verify` MUST 以非零状态退出且不能继续声明验证成功

View File

@@ -1,82 +0,0 @@
## Purpose
定义 Command 类型拨测目标:通过 `type: command` 配置执行本地命令(如进程检查、脚本健康检测),捕获 exit code、stdout、stderr按 expect 规则校验并生成 matched 判定。
## Requirements
### Requirement: command target 配置
系统 SHALL 支持 `type: command` 的 target 配置,通过 `command.exec``command.args` 描述本地命令,并使用 command 专用字段配置工作目录、环境变量和输出限制。
#### Scenario: 解析 command target
- **WHEN** YAML 中 target 配置 `type: command``command.exec: "pgrep"``command.args: ["nginx"]`
- **THEN** 系统 SHALL 将其解析为 command checker并保留 exec、args、cwd、env、maxOutputBytes、interval、timeout 和 expect 配置
#### Scenario: command target 缺少 exec
- **WHEN** YAML 中 target 配置 `type: command` 但缺少 `command.exec`
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 command.exec 字段
#### Scenario: cwd 相对配置文件目录解析
- **WHEN** command target 配置 `command.cwd: "scripts"` 且配置文件位于 `/opt/checker/probes.yaml`
- **THEN** 系统 SHALL 将 cwd 解析为 `/opt/checker/scripts`
#### Scenario: command 不使用 shell
- **WHEN** command target 配置 `exec``args`
- **THEN** 系统 MUST 直接执行该程序和参数,不通过 shell 解释整段命令字符串
#### Scenario: env 默认继承并允许覆盖
- **WHEN** command target 配置 `command.env: {LANG: "C"}` 且当前进程环境包含 `PATH`
- **THEN** 系统 SHALL 继承当前进程的全部环境变量,并将 `LANG` 覆盖为 `"C"`
#### Scenario: 不支持 stdin
- **WHEN** command target 配置并执行命令
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
### Requirement: command checker 执行
系统 SHALL 按 command target 配置执行本地命令记录执行耗时、退出码、stdout 和 stderr并在执行失败时产生结构化错误信息。
#### Scenario: 命令正常退出
- **WHEN** command target 执行的进程正常退出且 exit code 为 0
- **THEN** 系统 SHALL 记录 `durationMs``statusDetail="exitCode=0"`,并进入 expect 校验
#### Scenario: 命令非零退出
- **WHEN** command target 执行的进程正常退出但 exit code 为 1
- **THEN** 系统 SHALL 记录 `statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
#### Scenario: 命令启动失败
- **WHEN** command target 的 exec 不存在或无法启动
- **THEN** 系统 SHALL 记录 `matched=false`,并在 failure 中写入 kind=`error` 和可读错误信息
#### Scenario: 命令超时
- **WHEN** command target 在 timeout 时间内未结束
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息
#### Scenario: 命令输出超限
- **WHEN** command target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息
### Requirement: command expect 校验
系统 SHALL 支持 command 专用 expect包括 `exitCode``stdout``stderr`,并按 exitCode、duration、stdout、stderr 的阶段顺序快速失败。
#### Scenario: 默认 exitCode 成功语义
- **WHEN** command target 未显式配置 `expect.exitCode`
- **THEN** 系统 SHALL 使用默认 `expect.exitCode: [0]` 进行校验
#### Scenario: 显式 exitCode 校验
- **WHEN** command target 配置 `expect.exitCode: [0, 2]` 且实际 exit code 为 2
- **THEN** 系统 SHALL 判定 exitCode 阶段通过,并继续后续 expect 阶段
#### Scenario: exitCode 不匹配快速失败
- **WHEN** command target 配置 `expect.exitCode: [0]` 且实际 exit code 为 1
- **THEN** 系统 SHALL 立即返回 `matched=false`,并在 failure 中写入 phase=`exitCode`、path=`expect.exitCode`、expected 和 actual
#### Scenario: stdout 按配置顺序校验
- **WHEN** command target 配置 `expect.stdout` 为两个规则,第一条通过且第二条失败
- **THEN** 系统 SHALL 先执行第一条 stdout 规则,再执行第二条,并将 failure.path 指向失败的 `expect.stdout[1]`
#### Scenario: stderr 校验为空
- **WHEN** command target 配置 `expect.stderr: [{empty: true}]` 且实际 stderr 为空字符串
- **THEN** 系统 SHALL 判定 stderr 阶段通过
#### Scenario: stdout 失败后不检查 stderr
- **WHEN** command target 同时配置 stdout 和 stderr 规则,且 stdout 规则失败
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr 规则

View File

@@ -1,50 +0,0 @@
## Purpose
定义 Git hooks 自动化质量门禁行为,在 pre-commit 阶段自动运行代码检查和格式化,在 commit-msg 阶段校验提交信息格式。
## Requirements
### Requirement: pre-commit 自动质量检查
项目 SHALL 通过 husky 和 lint-staged 在 git commit 前自动对变更文件运行 ESLint 和 Prettier 检查。
#### Scenario: 变更 TypeScript 文件后提交
- **WHEN** 开发者 stage 了 `.ts``.tsx` 文件并执行 `git commit`
- **THEN** lint-staged SHALL 自动对变更文件运行 `eslint --fix``prettier --write`,修复后继续提交
#### Scenario: 变更 Markdown 或 JSON 文件后提交
- **WHEN** 开发者 stage 了 `.md``.json``.yaml``.yml` 文件并执行 `git commit`
- **THEN** lint-staged SHALL 自动对变更文件运行 `prettier --write`
#### Scenario: lint 检查失败阻止提交
- **WHEN** 变更文件存在无法自动修复的 ESLint 错误
- **THEN** pre-commit hook MUST 以非零状态退出,阻止提交
#### Scenario: 无变更文件提交
- **WHEN** 开发者执行 `git commit` 但无 stage 文件
- **THEN** lint-staged SHALL 正常通过,不阻止提交
### Requirement: 提交信息格式校验
项目 SHALL 通过 commitlint 在 git commit 时校验提交信息必须符合 "类型: 简短描述" 格式,类型限定为 feat/fix/refactor/docs/style/test/chore。
#### Scenario: 有效的中文提交信息
- **WHEN** 开发者提交信息为 "feat: 新增导入排序功能"
- **THEN** commit-msg hook SHALL 通过校验
#### Scenario: 缺少类型前缀的提交信息
- **WHEN** 开发者提交信息为 "新增导入排序功能"(无 "feat:" 前缀)
- **THEN** commit-msg hook MUST 以非零状态退出,提示正确格式
#### Scenario: 无效的提交类型
- **WHEN** 开发者提交信息使用不在允许列表中的类型(如 "update: 修改配置"
- **THEN** commit-msg hook MUST 以非零状态退出,提示可用类型
### Requirement: husky 初始化自动化
项目 SHALL 通过 `prepare` 生命周期脚本在 `bun install` 时自动初始化 husky。
#### Scenario: 首次安装依赖
- **WHEN** 开发者运行 `bun install`
- **THEN** husky SHALL 自动初始化,安装 pre-commit 和 commit-msg hooks
#### Scenario: 已有 husky 配置时安装
- **WHEN** 开发者运行 `bun install` 且 husky 已初始化
- **THEN** husky 初始化 SHALL 跳过,不覆盖已有配置

View File

@@ -1,69 +0,0 @@
## Purpose
定义 styles.css 中集中管理的前端样式工具类和 CSS 自定义属性,供 TDesign 组件之外的自定义组件StatusDot、StatusBar 等)引用。
## Requirements
### Requirement: 状态色 CSS 类
styles.css SHALL 定义状态指示相关的 CSS 类,颜色使用 TDesign tokens。
#### Scenario: StatusDot 颜色类
- **WHEN** StatusDot 组件渲染
- **THEN** 组件 SHALL 使用 `.status-dot` 基础类 + `.status-dot--up`background: `--td-success-color`)或 `.status-dot--down`background: `--td-error-color`)修饰类,不使用内联 style
#### Scenario: StatusDot 发光阴影
- **WHEN** StatusDot 组件渲染
- **THEN** `.status-dot--up` SHALL 定义 `box-shadow` 使用 `--td-success-color``.status-dot--down` SHALL 定义 `box-shadow` 使用 `--td-error-color`
#### Scenario: StatusBar 色块类
- **WHEN** StatusBar 组件渲染色块
- **THEN** 组件 SHALL 使用 `.status-bar-block` 基础类 + `.status-bar-block--up`background: `--td-success-color`)、`.status-bar-block--down`background: `--td-error-color`)或 `.status-bar-block--empty`background: `--td-bg-color-component-disabled`)修饰类,不使用内联 style
### Requirement: 可用率色阶 CSS 变量
styles.css SHALL 定义 10 级可用率色阶 CSS 自定义属性,使用项目自定义色值。
#### Scenario: 色阶变量定义
- **WHEN** 可用率进度条渲染
- **THEN** 色阶 SHALL 通过 CSS 自定义属性 `--avail-0``--avail-9` 定义,值为项目自定义色值(`#d54941``#3dba60`
#### Scenario: 色阶渐变方向
- **WHEN** 色阶变量被引用
- **THEN** 色阶 SHALL 从红色0-30%经橙色30-60%过渡到绿色60-100%
### Requirement: 辅助工具类
styles.css SHALL 定义前端组件复用的工具类。
#### Scenario: 文本禁用色类
- **WHEN** 延迟列无数据需要显示占位符
- **THEN** 组件 SHALL 使用 `.text-disabled`color: `--td-text-color-disabled`),不使用内联 style
#### Scenario: 等宽数字类
- **WHEN** 数值需要等宽显示
- **THEN** 组件 SHALL 使用 `.tabular-nums`font-variant-numeric: tabular-nums
#### Scenario: 延迟色值类
- **WHEN** 延迟数值渲染
- **THEN** 组件 SHALL 使用 `.latency-ok`color: `--td-success-color`)、`.latency-warn`color: `--td-warning-color`)或 `.latency-error`color: `--td-error-color`)类,不使用内联 style
#### Scenario: 全宽布局类
- **WHEN** 组件需要占满父容器宽度
- **THEN** 组件 SHALL 使用 `.full-width`width: 100%),不使用内联 style
#### Scenario: 可点击表格类
- **WHEN** PrimaryTable 行支持点击交互
- **THEN** 表格 SHALL 使用 `.clickable-table`cursor: pointer不使用内联 style
#### Scenario: Tab 面板内边距类
- **WHEN** Drawer 内 Tabs 面板需要内边距
- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名,不通过入侵 TDesign 内部类名覆盖
### Requirement: 异常行背景类
styles.css SHALL 定义 DOWN 行的背景色,使用安全选择器且不使用 `!important`
#### Scenario: DOWN 行背景色
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景(引用 `--td-error-color-light` token不使用 `!important`
#### Scenario: DOWN 行 hover 状态
- **WHEN** 鼠标悬停在 DOWN 行上
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色

View File

@@ -1,128 +0,0 @@
## Purpose
定义 HTTP 拨测中响应体校验方法集contains/regex/json/css/xpath、操作符系统和响应头校验的行为规范。
## Requirements
### Requirement: 响应体多种校验方法
系统 SHALL 支持对 HTTP 响应体进行五种可组合的校验方法contains子串、regex正则、jsonJSONPath、cssCSS 选择器、xpathXPath。这些方法 MUST 配置在 `expect.body` 有序数组中。
#### Scenario: contains 子串匹配
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体包含 `"healthy"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: contains 不匹配
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体不包含该文本
- **THEN** 系统 SHALL 判定 matched 为 false并记录该规则的 failure.path
#### Scenario: regex 正则匹配
- **WHEN** HTTP target 配置 `expect.body: [{regex: '"status"\\s*:\\s*"ok"'}]`,且响应体匹配该正则
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: regex 不匹配
- **WHEN** HTTP target 配置 regex body 规则,且响应体不匹配该正则
- **THEN** 系统 SHALL 判定 matched 为 false并记录该规则的 failure.path
#### Scenario: json JSONPath 等值匹配
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status", equals: "ok"}}]`,且响应 JSON 中 `$.status` 值为 `"ok"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: json JSONPath 值不匹配
- **WHEN** HTTP target 配置 json body 规则,且提取值不符合期望
- **THEN** 系统 SHALL 判定 matched 为 false并记录包含 JSONPath 的 failure.path
#### Scenario: json 解析失败
- **WHEN** HTTP target 配置了 json body 规则但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: css 选择器匹配
- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "div#health", equals: "OK"}}]`,且 HTML 中存在 `div#health` 元素文本为 `"OK"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: css 选择器匹配属性值
- **WHEN** HTTP target 配置 css 规则带 `attr: "content"` 用于提取属性,且属性值匹配期望
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: css 选择器无匹配元素
- **WHEN** HTTP target 配置了 css 选择器但 HTML 中无匹配元素
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: xpath 表达式匹配
- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/root/status/text()", equals: "ok"}}]`,且 XML 中 `/root/status` 节点文本为 `"ok"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: xpath 表达式无匹配节点
- **WHEN** HTTP target 配置了 xpath 表达式但 XML 中无匹配节点
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 多种 body 校验方法 AND 组合
系统 SHALL 支持在 `expect.body` 数组中同时配置多种 body 校验方法,所有方法均通过时 matched 方为 true。
#### Scenario: 多种方法全部通过
- **WHEN** HTTP target 的 `expect.body` 数组依次配置 contains、json、regex且全部通过
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 多种方法任一失败
- **WHEN** HTTP target 的 `expect.body` 数组第一条 contains 不通过,后续还有 json 规则
- **THEN** 系统 SHALL 判定 matched 为 false且不再检查后续 json 规则
### Requirement: 操作符系统
系统 SHALL 支持对提取值和文本值使用以下操作符进行比较equals默认等值、contains子串包含、match正则匹配、empty空值判断、exists存在性判断、gte/lte/gt/lt数值比较
#### Scenario: 标量值隐式 equals
- **WHEN** 配置的期望值为标量(字符串/数字/布尔/null`equals: "ok"`
- **THEN** 系统 SHALL 使用 equals 操作符,对实际值做严格相等比较
#### Scenario: 显式 contains 操作符
- **WHEN** 配置 `{contains: "success"}`,且实际值包含 `"success"`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: 显式 match 操作符
- **WHEN** 配置 `{match: '\\d+\\.\\d+\\.\\d+'}`,且实际值匹配该正则
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: empty 操作符判断为空
- **WHEN** 配置 `{empty: true}`,且实际值为空数组 `[]`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: empty 操作符判断非空
- **WHEN** 配置 `{empty: false}`,且实际值为 `[1, 2]`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: exists 操作符判断存在
- **WHEN** 配置 `{exists: false}`,且实际值不存在
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: gte 数值比较
- **WHEN** 配置 `{gte: 10}`,且实际值为 `15`(数字)
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: gt/lt 数值比较
- **WHEN** 配置 `{gt: 0, lt: 1000}`,且实际值为 `500`
- **THEN** 系统 SHALL 对同一字段进行多操作符复合比较,全部通过则该规则通过
### Requirement: 响应头校验
系统 SHALL 支持通过 `expect.headers` 配置对 HTTP 响应头进行键值规则校验header 名称匹配 MUST 不区分大小写。
#### Scenario: 响应头匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": {contains: "application/json"}}`,且响应包含该 header 且值匹配
- **THEN** 系统 SHALL 判定 headers 阶段通过
#### Scenario: 响应头不匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": {equals: "application/json"}}`,且响应 header 值为 `"text/html"`
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: 响应头缺失
- **WHEN** HTTP target 配置了某个 header 但响应中不存在该 header
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 结构化 expect 失败信息
系统 SHALL 在任一 expect 规则失败时生成结构化 failure用于标识失败阶段、规则路径、期望值、实际值和可读错误信息。
#### Scenario: body 规则失败信息
- **WHEN** HTTP target 的 `expect.body[1].json` 规则失败
- **THEN** failure SHALL 包含 kind=`mismatch`、phase=`body`、path 指向 `expect.body[1]`,并包含 message
#### Scenario: actual 值截断
- **WHEN** 失败规则的实际值超过系统允许记录的摘要长度
- **THEN** 系统 MUST 截断 actual 摘要,而不是持久化完整响应体或命令输出

View File

@@ -1,78 +0,0 @@
## Purpose
定义 Vite + React + TypeScript 前端开发工作流、开发期 API 代理和共享契约的行为要求。
## Requirements
### Requirement: Vite React 开发服务器
系统 SHALL 提供基于 Vite + React + TypeScript 的前端开发工作流,并支持热模块替换。
#### Scenario: 启动前端开发服务器
- **WHEN** 开发者启动前端开发命令
- **THEN** 前端 SHALL 由 Vite 提供服务,并启用 React 热模块替换
#### Scenario: 构建前端静态资源
- **WHEN** 开发者运行前端生产构建命令
- **THEN** 系统 SHALL 产出可由 Bun 后端服务的前端静态资源
### Requirement: 前端开发期 API 代理
前端开发服务器 SHALL 在本地开发期间将 `/api/*` 请求代理到 Bun 后端服务。
#### Scenario: 前端开发期调用拨测 API
- **WHEN** 浏览器从 Vite 开发源请求 `/api/summary``/api/targets` 等拨测 API
- **THEN** Vite SHALL 将请求转发到 Bun 后端服务,且不需要浏览器 CORS 配置
#### Scenario: 开发期访问非 API 前端路由
- **WHEN** 浏览器从 Vite 开发源请求非 API 前端路由
- **THEN** Vite SHALL 将该请求作为前端应用流量处理,而不是转发到后端
### Requirement: 开发期后端端口一致性
项目 SHALL 保证文档化的全栈开发命令中Vite proxy 目标端口与 Bun 后端监听端口来自同一配置来源。
#### Scenario: 使用默认开发端口
- **WHEN** 开发者未提供端口覆盖并运行文档化的全栈开发命令
- **THEN** Bun 后端 SHALL 监听默认端口,且 Vite SHALL 将 `/api/*` 代理到同一端口
#### Scenario: 使用 PORT 覆盖开发端口
- **WHEN** 开发者通过 `PORT` 覆盖后端端口并运行文档化的全栈开发命令
- **THEN** Bun 后端 SHALL 监听该端口,且 Vite SHALL 将 `/api/*` 代理到同一端口
#### Scenario: 避免代理端口与后端端口分叉
- **WHEN** 开发期脚本需要向 Vite 传递后端端口
- **THEN** 该代理端口 MUST 从文档化的后端端口配置派生,而不是作为独立对外配置导致分叉
### Requirement: 前端使用相对 API 路径
除非有文档化的部署配置覆盖该行为,前端代码 MUST 通过相对 `/api/*` URL 调用后端 API。
#### Scenario: 前端获取后端数据
- **WHEN** 前端代码调用后端 API
- **THEN** 请求 URL 默认 MUST 使用相对 `/api/*` 路径
#### Scenario: 运行环境变化
- **WHEN** host 或 port 在开发环境和生产环境之间变化
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作
### Requirement: 集成开发命令
项目 SHALL 提供一个文档化命令,用于在开发期间同时运行前端和后端。
#### Scenario: 启动全栈开发
- **WHEN** 开发者运行文档化的全栈开发命令
- **THEN** 系统 SHALL 启动 Vite 前端开发服务器和 Bun 后端服务器
### Requirement: 开发质量命令文档化
项目 SHALL 在前端开发工作流文档中说明日常检查和完整验证命令。
#### Scenario: 查阅开发命令
- **WHEN** 开发者阅读 README 的开发或测试章节
- **THEN** 文档 SHALL 说明 `check` 用于日常开发检查,`verify` 用于提交前或发布前完整验证
### Requirement: 共享 TypeScript 契约
项目 SHALL 为前端和后端共同使用的请求与响应类型提供共享 TypeScript 边界。
#### Scenario: 定义 API 响应结构
- **WHEN** 前端和后端都需要某个 API 响应类型
- **THEN** 该类型 SHALL 定义在 shared 模块中,而不是在两端重复定义
#### Scenario: 前端导入共享类型
- **WHEN** 前端代码导入共享 API 类型
- **THEN** 该导入 SHALL 不要求将后端运行时实现打包进前端

View File

@@ -1,127 +0,0 @@
## Purpose
定义 Bun 全栈应用运行时的 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
## Requirements
### Requirement: Bun HTTP 运行时
系统 SHALL 运行一个 Bun HTTP server由单个进程提供后端 API、健康检查、生产静态资源和 SPA fallback 行为。
#### Scenario: 启动运行时服务器
- **WHEN** server 进程成功启动
- **THEN** 它 SHALL 监听 YAML 配置文件中指定的 host 和 port并记录实际 server URL
#### Scenario: 通过 YAML 配置提供运行时参数
- **WHEN** 通过 YAML 配置文件提供 host、port、数据目录等参数
- **THEN** server SHALL 使用该值,且不需要重新构建
#### Scenario: CLI 只接受配置文件路径
- **WHEN** 用户通过命令行启动程序
- **THEN** 系统 SHALL 只接受一个命令行参数作为 YAML 配置文件路径
#### Scenario: 提供拨测相关 API
- **WHEN** server 启动完成
- **THEN** 系统 SHALL 提供 `/api/summary``/api/targets``/api/targets/:id/history``/api/targets/:id/trend` 端点
### Requirement: HTTP method 语义
系统 SHALL 为运行时端点提供明确的 HTTP method 语义,避免不支持的 method 被错误地当作成功请求处理。
#### Scenario: GET 请求访问运行时端点
- **WHEN** 客户端使用 `GET` 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回对应端点的成功响应
#### Scenario: HEAD 请求访问运行时端点
- **WHEN** 客户端使用 `HEAD` 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回与 `GET` 相同的成功状态和 headers但 MUST NOT 返回响应体
#### Scenario: 不支持的 method 访问运行时端点
- **WHEN** 客户端使用不支持的 method 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回 405 状态码和 Allow header
### Requirement: API 路由命名空间
系统 MUST 将 `/api/*` 保留给后端 API 路由。
#### Scenario: API 路由匹配
- **WHEN** 请求匹配已注册的 `/api/*` 路由
- **THEN** Bun server SHALL 返回 API handler 的响应
#### Scenario: API 路由未命中
- **WHEN** 请求访问未注册的 `/api/*` 路由
- **THEN** Bun server MUST 返回 JSON 404 响应,而不是前端 HTML 文档
### Requirement: API 错误响应一致性
系统 SHALL 为 API 命名空间内的错误返回机器可读 JSON 响应。
#### Scenario: 未知 API 路由
- **WHEN** 客户端请求未知的 `/api/*` 路由
- **THEN** Bun server MUST 返回包含 `error``status` 字段的 JSON 404 响应,而不是前端 HTML 文档
#### Scenario: API method 不允许
- **WHEN** 客户端使用不支持的 method 请求已存在的 API 路由
- **THEN** Bun server MUST 返回包含 `error``status` 字段的 JSON 405 响应
### Requirement: 健康检查端点
系统 SHALL 在前端 SPA fallback 之外暴露健康检查端点。
#### Scenario: 健康检查成功
- **WHEN** 客户端请求 `/health`
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
### Requirement: 生产静态资源服务
系统 SHALL 在生产模式下由 Bun runtime 服务 Vite 生产资源。
#### Scenario: 请求构建后的资源
- **WHEN** 客户端请求构建后的前端资源,例如 `/assets/app.js`
- **THEN** Bun server SHALL 返回该资源并带有适当的 content type
#### Scenario: 请求前端根路径
- **WHEN** 客户端请求 `/`
- **THEN** Bun server SHALL 返回前端入口 HTML 文档
### Requirement: 生产缓存策略
系统 SHALL 为生产静态资源和前端入口 HTML 使用明确的缓存策略。
#### Scenario: 请求前端入口 HTML
- **WHEN** 生产 Bun server 返回前端入口 HTML 文档
- **THEN** 响应 SHALL 使用 `Cache-Control: no-cache`
#### Scenario: 请求构建后的静态资源
- **WHEN** 生产 Bun server 返回 Vite 构建后的静态资源
- **THEN** 响应 SHALL 使用长缓存策略 `public, max-age=31536000, immutable`
#### Scenario: 请求未知静态资源
- **WHEN** 客户端请求不存在的 `/assets/*` 资源或带文件扩展名的未知路径
- **THEN** Bun server MUST 返回 404且 MUST NOT 返回前端入口 HTML 文档
### Requirement: 低风险安全响应头
系统 SHALL 在生产运行时响应中附加低风险安全响应头,提升基础安全性且不提前约束未来前端资源策略。
#### Scenario: 生产 HTML 响应包含安全头
- **WHEN** 生产 Bun server 返回前端 HTML 文档
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
#### Scenario: 生产 JSON 响应包含安全头
- **WHEN** 生产 Bun server 返回 `/health``/api/*` JSON 响应
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
#### Scenario: 生产静态资源响应包含安全头
- **WHEN** 生产 Bun server 返回 Vite 构建后的静态资源
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
### Requirement: SPA fallback 行为
系统 SHALL 在生产环境中为非 API、非静态资源的前端路由返回前端入口 HTML 文档。
#### Scenario: 刷新前端路由
- **WHEN** 客户端请求前端路由,例如 `/dashboard`
- **THEN** Bun server SHALL 返回前端入口 HTML 文档
#### Scenario: 保留 API 错误语义
- **WHEN** 客户端请求未知的 `/api/*` 路由
- **THEN** Bun server MUST NOT 返回前端入口 HTML 文档
### Requirement: 优雅关机
系统 SHALL 在收到终止信号时正确清理资源。
#### Scenario: SIGINT/SIGTERM 处理
- **WHEN** 开发模式进程收到 SIGINT 或 SIGTERM 信号
- **THEN** 系统 SHALL 调用 engine.stop() 停止调度、调用 store.close() 关闭数据库连接后退出进程

View File

@@ -1,124 +0,0 @@
## Purpose
定义拨测系统的 REST API 端点:总览统计、目标列表含分组和结构化采样数据、带时间范围和分页的历史记录、按时间范围的趋势聚合。
## Requirements
### Requirement: 总览统计 API
系统 SHALL 提供 `GET /api/summary` 端点,返回所有目标的总体统计信息(不含平均耗时)。
#### Scenario: 获取总览统计
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** 系统 SHALL 返回 JSON 包含 total总目标数、up正常数、down异常数、lastCheckTime最近一次检查时间
### Requirement: 目标列表 API
系统 SHALL 提供 `GET /api/targets` 端点,返回所有 typed target 及其最新状态、分组信息和结构化采样数据。
#### Scenario: 获取目标列表
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 系统 SHALL 返回 JSON 数组每个元素包含目标基本信息id、name、group、type、target、interval、最近一次检查结果timestamp、matched、durationMs、statusDetail、failure、统计摘要totalChecks、availability和结构化采样数据 recentSamples代替原 sparkline
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 latestCheck 为 nullrecentSamples 为空数组
### Requirement: 历史记录 API
系统 SHALL 提供 `GET /api/targets/:id/history` 端点,支持时间范围筛选和分页返回指定目标的拨测记录。
#### Scenario: 获取指定时间范围内的历史记录
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=1&pageSize=20`
- **THEN** 系统 SHALL 返回带分页信息的历史记录,包含 items、total、page、pageSize按时间倒序排列
#### Scenario: 使用默认分页参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO`(未指定 page 或 pageSize
- **THEN** 系统 SHALL 使用默认 page=1, pageSize=20
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/history` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 趋势 API 支持时间范围
系统 SHALL 提供 `GET /api/targets/:id/trend` 端点,支持 `from``to` 查询参数指定时间范围。
#### Scenario: 指定时间范围查询趋势
- **WHEN** 客户端请求 `GET /api/targets/1/trend?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回指定时间范围内按小时分组的聚合数据
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/trend` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 目标列表返回分组和采样数据
`GET /api/targets` SHALL 返回每个目标的分组信息和结构化采样数据,替代原有 sparkline。
#### Scenario: 返回分组信息
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
#### Scenario: 返回 recentSamples
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `recentSamples` 数组,每个元素包含 `timestamp`ISO 8601`durationMs`number | null`up`booleanmatched === true
#### Scenario: recentSamples 数量
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 每个目标的 recentSamples SHALL 最多包含 30 个元素,按时间倒序排列
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 recentSamples SHALL 为空数组
### Requirement: 新增共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `CheckResult``RecentSample``HistoryResponse` 类型。
#### Scenario: CheckResult 类型
- **WHEN** 前后端共享 `CheckResult` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``matched: boolean``durationMs: number | null``statusDetail: string | null``failure` 字段,不包含 success 字段
#### Scenario: RecentSample 类型
- **WHEN** 前后端共享 `RecentSample` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``durationMs: number | null``up: boolean` 字段,其中 up 为 boolean 且等于 matched
#### Scenario: HistoryResponse 类型
- **WHEN** 前后端共享 `HistoryResponse` 类型
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]``total: number``page: number``pageSize: number` 字段
### Requirement: 保留健康检查端点
系统 SHALL 保留 `GET /health` 端点,不受拨测功能影响。
#### Scenario: 访问健康检查
- **WHEN** 客户端请求 `GET /health`
- **THEN** 系统 SHALL 返回与之前格式一致的健康检查响应
### Requirement: API 错误处理
系统 SHALL 对不存在的目标 ID 和无效参数返回适当的 HTTP 错误响应。
#### Scenario: 查询不存在的目标
- **WHEN** 客户端请求 `GET /api/targets/999/history`
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
#### Scenario: 无效的 from/to 参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=invalid`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 无效的分页参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/trend` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 无效的目标 ID
- **WHEN** 客户端请求 `GET /api/targets/abc/history`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 失败信息 API 契约
系统 SHALL 通过 API 返回结构化 failure 信息,供 Dashboard 展示和后续排查。
#### Scenario: 返回 expect 不匹配信息
- **WHEN** 最近一次检查结果包含 failure.kind=`mismatch`
- **THEN** `/api/targets``/api/targets/:id/history` SHALL 返回该 failure 的 kind、phase、path、expected、actual、message 字段
#### Scenario: 无失败信息
- **WHEN** 检查结果 matched=true
- **THEN** API SHALL 返回 failure 为 null

View File

@@ -1,134 +0,0 @@
## Purpose
定义 HTTP 拨测工具的 YAML 配置文件格式、解析校验规则和 CLI 启动流程。
## Requirements
### Requirement: YAML 配置文件格式
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、checker 默认值和 typed target 列表(含可选 group 字段。target MUST 使用 `type` 字段声明 checker 类型HTTP 领域字段 MUST 放在 `http` 分组command 领域字段 MUST 放在 `command` 分组。
#### Scenario: 完整配置文件解析
- **WHEN** 系统启动并读取包含 server、runtime、defaults、targets含 group 字段)的 YAML 配置文件
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
#### Scenario: 最简 HTTP 配置文件解析
- **WHEN** 系统读取只包含一个 `type: http` target 和 `http.url` 的 YAML 配置文件(省略 server、runtime、defaults 和 expect
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, runtime.maxConcurrentChecks=20, http.method=GET, http.maxBodyBytes=100MB, group="default"
#### Scenario: 最简 command 配置文件解析
- **WHEN** 系统读取只包含一个 `type: command` target 和 `command.exec` 的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段interval=30s, timeout=10s, command.cwd 为配置文件所在目录, command.maxOutputBytes=100MB
#### Scenario: per-target 配置覆盖全局默认值
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
- **THEN** 该 target SHALL 使用其自身的值,不受 defaults 中对应字段影响
### Requirement: CLI 参数
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。
#### Scenario: 指定配置文件启动
- **WHEN** 用户执行 `./dial-server ./probes.yaml`
- **THEN** 系统 SHALL 读取并解析指定路径的 YAML 文件作为配置
#### Scenario: 未提供配置文件路径
- **WHEN** 用户启动程序时未提供任何命令行参数
- **THEN** 系统 SHALL 以错误退出并提示需要指定配置文件路径
#### Scenario: 配置文件不存在
- **WHEN** 用户指定的配置文件路径不存在
- **THEN** 系统 SHALL 以错误退出并提示文件不存在
### Requirement: 配置校验
系统 SHALL 在启动时对 YAML 配置进行完整校验,校验失败时以非零状态退出并输出清晰的错误信息。
#### Scenario: target 缺少必填字段
- **WHEN** YAML 中某个 target 缺少 name 或 type 字段
- **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段
#### Scenario: HTTP target 缺少 url
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
#### Scenario: command target 缺少 exec
- **WHEN** YAML 中某个 target 配置 `type: command` 但缺少 `command.exec`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 command.exec 字段
#### Scenario: target type 非法
- **WHEN** YAML 中某个 target 的 type 不是 `http``command`
- **THEN** 系统 SHALL 以错误退出,提示不支持的 target type
#### Scenario: target name 重复
- **WHEN** YAML 中存在两个 name 相同的 target
- **THEN** 系统 SHALL 以错误退出,提示重复的 name
#### Scenario: group 字段类型校验
- **WHEN** YAML 中某个 target 的 `group` 字段不是字符串
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
#### Scenario: interval 格式非法
- **WHEN** interval 或 timeout 值不是有效的时长格式(如 `30s``5m``500ms`
- **THEN** 系统 SHALL 以错误退出并提示格式错误
#### Scenario: maxConcurrentChecks 非法
- **WHEN** runtime.maxConcurrentChecks 不是正整数
- **THEN** 系统 SHALL 以错误退出并提示 runtime.maxConcurrentChecks 格式错误
#### Scenario: size 格式非法
- **WHEN** maxBodyBytes 或 maxOutputBytes 值不是有效的 size 格式
- **THEN** 系统 SHALL 以错误退出并提示支持 B、KB、MB、GB 格式
### Requirement: size 配置解析
系统 SHALL 支持使用单位字符串配置读取上限,单位包括 `B``KB``MB``GB`
#### Scenario: 解析 MB
- **WHEN** YAML 中配置 `maxBodyBytes: "100MB"`
- **THEN** 系统 SHALL 将其解析为 104857600 bytes
#### Scenario: 解析 KB
- **WHEN** YAML 中配置 `maxOutputBytes: "512KB"`
- **THEN** 系统 SHALL 将其解析为 524288 bytes
### Requirement: runtime 并发配置
系统 SHALL 支持 `runtime.maxConcurrentChecks` 配置全局最大并发检查数。
#### Scenario: 使用默认并发限制
- **WHEN** YAML 中未配置 runtime.maxConcurrentChecks
- **THEN** 系统 SHALL 使用默认值 20
#### Scenario: 配置并发限制
- **WHEN** YAML 中配置 `runtime.maxConcurrentChecks: 5`
- **THEN** 系统 SHALL 将全局最大并发检查数设置为 5
### Requirement: YAML 配置使用 Bun 内置解析
系统 SHALL 使用 Bun 内置的 `Bun.YAML.parse()` 解析配置文件,不引入外部 YAML 解析库。
#### Scenario: 解析 YAML 内容
- **WHEN** 系统读取 YAML 文件内容
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象
### Requirement: expect 配置增强
系统 SHALL 支持 typed target 的领域专用 expect 配置,包括 HTTP 的 `status``headers``body` 和 command 的 `exitCode``stdout``stderr`。内容类 expect MUST 使用数组表达配置顺序。
#### Scenario: 解析 HTTP expect 配置
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body 规则数组及内部方法
- **THEN** 系统 SHALL 正确解析并存储为 HTTP target 的 expect 字段
#### Scenario: 解析 command expect 配置
- **WHEN** YAML 配置文件中 command target 的 expect 包含 exitCode、stdout 和 stderr 规则数组
- **THEN** 系统 SHALL 正确解析并存储为 command target 的 expect 字段
#### Scenario: 解析 body 有序规则数组
- **WHEN** YAML 中 HTTP target 配置 `expect.body` 为 contains、json、regex 三个数组项
- **THEN** 系统 SHALL 保留数组顺序,供执行阶段按配置顺序快速失败
#### Scenario: 不配置 HTTP status
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `status: [200]` 语义
#### Scenario: 不配置 command exitCode
- **WHEN** command target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `exitCode: [0]` 语义
#### Scenario: 不配置 expect
- **WHEN** target 未配置任何 expect 规则
- **THEN** 系统 SHALL 正常处理expect 字段为 undefined

View File

@@ -1,34 +0,0 @@
## Purpose
定义拨测系统前端 Dashboard 页面:总览统计卡片、页面标题、加载和错误状态处理。分组表格布局见 `target-table`,目标详情 Drawer 见 `target-detail-drawer`,数据轮询和缓存见 `tanstack-query-data-layer`
## Requirements
### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部使用 TDesign Statistic 组件展示总览统计,包含总目标数、正常数和异常数。
#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 使用 TDesign Row/Col 布局展示 3 个 TDesign Card + Statistic 组合全部目标数color=blue、正常目标数color=green、异常目标数color=red
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** 统计卡片 SHALL 通过 TanStack Query 的 refetchInterval=8000 自动刷新数据
### Requirement: 页面标题
Dashboard 页面 SHALL 使用 TDesign Typography 组件渲染标题和副标题。
#### Scenario: 页面标题渲染
- **WHEN** Dashboard 页面渲染
- **THEN** 页面标题 SHALL 使用 TDesign Typography.Title 组件level="h1")渲染"DiAL",副标题 SHALL 使用 Typography.Text 组件theme="secondary")渲染"统一拨测平台"
### Requirement: 页面加载与错误状态
Dashboard SHALL 使用 TDesign 组件正确处理加载状态和 API 错误。
#### Scenario: 首次加载
- **WHEN** 页面首次加载且数据尚未返回
- **THEN** 表格 SHALL 显示 TDesign Loading 加载状态
#### Scenario: API 请求失败
- **WHEN** 前端 API 请求失败
- **THEN** 页面 SHALL 使用 TDesign Alert 组件theme=error显示错误提示

View File

@@ -1,128 +0,0 @@
## Purpose
定义基于 SQLite 的拨测数据持久化存储targets 同步含分组信息、check_results 追加写入、结构化采样数据查询、时间范围和分页查询、索引与聚合查询。
## Requirements
### Requirement: SQLite 数据库初始化
系统 SHALL 使用 Bun 内置 `bun:sqlite` 模块在配置的数据目录下创建 SQLite 数据库文件,并以 WAL 模式运行。数据库 schema MUST 支持 typed checker target 和结构化检查结果targets 表 MUST 包含 `grp` 列存储分组信息。
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表check_results 表包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idINTEGER NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREAL、status_detailTEXT、failureTEXT不包含 success 列
#### Scenario: 数据目录不存在
- **WHEN** 配置的数据目录路径不存在
- **THEN** 系统 SHALL 自动创建该目录
#### Scenario: 数据库已存在时启动
- **WHEN** 数据库文件已存在
- **THEN** 系统 SHALL 直接打开数据库,不重新建表
#### Scenario: 外键约束
- **THEN** 系统 SHALL 启用 `PRAGMA foreign_keys = ON`
#### Scenario: 级联删除
- **THEN** check_results 表的外键约束 SHALL 使用 `ON DELETE CASCADE`,确保删除目标时自动清理关联结果记录
### Requirement: targets 表同步
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示摘要、领域配置、调度配置、expect 配置和分组信息。
#### Scenario: 首次同步目标
- **WHEN** 数据库为空且 YAML 中定义了 N 个 typed target
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、type、target、config、interval_ms、timeout_ms、expect 和 grp
#### Scenario: 配置变更后重新同步
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
- **THEN** 系统 SHALL 根据 name 字段匹配:新增的插入、删除的移除、修改的更新(含 grp 字段)
### Requirement: check_results 表追加写入
系统 SHALL 将每次检查结果追加写入 check_results 表,不更新或删除已有记录。
#### Scenario: 写入检查结果
- **WHEN** 一次 checker 执行完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、status_detail、failure 的记录
#### Scenario: 写入结构化失败信息
- **WHEN** checker 执行失败或 expect 不匹配
- **THEN** 系统 SHALL 将首个失败原因序列化写入 failure 字段
### Requirement: 时间范围查询索引
系统 SHALL 在 check_results 表上创建 (target_id, timestamp) 复合索引,加速按目标和时间范围的查询。
#### Scenario: 查询某目标的历史记录
- **WHEN** 查询指定 target_id 的最近 N 条记录
- **THEN** 系统 SHALL 使用索引快速定位,无需全表扫描
### Requirement: 目标列表按分组排序
系统 SHALL 保证 targets 查询结果按分组排序返回。
#### Scenario: 分组排序查询
- **WHEN** 查询所有 targets
- **THEN** 结果 SHALL 将 "default" 分组目标排在首位,其余分组按 YAML 配置中首次出现的顺序(即 id 自增顺序)排列
### Requirement: 结构化采样数据查询
系统 SHALL 提供 `getRecentSamples` 方法替代 `getSparkline`,返回包含状态信息的结构化采样数据。
#### Scenario: 获取最近采样数据
- **WHEN** 调用 `getRecentSamples(targetId, 30)`
- **THEN** 系统 SHALL 返回最多 30 条记录,每条包含 timestamp、duration_ms、matched
#### Scenario: 采样数据排序
- **WHEN** 获取采样数据
- **THEN** 记录 SHALL 按 timestamp 降序排列(最新在前)
### Requirement: 趋势数据时间范围查询
系统 SHALL 支持按任意时间范围查询趋势聚合数据,替代固定 hours 参数。
#### Scenario: 按时间范围查询趋势
- **WHEN** 查询指定 target 在 from 到 to 时间范围内的趋势数据
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,包括每小时的 avgDurationMs、availability 和 totalChecks
### Requirement: 历史记录时间范围和分页查询
系统 SHALL 支持按时间范围筛选并分页查询历史记录。
#### Scenario: 按时间范围筛选历史记录
- **WHEN** 查询指定 target 在 from 到 to 时间范围内的历史记录
- **THEN** 系统 SHALL 返回该时间范围内的记录,按 timestamp 降序排列
#### Scenario: 分页查询历史记录
- **WHEN** 查询指定 page 和 pageSize 的历史记录
- **THEN** 系统 SHALL 返回对应页的数据和总记录数
### Requirement: 聚合查询支持
数据存储 SHALL 支持按时间段聚合查询用于计算可用率、平均耗时、P99 耗时等统计指标。
#### Scenario: 计算目标可用率
- **WHEN** 查询某目标在指定时间范围内的可用率
- **THEN** 系统 SHALL 返回 matched=true 的记录数占总记录数的百分比
#### Scenario: 计算目标平均耗时
- **WHEN** 查询某目标在指定时间范围内的平均耗时
- **THEN** 系统 SHALL 返回 duration_ms 的平均值(仅计算 matched=true 的记录)
#### Scenario: 按小时聚合趋势数据
- **WHEN** 查询某目标在指定时间范围内的趋势数据
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,包括每小时的平均耗时和可用率
#### Scenario: UP/DOWN 判定
- **THEN** 系统 SHALL 基于 latestCheck.matched 判定目标 UP 或 DOWNmatched=true 为 UPmatched=false 为 DOWN
### Requirement: 目标展示摘要持久化
数据存储 SHALL 为每个 target 持久化一个领域无关的展示摘要字段 `target`
#### Scenario: HTTP target 展示摘要
- **WHEN** 同步 HTTP target
- **THEN** targets.target SHALL 存储该 target 的 URL
#### Scenario: command target 展示摘要
- **WHEN** 同步 command target
- **THEN** targets.target SHALL 存储由 exec 和 args 组成的命令摘要
#### Scenario: HTTP target config 序列化
- **WHEN** 同步 HTTP target
- **THEN** targets.config SHALL 存储 JSON包含 url、method、headers、body、maxBodyBytes
#### Scenario: command target config 序列化
- **WHEN** 同步 command target
- **THEN** targets.config SHALL 存储 JSON包含 exec、args、cwd、env、maxOutputBytes

View File

@@ -1,141 +0,0 @@
## Purpose
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、expect 结果校验和结果持久化。
## Requirements
### Requirement: 按 interval 分组调度
系统 SHALL 将拨测目标按 interval 值分组,每组使用独立的定时器进行调度。
#### Scenario: 相同 interval 的目标共享定时器
- **WHEN** 多个 target 配置了相同的 interval如 30s
- **THEN** 系统 SHALL 使用同一个 `setInterval` 定时器,每次 tick 并发拨测所有该组目标
#### Scenario: 不同 interval 的目标各自调度
- **WHEN** target A 配置 15s intervaltarget B 配置 30s interval
- **THEN** 系统 SHALL 创建两个独立定时器,分别按各自频率调度
### Requirement: 组内并发拨测
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `runtime.maxConcurrentChecks` 限制。
#### Scenario: 同组目标并发执行
- **WHEN** 调度器触发一次 tick该组有 3 个目标,且全局并发余量至少为 3
- **THEN** 系统 SHALL 同时执行 3 个 checker而非顺序执行
#### Scenario: 单个目标失败不影响同组其他目标
- **WHEN** 同组中某个目标的检查请求超时或失败
- **THEN** 其他目标的检查 SHALL 正常完成并记录结果
#### Scenario: 全局并发限制生效
- **WHEN** 调度器同时触发 10 个目标且 runtime.maxConcurrentChecks 为 3
- **THEN** 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
### Requirement: HTTP 拨测执行
系统 SHALL 对 `type: http` 的目标执行 HTTP 请求,支持 GET、POST、PUT、DELETE、PATCH、HEAD 方法,并携带 `http.headers``http.body`
#### Scenario: 执行 GET 请求
- **WHEN** 目标配置 method 为 GET
- **THEN** 系统 SHALL 发送 GET 请求到目标 URL
#### Scenario: 执行 POST 请求带 body
- **WHEN** 目标配置 method 为 POST 且指定了 body 和 Content-Type header
- **THEN** 系统 SHALL 发送带指定 body 的 POST 请求
#### Scenario: 携带自定义 headers
- **WHEN** 目标配置了 headers如 Authorization
- **THEN** 系统 SHALL 在请求中包含所有配置的 headers
#### Scenario: HTTP body 读取上限
- **WHEN** HTTP response body 超过该 target 的 maxBodyBytes
- **THEN** 系统 MUST 停止读取并记录 `matched=false` 和结构化输出超限错误
### Requirement: 请求超时控制
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。
#### Scenario: HTTP 请求超时
- **WHEN** HTTP 请求在 timeout 时间内未收到响应
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
#### Scenario: command 执行超时
- **WHEN** command 进程在 timeout 时间内未退出
- **THEN** 系统 MUST 终止该子进程,记录为失败并标注超时错误
#### Scenario: 请求在超时前完成
- **WHEN** checker 在超时前完成执行
- **THEN** 系统 SHALL 正常记录执行结果并进入 expect 校验
### Requirement: expect 校验
系统 SHALL 在 checker 执行完成后根据目标类型的 expect 配置校验观测结果,校验结果和首个失败原因记入 check result。
#### Scenario: HTTP 默认状态码
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 按默认 `status: [200]` 校验响应状态码
#### Scenario: 校验 HTTP 状态码
- **WHEN** HTTP target 配置了 `expect.status: [200, 201]`
- **THEN** 系统 SHALL 检查响应状态码是否在列表中,将匹配结果记录到 matched 字段
#### Scenario: 校验 HTTP 响应头
- **WHEN** HTTP target 配置了 `expect.headers: {"Content-Type": {contains: "application/json"}}`
- **THEN** 系统 SHALL 检查响应头是否符合指定规则,全部匹配时继续后续阶段
#### Scenario: 校验 HTTP 响应体
- **WHEN** HTTP target 配置了有序 `expect.body` 规则数组
- **THEN** 系统 SHALL 按数组顺序执行 body 规则,任一失败立即记录 failure 并停止后续规则
#### Scenario: command 默认 exitCode
- **WHEN** command target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 按默认 `exitCode: [0]` 校验命令退出码
#### Scenario: 校验 command stdout
- **WHEN** command target 配置了有序 `expect.stdout` 规则数组
- **THEN** 系统 SHALL 按数组顺序执行 stdout 规则,任一失败立即记录 failure 并停止后续规则
#### Scenario: 校验耗时阈值
- **WHEN** 目标配置了 `expect.maxDurationMs`
- **THEN** 系统 SHALL 检查实际 durationMs 是否超过阈值,将匹配结果记录到 matched 字段
#### Scenario: 多条 expect 规则
- **WHEN** 目标同时配置状态、duration、元数据和内容规则
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true任一不通过则为 false 并记录首个失败原因
### Requirement: Body 校验按需解析
系统 SHALL 仅在 HTTP target 配置了 body 校验且 status、duration、headers 阶段均通过时才读取并解析响应体,避免不必要的读取和解析开销。
#### Scenario: status 失败时不读取 body
- **WHEN** HTTP target 的 status 阶段不匹配
- **THEN** 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response body
#### Scenario: 仅配置 contains 时不解析 JSON
- **WHEN** HTTP target 仅配置 body contains 规则而未配置 json/css/xpath 规则
- **THEN** 系统 SHALL 不执行 JSON.parse 或 HTML/XML 解析
#### Scenario: 配置 json 时解析 JSON 失败
- **WHEN** HTTP target 配置了 body json 规则但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false并记录 json 规则对应的 failure.path
### Requirement: 拨测结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、status_detail、failure 字段。
#### Scenario: 成功检查结果记录
- **WHEN** checker 成功执行且 expect 全部匹配
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、status_detailfailure 为 null
#### Scenario: 执行失败结果记录
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息
#### Scenario: expect 不匹配结果记录
- **WHEN** checker 执行成功但 expect 不匹配
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="mismatch" 和具体不匹配信息
### Requirement: runner 选择
系统 SHALL 根据 target.type 选择对应 runner 执行检查。
#### Scenario: 选择 HTTP runner
- **WHEN** target.type 为 `http`
- **THEN** 系统 SHALL 使用 HTTP runner 执行该目标
#### Scenario: 选择 command runner
- **WHEN** target.type 为 `command`
- **THEN** 系统 SHALL 使用 command runner 执行该目标

View File

@@ -1,80 +0,0 @@
## Purpose
定义将 Vite 前端资源与 Bun 后端打包为单个 standalone executable 的生产构建、运行配置和验证要求。
## Requirements
### Requirement: 生产构建顺序
生产构建 MUST 在编译 Bun 后端 executable 之前先构建 Vite 前端。
#### Scenario: 运行生产构建
- **WHEN** 开发者运行生产构建命令
- **THEN** 系统 MUST 在调用 Bun standalone executable 编译之前生成前端静态资源
#### Scenario: 前端构建失败
- **WHEN** 前端生产构建失败
- **THEN** 系统 MUST 停止生产构建,且不能输出 stale executable
### Requirement: 构建生成确定性
生产构建 SHALL 以稳定顺序生成嵌入静态资源清单,减少重复构建产生无意义差异。
#### Scenario: 生成静态资源清单
- **WHEN** 生产构建扫描 Vite 输出目录并生成嵌入资源模块
- **THEN** 资源条目 SHALL 按稳定顺序输出
#### Scenario: 重复构建相同前端产物
- **WHEN** Vite 输出内容未变化且生产构建重复运行
- **THEN** 生成的嵌入资源模块 SHALL 保持语义一致且不依赖文件系统遍历顺序
### Requirement: 单 executable 输出
生产构建 SHALL 输出一个 standalone executable其中包含 Bun 后端、必要 server 依赖和构建后的前端资源。构建成功后 SHALL 自动清理中间产物目录(`.build/`),构建失败时 SHALL 保留中间产物以便排查。
#### Scenario: 在目标机器运行 executable
- **WHEN** 生成的 executable 在兼容目标平台上运行
- **THEN** 它 SHALL 启动全栈应用,且不要求目标机器安装 Node.js、Bun、Vite 或 `node_modules`
#### Scenario: 服务嵌入的前端
- **WHEN** executable 收到前端根路径请求
- **THEN** 它 SHALL 从 executable 内包含的资源服务前端,且不需要外部 `dist/` 目录
#### Scenario: 服务嵌入 API 和页面
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/summary``/api/targets` 返回的数据
#### Scenario: 构建成功后清理中间产物
- **WHEN** 生产构建成功完成并输出 executable
- **THEN** 系统 SHALL 自动删除 `.build/` 目录及其所有内容
#### Scenario: 构建失败时保留中间产物
- **WHEN** 生产构建在任意步骤失败前端构建、中间产物生成、Bun 编译)
- **THEN** `.build/` 目录 SHALL 保留在磁盘上以供排查
### Requirement: 外部运行时配置
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。
#### Scenario: 修改监听端口
- **WHEN** 操作者修改受支持的 port 配置
- **THEN** 同一个 executable SHALL 在不重新构建的情况下监听新端口
#### Scenario: 缺少可选配置
- **WHEN** 可选运行时配置被省略
- **THEN** executable SHALL 使用文档化的默认值
### Requirement: 构建验证
项目 SHALL 提供验证,证明生产 executable 可以服务 API、健康检查、静态资源和 SPA fallback 路由,并且完整验证 MUST 针对当前源码重新构建后的 executable 运行。
#### Scenario: 验证 executable 路由
- **WHEN** 构建验证针对生成的 executable 运行
- **THEN** 它 SHALL 检查 `/api/summary``/api/targets``/health`、前端根路径、静态资源、未知 API、未知静态资源和前端 fallback 请求
#### Scenario: 验证生产模式和响应头
- **WHEN** 构建验证针对生成的 executable 运行
- **THEN** 它 SHALL 检查 API 响应处于 production runtime mode并验证代表性 HTML、JSON 和静态资源响应的缓存或低风险安全 headers
#### Scenario: 完整验证重新构建 executable
- **WHEN** 开发者运行完整验证命令
- **THEN** 系统 MUST 先基于当前源码执行生产构建,再对新生成的 executable 运行 smoke test
#### Scenario: 验证失败
- **WHEN** 任一代表性生产路由、响应头、生产模式或构建阶段检查失败
- **THEN** 验证 SHALL 使构建或测试命令失败

View File

@@ -1,79 +0,0 @@
## Purpose
定义 TanStack Query 数据层QueryClient 配置、queryKey 工厂、轮询策略、条件查询和开发调试面板。
## Requirements
### Requirement: TanStack Query 数据层
前端 SHALL 使用 TanStack Query@tanstack/react-query管理所有 API 请求,替代手写 fetch hooks。
#### Scenario: QueryClient 配置
- **WHEN** 应用启动
- **THEN** 系统 SHALL 创建 QueryClient默认配置 retry=1、refetchOnWindowFocus=true、staleTime=5000
#### Scenario: QueryClientProvider 挂载
- **WHEN** 应用渲染
- **THEN** 根组件 SHALL 包裹在 QueryClientProvider 中,提供 QueryClient 实例
### Requirement: queryKey 工厂
系统 SHALL 提供统一的 queryKey 工厂函数,确保 queryKey 的唯一性和一致性。
#### Scenario: summary queryKey
- **WHEN** 查询 summary 数据
- **THEN** queryKey SHALL 为 ["summary"]
#### Scenario: targets queryKey
- **WHEN** 查询 targets 数据
- **THEN** queryKey SHALL 为 ["targets"]
#### Scenario: trend queryKey
- **WHEN** 查询某目标的趋势数据
- **THEN** queryKey SHALL 为 ["trend", targetId, from, to]
#### Scenario: history queryKey
- **WHEN** 查询某目标的历史记录
- **THEN** queryKey SHALL 为 ["history", targetId, from, to, page]
### Requirement: Summary 轮询查询
系统 SHALL 使用 useQuery 实现总览统计的自动轮询。
#### Scenario: summary 自动轮询
- **WHEN** Dashboard 页面处于打开状态
- **THEN** 系统 SHALL 每 8 秒自动请求 /api/summary使用 refetchInterval=8000
#### Scenario: summary 后台刷新
- **WHEN** 页面处于后台标签页
- **THEN** 系统 SHALL 暂停轮询refetchIntervalInBackground=false
### Requirement: Targets 轮询查询
系统 SHALL 使用 useQuery 实现目标列表的自动轮询。
#### Scenario: targets 自动轮询
- **WHEN** Dashboard 页面处于打开状态
- **THEN** 系统 SHALL 每 8 秒自动请求 /api/targets使用 refetchInterval=8000
### Requirement: 条件查询
趋势和历史记录查询 SHALL 使用 enabled 条件控制,仅在目标被选中时触发。
#### Scenario: 未选中目标时不请求
- **WHEN** 用户未点击任何目标表格行
- **THEN** trend 和 history 的 useQuery SHALL enabled=false不发起请求
#### Scenario: 选中目标时自动请求
- **WHEN** 用户点击目标表格行
- **THEN** trend 和 history 的 useQuery SHALL enabled=true自动发起请求
#### Scenario: 时间范围变化时重新请求
- **WHEN** 用户更改时间范围
- **THEN** trend 和 history 的 useQuery SHALL 因 queryKey 变化自动重新请求
### Requirement: 开发调试面板
开发环境下 SHALL 挂载 TanStack Query Devtools。
#### Scenario: 开发环境显示 Devtools
- **WHEN** 应用在开发模式下运行
- **THEN** 页面 SHALL 显示 ReactQueryDevtools 浮动面板
#### Scenario: 生产环境排除 Devtools
- **WHEN** 应用在生产模式下构建
- **THEN** ReactQueryDevtools SHALL 不被包含在产物中

View File

@@ -1,128 +0,0 @@
## Purpose
定义目标详情 Drawer时间范围筛选TDesign RadioGroup + DateRangePicker、Tabs 组织概览/记录两个面板、统计图表和分页检查结果列表。
## Requirements
### Requirement: 目标详情 Drawer
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。
#### Scenario: 打开 Drawer
- **WHEN** 用户点击某个目标表格行
- **THEN** 系统 SHALL 从右侧滑出 Drawerplacement="right"),宽度为视口 60%
#### Scenario: Drawer 标题栏
- **WHEN** Drawer 渲染
- **THEN** 标题栏 SHALL 使用 TDesign Space 组件align="center")布局,包含 StatusDot、目标名称TDesign Typography.Text strong和类型标签TDesign Tag以及内建关闭按钮。不使用内联 style 的 flex 布局
#### Scenario: 关闭 Drawer
- **WHEN** 用户点击关闭按钮、ESC 键或遮罩层
- **THEN** Drawer SHALL 关闭
#### Scenario: Drawer 无底部按钮
- **WHEN** Drawer 渲染
- **THEN** Drawer SHALL 不显示底部操作栏footer={false}
#### Scenario: Drawer 数据同步
- **WHEN** Drawer 打开期间后台轮询刷新了 targets 数据
- **THEN** Drawer 中 selectedTarget 的状态 SHALL 随之同步更新
#### Scenario: 切换目标重置 Tab
- **WHEN** 用户从目标 A 切换到目标 B点击不同的表格行
- **THEN** Drawer SHALL 重置为概览 Tab使用 key={target.id} 确保组件状态不残留
#### Scenario: Drawer 内容区间距
- **WHEN** Drawer 内容渲染
- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom
### Requirement: 时间范围选择器
Drawer SHALL 在 Tabs 外层提供时间范围选择器,影响概览和记录两个面板的数据。时间选择器 SHALL 分两行显示:第一行为快捷按钮,第二行为日期时间范围选择器。
#### Scenario: 快捷时间按钮
- **WHEN** Drawer 渲染
- **THEN** 时间选择区第一行 SHALL 显示 TDesign RadioGroupvariant=default-filled快捷按钮1小时、6小时、24小时、7天
#### Scenario: 点击快捷按钮
- **WHEN** 用户点击快捷按钮(如 "24小时"
- **THEN** 系统 SHALL 自动设置对应的起止时间DateRangePicker 显示对应的时间范围,该按钮高亮
#### Scenario: 自定义日期时间范围
- **WHEN** 用户通过 TDesign DateRangePickermode=date, enableTimePicker, format="YYYY-MM-DD HH:mm")修改时间范围
- **THEN** 快捷按钮 SHALL 取消高亮,系统重新请求对应时间范围的数据
#### Scenario: 时间精度为分钟级
- **WHEN** 用户通过 DateRangePicker 选择时间
- **THEN** 选择器 SHALL 仅精确到分钟format="YYYY-MM-DD HH:mm"),秒列固定为 00
#### Scenario: DateRangePicker 全宽显示
- **WHEN** Drawer 渲染
- **THEN** DateRangePicker SHALL 通过 CSS 类 `.full-width` 占满时间选择区第二行的宽度,不使用内联 style 的 width: 100%
#### Scenario: 默认时间范围
- **WHEN** Drawer 打开
- **THEN** 时间选择器 SHALL 默认选中 "24小时" 快捷按钮
#### Scenario: 筛选触发数据刷新
- **WHEN** 时间范围发生变化
- **THEN** 系统 SHALL 重新请求趋势数据和历史记录
### Requirement: Tabs 内容组织
Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。TabPanel 内边距通过 className prop 控制。
#### Scenario: Tab 标签
- **WHEN** Drawer 渲染
- **THEN** Tabs SHALL 显示两个标签:概览、记录
#### Scenario: Tab 面板内边距
- **WHEN** TabPanel 渲染
- **THEN** TabPanel SHALL 通过 `className` prop 传入自定义类名(`tab-panel-padded`)控制内边距,不通过入侵 TDesign 内部类名(`.t-tab-panel`)覆盖
### Requirement: 概览面板
概览 Tab SHALL 按区域展示目标统计摘要、趋势图、状态分布和基本信息,每个区域使用 TDesign Divider 组件作为小标题分隔。
#### Scenario: 区域排列顺序
- **WHEN** 概览面板渲染
- **THEN** 面板 SHALL 按以下顺序展示区域:统计 → 趋势 → 状态分布 → 基本信息,每个区域前 SHALL 显示 TDesign Divideralign="left")作为小标题,不使用内联 style 的 h4 标签
#### Scenario: 区域间距
- **WHEN** 概览面板渲染
- **THEN** 各区域之间的间距 SHALL 通过 TDesign Space 组件direction="vertical")统一管理,不使用内联 style 的 margin
#### Scenario: 统计数值卡片
- **WHEN** 概览面板渲染
- **THEN** 面板 SHALL 在"统计"区域使用 TDesign Statistic 组件展示 4 个统计值总检查color=blue、正常color=green、异常color=red、可用率color=green, suffix="%"),使用 TDesign Row/Col 横向排列。Row 的外层间距 SHALL 通过 TDesign Space 或 CSS 类控制,不使用内联 style
#### Scenario: 趋势折线图
- **WHEN** 概览面板渲染且趋势数据可用
- **THEN** 面板 SHALL 在"趋势"区域展示 recharts 双 Y 轴折线图TrendChart耗时线--td-brand-color和可用率线--td-success-color
#### Scenario: 趋势数据加载中
- **WHEN** 概览面板渲染且趋势数据正在加载
- **THEN** "趋势"区域 SHALL 显示 TDesign Skeleton 加载占位
#### Scenario: 状态分布环形图
- **WHEN** 概览面板渲染
- **THEN** 面板 SHALL 在"状态分布"区域展示 recharts 环形图StatusDonut外圈显示 UP/DOWN 比例,中间显示可用率百分比
#### Scenario: 元信息展示
- **WHEN** 概览面板渲染
- **THEN** 面板 SHALL 在"基本信息"区域使用 TDesign Descriptions 组件展示目标元信息:目标地址、检查间隔、最新检查时间、状态详情
### Requirement: 记录面板
记录 Tab SHALL 展示分页检查结果列表,使用 TDesign PrimaryTable。
#### Scenario: 检查结果表格
- **WHEN** 记录面板渲染且数据可用
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果列包含状态StatusDot 圆点、时间YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位单元格仅显示数值居中对齐、详情statusDetail 和 failure.message 用冒号拼接)
#### Scenario: 服务端分页
- **WHEN** 检查结果总数超过一页
- **THEN** 表格 SHALL 使用内建 paginationdisableDataPage=true分页器显示在表格底部
#### Scenario: 翻页触发请求
- **WHEN** 用户切换分页页码
- **THEN** 系统 SHALL 请求对应页码的服务端数据,表格更新
#### Scenario: 记录数据加载中
- **WHEN** 历史记录正在加载
- **THEN** 表格 SHALL 显示 loading 状态

View File

@@ -1,45 +0,0 @@
## Purpose
定义 target 分组能力YAML 配置中的 group 字段、后端存储、API 传递和前端分组排序。
## Requirements
### Requirement: target 分组配置
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段,用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。
#### Scenario: 配置分组名称
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
#### Scenario: 不配置分组
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
#### Scenario: group 字段类型校验
- **WHEN** YAML 配置中某个 target 的 `group` 字段不是字符串
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
### Requirement: 分组排序
系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。
#### Scenario: default 分组排最前
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
#### Scenario: 自定义分组按出现顺序
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
### Requirement: 分组信息 API 传递
系统 SHALL 在 API 响应中返回每个 target 的分组信息。
#### Scenario: targets 列表包含分组
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
### Requirement: 分组存储
系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
#### Scenario: 持久化分组信息
- **WHEN** 系统同步 targets 到数据库
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`

View File

@@ -1,110 +0,0 @@
## Purpose
定义分组表格的列配置、排序、筛选、行交互和 DOWN 行视觉强化。
## Requirements
### Requirement: 分组表格展示
Dashboard SHALL 按 group 字段将目标分组,每个分组渲染一个独立的 TDesign PrimaryTable分组间使用 TDesign Space 垂直排列。
#### Scenario: 按分组渲染独立表格
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面 SHALL 按 group 字段将目标分组,每个分组包含带统计的分组标题和一个独立 PrimaryTable
#### Scenario: 分组顺序
- **WHEN** 页面渲染多个分组
- **THEN** "default" 分组 SHALL 排在最上面,其余分组按 YAML 配置中首次出现的顺序排列
#### Scenario: 分组标题统计标签
- **WHEN** 页面渲染某个分组的标题
- **THEN** 标题 SHALL 使用 TDesign Tag 组件显示分组名称和三个统计标签总数theme=primary, variant=light、正常数theme=success, variant=light、异常数theme=danger, variant=light
#### Scenario: "default" 分组显示名称
- **WHEN** 分组名称为 "default"
- **THEN** 分组标题 SHALL 显示 "默认分组"
#### Scenario: Dashboard 容器占满宽度
- **WHEN** 用户打开 Dashboard 页面
- **THEN** Dashboard 容器 SHALL 占满浏览器宽度,不设置 max-width 限制
#### Scenario: 分组间统一间距
- **WHEN** 页面渲染多个分组
- **THEN** 分组之间 SHALL 使用 TDesign Space 组件direction=vertical, size=32px统一间距
### Requirement: 表格列定义
每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。列渲染不使用内联 style。
#### Scenario: 状态列
- **WHEN** 表格渲染
- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60pxfixed="left"居中对齐支持筛选UP/DOWN/全部。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style
#### Scenario: 名称列
- **WHEN** 表格渲染
- **THEN** 名称列 SHALL 显示目标名称支持字母排序zh-CNellipsis 超长名称自动省略并 Tooltip 显示全名
#### Scenario: 类型列
- **WHEN** 表格渲染
- **THEN** 类型列 SHALL 使用 TDesign Tag 组件size=small, theme=primary, variant=light-outline显示类型名称支持单选筛选
#### Scenario: 可用率列
- **WHEN** 表格渲染
- **THEN** 可用率列 SHALL 使用 TDesign Progress 组件theme=line, size=small渲染颜色通过 CSS 自定义属性 `--avail-N`(基于项目自定义色值)控制,每 10% 一档label 显示百分比数值支持排序升序优先最差排最前。color-threshold 函数 SHALL 返回 CSS 自定义属性引用而非硬编码色值
#### Scenario: 最近状态列
- **WHEN** 表格渲染
- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染 30 格采样色块,宽度 220px。StatusBar SHALL 通过 CSS 类(`.status-bar-block--up` / `.status-bar-block--down` / `.status-bar-block--empty`)控制色块颜色,不使用内联 style
#### Scenario: 延迟列
- **WHEN** 表格渲染
- **THEN** 延迟列 SHALL 显示最近一次检查的延迟毫秒数,右对齐。颜色 SHALL 通过 CSS 类实现≤100ms 使用 `.latency-ok`、100-500ms 使用 `.latency-warn`、>500ms 使用 `.latency-error`。无数据 SHALL 使用 `.text-disabled` 类显示 "-",数值 SHALL 使用 `.tabular-nums` 类等宽显示。不使用内联 style
#### Scenario: 间隔列
- **WHEN** 表格渲染
- **THEN** 间隔列 SHALL 显示检查间隔(如 "5s"、"30s"),居中对齐,宽度 72px
### Requirement: 默认排序
表格 SHALL 默认按状态降序排列异常DOWN目标排在最前面。
#### Scenario: 页面初始排序
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 每个分组表格 SHALL 默认按状态降序排列DOWN 目标排在同组最前面
### Requirement: DOWN 行视觉强化
表格中状态为 DOWN 的行 SHALL 具有视觉区分,使用安全 CSS 选择器实现。
#### Scenario: DOWN 行背景色
- **WHEN** 目标最近一次检查 matched=false
- **THEN** 该行 SHALL 通过 `.t-table tr.row-down` CSS 选择器获得浅红色背景(`--td-error-color-light`),不使用 `!important`
#### Scenario: DOWN 行 hover 状态
- **WHEN** 鼠标悬停在 DOWN 行上
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色,与正常行 hover 效果协调
### Requirement: 行点击交互
表格行 SHALL 支持点击打开目标详情 Drawer。
#### Scenario: 点击行打开 Drawer
- **WHEN** 用户点击某一行
- **THEN** 系统 SHALL 打开该目标的详情 Drawer
#### Scenario: 行 hover 效果
- **WHEN** 鼠标悬停在表格行上
- **THEN** 行 SHALL 显示 hover 高亮效果TDesign Table hover prop
#### Scenario: 行 cursor 样式
- **WHEN** 鼠标悬停在表格行上
- **THEN** cursor SHALL 显示为 pointer
### Requirement: 表格外观
表格 SHALL 使用 TDesign PrimaryTable 统一外观。
#### Scenario: 表格样式
- **WHEN** 表格渲染
- **THEN** 表格 SHALL 设置 size="small"、stripe、hover、bordered
### Requirement: 列定义复用
所有分组的表格 SHALL 共享同一套列定义常量。
#### Scenario: 列定义提取为常量
- **WHEN** 多个分组表格渲染
- **THEN** 列定义 SHALL 从独立的 constants/target-table-columns.tsx 导入,不在组件中重复定义

View File

@@ -1,42 +0,0 @@
## Purpose
定义目标类型Target Type的前端显示名称映射系统支持从后端类型标识符到 TDesign Tag 组件展示的可扩展转换。
## Requirements
### Requirement: 类型显示名称映射
系统 SHALL 提供目标类型到显示名称的映射,将后端类型标识符转换为 TDesign Tag 组件的展示文本。
#### Scenario: HTTP 类型显示
- **WHEN** 目标类型为 "http"
- **THEN** 前端 SHALL 使用 TDesign Tag 组件size=small, theme=primary, variant=light-outline显示 "HTTP"
#### Scenario: Command 类型显示
- **WHEN** 目标类型为 "command"
- **THEN** 前端 SHALL 使用 TDesign Tag 组件显示 "CMD"
#### Scenario: 未知类型处理
- **WHEN** 目标类型不在映射表中
- **THEN** 前端 SHALL 将类型名称转换为大写显示在 TDesign Tag 组件中
### Requirement: 映射可扩展性
类型映射系统 SHALL 支持后续新增类型,无需修改多处代码。
#### Scenario: 新增类型映射
- **WHEN** 需要新增目标类型(如 "tcp"、"dns"、"grpc"
- **THEN** 开发者 SHALL 仅需在映射常量中添加一条记录
#### Scenario: 映射单一数据源
- **WHEN** 前端组件需要显示目标类型
- **THEN** 组件 SHALL 调用统一的映射函数,不直接硬编码映射逻辑
### Requirement: 类型安全
类型映射系统 SHALL 提供类型安全的访问方式。
#### Scenario: TypeScript 类型推导
- **WHEN** 使用映射常量
- **THEN** TypeScript SHALL 能够推导出正确的类型(使用 `as const`
#### Scenario: 运行时安全
- **WHEN** 传入无效类型
- **THEN** 系统 SHALL 返回 fallback 值,不抛出异常

View File

@@ -1,55 +1,77 @@
{
"name": "dial-server",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "bun run scripts/dev.ts",
"dev:server": "bun --watch src/server/dev.ts",
"dev:web": "bunx --bun vite --host 127.0.0.1",
"build:web": "bunx --bun vite build",
"dev:web": "bunx --bun vite --host",
"build": "bun run scripts/build.ts",
"start": "bun src/server/dev.ts",
"lint": "eslint .",
"format": "prettier . --write",
"format:check": "prettier . --check",
"check": "bun run typecheck && bun run lint && bun run format:check && bun test",
"verify": "bun run check && bun run build && bun run test:smoke",
"schema": "bun run scripts/generate-config-schema.ts",
"schema:check": "bun run scripts/generate-config-schema.ts --check",
"check": "bun run schema:check && bun run typecheck && bun run lint && bun test",
"verify": "bun run check && bun run build",
"test": "bun test",
"test:smoke": "bun run scripts/smoke.ts",
"clean": "bun run scripts/clean.ts",
"release": "bun run scripts/release.ts",
"typecheck": "tsc --noEmit",
"prepare": "husky"
"prepare": "husky",
"version:patch": "bun run scripts/bump-version.ts patch",
"version:minor": "bun run scripts/bump-version.ts minor",
"version:major": "bun run scripts/bump-version.ts major",
"version:set": "bun run scripts/bump-version.ts set"
},
"devDependencies": {
"@commitlint/cli": "^21.0.0",
"@commitlint/config-conventional": "^21.0.0",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@eslint/js": "^10.0.1",
"@tanstack/react-query-devtools": "^5.100.10",
"@types/bun": "^1.3.13",
"@testing-library/react": "^16.3.2",
"@types/bun": "^1.3.14",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@types/tar-stream": "^3.1.4",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-perfectionist": "^5.9.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.4",
"prettier": "^3.8.3",
"tar-stream": "^3.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.11"
"typescript-eslint": "^8.59.3",
"vite": "^8.0.13"
},
"dependencies": {
"@ai-sdk/anthropic": "^3",
"@ai-sdk/openai": "^3",
"@number-flow/react": "^0.6.0",
"@sinclair/typebox": "^0.34.49",
"@tanstack/react-query": "^5.100.10",
"@xmldom/xmldom": "^0.9.10",
"ai": "^6",
"ajv": "^8.20.0",
"cheerio": "^1.2.0",
"croner": "^10.0.1",
"es-toolkit": "^1.46.1",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"pino-roll": "^4.0.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"recharts": "^3.8.1",
"systeminformation": "^5.31.6",
"tdesign-icons-react": "^0.6.4",
"tdesign-react": "^1.16.9",
"xpath": "^0.0.34"

8106
probe-config.schema.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,67 @@
# yaml-language-server: $schema=./probe-config.schema.json
server:
host: "127.0.0.1"
port: 3000
dataDir: "/tmp/probes_data"
listen:
host: "127.0.0.1"
port: "${server_port|3000}"
storage:
dataDir: "/tmp/probes_data"
# logging:
# level: "info"
# console:
# level: "info"
# file:
# level: "info"
# path: "/var/log/dial/dial.log"
# rotation:
# size: "50MB"
# frequency: "daily"
# maxFiles: 14
runtime:
maxConcurrentChecks: 20
probes:
execution:
maxConcurrentChecks: "${max_checks|20}"
defaults:
interval: "30s"
timeout: "10s"
http:
method: GET
maxBodyBytes: "10MB"
command:
maxOutputBytes: "1MB"
variables:
env_name: "演示"
httpbin_base: "https://httpbin.org"
api_token: "Bearer demo-token"
max_checks: 20
server_port: 3000
sqlite_url: "sqlite://:memory:"
targets:
# ========== HTTP targets ==========
- name: "Baidu 首页可用"
- id: "baidu-home"
name: "Baidu 首页可用"
description: "监控百度首页的可用性和响应时间"
type: http
group: "搜索引擎"
http:
url: "https://www.baidu.com"
expect:
status: [200]
maxDurationMs: 5000
durationMs:
lte: 5000
- name: "JSON API — 完整流水线"
- id: "httpbin-json"
name: "${env_name} JSON API — 完整流水线"
type: http
group: "后端服务"
interval: "1m"
timeout: "15s"
http:
url: "https://httpbin.org/json"
url: "${httpbin_base}/json"
headers:
Accept: "application/json"
Authorization: "${api_token|Bearer fallback-token}"
expect:
headers:
Content-Type:
contains: "application/json"
maxDurationMs: 8000
durationMs:
lte: 8000
body:
- json:
path: "$.slideshow.title"
@@ -48,38 +69,13 @@ targets:
- json:
path: "$.slideshow.slides[0].title"
contains: "Wake"
- json:
path: "$.slideshow.slides[0].type"
equals: "all"
- regex: '"title"'
- name: "HTML 页面 — CSS 选择器"
- id: "httpbin-post"
name: "POST 接口测试"
type: http
http:
url: "https://httpbin.org/html"
expect:
body:
- css:
selector: "h1"
contains: "Moby-Dick"
- css:
selector: "body"
exists: true
- name: "HTML 页面 — XPath 提取节点文本"
type: http
http:
url: "https://httpbin.org/html"
expect:
body:
- xpath:
path: "/html/body/h1/text()"
contains: "Melville"
- name: "POST 接口测试"
type: http
http:
url: "https://httpbin.org/post"
url: "${httpbin_base}/post"
method: POST
headers:
Content-Type: "application/json"
@@ -94,119 +90,291 @@ targets:
path: "$.json.version"
gte: 1
- name: "请求头验证"
type: http
http:
url: "https://httpbin.org/headers"
headers:
X-Custom-Header: "dial-server"
expect:
status: [200]
body:
- json:
path: "$.headers.X-Custom-Header"
equals: "dial-server"
# ========== Cmd targets ==========
- name: "响应头自定义校验"
type: http
http:
url: "https://httpbin.org/response-headers"
headers:
accept: "application/json"
expect:
body:
- json:
path: "$.Content-Type"
equals: "application/json"
- name: "多状态码允许"
type: http
http:
url: "https://httpbin.org/status/200"
expect:
status: [200, 201, 204]
# ========== Command targets ==========
- name: "uname 输出匹配"
type: command
- id: "bun-version"
name: "Bun 版本输出匹配"
type: cmd
group: "系统检查"
command:
exec: "uname"
args: ["-s"]
cmd:
exec: "bun"
args: ["--version"]
expect:
exitCode: [0]
stdout:
- match: "^[A-Z][a-z]+$"
- regex: "^\\d+\\.\\d+\\.\\d+"
- name: "echo 自定义文本输出"
type: command
command:
exec: "echo"
args: ["check ok"]
expect:
stdout:
- equals: "check ok\n"
maxDurationMs: 3000
- name: "ls 目录无 stderr"
type: command
command:
exec: "ls"
args: ["/tmp"]
cwd: "/"
expect:
exitCode: [0]
stderr:
- empty: true
- name: "date 输出包含年份"
type: command
command:
exec: "date"
args: ["+%Y"]
expect:
stdout:
- match: "^20\\d{2}\n?$"
- name: "wc 行数计数"
type: command
command:
exec: "wc"
args: ["-l"]
cwd: "/etc"
env:
LANG: "C"
expect:
stdout:
- match: "\\d+"
- name: "hostname 非空输出"
type: command
command:
exec: "hostname"
expect:
stdout:
- match: ".+"
- name: "多规则 stdout 顺序校验"
type: command
- id: "bun-stdout-rules"
name: "多规则 stdout 顺序校验"
type: cmd
interval: "5m"
command:
exec: "echo"
args: ["version: 2.0.1, status: healthy"]
cmd:
exec: "bun"
args: ["-e", "console.log('version: 2.0.1, status: healthy')"]
expect:
stdout:
- contains: "version:"
- match: "\\d+\\.\\d+\\.\\d+"
- regex: "\\d+\\.\\d+\\.\\d+"
- contains: "healthy"
- name: "stderr 内容检查"
type: command
command:
exec: "ls"
args: ["/nonexistent-path-checker-test"]
- id: "bun-stderr"
name: "stderr 内容检查"
type: cmd
cmd:
exec: "bun"
args: ["-e", "process.stderr.write('simulated error\\n'); process.exit(1)"]
expect:
exitCode: [0, 1, 2]
exitCode: [1]
stderr:
- contains: "No such file"
- contains: "simulated error"
# ========== DB targets ==========
- id: "sqlite-connect"
name: "SQLite 内存数据库连接测试"
type: db
group: "数据库"
db:
url: "${sqlite_url}"
expect:
durationMs:
lte: 1000
- id: "sqlite-query"
name: "SQLite 内存数据库多列结果校验"
type: db
db:
url: "${sqlite_url}"
query: "SELECT 1 as id, 'Alice' as name, 'engineer' as role"
expect:
rowCount: 1
rows:
- id:
gte: 1
name:
exists: true
role:
contains: "engineer"
result:
- json:
path: "$.rows[0].role"
equals: "engineer"
# ========== TCP targets ==========
- id: "redis-port"
name: "Redis 端口可达"
type: tcp
group: "基础设施"
tcp:
host: "127.0.0.1"
port: 6379
expect:
durationMs:
lte: 3000
- id: "smtp-banner"
name: "SMTP Banner 探测"
type: tcp
group: "基础设施"
tcp:
host: "127.0.0.1"
port: 25
readBanner: true
bannerReadTimeout: 3000
expect:
banner:
- contains: "ESMTP"
# ========== ICMP targets ==========
- id: "gateway-icmp"
name: "网关 ICMP 可达"
type: icmp
group: "基础设施"
icmp:
host: "127.0.0.1"
count: 3
packetSize: 56
expect:
alive: true
packetLossPercent:
lte: 10
avgLatencyMs:
lte: 100
maxLatencyMs:
lte: 300
durationMs:
lte: 5000
# ========== DNS targets ==========
# 本机 DNS 解析检查system 模式)
- id: "dns-system-localhost"
name: "本机 DNS 解析"
type: dns
group: "DNS"
dns:
resolver: system
name: "localhost"
family: ipv4
expect:
values:
exact:
- "127.0.0.1"
durationMs:
lte: 200
# DNS server 拨测server 模式A 记录)
- id: "dns-server-cf"
name: "Cloudflare DNS A 记录"
type: dns
group: "DNS"
dns:
resolver: server
server: "1.1.1.1"
name: "example.com"
recordType: A
expect:
rcode: ["NOERROR"]
ttlMin:
gte: 60
durationMs:
lte: 500
# 负向 DNS 检查NXDOMAIN
- id: "dns-nxdomain-check"
name: "负向 DNS 检查"
type: dns
group: "DNS"
dns:
resolver: server
server: "1.1.1.1"
name: "this-domain-should-not-exist.example.com"
recordType: A
expect:
rcode: ["NXDOMAIN"]
# MX 记录检查
- id: "dns-mx-check"
name: "MX 记录检查"
type: dns
group: "DNS"
dns:
resolver: server
server: "1.1.1.1"
name: "gmail.com"
recordType: MX
expect:
rcode: ["NOERROR"]
# ========== UDP targets ==========
- id: "udp-heartbeat"
name: "UDP 心跳检测"
type: udp
group: "基础设施"
udp:
host: "127.0.0.1"
port: 9000
payload: "PING"
expect:
response:
- contains: "PONG"
durationMs:
lte: 100
- id: "udp-binary-probe"
name: "UDP 二进制协议探测"
type: udp
group: "基础设施"
udp:
host: "127.0.0.1"
port: 5683
payload: "400100"
encoding: hex
responseEncoding: hex
expect:
responseSize:
gte: 4
durationMs:
lte: 200
- id: "udp-fire-and-forget"
name: "UDP 发送验证(不等待响应)"
type: udp
group: "基础设施"
udp:
host: "127.0.0.1"
port: 514
payload: "<14>health check"
expect:
responded: false
- id: "llm-openai-probe"
name: "OpenAI Chat Completions 健康检查"
type: llm
group: "AI 服务"
llm:
provider: openai
url: "https://open.bigmodel.cn/api/paas/v4"
model: "glm-4.7-flash"
prompt: "Say OK"
key: "d1e97306540d12bb2f834be961fcacb1.SNBShlCxWYJCx0qZ"
expect:
status:
- 200
finishReason: "stop"
output:
- contains: "OK"
# ========== WS targets ==========
- id: "ws-reachability"
name: "WebSocket 服务可达"
type: ws
group: "基础设施"
ws:
url: "wss://echo.websocket.org"
expect:
durationMs:
lte: 5000
- id: "ws-echo-check"
name: "WebSocket Echo 交互检查"
type: ws
group: "基础设施"
ws:
url: "wss://echo.websocket.org"
send: "hello"
receiveTimeout: 3000
expect:
message:
- contains: "hello"
durationMs:
lte: 5000
- id: "local-cpu"
name: "本机 CPU"
type: cpu
group: "基础设施"
interval: "30s"
timeout: "5s"
cpu:
sampleDuration: "1s"
expect:
usagePercent:
lte: 85
maxCoreUsagePercent:
lte: 95
- id: "local-memory"
name: "本机内存"
type: mem
group: "基础设施"
interval: "30s"
timeout: "5s"
mem: {}
expect:
usagePercent:
lte: 85

127
scripts/build-common.ts Normal file
View File

@@ -0,0 +1,127 @@
import { readdir, rm, writeFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { validateVersion } from "./bump-version-logic";
export const projectRoot = fileURLToPath(new URL("..", import.meta.url));
export const distWebDir = join(projectRoot, "dist/web");
export const buildDir = join(projectRoot, ".build");
export const packageJsonPath = join(projectRoot, "package.json");
export async function cleanup() {
await rm(buildDir, { force: true, recursive: true });
}
export async function codeGeneration() {
console.log("Step 2/3: Code generation...");
await rm(buildDir, { force: true, recursive: true });
await Bun.write(join(buildDir, ".gitkeep"), "");
const packageJson = (await Bun.file(packageJsonPath).json()) as { version: string };
const version = packageJson.version;
if (typeof version !== "string") {
console.error("package.json does not have a valid version field");
process.exit(1);
}
validateVersion(version);
const allFiles = await scanDir(distWebDir, "/");
const importLines: string[] = [];
const fileEntries: string[] = [];
let indexHtmlVar = "";
for (let i = 0; i < allFiles.length; i++) {
const urlPath = allFiles[i]!;
const varName = `f${i}`;
const filePath = toImportSpecifier(buildDir, join(distWebDir, urlPath.slice(1)));
importLines.push(`import ${varName} from "./${filePath}" with { type: "file" };`);
if (urlPath === "/index.html") {
indexHtmlVar = varName;
} else {
fileEntries.push(` "${urlPath}": Bun.file(${varName}),`);
}
}
if (!indexHtmlVar) {
console.error("index.html not found in dist/web/");
process.exit(1);
}
const staticAssetsTs = [
`import type { StaticAssets } from "../src/server/static";`,
"",
...importLines,
"",
`export const staticAssets: StaticAssets = {`,
` files: {`,
...fileEntries,
` },`,
` indexHtml: Bun.file(${indexHtmlVar}),`,
`};`,
"",
].join("\n");
await writeFile(join(buildDir, "static-assets.ts"), staticAssetsTs);
const serverEntryTs = [
`import { bootstrap } from "../src/server/bootstrap";`,
`import { readRuntimeConfig } from "../src/server/config";`,
`import { staticAssets } from "./static-assets";`,
"",
`const APP_VERSION = "${version}" as const;`,
"",
`async function main() {`,
` const { configPath } = readRuntimeConfig();`,
` await bootstrap({ configPath, mode: "production", staticAssets, version: APP_VERSION });`,
`}`,
"",
`void main().catch((error) => {`,
` console.error("启动失败:", error instanceof Error ? error.message : error);`,
` process.exit(1);`,
`});`,
"",
].join("\n");
await writeFile(join(buildDir, "server-entry.ts"), serverEntryTs);
return version;
}
export async function scanDir(dir: string, prefix: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const paths: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const urlPath = `${prefix}${entry.name}`;
if (entry.isDirectory()) {
paths.push(...(await scanDir(fullPath, `${urlPath}/`)));
} else {
paths.push(urlPath);
}
}
return paths;
}
export function toImportSpecifier(
fromDir: string,
targetPath: string,
relativePath: (from: string, to: string) => string = relative,
) {
return relativePath(fromDir, targetPath).replaceAll("\\", "/");
}
export async function viteBuild() {
console.log("Step 1/3: Vite build...");
const proc = Bun.spawn(["bunx", "--bun", "vite", "build"], {
cwd: projectRoot,
stderr: "inherit",
stdout: "inherit",
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.error("Vite build failed");
process.exit(1);
}
}

View File

@@ -1,144 +1,52 @@
import { $ } from "bun";
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
import { dirname, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { rm } from "node:fs/promises";
import { join } from "node:path";
const buildDir = fileURLToPath(new URL("../.build/", import.meta.url));
const webDistDir = fileURLToPath(new URL("../dist/web/", import.meta.url));
const executablePath = fileURLToPath(new URL("../dist/dial-server", import.meta.url));
const generatedAssetsPath = fileURLToPath(new URL("../.build/static-assets.ts", import.meta.url));
const generatedEntryPath = fileURLToPath(new URL("../.build/server-entry.ts", import.meta.url));
import { buildDir, cleanup, codeGeneration, projectRoot, viteBuild } from "./build-common";
await rm(buildDir, { force: true, recursive: true });
await rm(executablePath, { force: true });
await mkdir(buildDir, { recursive: true });
const executablePath = join(projectRoot, "dist/dial-server");
await $`bunx --bun vite build`;
const files = await listFiles(webDistDir);
const indexPath = files.find((file) => normalize(relative(webDistDir, file)) === "index.html");
if (!indexPath) {
throw new Error("Vite build 未生成 dist/web/index.html");
async function build() {
try {
await viteBuild();
await codeGeneration();
await bunCompile();
await cleanup();
console.log(`Built executable: ${executablePath}`);
} catch (error) {
await cleanup();
console.error("Build failed:", error);
process.exit(1);
}
}
const assetFiles = files.filter((file) => file !== indexPath);
await writeGeneratedAssets(indexPath, assetFiles);
await writeGeneratedEntry();
const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"];
const result = await Bun.build({
compile: target
? {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
target: target as Bun.Build.CompileTarget,
}
: {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
},
entrypoints: [generatedEntryPath],
minify: true,
sourcemap: "linked",
});
if (!result.success) {
async function bunCompile() {
console.log("Step 3/3: Bun compile...");
await rm(executablePath, { force: true });
throw new Error("Bun executable 构建失败");
}
console.log(`Built executable: ${executablePath}`);
await rm(buildDir, { force: true, recursive: true });
async function listFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const path = `${directory.replace(/\/$/, "")}/${entry.name}`;
if (entry.isDirectory()) {
return listFiles(path);
}
return [path];
}),
);
return files.flat().sort((left, right) => normalize(left).localeCompare(normalize(right)));
}
function normalize(path: string): string {
return path.split(sep).join("/");
}
function toImportPath(path: string): string {
const rel = normalize(relative(buildDir, path));
return rel.startsWith(".") ? rel : `./${rel}`;
}
async function writeGeneratedAssets(indexPath: string, assetFiles: string[]) {
const imports = [
`import type { StaticAssets } from "../src/server/app";`,
`import indexPath from "${toImportPath(indexPath)}" with { type: "file" };`,
...assetFiles.map((file, index) => `import asset${index}Path from "${toImportPath(file)}" with { type: "file" };`),
];
const assetEntries = assetFiles.map((file, index) => {
const urlPath = `/${normalize(relative(webDistDir, file))}`;
return ` ${JSON.stringify(urlPath)}: Bun.file(asset${index}Path),`;
const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"];
const result = await Bun.build({
compile: target
? {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
target: target as Bun.Build.CompileTarget,
}
: {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
},
entrypoints: [join(buildDir, "server-entry.ts")],
minify: true,
sourcemap: "linked",
});
const source = `${imports.join("\n")}
export const staticAssets: StaticAssets = {
indexHtml: Bun.file(indexPath),
files: {
${assetEntries.join("\n")}
},
};
`;
await mkdir(dirname(generatedAssetsPath), { recursive: true });
await writeFile(generatedAssetsPath, source);
if (!result.success) {
console.error("Bun compile failed:", result.logs);
await cleanup();
process.exit(1);
}
}
async function writeGeneratedEntry() {
await writeFile(
generatedEntryPath,
`import { loadConfig } from "../src/server/checker/config-loader";
import { ProbeStore } from "../src/server/checker/store";
import { ProbeEngine } from "../src/server/checker/engine";
import { startServer } from "../src/server/server";
import { readRuntimeConfig } from "../src/server/config";
import { registerCheckers } from "../src/server/checker/runner";
import { staticAssets } from "./static-assets";
async function main() {
registerCheckers();
const { configPath } = readRuntimeConfig();
const config = await loadConfig(configPath);
const store = new ProbeStore(config.dataDir + "/probe.db");
store.syncTargets(config.targets);
const engine = new ProbeEngine(store, config.targets, config.maxConcurrentChecks);
engine.start();
startServer({
config: { host: config.host, port: config.port },
mode: "production",
staticAssets,
store,
});
}
void main().catch((error) => {
console.error("启动失败:", error instanceof Error ? error.message : error);
process.exit(1);
});
`,
);
}
await build();

View File

@@ -0,0 +1,40 @@
const VERSION_REGEX = /^\d+\.\d+\.\d+$/;
export function bumpVersion(current: string, command: "major" | "minor" | "patch" | "set", target?: string): string {
validateVersion(current);
const [major, minor, patch] = parseVersion(current);
switch (command) {
case "major":
return formatVersion(major + 1, 0, 0);
case "minor":
return formatVersion(major, minor + 1, 0);
case "patch":
return formatVersion(major, minor, patch + 1);
case "set": {
if (!target) {
throw new Error("set command requires a target version");
}
validateVersion(target);
return target;
}
}
}
export function formatVersion(major: number, minor: number, patch: number): string {
return `${major}.${minor}.${patch}`;
}
export function parseVersion(version: string): [number, number, number] {
const parts = version.split(".").map((p) => parseInt(p, 10));
if (parts.length !== 3 || parts.some(isNaN)) {
throw new Error(`Invalid version format: ${version}`);
}
return [parts[0]!, parts[1]!, parts[2]!];
}
export function validateVersion(version: string): void {
if (!VERSION_REGEX.test(version)) {
throw new Error(`Invalid version format: ${version}. Expected MAJOR.MINOR.PATCH (e.g., 0.1.0)`);
}
}

45
scripts/bump-version.ts Normal file
View File

@@ -0,0 +1,45 @@
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { bumpVersion, validateVersion } from "./bump-version-logic";
const PACKAGE_JSON_PATH = resolve(import.meta.dir, "..", "package.json");
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: bun run bump-version.ts <patch|minor|major|set> [version]");
process.exit(1);
}
const command = args[0];
if (command !== "patch" && command !== "minor" && command !== "major" && command !== "set") {
console.error(`Unknown command: ${command}. Expected patch, minor, major, or set`);
process.exit(1);
}
if (command === "set" && args.length < 2) {
console.error("Usage: bun run bump-version.ts set <version>");
process.exit(1);
}
const packageJson = (await Bun.file(PACKAGE_JSON_PATH).json()) as { version: string };
const currentVersion = packageJson.version;
if (typeof currentVersion !== "string") {
console.error("package.json does not have a valid version field");
process.exit(1);
}
validateVersion(currentVersion);
const targetVersion = command === "set" ? args[1] : undefined;
const nextVersion = bumpVersion(currentVersion, command, targetVersion);
packageJson.version = nextVersion;
writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(packageJson, null, 2) + "\n");
console.log(`${currentVersion} -> ${nextVersion}`);
}
void main();

View File

@@ -3,12 +3,23 @@ import { resolve } from "node:path";
const root = resolve(import.meta.dir, "..");
const patterns: Array<{ desc: string; glob: string }> = [
{ desc: "Bun 构建缓存", glob: ".build/" },
{ desc: "Bun 构建临时文件", glob: ".*.bun-build" },
const dirs: Array<{ desc: string; path: string }> = [
{ desc: "构建产物", path: "dist" },
{ desc: "Bun 构建缓存", path: ".build" },
{ desc: "Playwright 测试报告", path: "playwright-report" },
{ desc: "测试结果", path: "test-results" },
{ desc: "发布产物", path: "dist/release" },
];
for (const { desc, glob } of patterns) {
const filePatterns: Array<{ desc: string; glob: string }> = [{ desc: "Bun 构建临时文件", glob: ".*.bun-build" }];
for (const { desc, path } of dirs) {
const full = resolve(root, path);
await rm(full, { force: true, recursive: true });
console.log(`已清理 ${desc}: ${path}`);
}
for (const { desc, glob } of filePatterns) {
const entries = await Array.fromAsync(new Bun.Glob(glob).scan({ cwd: root, dot: true }));
if (entries.length === 0) continue;
for (const entry of entries) {

View File

@@ -1,57 +1,26 @@
interface ChildProcessInfo {
name: string;
process: Bun.Subprocess;
}
import { fileURLToPath } from "node:url";
const configPath = process.argv[2];
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
const env = {
...process.env,
BACKEND_PORT: process.env["PORT"] ?? "3000",
};
const children: ChildProcessInfo[] = [
{
name: "server",
process: Bun.spawn(["bun", "run", "dev:server", ...(configPath ? [configPath] : [])], {
env,
stderr: "inherit",
stdout: "inherit",
}),
},
{
name: "web",
process: Bun.spawn(["bun", "run", "dev:web"], {
env,
stderr: "inherit",
stdout: "inherit",
}),
},
];
const stopChildren = () => {
for (const child of children) {
child.process.kill();
}
};
process.on("SIGINT", () => {
stopChildren();
process.exit(130);
const apiServer = Bun.spawn(["bun", "--watch", "src/server/dev.ts", ...process.argv.slice(2)], {
cwd: projectRoot,
stderr: "inherit",
stdout: "inherit",
});
process.on("SIGTERM", () => {
stopChildren();
process.exit(143);
const viteServer = Bun.spawn(["bunx", "--bun", "vite", "--host"], {
cwd: projectRoot,
stderr: "inherit",
stdout: "inherit",
});
const firstExit = await Promise.race(
children.map(async (child) => ({ code: await child.process.exited, name: child.name })),
);
stopChildren();
if (firstExit.code !== 0) {
console.error(`${firstExit.name} exited with code ${firstExit.code}`);
process.exit(firstExit.code ?? 1);
function shutdown() {
apiServer.kill();
viteServer.kill();
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
await Promise.race([apiServer.exited, viteServer.exited]);
shutdown();

View File

@@ -0,0 +1,16 @@
import { createDefaultCheckerRegistry } from "../src/server/checker/runner";
import { createProbeConfigJsonSchema } from "../src/server/checker/schema/export";
const schemaPath = "probe-config.schema.json";
const schema = `${JSON.stringify(createProbeConfigJsonSchema(createDefaultCheckerRegistry()), null, 2)}\n`;
if (process.argv.includes("--check")) {
const existing = await Bun.file(schemaPath)
.text()
.catch(() => null);
if (existing !== schema) {
throw new Error(`${schemaPath} 未同步,请运行 bun run schema`);
}
} else {
await Bun.write(schemaPath, schema);
}

196
scripts/release.ts Normal file
View File

@@ -0,0 +1,196 @@
import { createHash } from "node:crypto";
import { mkdir, rm, stat } from "node:fs/promises";
import { join, relative } from "node:path";
import { createGzip } from "node:zlib";
import tar from "tar-stream";
import { buildDir, cleanup, codeGeneration, projectRoot, viteBuild } from "./build-common";
const releaseDir = join(projectRoot, "dist/release");
const binariesDir = join(releaseDir, "binaries");
const packagesDir = join(releaseDir, "packages");
export interface ReleaseTarget {
arch: string;
bunTarget: string;
displayName: string;
os: string;
}
export const ALL_TARGETS: ReleaseTarget[] = [
{ arch: "x64", bunTarget: "bun-linux-x64", displayName: "Linux x64 (glibc)", os: "linux" },
{ arch: "arm64", bunTarget: "bun-linux-arm64", displayName: "Linux ARM64 (glibc)", os: "linux" },
{ arch: "x64-musl", bunTarget: "bun-linux-x64-musl", displayName: "Linux x64 (musl)", os: "linux" },
{ arch: "arm64-musl", bunTarget: "bun-linux-arm64-musl", displayName: "Linux ARM64 (musl)", os: "linux" },
{ arch: "x64", bunTarget: "bun-windows-x64", displayName: "Windows x64", os: "windows" },
{ arch: "x64", bunTarget: "bun-darwin-x64", displayName: "macOS x64 (Intel)", os: "darwin" },
{ arch: "arm64", bunTarget: "bun-darwin-arm64", displayName: "macOS ARM64 (Apple Silicon)", os: "darwin" },
];
export function archiveName(target: ReleaseTarget, version: string): string {
return `dial-server_${version}_${target.os}_${target.arch}.tar.gz`;
}
export function checksumName(target: ReleaseTarget, version: string): string {
return `${archiveName(target, version)}.sha256`;
}
export async function compileTarget(target: ReleaseTarget, version: string): Promise<string> {
const outfile = join(binariesDir, execName(target, version));
console.log(` 编译 ${target.displayName}...`);
const result = await Bun.build({
compile: {
autoloadBunfig: true,
autoloadDotenv: true,
outfile,
target: target.bunTarget as Bun.Build.CompileTarget,
},
entrypoints: [join(buildDir, "server-entry.ts")],
minify: true,
});
if (!result.success) {
console.error(` 编译失败 (${target.displayName}):`, result.logs);
process.exit(1);
}
return outfile;
}
export async function computeChecksum(archivePath: string): Promise<string> {
const content = await Bun.file(archivePath).arrayBuffer();
const hash = createHash("sha256").update(Buffer.from(content)).digest("hex");
const filename = relative(packagesDir, archivePath);
const checksumPath = `${archivePath}.sha256`;
const checksumContent = `${hash} ${filename}\n`;
await Bun.write(checksumPath, checksumContent);
return checksumPath;
}
export function execName(target: ReleaseTarget, version: string): string {
const suffix = target.os === "windows" ? ".exe" : "";
return `dial-server-${version}-${target.os}-${target.arch}${suffix}`;
}
export async function packageTarget(target: ReleaseTarget, version: string, binaryPath: string): Promise<string> {
const archivePath = join(packagesDir, archiveName(target, version));
const prefix = `dial-server_${version}_${target.os}_${target.arch}`;
const binaryName = target.os === "windows" ? "dial-server.exe" : "dial-server";
const binaryContent = await Bun.file(binaryPath).arrayBuffer();
const probesContent = await Bun.file(join(projectRoot, "probes.example.yaml")).arrayBuffer();
const licenseContent = await Bun.file(join(projectRoot, "LICENSE")).arrayBuffer();
const pack = tar.pack();
pack.entry(
{ mode: 0o755, name: `${prefix}/${binaryName}`, size: binaryContent.byteLength },
Buffer.from(binaryContent),
);
pack.entry(
{ mode: 0o644, name: `${prefix}/probes.example.yaml`, size: probesContent.byteLength },
Buffer.from(probesContent),
);
pack.entry({ mode: 0o644, name: `${prefix}/LICENSE`, size: licenseContent.byteLength }, Buffer.from(licenseContent));
pack.finalize();
const gzip = createGzip();
const chunks: Buffer[] = [];
await new Promise<void>((resolve, reject) => {
pack.pipe(gzip);
gzip.on("data", (chunk: Buffer) => chunks.push(chunk));
gzip.on("end", resolve);
gzip.on("error", reject);
});
await Bun.write(archivePath, Buffer.concat(chunks));
return archivePath;
}
export function parseTargets(args: string[]): ReleaseTarget[] {
const targetIndex = args.indexOf("--target");
if (targetIndex === -1 || targetIndex === args.length - 1) {
return ALL_TARGETS;
}
const targetValues = args[targetIndex + 1]!.split(",");
const targets: ReleaseTarget[] = [];
for (const value of targetValues) {
const bunTarget = `bun-${value.trim()}`;
const found = ALL_TARGETS.find((t) => t.bunTarget === bunTarget);
if (!found) {
const available = ALL_TARGETS.map((t) => t.bunTarget.replace(/^bun-/, "")).join(", ");
console.error(`无效的 target: ${value.trim()}`);
console.error(`可用的 target 值: ${available}`);
process.exit(1);
}
targets.push(found);
}
return targets;
}
export async function printReport(binaries: string[], archives: string[]): Promise<void> {
console.log("\n=== Release 报告 ===\n");
console.log("裸二进制:");
for (const binary of binaries) {
const size = (await stat(binary)).size;
const mb = (size / 1024 / 1024).toFixed(1);
console.log(` ${relative(projectRoot, binary)} (${mb} MB)`);
}
console.log("\n压缩包:");
for (const archive of archives) {
const size = (await stat(archive)).size;
const mb = (size / 1024 / 1024).toFixed(1);
console.log(` ${relative(projectRoot, archive)} (${mb} MB)`);
}
}
async function release() {
const targets = parseTargets(process.argv);
console.log(`Release 目标: ${targets.map((t) => t.displayName).join(", ")}\n`);
try {
await viteBuild();
const version = await codeGeneration();
console.log(`\n版本: ${version}`);
console.log(`编译 ${targets.length} 个目标...\n`);
await rm(releaseDir, { force: true, recursive: true });
await mkdir(binariesDir, { recursive: true });
await mkdir(packagesDir, { recursive: true });
const binaries: string[] = [];
for (const target of targets) {
const binaryPath = await compileTarget(target, version);
binaries.push(binaryPath);
}
const archives: string[] = [];
for (let i = 0; i < targets.length; i++) {
const target = targets[i]!;
const binaryPath = binaries[i]!;
console.log(` 打包 ${target.displayName}...`);
const archivePath = await packageTarget(target, version, binaryPath);
await computeChecksum(archivePath);
archives.push(archivePath);
}
await cleanup();
await printReport(binaries, archives);
console.log("\nRelease 完成!");
} catch (error) {
await cleanup();
console.error("Release 失败:", error);
process.exit(1);
}
}
if (import.meta.main) {
await release();
}

View File

@@ -1,168 +0,0 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { access } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { HealthResponse, SummaryResponse } from "../src/shared/api";
const executablePath = process.argv[2] ?? fileURLToPath(new URL("../dist/dial-server", import.meta.url));
await assertExecutableExists(executablePath);
const tempDir = mkdtempSync(join(tmpdir(), "dial-smoke-"));
const configPath = join(tempDir, "probes.yaml");
const port = getFreePort();
const baseUrl = `http://127.0.0.1:${port}`;
writeFileSync(
configPath,
`server:
port: ${port}
targets:
- name: "httpbin"
type: http
http:
url: "https://httpbin.org/get"
interval: "5m"
timeout: "15s"
expect:
status: [200]
`,
);
const app = Bun.spawn([executablePath, configPath], {
env: { ...process.env },
stderr: "pipe",
stdout: "pipe",
});
const stdout = readStream(app.stdout);
const stderr = readStream(app.stderr);
try {
await waitForServer(`${baseUrl}/health`);
const { body: health, response: healthResponse } = await expectJson<HealthResponse>(`${baseUrl}/health`, 200);
assert(health.ok === true, "健康检查响应缺少 ok=true");
assertSecurityHeaders(healthResponse, "/health");
const { body: summary } = await expectJson<SummaryResponse>(`${baseUrl}/api/summary`, 200);
assert(summary.total === 1, "总览统计: total 应为 1");
assertSecurityHeaders(await fetch(`${baseUrl}/api/summary`), "/api/summary");
const { body: targets } = await expectJson<unknown[]>(`${baseUrl}/api/targets`, 200);
assert(Array.isArray(targets), "/api/targets 应返回数组");
assert(targets.length === 1, "/api/targets 应有 1 个目标");
assert((targets[0] as { name: string }).name === "httpbin", "目标名称应为 httpbin");
const missingApi = await fetch(`${baseUrl}/api/not-found`);
assert(missingApi.status === 404, "未知 API 应返回 404");
const missingTarget = await fetch(`${baseUrl}/api/targets/99999/history`);
assert(missingTarget.status === 404, "不存在的目标应返回 404");
const { body: rootHtml, response: rootResponse } = await expectText(`${baseUrl}/`, 200);
assert(rootHtml.includes("DiAL"), "前端根页面缺少标题");
assert(rootResponse.headers.get("cache-control") === "no-cache", "前端根页面应使用 no-cache");
const { body: fallbackHtml } = await expectText(`${baseUrl}/dashboard`, 200);
assert(fallbackHtml.includes("DiAL"), "SPA fallback 未返回前端入口页面");
const assetPath = /(?:src|href)="(\/assets\/[^"]+)"/.exec(rootHtml)?.[1];
assert(assetPath !== undefined, "前端入口页面未引用 /assets/* 资源");
const asset = await fetch(`${baseUrl}${assetPath}`);
assert(asset.status === 200, `静态资源 ${assetPath} 未返回 200`);
const missingAsset = await expectText(`${baseUrl}/assets/not-found.js`, 404);
assert(!missingAsset.body.includes("DiAL"), "未知静态资源不应返回前端入口页面");
console.log(`Smoke test passed: ${baseUrl}`);
} catch (error) {
app.kill();
const [out, err] = await Promise.all([stdout, stderr]);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`executable smoke test 失败: ${message}\nstdout:\n${out}\nstderr:\n${err}`, { cause: error });
} finally {
app.kill();
rmSync(tempDir, { force: true, recursive: true });
}
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
async function assertExecutableExists(path: string) {
try {
await access(path);
} catch (error) {
throw new Error(`找不到 executable: ${path},请先运行 bun run build`, { cause: error });
}
}
function assertSecurityHeaders(response: Response, label: string) {
assert(response.headers.get("x-content-type-options") === "nosniff", `${label} 缺少 nosniff 安全头`);
assert(
response.headers.get("referrer-policy") === "strict-origin-when-cross-origin",
`${label} 缺少 Referrer-Policy 安全头`,
);
}
async function expectJson<T = unknown>(url: string, status: number): Promise<{ body: T; response: Response }> {
const response = await fetch(url);
assert(response.status === status, `${url} 应返回 ${status},实际为 ${response.status}`);
assert(response.headers.get("content-type")?.includes("application/json") === true, `${url} 应返回 JSON`);
return { body: (await response.json()) as T, response };
}
async function expectText(url: string, status: number): Promise<{ body: string; response: Response }> {
const response = await fetch(url);
assert(response.status === status, `${url} 应返回 ${status},实际为 ${response.status}`);
return { body: await response.text(), response };
}
function getFreePort(): number {
const server = Bun.serve({
fetch: () => new Response("ok"),
hostname: "127.0.0.1",
port: 0,
});
const port = server.port;
void server.stop(true);
if (port === undefined) {
throw new Error("无法分配 smoke test 端口");
}
return port;
}
async function readStream(stream: null | ReadableStream<Uint8Array>): Promise<string> {
if (!stream) return "";
return new Response(stream).text();
}
async function waitForServer(url: string) {
const deadline = Date.now() + 8_000;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (response.ok) return;
} catch {
await Bun.sleep(100);
}
}
throw new Error(`服务未在超时时间内启动: ${url}`);
}

11
src/pino-roll.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
declare module "pino-roll" {
interface RollingStreamOptions {
file: string;
frequency?: string;
limit?: { count?: number };
mkdir?: boolean;
size?: string;
}
export default function build(options: RollingStreamOptions): Promise<NodeJS.WritableStream>;
}

View File

@@ -1,80 +0,0 @@
import type { RuntimeMode } from "../shared/api";
import type { ProbeStore } from "./checker/store";
import { createApiError, jsonResponse } from "./helpers";
import { guardGetHead } from "./middleware";
import { handleHealth } from "./routes/health";
import { handleHistory } from "./routes/history";
import { handleSummary } from "./routes/summary";
import { handleTargets } from "./routes/targets";
import { handleTrend } from "./routes/trend";
import { serveStaticAsset } from "./static";
export interface AppOptions {
mode: RuntimeMode;
staticAssets?: StaticAssets;
store?: ProbeStore;
}
export interface StaticAssets {
files: Record<string, Blob>;
indexHtml: Blob;
}
export function createFetchHandler(options: AppOptions) {
return (request: Request): Response => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return handleHealth(request.method, options.mode);
}
if (url.pathname.startsWith("/api/") && options.store) {
return handleApiRoute(url, request, options.store, options.mode);
}
if (url.pathname.startsWith("/api/")) {
return jsonResponse(createApiError("Service not ready", 503), {
method: request.method,
mode: options.mode,
status: 503,
});
}
if (options.staticAssets) {
return serveStaticAsset(url.pathname, options.staticAssets, options.mode);
}
return new Response("开发期请通过 Vite 前端地址访问页面。", {
headers: { "Content-Type": "text/plain; charset=utf-8" },
status: 404,
});
};
}
function handleApiRoute(url: URL, request: Request, store: ProbeStore, mode: RuntimeMode): Response {
const guardResult = guardGetHead(request.method, mode);
if (guardResult) return guardResult;
const method = request.method;
if (url.pathname === "/api/summary") {
return handleSummary(store, method, mode);
}
if (url.pathname === "/api/targets") {
return handleTargets(store, method, mode);
}
const historyMatch = /^\/api\/targets\/([^/]+)\/history$/.exec(url.pathname);
if (historyMatch) {
return handleHistory(historyMatch[1]!, url, method, store, mode);
}
const trendMatch = /^\/api\/targets\/([^/]+)\/trend$/.exec(url.pathname);
if (trendMatch) {
return handleTrend(trendMatch[1]!, url, method, store, mode);
}
return jsonResponse(createApiError("API route not found", 404), { method, mode, status: 404 });
}

132
src/server/bootstrap.ts Normal file
View File

@@ -0,0 +1,132 @@
import { join } from "node:path";
import type { RuntimeMode } from "../shared/api";
import type { ResolvedLoggingConfig } from "./checker/types";
import type { Logger } from "./logger";
import type { StartServerOptions } from "./server";
import type { StaticAssets } from "./static";
import { loadConfig, type ResolvedConfig } from "./checker/config-loader";
import { ProbeEngine } from "./checker/engine";
import { ProbeStore } from "./checker/store";
import { createConsoleFallback, createRuntimeLogger } from "./logger";
import { startServer } from "./server";
export interface BootstrapDependencies {
createEngine?: (
store: ProbeStore,
targets: ResolvedConfig["targets"],
maxConcurrentChecks: number,
retentionMs: number,
logger: Logger,
) => BootstrapEngine;
createLogger?: (config: ResolvedLoggingConfig, mode: string, version: string) => Promise<Logger>;
createStore?: (dbPath: string) => ProbeStore;
exit?: (code: number) => never;
loadConfig?: (configPath: string) => Promise<ResolvedConfig>;
logError?: (...data: unknown[]) => void;
onSignal?: (signal: ShutdownSignal, handler: () => void) => void;
startServer?: (options: StartServerOptions) => unknown;
}
export interface BootstrapOptions {
configPath: string;
mode: RuntimeMode;
staticAssets?: StaticAssets;
version: string;
}
type BootstrapEngine = Pick<ProbeEngine, "start" | "stop">;
type ShutdownSignal = "SIGINT" | "SIGTERM";
export async function bootstrap(options: BootstrapOptions, dependencies: BootstrapDependencies = {}): Promise<void> {
const load = dependencies.loadConfig ?? loadConfig;
const createStore = dependencies.createStore ?? ((dbPath: string) => new ProbeStore(dbPath));
const createEngine =
dependencies.createEngine ??
((
store: ProbeStore,
targets: ResolvedConfig["targets"],
maxConcurrentChecks: number,
retentionMs: number,
logger: Logger,
) => new ProbeEngine(store, targets, maxConcurrentChecks, retentionMs, logger));
const buildLogger = dependencies.createLogger ?? createRuntimeLogger;
const serve = dependencies.startServer ?? startServer;
const onSignal =
dependencies.onSignal ??
((signal: ShutdownSignal, handler: () => void) => {
process.on(signal, handler);
});
const exit = dependencies.exit ?? ((code: number) => process.exit(code));
const logError =
dependencies.logError ??
((...data: unknown[]) => {
createConsoleFallback().fatal(
data.map((item) => (item instanceof Error ? item.message : String(item))).join(" "),
);
});
let store: ProbeStore | undefined;
let engine: BootstrapEngine | undefined;
let logger: Logger | undefined;
try {
const config = await load(options.configPath);
try {
logger = await buildLogger(config.logging, options.mode, options.version);
} catch (logInitError) {
logError("日志初始化失败:", logInitError instanceof Error ? logInitError.message : logInitError);
exit(1);
}
logger!.info({ configPath: options.configPath, mode: options.mode, version: options.version }, "配置加载成功");
store = createStore(join(config.dataDir, "probe.db"));
store.syncTargets(config.targets);
logger!.info({ dataDir: config.dataDir }, "数据库初始化成功");
engine = createEngine(
store,
config.targets,
config.maxConcurrentChecks,
config.retentionMs,
logger!.child({ component: "engine" }),
);
engine.start();
logger!.info(
{ maxConcurrentChecks: config.maxConcurrentChecks, targetCount: config.targets.length },
"调度引擎启动",
);
const shutdown = () => {
logger?.info("收到退出信号,开始优雅关机");
engine?.stop();
store?.close();
logger?.flush();
exit(0);
};
onSignal("SIGINT", shutdown);
onSignal("SIGTERM", shutdown);
serve({
config: { host: config.host, port: config.port },
logger: logger!.child({ component: "server" }),
mode: options.mode,
staticAssets: options.staticAssets,
store,
version: options.version,
});
} catch (error) {
engine?.stop();
store?.close();
if (logger) {
logger.fatal({ error: error instanceof Error ? error.message : String(error) }, "启动失败");
logger.flush();
} else {
logError("启动失败:", error instanceof Error ? error.message : error);
}
exit(1);
}
}

View File

@@ -1,8 +1,24 @@
import { isNumber, isPlainObject, isString } from "es-toolkit";
import { dirname, resolve } from "node:path";
import type { DefaultsConfig, EngineRuntimeConfig, ProbeConfig, ResolvedTarget, TargetConfig } from "./types";
import type { ConfigValidationIssue } from "./schema/issues";
import type {
ExecutionConfig,
LoggingConfig,
LogLevel,
RawTargetConfig,
ResolvedLoggingConfig,
ResolvedTargetBase,
RotationFrequency,
ServerStorageConfig,
} from "./types";
import { normalizeAuthoringConfig } from "./normalizer";
import { checkerRegistry } from "./runner";
import { issue, throwConfigIssues } from "./schema/issues";
import { asValidatedConfig, type NormalizedProbeConfig } from "./schema/types";
import { validateProbeConfigContract } from "./schema/validate";
import { parseDuration, parseSize } from "./utils";
const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 3000;
@@ -10,14 +26,26 @@ const DEFAULT_DATA_DIR = "./data";
const DEFAULT_INTERVAL = "30s";
const DEFAULT_TIMEOUT = "10s";
const DEFAULT_MAX_CONCURRENT_CHECKS = 20;
const DEFAULT_RETENTION = "7d";
const DEFAULT_LOG_LEVEL: LogLevel = "info";
const DEFAULT_ROTATION_SIZE = "50MB";
const DEFAULT_ROTATION_FREQUENCY: RotationFrequency = "daily";
const DEFAULT_ROTATION_MAX_FILES = 14;
const MINIMUM_INTERVAL_MS = parseDuration("10s");
const VALID_LOG_LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"];
const VALID_ROTATION_FREQUENCIES: RotationFrequency[] = ["hourly", "daily", "weekly"];
export interface ResolvedConfig {
configDir: string;
dataDir: string;
host: string;
logging: ResolvedLoggingConfig;
maxConcurrentChecks: number;
port: number;
targets: ResolvedTarget[];
retentionMs: number;
targets: ResolvedTargetBase[];
}
export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
@@ -28,121 +56,355 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
}
const content = await file.text();
const raw = Bun.YAML.parse(content) as null | ProbeConfig;
const parsed = Bun.YAML.parse(content);
if (!raw) {
if (!parsed) {
throw new Error("配置文件内容为空或格式无效");
}
validateConfig(raw);
const configDir = dirname(resolve(configPath));
const server = raw.server ?? {};
const runtime = raw.runtime ?? {};
const defaults = raw.defaults ?? {};
const host = server.host ?? DEFAULT_HOST;
const port = server.port ?? DEFAULT_PORT;
const dataDir = server.dataDir ?? DEFAULT_DATA_DIR;
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error(`无效端口号: ${port},需要 0-65535 之间的整数`);
const normalizeResult = normalizeAuthoringConfig(parsed, checkerRegistry);
if (normalizeResult.issues.length > 0) {
throwConfigIssues(dedupeIssues(normalizeResult.issues));
}
const maxConcurrentChecks = validateRuntime(runtime);
const defaultIntervalMs = parseDuration(defaults.interval ?? DEFAULT_INTERVAL);
const defaultTimeoutMs = parseDuration(defaults.timeout ?? DEFAULT_TIMEOUT);
const normalizedConfig = normalizeResult.config;
const contractResult = validateProbeConfigContract(normalizedConfig, checkerRegistry);
if (contractResult.config === null && !canRunSemanticValidation(normalizedConfig)) {
throwConfigIssues(contractResult.issues);
}
const semanticInput = (contractResult.config ?? normalizedConfig) as NormalizedProbeConfig;
const validationIssues = validateConfig(semanticInput);
const targets: ResolvedTarget[] = raw.targets.map((target) =>
resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir),
const allIssues = [...contractResult.issues, ...validationIssues];
if (contractResult.config === null) {
if (allIssues.length > 0) {
throwConfigIssues(dedupeIssues(allIssues));
}
throw new Error("配置文件内容为空或格式无效");
}
const raw = contractResult.config;
const validated = asValidatedConfig(raw);
const configDir = dirname(resolve(configPath));
const server = validated.server ?? {};
const listen = server.listen ?? {};
const storage = server.storage ?? {};
const host = listen.host ?? DEFAULT_HOST;
const port = listen.port ?? DEFAULT_PORT;
const dataDir = resolve(configDir, storage.dataDir ?? DEFAULT_DATA_DIR);
const probes = validated.probes ?? {};
const execution = probes.execution ?? {};
const maxConcurrentChecks = resolveMaxConcurrentChecks(execution);
const retentionMs = resolveRetention(storage);
const logging = resolveLogging(server.logging ?? {}, dataDir, configDir);
const allRuntimeIssues = [...allIssues];
validateLoggingConfig(server.logging, allRuntimeIssues);
if (allRuntimeIssues.length > 0) {
throwConfigIssues(dedupeIssues(allRuntimeIssues));
}
const defaultIntervalMs = parseDuration(DEFAULT_INTERVAL);
const defaultTimeoutMs = parseDuration(DEFAULT_TIMEOUT);
const targets: ResolvedTargetBase[] = validated.targets.map((target) =>
resolveTarget(target, defaultIntervalMs, defaultTimeoutMs, configDir),
);
return { configDir, dataDir, host, maxConcurrentChecks, port, targets };
return { configDir, dataDir, host, logging, maxConcurrentChecks, port, retentionMs, targets };
}
function canRunSemanticValidation(value: unknown): boolean {
return isPlainObject(value);
}
function dedupeIssues(issues: ConfigValidationIssue[]): ConfigValidationIssue[] {
const seen = new Set<string>();
const result: ConfigValidationIssue[] = [];
for (const item of issues) {
const key = `${item.code}:${item.path}:${item.message}:${item.targetName ?? ""}:${item.targetId ?? ""}`;
if (seen.has(key)) continue;
seen.add(key);
result.push(item);
}
return result;
}
export { parseDuration } from "./utils";
function isAbsolute(p: string): boolean {
return p.startsWith("/") || /^[A-Za-z]:/.test(p);
}
function resolveLogging(logging: LoggingConfig, dataDir: string, configDir: string): ResolvedLoggingConfig {
const globalLevel = resolveLogLevel(logging.level, DEFAULT_LOG_LEVEL);
const consoleLevel = resolveLogLevel(logging.console?.level, globalLevel);
const fileLevel = resolveLogLevel(logging.file?.level, globalLevel);
const rawPath = logging.file?.path;
const filePath = rawPath
? isAbsolute(rawPath)
? rawPath
: resolve(configDir, rawPath)
: resolve(dataDir, "logs/dial.log");
const rotationRaw = logging.file?.rotation;
const rotationSizeRaw = rotationRaw?.size ?? DEFAULT_ROTATION_SIZE;
const rotationSizeBytes = parseSize(rotationSizeRaw);
const rotationFrequency = rotationRaw?.frequency ?? DEFAULT_ROTATION_FREQUENCY;
const rotationMaxFiles = rotationRaw?.maxFiles ?? DEFAULT_ROTATION_MAX_FILES;
return {
consoleLevel,
fileLevel,
filePath,
rotationFrequency,
rotationMaxFiles,
rotationSizeBytes,
rotationSizeRaw,
};
}
function resolveLogLevel(level: unknown, fallback: LogLevel): LogLevel {
if (!isString(level)) return fallback;
if (VALID_LOG_LEVELS.includes(level as LogLevel)) return level as LogLevel;
return fallback;
}
function resolveMaxConcurrentChecks(execution: ExecutionConfig): number {
if (execution.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
if (
!isNumber(execution.maxConcurrentChecks) ||
!Number.isInteger(execution.maxConcurrentChecks) ||
execution.maxConcurrentChecks <= 0
)
return DEFAULT_MAX_CONCURRENT_CHECKS;
return execution.maxConcurrentChecks;
}
function resolveRetention(storage: ServerStorageConfig): number {
return parseDuration(storage.retention ?? DEFAULT_RETENTION);
}
function resolveTarget(
target: TargetConfig,
defaults: DefaultsConfig,
target: RawTargetConfig,
defaultIntervalMs: number,
defaultTimeoutMs: number,
configDir: string,
): ResolvedTarget {
const intervalMs = parseDuration(target.interval ?? defaults.interval ?? DEFAULT_INTERVAL);
const timeoutMs = parseDuration(target.timeout ?? defaults.timeout ?? DEFAULT_TIMEOUT);
): ResolvedTargetBase {
const intervalMs = parseDuration(target.interval ?? DEFAULT_INTERVAL);
const timeoutMs = parseDuration(target.timeout ?? DEFAULT_TIMEOUT);
const checker = checkerRegistry.get(target.type);
const result = checker.resolve(target, { configDir, defaultIntervalMs, defaults, defaultTimeoutMs });
const result = checker.resolve(target, { configDir, defaultIntervalMs, defaultTimeoutMs });
result.intervalMs = intervalMs;
result.timeoutMs = timeoutMs;
result.description = target.description ?? null;
return result;
}
function validateConfig(config: ProbeConfig): void {
if (!config.targets || !Array.isArray(config.targets) || config.targets.length === 0) {
throw new Error("配置文件必须包含至少一个 target");
function tryParseDuration(value: string): null | number {
try {
return parseDuration(value);
} catch {
return null;
}
}
const names = new Set<string>();
function validateConfig(config: NormalizedProbeConfig): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
if (!Array.isArray(config.targets) || config.targets.length === 0) {
issues.push(issue("required", "targets", "配置文件必须包含至少一个 target"));
return issues;
}
const ids = new Set<string>();
const supportedTypes = checkerRegistry.supportedTypes;
for (let i = 0; i < config.targets.length; i++) {
const raw = config.targets[i] as unknown as Record<string, unknown>;
const rawTarget = config.targets[i] as unknown;
if (!isPlainObject(rawTarget)) {
issues.push(issue("invalid-type", `targets[${i}]`, "必须为对象"));
continue;
}
const raw = rawTarget as Record<string, unknown>;
const name = raw["name"];
if (!name || typeof name !== "string" || name.trim() === "") {
throw new Error(`${i + 1} 个 target 缺少 name 字段`);
const id: unknown = raw["id"];
if (!isString(id) || id.trim() === "") {
issues.push(issue("required", `targets[${i}].id`, "缺少 id 字段"));
continue;
}
const type = raw["type"];
if (!type || typeof type !== "string") {
throw new Error(`target "${name}" 缺少 type 字段`);
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(id)) {
issues.push(issue("invalid-format", `targets[${i}].id`, "id 不符合命名规则", id));
}
const nameValue: unknown = raw["name"];
const name = isString(nameValue) ? nameValue : id;
if (isString(nameValue) && nameValue.trim() === "") {
issues.push(issue("invalid-value", `targets[${i}].name`, "name 不能为空白", name));
}
const type: unknown = raw["type"];
if (!isString(type)) {
issues.push(issue("required", `targets[${i}].type`, "缺少 type 字段", name));
continue;
}
if (!supportedTypes.includes(type)) {
throw new Error(`target "${name}" 使用不支持的 type: "${type}",支持: ${supportedTypes.join(", ")}`);
issues.push(
issue(
"unsupported-type",
`targets[${i}].type`,
`使用不支持的 type: "${type}",支持: ${supportedTypes.join(", ")}`,
name,
),
);
}
const group = raw["group"];
if (group !== undefined && typeof group !== "string") {
throw new Error(`target "${name}" 的 group 字段必须为字符串`);
const group: unknown = raw["group"];
if (group !== undefined && !isString(group)) {
issues.push(issue("invalid-type", `targets[${i}].group`, "必须为字符串", name));
}
if (names.has(name)) {
throw new Error(`target name 重复: "${name}"`);
if (ids.has(id)) {
issues.push(issue("duplicate-id", `targets[${i}].id`, `target id 重复: "${id}"`, name));
}
names.add(name);
ids.add(id);
}
for (const checker of checkerRegistry.definitions) {
issues.push(...checker.validate({ targets: config.targets }));
}
validateDurationValue(
isString(config.server?.storage?.retention) ? config.server.storage.retention : undefined,
"server.storage.retention",
issues,
);
for (let i = 0; i < config.targets.length; i++) {
const target = config.targets[i] as unknown;
if (!isPlainObject(target)) continue;
const targetRecord = target as Record<string, unknown>;
const targetNameValue: unknown = targetRecord["name"];
const targetIdValue: unknown = targetRecord["id"];
const targetName = isString(targetNameValue)
? targetNameValue
: isString(targetIdValue)
? targetIdValue
: undefined;
const intervalRaw = isString(targetRecord["interval"]) ? targetRecord["interval"] : undefined;
const timeoutRaw = isString(targetRecord["timeout"]) ? targetRecord["timeout"] : undefined;
validateDurationValue(intervalRaw, `targets[${i}].interval`, issues, targetName);
validateDurationValue(timeoutRaw, `targets[${i}].timeout`, issues, targetName);
const intervalMs = tryParseDuration(intervalRaw ?? DEFAULT_INTERVAL);
const timeoutMs = tryParseDuration(timeoutRaw ?? DEFAULT_TIMEOUT);
if (intervalMs !== null && intervalMs < MINIMUM_INTERVAL_MS) {
issues.push(issue("invalid-value", `targets[${i}].interval`, "interval 不能小于 10s", targetName));
}
if (intervalMs !== null && timeoutMs !== null && timeoutMs > intervalMs) {
issues.push(issue("invalid-value", `targets[${i}].timeout`, "timeout 不能大于 interval", targetName));
}
}
return issues;
}
function validateDurationValue(
value: string | undefined,
path: string,
issues: ConfigValidationIssue[],
targetName?: string,
): void {
if (value === undefined) return;
try {
parseDuration(value);
} catch (error) {
issues.push(issue("invalid-duration", path, error instanceof Error ? error.message : "时长格式不合法", targetName));
}
}
function validateRuntime(runtime: EngineRuntimeConfig): number {
if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
function validateLoggingConfig(logging: LoggingConfig | undefined, issues: ConfigValidationIssue[]): void {
if (logging === undefined) return;
if (
typeof runtime.maxConcurrentChecks !== "number" ||
!Number.isInteger(runtime.maxConcurrentChecks) ||
runtime.maxConcurrentChecks <= 0
) {
throw new Error("runtime.maxConcurrentChecks 必须为正整数");
if (logging.level !== undefined && !VALID_LOG_LEVELS.includes(logging.level)) {
issues.push(
issue(
"invalid-value",
"server.logging.level",
`日志等级非法: "${logging.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
),
);
}
return runtime.maxConcurrentChecks;
}
const DURATION_REGEX = /^(\d+(?:\.\d+)?)(ms|s|m)$/;
export function parseDuration(value: string): number {
const match = DURATION_REGEX.exec(value);
if (!match) {
throw new Error(`无效的时长格式: "${value}",支持格式如 "30s"、"5m"、"500ms"`);
if (logging.console?.level !== undefined && !VALID_LOG_LEVELS.includes(logging.console.level)) {
issues.push(
issue(
"invalid-value",
"server.logging.console.level",
`日志等级非法: "${logging.console.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
),
);
}
const num = parseFloat(match[1]!);
const unit = match[2]!;
if (logging.file?.level !== undefined && !VALID_LOG_LEVELS.includes(logging.file.level)) {
issues.push(
issue(
"invalid-value",
"server.logging.file.level",
`日志等级非法: "${logging.file.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
),
);
}
if (unit === "ms") return num;
if (unit === "s") return num * 1000;
return num * 60 * 1000;
if (logging.file?.path !== undefined) {
if (!isString(logging.file.path) || logging.file.path.trim() === "") {
issues.push(issue("invalid-value", "server.logging.file.path", "日志路径不能为空字符串或空白字符串"));
}
}
const rotation = logging.file?.rotation;
if (rotation?.size !== undefined) {
try {
const bytes = parseSize(rotation.size);
if (bytes <= 0) {
issues.push(issue("invalid-value", "server.logging.file.rotation.size", "滚动大小必须为正整数字节数"));
}
} catch (error) {
issues.push(
issue(
"invalid-value",
"server.logging.file.rotation.size",
error instanceof Error ? error.message : "size 格式非法",
),
);
}
}
if (rotation?.frequency !== undefined && !VALID_ROTATION_FREQUENCIES.includes(rotation.frequency)) {
issues.push(
issue(
"invalid-value",
"server.logging.file.rotation.frequency",
`滚动频率非法: "${rotation.frequency}",支持: ${VALID_ROTATION_FREQUENCIES.join(", ")}`,
),
);
}
if (rotation?.maxFiles !== undefined) {
if (!isNumber(rotation.maxFiles) || !Number.isInteger(rotation.maxFiles) || rotation.maxFiles <= 0) {
issues.push(issue("invalid-value", "server.logging.file.rotation.maxFiles", "maxFiles 必须为正整数"));
}
}
}

View File

@@ -1,74 +1,118 @@
import { groupBy, Semaphore } from "es-toolkit";
import { isError, Semaphore } from "es-toolkit";
import type { Logger } from "../logger";
import type { ProbeStore } from "./store";
import type { CheckResult, ResolvedTarget } from "./types";
import type { CheckResult, ResolvedTargetBase } from "./types";
import { createNoopLogger } from "../logger";
import { errorFailure } from "./expect/failure";
import { checkerRegistry } from "./runner";
const PRUNE_INTERVAL_MS = 3600000;
export class ProbeEngine {
private abort: AbortController | null = null;
private lastMatched = new Map<string, boolean>();
private logger: Logger;
private pruneTimer: null | ReturnType<typeof setInterval> = null;
private retentionMs: number;
private semaphore: Semaphore;
private store: ProbeStore;
private targetNameToId = new Map<string, number>();
private targets: ResolvedTarget[];
private timers: Array<ReturnType<typeof setInterval>> = [];
private targetIds = new Set<string>();
private targets: ResolvedTargetBase[];
constructor(store: ProbeStore, targets: ResolvedTarget[], maxConcurrentChecks?: number) {
constructor(
store: ProbeStore,
targets: ResolvedTargetBase[],
maxConcurrentChecks?: number,
retentionMs?: number,
logger?: Logger,
) {
this.store = store;
this.targets = targets;
this.semaphore = new Semaphore(maxConcurrentChecks ?? 20);
this.retentionMs = retentionMs ?? 0;
this.logger = logger ?? createNoopLogger();
this.refreshCache();
this.initStateCache();
}
start(): void {
const groups = groupBy(this.targets, (t) => t.intervalMs);
this.abort = new AbortController();
const signal = this.abort.signal;
for (const [intervalMs, groupTargets] of Object.entries(groups)) {
void this.probeGroup(groupTargets);
for (const target of this.targets) {
void this.runLoop(target, signal);
}
const timer = setInterval(() => {
void this.probeGroup(groupTargets);
}, Number(intervalMs));
this.timers.push(timer);
if (this.retentionMs > 0) {
this.store.prune(this.retentionMs);
this.pruneTimer = setInterval(() => {
this.store.prune(this.retentionMs);
}, PRUNE_INTERVAL_MS);
}
}
stop(): void {
for (const timer of this.timers) {
clearInterval(timer);
this.abort?.abort();
this.abort = null;
if (this.pruneTimer) {
clearInterval(this.pruneTimer);
this.pruneTimer = null;
}
this.timers = [];
}
private async probeGroup(targets: ResolvedTarget[]): Promise<void> {
const results = await Promise.allSettled(
targets.map(async (target) => {
await this.semaphore.acquire();
try {
return await this.runCheck(target);
} finally {
this.semaphore.release();
}
}),
);
for (const result of results) {
if (result.status === "fulfilled") {
this.writeResult(result.value);
} else {
console.warn("探针执行失败:", result.reason);
}
private initStateCache(): void {
const latestMap = this.store.getLatestChecksMap();
for (const [id, row] of latestMap) {
this.lastMatched.set(id, row.matched === 1);
}
}
private logCheckDebug(result: CheckResult): void {
this.logger.debug({
durationMs: result.durationMs,
failureMessage: result.failure?.message ?? null,
failurePhase: result.failure?.phase ?? null,
matched: result.matched,
targetId: result.targetId,
});
}
private logStateChange(result: CheckResult): void {
const previous = this.lastMatched.get(result.targetId);
const current = result.matched;
if (previous === undefined) {
if (!current) {
this.logger.warn(
{ durationMs: result.durationMs, failurePhase: result.failure?.phase, targetId: result.targetId },
`目标首次 DOWN: ${result.targetId}`,
);
}
} else if (previous && !current) {
this.logger.warn(
{ durationMs: result.durationMs, failurePhase: result.failure?.phase, targetId: result.targetId },
`目标状态变化 UP → DOWN: ${result.targetId}`,
);
} else if (!previous && current) {
this.logger.info(
{ durationMs: result.durationMs, targetId: result.targetId },
`目标恢复 DOWN → UP: ${result.targetId}`,
);
}
this.lastMatched.set(result.targetId, current);
}
private refreshCache(): void {
this.targetNameToId.clear();
this.targetIds.clear();
for (const target of this.store.getTargets()) {
this.targetNameToId.set(target.name, target.id);
this.targetIds.add(target.id);
}
}
private async runCheck(target: ResolvedTarget): Promise<CheckResult> {
private async runCheck(target: ResolvedTargetBase): Promise<CheckResult> {
const checker = checkerRegistry.get(target.type);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), target.timeoutMs);
@@ -80,17 +124,97 @@ export class ProbeEngine {
}
}
private async runLoop(target: ResolvedTargetBase, signal: AbortSignal): Promise<void> {
while (!signal.aborted) {
const start = performance.now();
try {
await this.runOnce(target, signal);
} catch {
break;
}
const elapsed = performance.now() - start;
if (elapsed > target.intervalMs) {
this.logger.warn(
{ elapsed, intervalMs: target.intervalMs, targetId: target.id },
`拨测超时: ${target.id} 耗时 ${Math.round(elapsed)}ms > 间隔 ${target.intervalMs}ms`,
);
}
const delay = Math.max(0, target.intervalMs - elapsed);
try {
await sleep(delay, signal);
} catch {
break;
}
}
}
private async runOnce(target: ResolvedTargetBase, signal?: AbortSignal): Promise<CheckResult> {
await this.semaphore.acquire();
if (signal?.aborted) {
this.semaphore.release();
throw new DOMException("Aborted", "AbortError");
}
try {
const result = await this.runCheck(target);
this.writeResult(result);
this.logStateChange(result);
this.logCheckDebug(result);
return result;
} catch (error) {
const reason = formatReason(error);
this.logger.error({ reason, targetId: target.id, targetType: target.type }, `探针执行失败: ${reason}`);
const errorResult: CheckResult = {
detail: null,
durationMs: null,
failure: errorFailure("internal", "engine", reason),
matched: false,
observation: null,
targetId: target.id,
timestamp: new Date().toISOString(),
};
this.writeResult(errorResult);
return errorResult;
} finally {
this.semaphore.release();
}
}
private writeResult(result: CheckResult): void {
const targetId = this.targetNameToId.get(result.targetName);
if (!targetId) return;
if (!this.targetIds.has(result.targetId)) return;
this.store.insertCheckResult({
durationMs: result.durationMs,
failure: result.failure,
matched: result.matched,
statusDetail: result.statusDetail,
targetId,
observation: result.observation ?? null,
targetId: result.targetId,
timestamp: result.timestamp,
});
}
}
function formatReason(reason: unknown): string {
return isError(reason) ? reason.message : String(reason);
}
function sleep(ms: number, signal: AbortSignal): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (signal.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, ms);
function onAbort() {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
}
signal.addEventListener("abort", onAbort, { once: true });
});
}

View File

@@ -0,0 +1,281 @@
import { DOMParser } from "@xmldom/xmldom";
import * as cheerio from "cheerio";
import { isPlainObject } from "es-toolkit";
import * as xpath from "xpath";
import type { CheckFailure } from "../types";
import type {
ContentCssExpectation,
ContentExpectation,
ContentExpectations,
ContentJsonExpectation,
ContentValueExpectation,
ContentXpathExpectation,
ExpectationResult,
RawContentCssExpectation,
RawContentExpectation,
RawContentExpectations,
RawContentJsonExpectation,
RawContentXpathExpectation,
ValueExpectation,
ValueMatcher,
} from "./types";
import { errorFailure, mismatchFailure } from "./failure";
import { MATCHER_KEY_SET } from "./keys";
import { applyValueMatcher, displayValueExpectation, evaluateJsonPath } from "./value";
type ParsedJsonResult = { error: string; ok: false } | { ok: true; value: unknown };
export function checkContentExpectations(
source: unknown,
expectations: ContentExpectations | undefined,
options: { path?: string; phase: CheckFailure["phase"] },
): ExpectationResult {
if (!expectations || expectations.length === 0) return { failure: null, matched: true };
const basePath = options.path ?? options.phase;
let parsedJson: ParsedJsonResult | undefined;
for (let i = 0; i < expectations.length; i++) {
const expectation = expectations[i]!;
if (expectation.kind === "json" && parsedJson === undefined) {
parsedJson = parseJsonSource(source);
}
const result = checkSingleContentExpectation(source, expectation, `${basePath}[${i}]`, options.phase, parsedJson);
if (!result.matched) return result;
}
return { failure: null, matched: true };
}
export function resolveContentExpectations(raw: RawContentExpectations | undefined): ContentExpectations | undefined {
if (raw === undefined) return undefined;
return raw.map((entry) => resolveContentExpectation(entry));
}
function checkCssExpectation(
source: unknown,
expectation: ContentCssExpectation,
expectationPath: string,
phase: CheckFailure["phase"],
): ExpectationResult {
const fullPath = `${expectationPath}.css(${expectation.selector}${expectation.attr ? `@${expectation.attr}` : ""})`;
let $: cheerio.CheerioAPI;
try {
$ = cheerio.load(contentText(source));
} catch {
return { failure: errorFailure(phase, fullPath, "failed to parse HTML"), matched: false };
}
const el = $(expectation.selector).first();
const actual = el.length === 0 ? undefined : expectation.attr ? el.attr(expectation.attr) : el.text();
if (!applyValueMatcher(actual, expectation.matcher)) {
return {
failure: mismatchFailure(
phase,
fullPath,
displayValueExpectation(expectation.matcher),
actual,
`css selector ${expectation.selector} mismatch`,
),
matched: false,
};
}
return { failure: null, matched: true };
}
function checkJsonExpectation(
expectation: ContentJsonExpectation,
expectationPath: string,
phase: CheckFailure["phase"],
parsedJson?: ParsedJsonResult,
): ExpectationResult {
const fullPath = `${expectationPath}.json(${expectation.path})`;
if (!parsedJson?.ok) {
return { failure: errorFailure(phase, fullPath, parsedJson?.error ?? "content is not valid JSON"), matched: false };
}
const actual = evaluateJsonPath(parsedJson.value, expectation.path);
if (!applyValueMatcher(actual, expectation.matcher)) {
return {
failure: mismatchFailure(
phase,
fullPath,
displayValueExpectation(expectation.matcher),
actual,
`json path ${expectation.path} mismatch`,
),
matched: false,
};
}
return { failure: null, matched: true };
}
function checkSingleContentExpectation(
source: unknown,
expectation: ContentExpectation,
expectationPath: string,
phase: CheckFailure["phase"],
parsedJson?: ParsedJsonResult,
): ExpectationResult {
switch (expectation.kind) {
case "css":
return checkCssExpectation(source, expectation, expectationPath, phase);
case "json":
return checkJsonExpectation(expectation, expectationPath, phase, parsedJson);
case "value":
return checkValueContentExpectation(source, expectation, expectationPath, phase);
case "xpath":
return checkXpathExpectation(source, expectation, expectationPath, phase);
}
}
function checkValueContentExpectation(
source: unknown,
expectation: ContentValueExpectation,
expectationPath: string,
phase: CheckFailure["phase"],
): ExpectationResult {
if (!applyValueMatcher(source, expectation.matcher, { stringifyNonString: true })) {
return {
failure: mismatchFailure(
phase,
expectationPath,
displayValueExpectation(expectation.matcher),
source,
`${phase} expectation mismatch`,
),
matched: false,
};
}
return { failure: null, matched: true };
}
function checkXpathExpectation(
source: unknown,
expectation: ContentXpathExpectation,
expectationPath: string,
phase: CheckFailure["phase"],
): ExpectationResult {
const fullPath = `${expectationPath}.xpath(${expectation.path})`;
let doc: ReturnType<DOMParser["parseFromString"]>;
try {
doc = new DOMParser().parseFromString(contentText(source), "text/xml");
} catch {
return { failure: errorFailure(phase, fullPath, "failed to parse XML/HTML"), matched: false };
}
const result = xpath.select(expectation.path, doc as unknown as Node);
const actual = xpathValue(result);
if (!applyValueMatcher(actual, expectation.matcher)) {
return {
failure: mismatchFailure(
phase,
fullPath,
displayValueExpectation(expectation.matcher),
actual,
`xpath ${expectation.path} mismatch`,
),
matched: false,
};
}
return { failure: null, matched: true };
}
function contentText(source: unknown): string {
if (source === null || source === undefined) return "";
if (typeof source === "string") return source;
if (typeof source === "number" || typeof source === "boolean" || typeof source === "bigint") return String(source);
if (typeof source === "symbol") return source.description ?? "";
if (typeof source === "function") return source.name;
return JSON.stringify(source) ?? "";
}
function extractDirectMatcher(raw: Record<string, unknown>): ValueMatcher {
const matcher: ValueMatcher = {};
for (const [key, value] of Object.entries(raw)) {
if (MATCHER_KEY_SET.has(key) && value !== undefined) {
(matcher as Record<string, unknown>)[key] = value;
}
}
return matcher;
}
function extractExtractorMatcher(raw: Record<string, unknown>, ownFields: ReadonlySet<string>): ValueExpectation {
const matcher: ValueMatcher = {};
for (const [key, value] of Object.entries(raw)) {
if (ownFields.has(key)) continue;
if (MATCHER_KEY_SET.has(key) && value !== undefined) {
(matcher as Record<string, unknown>)[key] = value;
}
}
if (Object.keys(matcher).length === 0) return { exists: true };
return matcher;
}
function parseJsonSource(source: unknown): ParsedJsonResult {
if (typeof source !== "string") return { ok: true, value: source };
try {
return { ok: true, value: JSON.parse(source) as unknown };
} catch {
return { error: "content is not valid JSON", ok: false };
}
}
function resolveContentExpectation(raw: RawContentExpectation): ContentExpectation {
if (!isPlainObject(raw)) {
return { kind: "value", matcher: { equals: raw } };
}
const record = raw as Record<string, unknown>;
if (isPlainObject(record["json"])) {
const json = record["json"] as RawContentJsonExpectation;
return {
kind: "json",
matcher: extractExtractorMatcher(json as unknown as Record<string, unknown>, new Set(["path"])),
path: json.path,
};
}
if (isPlainObject(record["css"])) {
const css = record["css"] as RawContentCssExpectation;
const expectation: ContentCssExpectation = {
kind: "css",
matcher: extractExtractorMatcher(css as unknown as Record<string, unknown>, new Set(["attr", "selector"])),
selector: css.selector,
};
if (css.attr !== undefined) expectation.attr = css.attr;
return expectation;
}
if (isPlainObject(record["xpath"])) {
const xpathExpectation = record["xpath"] as RawContentXpathExpectation;
return {
kind: "xpath",
matcher: extractExtractorMatcher(xpathExpectation as unknown as Record<string, unknown>, new Set(["path"])),
path: xpathExpectation.path,
};
}
return { kind: "value", matcher: extractDirectMatcher(record) };
}
function xpathValue(result: unknown): unknown {
if (!Array.isArray(result)) return result;
if (result.length === 0) return undefined;
const node = (result as unknown[])[0]!;
if (typeof node !== "object" || node === null) return node;
const asNode = node as Node;
return asNode.nodeValue ?? (asNode as unknown as Element).textContent ?? "";
}

View File

@@ -1,4 +1,6 @@
import type { CheckFailure } from "../../types";
import { isString } from "es-toolkit";
import type { CheckFailure } from "../types";
export function errorFailure(phase: CheckFailure["phase"], path: string, message: string): CheckFailure {
return {
@@ -28,7 +30,9 @@ export function mismatchFailure(
export function truncateActual(value: unknown, maxLen = 200): unknown {
if (value === undefined || value === null) return value;
const str = typeof value === "string" ? value : JSON.stringify(value);
const str = isString(value) ? value : typeof value === "object" && value !== null ? JSON.stringify(value) : undefined;
if (str === undefined) return value;
if (str.length <= maxLen) return value;
return str.slice(0, maxLen) + "...";
return `${str.slice(0, maxLen)}…(共 ${str.length} 字符)`;
}

View File

@@ -0,0 +1,14 @@
import type { ExpectationResult, KeyedExpectations } from "./types";
import { checkKeyedExpectations } from "./keyed";
export function checkHeaderExpectations(
headers: Record<string, unknown>,
expectations?: KeyedExpectations,
): ExpectationResult {
return checkKeyedExpectations(headers, expectations, {
normalizeKey: (key) => key.toLowerCase(),
path: "headers",
phase: "headers",
});
}

View File

@@ -0,0 +1,46 @@
import type { CheckFailure } from "../types";
import type { ExpectationResult, KeyedExpectations, RawKeyedExpectations } from "./types";
import { mismatchFailure } from "./failure";
import { applyValueMatcher, displayValueExpectation, resolveValueExpectation } from "./value";
export function checkKeyedExpectations(
actual: Record<string, unknown>,
expectations: KeyedExpectations | undefined,
options: { normalizeKey?: (key: string) => string; path?: string; phase: CheckFailure["phase"] },
): ExpectationResult {
if (!expectations || expectations.length === 0) return { failure: null, matched: true };
const normalizeKey = options.normalizeKey ?? ((key: string) => key);
const basePath = options.path ?? options.phase;
const actualMap = new Map<string, unknown>();
for (const [key, value] of Object.entries(actual)) {
actualMap.set(normalizeKey(key), value);
}
for (const expectation of expectations) {
const actualValue = actualMap.get(normalizeKey(expectation.key));
if (!applyValueMatcher(actualValue, expectation.matcher)) {
return {
failure: mismatchFailure(
options.phase,
`${basePath}.${expectation.key}`,
displayValueExpectation(expectation.matcher),
actualValue,
`${expectation.key} mismatch`,
),
matched: false,
};
}
}
return { failure: null, matched: true };
}
export function resolveKeyedExpectations(raw: RawKeyedExpectations | undefined): KeyedExpectations | undefined {
if (raw === undefined) return undefined;
return Object.entries(raw).map(([key, value]) => ({
key,
matcher: resolveValueExpectation(value),
}));
}

View File

@@ -0,0 +1,7 @@
export const MatcherKeys = ["contains", "empty", "equals", "exists", "gt", "gte", "lt", "lte", "regex"] as const;
export const MATCHER_KEY_SET: ReadonlySet<string> = new Set<string>(MatcherKeys);
export const ContentExtractorKeys = ["css", "json", "xpath"] as const;
export const CONTENT_EXTRACTOR_KEY_SET: ReadonlySet<string> = new Set<string>(ContentExtractorKeys);

View File

@@ -0,0 +1,50 @@
import { isPlainObject } from "es-toolkit";
import { resolveContentExpectations } from "./content";
import { CONTENT_EXTRACTOR_KEY_SET, MATCHER_KEY_SET } from "./keys";
import { isValueMatcherObject, isValueMatcherPrimitive, resolveValueExpectation } from "./value";
type ExpectRecord = Record<string, unknown>;
export function compactExpect(original: ExpectRecord, overrides: ExpectRecord): ExpectRecord {
const result: ExpectRecord = {};
for (const [key, value] of Object.entries(original)) {
if (value !== undefined) result[key] = value;
}
for (const [key, value] of Object.entries(overrides)) {
if (value !== undefined) result[key] = value;
}
return result;
}
export function normalizeContent(value: unknown): unknown {
if (value === undefined) return undefined;
if (!Array.isArray(value)) return value;
return (value as unknown[]).map((entry): unknown => {
if (!canNormalizeContentEntry(entry)) return entry;
const resolved = resolveContentExpectations([entry] as never);
return resolved?.[0];
});
}
export function normalizeKeyed(value: unknown): unknown {
if (value === undefined) return undefined;
if (!isPlainObject(value)) return value;
return Object.entries(value as ExpectRecord).map(([key, item]) => ({ key, matcher: normalizeValue(item) }));
}
export function normalizeValue(value: unknown): unknown {
if (value === undefined) return undefined;
if (isValueMatcherPrimitive(value) || isValueMatcherObject(value)) return resolveValueExpectation(value);
return value;
}
function canNormalizeContentEntry(value: unknown): boolean {
if (!isPlainObject(value)) return false;
const keys = Object.keys(value);
const extractorKeys = keys.filter((key) => CONTENT_EXTRACTOR_KEY_SET.has(key));
const matcherKeys = keys.filter((key) => MATCHER_KEY_SET.has(key));
if (extractorKeys.length === 0) return matcherKeys.length > 0 && matcherKeys.length === keys.length;
if (extractorKeys.length !== 1 || matcherKeys.length > 0 || keys.length !== 1) return false;
return isPlainObject((value as ExpectRecord)[extractorKeys[0]!]);
}

View File

@@ -0,0 +1,151 @@
export function isUnsafeRegex(pattern: string): boolean {
const groups = findQuantifiedGroups(pattern);
return groups.some((group) => containsQuantifier(group) || containsOverlappingAlternation(group));
}
function containsOverlappingAlternation(pattern: string): boolean {
const branches = splitTopLevelAlternation(stripGroupPrefix(pattern));
if (branches.length < 2) return false;
for (let i = 0; i < branches.length; i++) {
const current = branches[i]!;
if (current === "") continue;
for (let j = i + 1; j < branches.length; j++) {
const next = branches[j]!;
if (next === "") continue;
if (current === next || current.startsWith(next) || next.startsWith(current)) return true;
}
}
return false;
}
function containsQuantifier(pattern: string): boolean {
const input = stripGroupPrefix(pattern);
let inCharClass = false;
for (let i = 0; i < input.length; i++) {
const char = input[i]!;
if (isEscaped(input, i)) continue;
if (char === "[") {
inCharClass = true;
continue;
}
if (char === "]") {
inCharClass = false;
continue;
}
if (inCharClass) continue;
if (char === "*" || char === "+" || char === "?") return true;
if (char === "{" && readQuantifierBody(input, i) !== null) return true;
}
return false;
}
function findQuantifiedGroups(pattern: string): string[] {
const groups: string[] = [];
const stack: number[] = [];
let inCharClass = false;
for (let i = 0; i < pattern.length; i++) {
const char = pattern[i]!;
if (isEscaped(pattern, i)) continue;
if (char === "[") {
inCharClass = true;
continue;
}
if (char === "]") {
inCharClass = false;
continue;
}
if (inCharClass) continue;
if (char === "(") {
stack.push(i);
continue;
}
if (char === ")") {
const start = stack.pop();
if (start === undefined) continue;
if (hasRepeatingQuantifierAt(pattern, i + 1)) {
groups.push(pattern.slice(start + 1, i));
}
}
}
return groups;
}
function hasRepeatingQuantifierAt(pattern: string, index: number): boolean {
const char = pattern[index];
if (char === "*" || char === "+") return true;
if (char !== "{") return false;
const body = readQuantifierBody(pattern, index);
if (body === null) return false;
const parts = body.split(",");
if (parts.length === 1) return Number(parts[0]) > 1;
if (parts[1] === "") return true;
return Number(parts[1]) > 1;
}
function isEscaped(pattern: string, index: number): boolean {
let slashCount = 0;
for (let i = index - 1; i >= 0 && pattern[i] === "\\"; i--) {
slashCount++;
}
return slashCount % 2 === 1;
}
function readQuantifierBody(pattern: string, index: number): null | string {
const end = pattern.indexOf("}", index + 1);
if (end === -1) return null;
const body = pattern.slice(index + 1, end);
return /^\d+(?:,\d*)?$/.test(body) ? body : null;
}
function splitTopLevelAlternation(pattern: string): string[] {
const branches: string[] = [];
let start = 0;
let depth = 0;
let inCharClass = false;
for (let i = 0; i < pattern.length; i++) {
const char = pattern[i]!;
if (isEscaped(pattern, i)) continue;
if (char === "[") {
inCharClass = true;
continue;
}
if (char === "]") {
inCharClass = false;
continue;
}
if (inCharClass) continue;
if (char === "(") {
depth++;
continue;
}
if (char === ")") {
depth = Math.max(0, depth - 1);
continue;
}
if (char === "|" && depth === 0) {
branches.push(pattern.slice(start, i));
start = i + 1;
}
}
branches.push(pattern.slice(start));
return branches;
}
function stripGroupPrefix(pattern: string): string {
if (pattern.startsWith("?:") || pattern.startsWith("?=") || pattern.startsWith("?!")) return pattern.slice(2);
if (pattern.startsWith("?<=") || pattern.startsWith("?<!")) return pattern.slice(3);
const namedCapture = /^\?<[^>]+>/.exec(pattern);
return namedCapture ? pattern.slice(namedCapture[0].length) : pattern;
}

View File

@@ -0,0 +1,27 @@
import { isNumber } from "es-toolkit";
import type { ExpectationResult } from "./types";
import { mismatchFailure } from "./failure";
export function checkStatusCode(statusCode: number, allowed: Array<number | string>): ExpectationResult {
const matched = allowed.some((pattern) => {
if (isNumber(pattern)) return statusCode === pattern;
const base = parseInt(pattern[0]!, 10) * 100;
return statusCode >= base && statusCode < base + 100;
});
if (!matched) {
return {
failure: mismatchFailure(
"status",
"status",
allowed,
statusCode,
`status ${statusCode} not in [${allowed.join(", ")}]`,
),
matched: false,
};
}
return { failure: null, matched: true };
}

Some files were not shown because too many files have changed in this diff Show More