Compare commits
83 Commits
a5cf6065c2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8fd8bd9c | |||
| 3390eb5e8d | |||
| 145bb8fd04 | |||
| 358f8d011a | |||
| c2dcfab80c | |||
| f38286d74d | |||
| 08b61cbf47 | |||
| c120690cf1 | |||
| 77c6015b3a | |||
| c1db793073 | |||
| 714b635aef | |||
| a6504d5a62 | |||
| 483cdc596b | |||
| 4f33fba793 | |||
| 6601ab458d | |||
| cfca03b4d6 | |||
| cf847ccd7a | |||
| 6e53c8130d | |||
| 6ca8b36542 | |||
| 79358ba50d | |||
| e448cb4654 | |||
| 5238dbe77d | |||
| 007d74934d | |||
| 0d709c7681 | |||
| b432581444 | |||
| ccd16a583e | |||
| 8eac814cc6 | |||
| f3df3a203b | |||
| 60a54b483f | |||
| 6098be2d9e | |||
| b591dcca97 | |||
| 9b53c746f6 | |||
| 375dd3492b | |||
| 22c06820fa | |||
| 12cd05b04e | |||
| 8d8549d07f | |||
| 7a635a0a9f | |||
| 349896bd02 | |||
| 52262a31f6 | |||
| 550c427814 | |||
| c51bc5a0d8 | |||
| 393e8da5fd | |||
| 0a9a9016be | |||
| 31fd3a2a43 | |||
| f7193e98ff | |||
| 7926514986 | |||
| 366b3211c8 | |||
| e924732a02 | |||
| 04c24e6796 | |||
| 146cef982e | |||
| c36df94e59 | |||
| f8d563c668 | |||
| 88f4119a4e | |||
| c46ab14cce | |||
| 8793fbd786 | |||
| 2b08f81a0d | |||
| 86b8cf1950 | |||
| d6a77b2c6e | |||
| 28e46b8431 | |||
| 9904f198aa | |||
| c61a4a6091 | |||
| 1c5cfafda6 | |||
| e983e5d75d | |||
| 0fa2c0c811 | |||
| 6e485cc991 | |||
| bcfac52112 | |||
| 31aeee6d60 | |||
| a62007083d | |||
| 76b47006fe | |||
| 147a2559ae | |||
| 6ea185315f | |||
| ecd47748d2 | |||
| bcfb907bd3 | |||
| 26f0bfe104 | |||
| bb6b2bc20b | |||
| c396c29402 | |||
| aade0bbff7 | |||
| 7b20b59b79 | |||
| bce0f8e7a8 | |||
| 2fd0f206be | |||
| 87d946a441 | |||
| ad87be6956 | |||
| 9a71b7967c |
8
.claude/settings.json
Normal file
8
.claude/settings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"tdesign-mcp-server": {
|
||||
"command": "bunx",
|
||||
"args": ["tdesign-mcp-server@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
15
.dockerignore
Normal file
15
.dockerignore
Normal 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
32
.gitattributes
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# 跨平台行尾规范
|
||||
# 所有文本文件统一用 LF(Unix 风格),避免 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
5
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||
"*.{ts,tsx}": ["eslint --fix"],
|
||||
"*.{md,json,yaml,yml}": ["prettier --write"]
|
||||
}
|
||||
|
||||
19
.pi/extensions/pi-permission-system/config.json
Normal file
19
.pi/extensions/pi-permission-system/config.json
Normal 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
8
.pi/mcp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"tdesign-mcp-server": {
|
||||
"command": "bunx",
|
||||
"args": ["tdesign-mcp-server@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ bun.lock
|
||||
.agents/
|
||||
skills-lock.json
|
||||
data/
|
||||
probe-config.schema.json
|
||||
|
||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal 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
|
||||
}
|
||||
707
DEVELOPMENT.md
707
DEVELOPMENT.md
@@ -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 HttpChecker(resolve/execute/serialize)
|
||||
expect.ts HTTP 专用断言(status/headers)
|
||||
command/ Command Checker 子包
|
||||
runner.ts CommandChecker(resolve/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 | 主流三方库 | cheerio(HTML 解析)、xpath + @xmldom/xmldom(XML 解析) |
|
||||
| 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` 表:name(UNIQUE)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grp
|
||||
- `check_results` 表:target_id(FK CASCADE)、timestamp、matched(0/1)、duration_ms、status_detail、failure(JSON)
|
||||
- 复合索引:`(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()` 写入 SQLite,engine 通过 `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
|
||||
└── QueryClientProvider(TanStack Query 全局挂载)
|
||||
└── App(根组件)
|
||||
├── SummaryCards(总览统计卡片)
|
||||
│ └── useSummary() ─── GET /api/summary(8s 轮询)
|
||||
└── TargetBoard(目标列表)
|
||||
├── useTargets() ─── GET /api/targets(8s 轮询)
|
||||
└── 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/summary(8s 自动轮询)
|
||||
├── useTargets() → /api/targets(8s 自动轮询)
|
||||
└── 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/ → 返回前端 SPA(index.html)
|
||||
访问 http://127.0.0.1:3000/api/* → 返回后端 API
|
||||
访问 /assets/* → 返回带不可变缓存的静态资源
|
||||
```
|
||||
|
||||
SPA fallback 逻辑(`src/server/static.ts`):
|
||||
|
||||
- `/` → index.html
|
||||
- 匹配 `/assets/*` → 返回对应文件(未匹配则 404)
|
||||
- 其他路径(如 `/dashboard`)→ fallback 到 index.html(SPA 路由)
|
||||
|
||||
### 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
45
Dockerfile
Normal 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
233
README.md
@@ -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 存储,无需额外数据库服务
|
||||
|
||||
## 应用截图
|
||||
|
||||
| | 亮色 | 暗色 |
|
||||
| ------ | --------------------------------------------------- | ------------------------------------------------- |
|
||||
| 主页 |  |  |
|
||||
| 详情页 |  |  |
|
||||
|
||||
## 快速开始
|
||||
|
||||
**前置条件:** [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
|
||||
|
||||
BIN
assets/screenshot/dark_detail.png
Normal file
BIN
assets/screenshot/dark_detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
BIN
assets/screenshot/dark_index.png
Normal file
BIN
assets/screenshot/dark_index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
BIN
assets/screenshot/light_detail.png
Normal file
BIN
assets/screenshot/light_detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 460 KiB |
BIN
assets/screenshot/light_index.png
Normal file
BIN
assets/screenshot/light_index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
436
bun.lock
436
bun.lock
@@ -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
3
bunfig.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[test]
|
||||
preload = ["./tests/setup.ts"]
|
||||
exclude = ["./tests/e2e/**"]
|
||||
17
docker/probes.yaml
Normal file
17
docker/probes.yaml
Normal 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
127
docs/README.md
Normal 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
115
docs/development/README.md
Normal 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 协作方式或开发文档索引时,必须更新本文档。
|
||||
114
docs/development/architecture.md
Normal file
114
docs/development/architecture.md
Normal 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
142
docs/development/backend.md
Normal 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
221
docs/development/checker.md
Normal 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 可写 DSL,Normalized 描述 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 时,必须更新本文档。
|
||||
130
docs/development/frontend.md
Normal file
130
docs/development/frontend.md
Normal 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
127
docs/development/release.md
Normal 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、脚本参数或发布验证方式时,必须更新本文档。
|
||||
@@ -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 先探测再决定,不把仓库结构写死在提示词里
|
||||
|
||||
|
||||
@@ -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. 收尾
|
||||
|
||||
列出所有修改的文件、备份文件、测试命令与结果、文档同步摘要和剩余风险。
|
||||
列出所有修改的文件、备份文件、验证命令或检查结果、文档同步摘要和剩余风险。
|
||||
|
||||
若本次因缺少测试结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
|
||||
若本次因缺少验证结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
|
||||
|
||||
@@ -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. 收尾
|
||||
|
||||
|
||||
@@ -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
35
docs/user/README.md
Normal 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)。
|
||||
49
docs/user/checkers/README.md
Normal file
49
docs/user/checkers/README.md
Normal 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
38
docs/user/checkers/cmd.md
Normal 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
74
docs/user/checkers/cpu.md
Normal 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
38
docs/user/checkers/db.md
Normal 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
104
docs/user/checkers/dns.md
Normal 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"`。
|
||||
48
docs/user/checkers/http.md
Normal file
48
docs/user/checkers/http.md
Normal 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。
|
||||
45
docs/user/checkers/icmp.md
Normal file
45
docs/user/checkers/icmp.md
Normal 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
53
docs/user/checkers/llm.md
Normal 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
119
docs/user/checkers/mem.md
Normal 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
35
docs/user/checkers/tcp.md
Normal 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
43
docs/user/checkers/udp.md
Normal 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
81
docs/user/checkers/ws.md
Normal 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
135
docs/user/configuration.md
Normal 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
109
docs/user/deployment.md
Normal 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
161
docs/user/expectations.md
Normal 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 字段、快速失败顺序或已移除字段说明时,必须更新本文档。
|
||||
73
docs/user/troubleshooting.md
Normal file
73
docs/user/troubleshooting.md
Normal 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 风险检测。被拒绝时应改写为更明确、回溯风险更低的表达式。
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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文档
|
||||
|
||||
181
openspec/schemas/fast-drive/schema.yaml
Normal file
181
openspec/schemas/fast-drive/schema.yaml
Normal 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.
|
||||
77
openspec/schemas/fast-drive/templates/design.md
Normal file
77
openspec/schemas/fast-drive/templates/design.md
Normal 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 |
|
||||
19
openspec/schemas/fast-drive/templates/tasks.md
Normal file
19
openspec/schemas/fast-drive/templates/tasks.md
Normal 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
|
||||
@@ -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 的客户端路由
|
||||
@@ -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 的对象
|
||||
@@ -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** 计算 availability(upCount / 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()`,因需要在事务闭包内持有引用
|
||||
@@ -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 直接展示而不做类型判断
|
||||
@@ -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 以非零状态退出且不能继续声明验证成功
|
||||
@@ -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 规则
|
||||
@@ -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 跳过,不覆盖已有配置
|
||||
@@ -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 状态色
|
||||
@@ -1,128 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 HTTP 拨测中响应体校验方法集(contains/regex/json/css/xpath)、操作符系统和响应头校验的行为规范。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 响应体多种校验方法
|
||||
系统 SHALL 支持对 HTTP 响应体进行五种可组合的校验方法:contains(子串)、regex(正则)、json(JSONPath)、css(CSS 选择器)、xpath(XPath)。这些方法 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 摘要,而不是持久化完整响应体或命令输出
|
||||
@@ -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 不要求将后端运行时实现打包进前端
|
||||
@@ -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() 关闭数据库连接后退出进程
|
||||
@@ -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 为 null,recentSamples 为空数组
|
||||
|
||||
### 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`(boolean,matched === 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
|
||||
@@ -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
|
||||
@@ -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)显示错误提示
|
||||
@@ -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 表包含 id(INTEGER PRIMARY KEY AUTOINCREMENT)、target_id(INTEGER NOT NULL)、timestamp(TEXT NOT NULL)、matched(INTEGER NOT NULL)、duration_ms(REAL)、status_detail(TEXT)、failure(TEXT),不包含 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 或 DOWN:matched=true 为 UP,matched=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
|
||||
@@ -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 interval,target 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_detail,failure 为 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 执行该目标
|
||||
@@ -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 使构建或测试命令失败
|
||||
@@ -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 不被包含在产物中
|
||||
@@ -1,128 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义目标详情 Drawer:时间范围筛选(TDesign RadioGroup + DateRangePicker)、Tabs 组织概览/记录两个面板、统计图表和分页检查结果列表。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 目标详情 Drawer
|
||||
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。
|
||||
|
||||
#### Scenario: 打开 Drawer
|
||||
- **WHEN** 用户点击某个目标表格行
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="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 RadioGroup(variant=default-filled)快捷按钮:1小时、6小时、24小时、7天
|
||||
|
||||
#### Scenario: 点击快捷按钮
|
||||
- **WHEN** 用户点击快捷按钮(如 "24小时")
|
||||
- **THEN** 系统 SHALL 自动设置对应的起止时间,DateRangePicker 显示对应的时间范围,该按钮高亮
|
||||
|
||||
#### Scenario: 自定义日期时间范围
|
||||
- **WHEN** 用户通过 TDesign DateRangePicker(mode=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 Divider(align="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 使用内建 pagination(disableDataPage=true),分页器显示在表格底部
|
||||
|
||||
#### Scenario: 翻页触发请求
|
||||
- **WHEN** 用户切换分页页码
|
||||
- **THEN** 系统 SHALL 请求对应页码的服务端数据,表格更新
|
||||
|
||||
#### Scenario: 记录数据加载中
|
||||
- **WHEN** 历史记录正在加载
|
||||
- **THEN** 表格 SHALL 显示 loading 状态
|
||||
@@ -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"`
|
||||
@@ -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 组件渲染,标题显示"#",宽度 60px,fixed="left",居中对齐,支持筛选(UP/DOWN/全部)。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style
|
||||
|
||||
#### Scenario: 名称列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 名称列 SHALL 显示目标名称,支持字母排序(zh-CN),ellipsis 超长名称自动省略并 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 导入,不在组件中重复定义
|
||||
@@ -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 值,不抛出异常
|
||||
50
package.json
50
package.json
@@ -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
8106
probe-config.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
127
scripts/build-common.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
174
scripts/build.ts
174
scripts/build.ts
@@ -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();
|
||||
|
||||
40
scripts/bump-version-logic.ts
Normal file
40
scripts/bump-version-logic.ts
Normal 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
45
scripts/bump-version.ts
Normal 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();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
16
scripts/generate-config-schema.ts
Normal file
16
scripts/generate-config-schema.ts
Normal 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
196
scripts/release.ts
Normal 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();
|
||||
}
|
||||
168
scripts/smoke.ts
168
scripts/smoke.ts
@@ -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
11
src/pino-roll.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
@@ -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
132
src/server/bootstrap.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 必须为正整数"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
281
src/server/checker/expect/content.ts
Normal file
281
src/server/checker/expect/content.ts
Normal 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 ?? "";
|
||||
}
|
||||
@@ -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} 字符)`;
|
||||
}
|
||||
14
src/server/checker/expect/headers.ts
Normal file
14
src/server/checker/expect/headers.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
46
src/server/checker/expect/keyed.ts
Normal file
46
src/server/checker/expect/keyed.ts
Normal 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),
|
||||
}));
|
||||
}
|
||||
7
src/server/checker/expect/keys.ts
Normal file
7
src/server/checker/expect/keys.ts
Normal 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);
|
||||
50
src/server/checker/expect/normalize.ts
Normal file
50
src/server/checker/expect/normalize.ts
Normal 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]!]);
|
||||
}
|
||||
151
src/server/checker/expect/redos.ts
Normal file
151
src/server/checker/expect/redos.ts
Normal 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;
|
||||
}
|
||||
27
src/server/checker/expect/status.ts
Normal file
27
src/server/checker/expect/status.ts
Normal 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
Reference in New Issue
Block a user