1
0

feat: 配置变量系统与 target id/name 双字段标识

- 新增顶层 variables 段支持 string/number/boolean 字面量
- target 字符串字段支持 、、{...} 转义语法
- 变量解析优先级: variables -> process.env -> 默认值 -> 报错
- 完整引用保留原始类型,部分引用拼接为字符串
- 变量替换在 YAML 解析后、AJV 校验前执行
- 替换仅作用于 targets,跳过 id/type 字段
- target 新增必填 id 字段作为唯一标识,name 改为可选展示名称
- 数据库存储/API/前端全面迁移到 id 标识
- 统一 checker 运行时类型检查为 es-toolkit predicates
- 同步 delta specs 到主 specs,归档 config-variables 变更
This commit is contained in:
2026-05-17 00:37:54 +08:00
parent 366b3211c8
commit 7926514986
53 changed files with 1538 additions and 333 deletions

View File

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

View File

@@ -1,4 +1,5 @@
import { isEmptyObject, isEqual, isNil, isPlainObject } from "es-toolkit";
import { isArray } from "es-toolkit/compat";
import type { ExpectOperator, ExpectValue } from "../types";
@@ -14,7 +15,7 @@ export function applyOperator(actual: unknown, op: ExpectOperator): boolean {
break;
case "empty": {
const isEmpty =
isNil(actual) || actual === "" || (Array.isArray(actual) && actual.length === 0) || isEmptyObject(actual);
isNil(actual) || actual === "" || (isArray(actual) && actual.length === 0) || isEmptyObject(actual);
if (expected !== isEmpty) return false;
break;
}
@@ -67,7 +68,7 @@ export function evaluateJsonPath(json: unknown, path: string): unknown {
if (bracketMatch) {
current = (current as Record<string, unknown>)?.[bracketMatch[1]!];
const idx = parseInt(bracketMatch[2]!, 10);
if (!Array.isArray(current) || idx >= current.length) return undefined;
if (!isArray(current) || idx >= current.length) return undefined;
current = current[idx];
} else {
if (current === null || current === undefined) return undefined;

View File

@@ -1,3 +1,6 @@
import { isBoolean, isNumber, isPlainObject, isString } from "es-toolkit";
import { isArray } from "es-toolkit/compat";
import type { ConfigValidationIssue } from "../schema/issues";
import type { JsonValue } from "../types";
@@ -9,17 +12,17 @@ const OPERATOR_KEY_SET = new Set<string>(OperatorKeys);
export function isJsonValue(value: unknown): value is JsonValue {
if (value === null) return true;
if (typeof value === "string" || typeof value === "boolean") return true;
if (typeof value === "number") return Number.isFinite(value);
if (Array.isArray(value)) return value.every(isJsonValue);
if (typeof value === "object") {
return Object.values(value as Record<string, unknown>).every(isJsonValue);
if (isString(value) || isBoolean(value)) return true;
if (isNumber(value)) return Number.isFinite(value);
if (isArray(value)) return value.every(isJsonValue);
if (isPlainObject(value)) {
return Object.values(value).every(isJsonValue);
}
return false;
}
export function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
return isPlainObject(value);
}
export function validateOperatorObject(
@@ -54,21 +57,21 @@ export function validateOperatorValue(
): ConfigValidationIssue[] {
switch (key) {
case "contains":
return typeof value === "string" ? [] : [issue("invalid-type", path, "必须为字符串", targetName)];
return isString(value) ? [] : [issue("invalid-type", path, "必须为字符串", targetName)];
case "empty":
case "exists":
return typeof value === "boolean" ? [] : [issue("invalid-type", path, "必须为布尔值", targetName)];
return isBoolean(value) ? [] : [issue("invalid-type", path, "必须为布尔值", targetName)];
case "equals":
return isJsonValue(value) ? [] : [issue("invalid-type", path, "必须为 JSON value", targetName)];
case "gt":
case "gte":
case "lt":
case "lte":
return typeof value === "number" && Number.isFinite(value)
return isNumber(value) && Number.isFinite(value)
? []
: [issue("invalid-type", path, "必须为有限数字", targetName)];
case "match":
if (typeof value !== "string") return [issue("invalid-type", path, "必须为字符串", targetName)];
if (!isString(value)) return [issue("invalid-type", path, "必须为字符串", targetName)];
try {
new RegExp(value);
} catch {