1
0

feat: 重构配置校验为 TypeBox + Ajv + semantic validator,严格禁止未知字段

- 新增 config-contract 模块(TypeBox fragments、Ajv 契约校验、ConfigValidationIssue)
- CheckerDefinition 扩展为含 configKey、schemas、validate 的完整插件接口
- HTTP/Command 各自维护 contract.ts + validate.ts,校验从 resolve 中分离
- resolve 不再承担校验,只做默认值合并和路径/单位解析
- config-loader 流程: unknown → RawProbeConfig → ValidatedProbeConfig → ResolvedConfig
- 导出 probe-config.schema.json,新增 schema/schema:check 脚本
- 更新 DEVELOPMENT.md 新增 1.7 开发新 Checker 完整指引
- 同步更新 4 个 main specs(probe-config、command-checker、expect-body-checkers、checker-runner-abstraction)
This commit is contained in:
2026-05-13 12:19:36 +08:00
parent bce0f8e7a8
commit 7b20b59b79
38 changed files with 3034 additions and 675 deletions

View File

@@ -35,27 +35,32 @@ src/
trend.ts GET /api/targets/:id/trend
checker/
types.ts 类型定义
config-loader.ts YAML 配置解析与校验
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析
config-contract/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
store.ts SQLite 数据存储
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
size.ts 大小单位解析
runner/ Checker 统一抽象与注册机制
types.ts Checker 接口、CheckerContext、ResolveContext
types.ts CheckerDefinition、CheckerContext、ResolveContext
registry.ts CheckerRegistry 注册中心
index.ts 注册入口registerCheckers
shared/ 共享 expect 断言函数(跨 checker 复用)
shared/ 共享 expect 断言和启动期 validator(跨 checker 复用)
failure.ts 失败信息类型
operator.ts 操作符系统applyOperator、evaluateJsonPath
duration.ts 耗时断言
text.ts 文本规则断言
body.ts Body 规则断言JSONPath/XPath/CSS/contains/regex
validate.ts 共享 operator/text/body 语义校验
http/ HTTP Checker 子包
contract.ts HTTP defaults、target.http、expect TypeBox 契约
runner.ts HttpCheckerresolve/execute/serialize
expect.ts HTTP 专用断言status/headers
validate.ts HTTP 配置与 expect 启动期校验
validate.ts HTTP 专属启动期语义校验
command/ Command Checker 子包
contract.ts Command defaults、target.command、expect TypeBox 契约
runner.ts CommandCheckerresolve/execute/serialize
expect.ts Command 专用断言exitCode
validate.ts Command 专属启动期语义校验
shared/
api.ts 前后端共享 TypeScript 类型
web/ Vite + React 前端 Dashboard
@@ -63,9 +68,10 @@ src/
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数)
hooks/ TanStack Query 数据层useTargetDetail 集成轮询/条件查询)
utils/ 前端工具函数
scripts/ 开发、构建和 smoke test 脚本
scripts/ 开发、构建、schema 生成和 smoke test 脚本
tests/ Bun test 测试
openspec/ OpenSpec 变更与规格文档
probe-config.schema.json 用户配置 JSON Schema 导出物
```
## 前后端边界
@@ -143,9 +149,417 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
- **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string`
- **后端内部扩展**`checker/types.ts``CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetName` 等内部字段
- 存储层类型(`StoredTarget``StoredCheckResult`)独立定义,与 API 类型分离
- 配置类型`ProbeConfig``TargetConfig`)支持 discriminated union通过 `type` 字段区分 http/command
- 配置类型按生命周期区分YAML 解析后的 `RawProbeConfig`、已通过契约与语义校验的 `ValidatedProbeConfig`、运行期使用的 `ResolvedConfig`/`ResolvedTarget`
### 1.6 数据存储规范
### 1.6 配置契约与校验
配置加载流程固定为:`unknown -> RawProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析checker 专属规则必须下沉到对应 checker 的 `contract.ts``validate.ts`
契约层使用 `src/server/checker/config-contract/` 中的 TypeBox fragments 生成 JSON Schema并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers``defaults.http.headers``expect.headers``command.env`
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName``defaults`/root 路径。
新增或修改配置字段时必须同步更新TypeBox schema fragments、`probe-config.schema.json` 导出、对应语义 validator、单元测试和 README/DEVELOPMENT 用户文档。提交前运行 `bun run schema:check` 确认导出 schema 与 fragments 一致。
### 1.7 开发新 Checker
Checker 是本项目的核心扩展单元。得益于插件式注册架构,完成一个新 checker 后,**配置校验、引擎调度、数据存储、API 层会自动适配**,无需修改这些中间层代码。
以下以新增 `tcp` 类型 checker 为例,说明完整的开发步骤。
#### 1.7.1 架构总览
```
checkerRegistry单例
├── registerCheckers() ← 注册入口,所有 checker 在此集中注册
│ ├── HttpChecker
│ ├── CommandChecker
│ └── TcpChecker ← 新增
├── config-contract/schema.ts ← 自动遍历 registry 生成全量 JSON Schema
├── config-loader.ts ← 自动遍历 registry 调用 validate() + resolve()
├── engine.ts ← 自动按 target.type 分发到 execute()
└── store.ts ← 自动按 target.type 分发到 serialize()
```
每个 checker 是 `src/server/checker/runner/<type>/` 下的自包含模块,包含四个文件:
| 文件 | 职责 |
| ------------- | ------------------------------------------------------------------------------------- |
| `contract.ts` | TypeBox 契约 schemaconfig / defaults / expect 三部分) |
| `validate.ts` | 启动期语义校验JSON Schema 无法表达的规则) |
| `runner.ts` | Checker 类resolve默认值合并 + 解析、execute执行检查、serializeDB 持久化) |
| `expect.ts` | Checker 专用断言函数 |
#### 1.7.2 步骤一:定义类型
`src/server/checker/types.ts` 中添加 checker 专属类型接口,并更新联合类型:
```typescript
// 1. 添加 TargetConfigYAML 中 target.tcp 字段的原始类型)
export interface TcpTargetConfig {
host: string;
port: number;
timeout?: number;
}
// 2. 添加 ExpectConfig 扩展(如果 checker 有专属 expect 字段)
export interface TcpExpectConfig {
connected?: boolean;
}
// 3. 添加 DefaultsConfigdefaults.tcp 字段)
export interface TcpDefaultsConfig {
timeout?: number;
}
// 4. 添加 Resolved 变体(运行期已合并默认值、已解析路径)
export interface ResolvedTcpTarget {
type: "tcp";
name: string;
group: string;
intervalMs: number;
timeoutMs: number;
tcp: {
host: string;
port: number;
connectTimeout: number;
};
expect?: TcpExpectConfig;
}
```
然后更新以下联合类型:
```typescript
// TargetConfig 联合 — 新增一个分支
export type TargetConfig = BaseTargetConfig &
(
| { http: HttpTargetConfig; type: "http" }
| { command: CommandTargetConfig; type: "command" }
| { tcp: TcpTargetConfig; type: "tcp" } // ← 新增
);
// ResolvedTarget 联合
export type ResolvedTarget = ResolvedHttpTarget | ResolvedCommandTarget | ResolvedTcpTarget; // ← 新增
// DefaultsConfig — 新增可选字段
export interface DefaultsConfig {
interval?: string;
timeout?: string;
http?: HttpDefaultsConfig;
command?: CommandDefaultsConfig;
tcp?: TcpDefaultsConfig; // ← 新增
}
// TargetType 联合
export type TargetType = "command" | "http" | "tcp"; // ← 新增
// ExpectConfig — 如有专属字段则扩展
export interface ExpectConfig {
// ... 现有字段
connected?: boolean; // ← TcpChecker 专属(如果复用公共字段则不需要)
}
```
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
`src/server/checker/runner/tcp/contract.ts` 中定义三部分 schema
```typescript
import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
import { sizeSchema } from "../../config-contract/fragments"; // 复用共享 fragments
export const tcpCheckerSchemas: CheckerSchemas = {
// target.tcp 字段的 schema
config: Type.Object(
{
host: Type.String(),
port: Type.Integer({ maximum: 65535, minimum: 0 }),
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
},
{ additionalProperties: false },
),
// defaults.tcp 字段的 schema
defaults: Type.Object(
{
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
},
{ additionalProperties: false },
),
// target.expect 中 tcp 专属字段的 schema如果无专属字段则用 Type.Object({})
expect: Type.Object(
{
connected: Type.Optional(Type.Boolean()),
},
{ additionalProperties: false },
),
};
```
**可复用的共享 fragments**(来自 `config-contract/fragments.ts`
| Fragment | 用途 |
| ---------------------------- | ---------------------------------------------- |
| `durationSchema` | 时长字符串(`"30s"``"5m"``"500ms"` |
| `httpMethodSchema` | HTTP 方法枚举 |
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
| `statusCodePatternSchema` | 状态码(`100`-`599``"2xx"` |
| `stringMapSchema` | `Record<string, string>`(用于 headers / env |
| `createBodyRulesSchema()` | body 规则数组json/css/xpath/contains/regex |
| `createTextRulesSchema()` | 文本规则数组stdout/stderr |
| `createPureOperatorSchema()` | 操作符对象 |
| `operatorProperties()` | 所有操作符字段的 Record |
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers``command.env`)可以开放任意键名。
#### 1.7.4 步骤三:实现语义校验
`src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则:
```typescript
import type { ConfigValidationIssue } from "../../config-contract/issues";
import type { CheckerValidationInput } from "../types";
import { issue } from "../../config-contract/issues";
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
// 1. 校验 defaults.tcp如需要
const defaults = input.defaults.tcp;
if (defaults) {
// 语义校验示例connectTimeout 不能超过某个上限
}
// 2. 遍历所有 tcp 类型的 target
for (const target of input.targets) {
if (target.type !== "tcp") continue;
const name = target.name;
// 校验 target.tcp 中的语义规则
const tcp = (target as any).tcp;
if (tcp) {
// 示例host 不能为空字符串
if (typeof tcp.host === "string" && tcp.host.trim() === "") {
issues.push(issue("invalid-value", "tcp.host", "host 不能为空字符串", name));
}
}
// 校验 expect如有公共部分可使用 shared/validate.ts 的工具函数)
// validateBodyRules、validateTextRules、validateOperatorObject 等
}
return issues;
}
```
**共享校验工具**`runner/shared/validate.ts`
| 函数 | 用途 |
| --------------------------------------------------------- | --------------------------------- |
| `validateBodyRules(body, path, targetName)` | 校验 body 规则数组 |
| `validateTextRules(rules, path, targetName)` | 校验文本规则数组stdout/stderr |
| `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 |
| `validateJsonPath(path, rulePath, targetName)` | 校验 JSONPath 格式 |
#### 1.7.5 步骤四:实现 Checker 类
`src/server/checker/runner/tcp/runner.ts` 中实现 `CheckerDefinition` 接口的全部成员:
```typescript
import type { CheckResult, ResolvedTarget, TargetConfig } from "../../types";
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
import { tcpCheckerSchemas } from "./contract";
import { validateTcpConfig } from "./validate";
export class TcpChecker implements Checker {
readonly configKey = "tcp"; // YAML 中 target.tcp / defaults.tcp 的键名
readonly type = "tcp"; // target.type 的判别值
readonly schemas = tcpCheckerSchemas;
// 启动期语义校验入口
validate(input: CheckerValidationInput): ConfigValidationIssue[] {
return validateTcpConfig(input);
}
// 将原始配置解析为运行期配置(合并默认值、解析路径和单位)
resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget {
const t = target as TargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
const defaults = context.defaults.tcp;
return {
expect: target.expect,
group: target.group ?? "default",
intervalMs: context.defaultIntervalMs,
name: t.name,
tcp: {
connectTimeout: t.tcp.connectTimeout ?? defaults?.connectTimeout ?? 3000,
host: t.tcp.host,
port: t.tcp.port,
},
timeoutMs: context.defaultTimeoutMs,
type: "tcp",
} satisfies ResolvedTcpTarget;
}
// 执行实际检查,评估 expect返回 CheckResult
async execute(target: ResolvedTarget, ctx: CheckerContext): Promise<CheckResult> {
const t = target as ResolvedTcpTarget;
const timestamp = new Date().toISOString();
const start = performance.now();
try {
// 执行检查逻辑(如 TCP 连接)
// ...
// 评估 expect 规则
// 首个失败即停止,返回 failure
const durationMs = Math.round(performance.now() - start);
return {
durationMs,
failure: null,
matched: true,
statusDetail: "TCP connected",
targetName: t.name,
timestamp,
};
} catch (error) {
const durationMs = Math.round(performance.now() - start);
return {
durationMs,
failure: errorFailure("connection", "connection", isError(error) ? error.message : String(error)),
matched: false,
statusDetail: null,
targetName: t.name,
timestamp,
};
}
}
// 序列化为 DB 存储格式
serialize(target: ResolvedTarget): { config: string; target: string } {
const t = target as ResolvedTcpTarget;
return {
config: JSON.stringify({ host: t.tcp.host, port: t.tcp.port, connectTimeout: t.tcp.connectTimeout }),
target: `${t.tcp.host}:${t.tcp.port}`,
};
}
}
```
**`resolve()` 规范**
- 只做默认值合并、路径解析、单位转换,**不执行校验**
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值
**`execute()` 规范**
- 始终记录 `timestamp`ISO 字符串)和 `start = performance.now()`
- 通过 `ctx.signal``AbortSignal`)支持超时取消
- 首个 expect 失败即停止,返回带 `failure` 的结果
- 成功时 `failure: null, matched: true`
- 异常时使用 `errorFailure(phase, path, message)` 构造 failure
- 不匹配时使用 `mismatchFailure(phase, path, expected, actual, message)` 构造 failure
**可用的共享断言工具**`runner/shared/`
| 模块 | 函数 | 用途 |
| ------------- | ----------------------------------------------------- | ---------------------- |
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
| `body.ts` | `checkBodyExpect(body, rules)` | Body 规则断言 |
| `text.ts` | `checkTextRules(text, rules, phase)` | 文本规则断言 |
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
#### 1.7.6 步骤五:注册 Checker
`src/server/checker/runner/index.ts` 中注册:
```typescript
import { TcpChecker } from "./tcp/runner"; // ← 新增导入
export function registerCheckers(registry = checkerRegistry): void {
registry.register(new HttpChecker());
registry.register(new CommandChecker());
registry.register(new TcpChecker()); // ← 新增注册
}
```
注册后,以下管线会自动适配,**无需修改**
| 模块 | 自动行为 |
| ----------------------------- | ------------------------------------------------------------------------ |
| `config-contract/schema.ts` | 遍历 registry 生成全量 JSON Schemadefaults.tcp + target.tcp + expect |
| `config-contract/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` |
| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` |
| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` |
| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` |
#### 1.7.7 步骤六:更新前端展示
| 文件 | 修改内容 |
| ------------------------------------------- | ------------------------------------------------------------ |
| `src/web/constants/target-type-display.ts` | 在 `TARGET_TYPE_DISPLAY` 中添加 `"tcp": "TCP"` |
| `src/web/constants/target-table-filters.ts` | 在 `typeFilter.list` 中添加 `{ label: "TCP", value: "tcp" }` |
#### 1.7.8 步骤七:编写测试
测试文件放在 `tests/server/checker/runner/tcp/` 下,镜像源文件结构。必须覆盖:
| 测试类别 | 覆盖内容 | 参考 |
| ---------------- | ------------------------------------------ | ---------------------------------------------------------- |
| **契约测试** | TypeBox schema 与 JSON Schema 导出一致性 | `config-contract/validate.test.ts` |
| **语义校验测试** | `validateTcpConfig()` 各种合法/非法输入 | `http/validate.test.ts`(通过 `runner.test.ts` 间接测试) |
| **resolve 测试** | 默认值合并、路径解析、单位转换 | `http/runner.test.ts``HttpChecker.resolve` describe 块 |
| **execute 测试** | 成功/失败/超时/expect 各种规则组合 | `http/runner.test.ts` 的集成测试 |
| **注册测试** | fresh registry 不污染全局、多 checker 注册 | `registry.test.ts` |
| **配置加载测试** | 含新 checker 的 YAML 完整加载流程 | `config-loader.test.ts` |
#### 1.7.9 步骤八:更新文档和 Schema
| 操作 | 命令/文件 |
| --------------------------------- | -------------------------------------------- |
| 重新生成 JSON Schema 导出 | `bun run schema` |
| 检查导出 schema 与 fragments 一致 | `bun run schema:check` |
| 更新配置示例 | `probes.example.yaml` 中添加新类型示例 |
| 更新用户文档 | `README.md` 中的配置格式说明 |
| 更新项目结构 | `DEVELOPMENT.md` 项目结构中的 runner/ 目录树 |
#### 1.7.10 完整检查清单
```
□ src/server/checker/types.ts — 新增类型接口 + 更新联合类型
□ src/server/checker/runner/tcp/contract.ts — TypeBox schemas
□ src/server/checker/runner/tcp/validate.ts — 语义校验
□ src/server/checker/runner/tcp/runner.ts — Checker 类
□ src/server/checker/runner/tcp/expect.ts — 专用断言(如需要)
□ src/server/checker/runner/index.ts — 注册
□ src/web/constants/target-type-display.ts — 前端类型标签
□ src/web/constants/target-table-filters.ts — 前端类型筛选
□ tests/ — 契约 + 校验 + resolve + execute + 注册 测试
□ probes.example.yaml — 配置示例
□ bun run schema + bun run schema:check — Schema 导出同步
□ bun run check — 全量质量检查通过
□ bun run verify — 完整验证(含 build + smoke test
□ README.md — 用户文档
□ DEVELOPMENT.md — 项目结构目录树
```
### 1.8 数据存储规范
基于 `bun:sqlite`WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
@@ -168,7 +582,7 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
- `check_results`target_idFK CASCADE、timestamp、matched0/1、duration_ms、status_detail、failureJSON
- 复合索引:`(target_id, timestamp)`
### 1.7 拨测引擎
### 1.9 拨测引擎
- **调度**`ProbeEngine``es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
- **并发控制**`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks``acquire()` 阻塞等待
@@ -177,7 +591,7 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射
- **生命周期**`start()`/`stop()` 管理定时器,`stop()` 清理所有 `setInterval`
### 1.8 expect 断言系统
### 1.10 expect 断言系统
两层模型:**观测值收集** → **规则校验**
@@ -209,14 +623,14 @@ runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
**操作符**`equals`(深度比较,`es-toolkit/isEqual`)、`contains``match`(正则)、`empty``isNil`+`isEmptyObject`)、`exists``gte`/`lte`/`gt`/`lt`
### 1.9 错误模式
### 1.11 错误模式
- **API 错误**`{ error: "描述", status: <code> }`,状态码 400/404/405/503
- **CheckFailure**`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
- **错误处理**expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`,请求/TLS/timeout 错误归属 `phase:"request"`body 超限/解码/解析错误归属 `phase:"body"`
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
### 1.10 测试规范
### 1.12 测试规范
- 测试文件与源文件对应:`tests/server/checker/runner/shared/body.test.ts``src/server/checker/runner/shared/body.ts`
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
@@ -591,12 +1005,14 @@ bun run test:smoke
### 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` | 清理构建缓存与临时文件 |
| 脚本 | 文件 | 说明 |
| ---------------------- | ----------------------------------- | ------------------------------- |
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
### 3.7 环境变量
@@ -648,9 +1064,10 @@ bun run test:smoke
```bash
bun run lint # ESLint 检查含类型感知规则、导入排序、导入验证、Prettier 格式)
bun run format # Prettier 自动格式化
bun run schema:check # 检查 probe-config.schema.json 是否与 TypeBox fragments 同步
bun run typecheck # TypeScript 类型检查(含 noUnusedLocals、noPropertyAccessFromIndexSignature
bun test # 运行所有测试
bun run check # 一键运行 typecheck + lint + test
bun run check # 一键运行 schema:check + typecheck + lint + test
```
`check` 是日常开发推荐的质量检查命令。