feat: frozen schema + CI validation to prevent client-breaking data

引入 frozen JSON Schema 契约(schemas/)和自动校验,作为已发布客户端的兼容防线。

背景:
PR #1 把 reasoning 模型 defaultTemperature/defaultTopP 写为 null,已发布客户端
schema 严格 number → readComputeConfig 校验失败 → sync 死锁。详见 desirecore PR #471。

本次新增:
- schemas/provider.schema.json: 镜像 desirecore d185299(fix #471 之前)的 strict
  computeProviderSchema/providerModelSchema,禁止 null/string/未知字段
- schemas/{manifest,service-map,pricing,providers-index}.schema.json: 配套契约
- scripts/validate.mjs: 扫所有数据文件自动校验
- __tests__/validate.test.mjs: 28 个测试,含 PR #1 反例的回归测试
- .github/workflows/validate.yml: PR/push 自动跑 validate + test

未来新增字段流程:
1. 先在 desirecore 主仓升级 schema 接受新字段
2. 发布新客户端,等用户升级
3. 再更新本仓库 frozen schema 和数据

否则老客户端因 additionalProperties: false 拒绝未知字段而死锁。
This commit is contained in:
2026-04-25 21:09:23 +08:00
parent 10465e3570
commit bd448f6c43
11 changed files with 902 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://desirecore.net/schemas/config-center/manifest.schema.json",
"title": "ConfigCenterManifest",
"description": "manifest.json 数据契约。客户端通过 presetDataVersion 判断是否合并更新。",
"type": "object",
"required": ["version", "presetDataVersion", "updatedAt"],
"properties": {
"version": {
"type": "string",
"description": "配置中心格式版本semver如 1.0.0",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"presetDataVersion": {
"type": "integer",
"description": "预置数据版本号(递增整数)。客户端运行时通过此字段决定是否合并新数据。每次内容变更必须递增。",
"minimum": 1
},
"updatedAt": {
"type": "string",
"description": "最后更新日期YYYY-MM-DD 格式",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"description": {
"type": "string",
"description": "仓库说明"
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://desirecore.net/schemas/config-center/pricing.schema.json",
"title": "Pricing",
"description": "compute/pricing.json定价相关元数据。",
"type": "object",
"required": ["markupRatio", "usdToCny"],
"properties": {
"markupRatio": {
"type": "number",
"description": "定价加成倍率(如 1.5 表示在原价基础上加 50%",
"exclusiveMinimum": 0
},
"usdToCny": {
"type": "number",
"description": "USD → CNY 汇率",
"exclusiveMinimum": 0
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,214 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://desirecore.net/schemas/config-center/provider.schema.json",
"title": "Provider",
"description": "frozen baseline schema 镜像 desirecore d185299fix #471 之前)的 computeProviderSchema/providerModelSchema作为已发布客户端兼容契约。任何写入 compute/providers/*.json 或 compute/coding-plans/*.json 的数据都必须通过此校验,否则会破坏老客户端(已发布版本的 schema 无法理解超出此契约的字段或类型)。\n\n关键约束\n- defaultTemperature / defaultTopP 必须是 number不接受 null—— 历史教训:曾因 null 写入导致老客户端校验失败死锁\n- 顶层与 model 内 additionalProperties 均为 false —— 新增字段必须先升级老客户端 schema 再推送数据",
"type": "object",
"required": [
"id",
"provider",
"label",
"baseUrl",
"apiKeyRef",
"apiKeyVerified",
"enabled",
"status",
"services",
"models"
],
"properties": {
"id": {
"type": "string",
"description": "提供商唯一标识符(如 provider-openai-001",
"minLength": 1
},
"provider": {
"type": "string",
"description": "提供商标识openai、anthropic、deepseek、dashscope阿里云等",
"minLength": 1
},
"label": {
"type": "string",
"description": "提供商显示名称(如 OpenAI、Anthropic、阿里云 DashScope",
"minLength": 1
},
"baseUrl": {
"type": "string",
"description": "API 基础 URL如 https://api.openai.com/v1",
"minLength": 1
},
"apiFormat": {
"type": "string",
"description": "API 协议格式openai-completions、anthropic-messages、openai-responses、google-generative-ai 等"
},
"apiKeyRef": {
"type": "string",
"description": "密钥引用名,对应 secrets.json 中的 key空字符串表示未配置密钥"
},
"apiKeyVerified": {
"type": "boolean",
"description": "API Key 是否已通过验证"
},
"enabled": {
"type": "boolean",
"description": "是否启用此提供商"
},
"status": {
"type": "string",
"enum": ["configured", "unconfigured", "error"],
"description": "提供商状态"
},
"services": {
"type": "array",
"description": "支持的服务类型列表(如 chat、reasoning、vision、embedding",
"items": { "type": "string" }
},
"priceCurrency": {
"type": "string",
"enum": ["USD", "CNY"],
"description": "价格货币单位。models 中的 inputPrice/outputPrice 均以此货币计价"
},
"accessMode": {
"type": "string",
"enum": ["api", "coding-plan"],
"description": "接入模式api按量付费或 coding-plan编程订阅套餐"
},
"brandGroup": {
"type": "string",
"description": "品牌分组标识UI 按此字段排序"
},
"codingPlan": {
"type": "object",
"description": "Coding Plan 专属配置(仅当 accessMode = coding-plan 时有效)",
"properties": {
"planTier": { "type": "string" },
"planLabel": { "type": "string" },
"quotas": {
"type": "object",
"properties": {
"per5h": { "type": "number", "minimum": 0 },
"perWeek": { "type": "number", "minimum": 0 },
"perMonth": { "type": "number", "minimum": 0 },
"per7d": { "type": "number", "minimum": 0 }
},
"additionalProperties": false
},
"usageTracking": {
"type": "object",
"properties": {
"method": {
"type": "string",
"enum": ["rest-api", "response-header", "manual", "none"]
},
"endpoint": { "type": "string" },
"headerKeys": {
"type": "object",
"properties": {
"remaining": { "type": "string" },
"limit": { "type": "string" },
"reset": { "type": "string" }
},
"additionalProperties": false
},
"consoleUrl": { "type": "string" }
},
"additionalProperties": false
},
"modelIdOverride": { "type": "string" },
"maxConcurrent": { "type": "number", "minimum": 1 },
"expiresAt": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
},
"models": {
"type": "array",
"description": "此提供商下的可用模型列表",
"items": { "$ref": "#/definitions/model" }
},
"tombstones": {
"type": "array",
"description": "预置显式删除的模型 modelName 白名单",
"items": { "type": "string" }
}
},
"additionalProperties": false,
"definitions": {
"model": {
"type": "object",
"required": ["modelName", "displayName", "serviceType", "capabilities"],
"properties": {
"modelName": {
"type": "string",
"description": "模型 ID用于 API 调用(如 gpt-5-mini、claude-sonnet-4",
"minLength": 1
},
"displayName": {
"type": "string",
"description": "模型显示名称(如 GPT-5 Mini、Claude Sonnet 4",
"minLength": 1
},
"serviceType": {
"type": ["string", "array"],
"items": { "type": "string" },
"description": "服务类型支持单个字符串或数组chat、reasoning、fast、responses、translation、tts、asr、voice_clone、realtime_voice、simultaneous_interpret、vision、ocr、image_gen、video_gen、embedding、rerank、omni、computer_use 等"
},
"description": {
"type": "string",
"description": "模型简要描述"
},
"contextWindow": {
"type": "number",
"description": "上下文窗口大小token 数)",
"minimum": 0
},
"maxOutputTokens": {
"type": "number",
"description": "单次请求最大输出 token 数",
"minimum": 0
},
"capabilities": {
"type": "array",
"description": "模型能力标签chat、vision、tool_use、code、reasoning 等",
"items": { "type": "string" }
},
"inputPrice": {
"type": "number",
"description": "输入价格(每百万 token货币由 Provider.priceCurrency 决定",
"minimum": 0
},
"outputPrice": {
"type": "number",
"description": "输出价格(每百万 token货币由 Provider.priceCurrency 决定",
"minimum": 0
},
"defaultTemperature": {
"type": "number",
"description": "默认温度参数0-2。【重要】必须是 number禁止写为 null 或字符串。reasoning 等不支持温度的模型应完全省略此字段。null 会破坏 fix #471 之前发布的客户端schema 严格 number导致 readComputeConfig 死锁。",
"minimum": 0,
"maximum": 2
},
"defaultTopP": {
"type": "number",
"description": "默认 Top-P 参数0-1。【重要】必须是 number禁止写为 null 或字符串。reasoning 等不支持 Top-P 的模型应完全省略此字段。null 会破坏 fix #471 之前发布的客户端,导致死锁。",
"minimum": 0,
"maximum": 1
},
"extra": {
"type": "object",
"description": "模型特定配置:如 TTS 模型音色列表、ASR 支持格式等",
"additionalProperties": true
},
"apiModelId": {
"type": "string",
"description": "实际传给上游 API 的 model 参数。当 modelName 与 API 期望名称不同时使用"
},
"source": {
"type": "string",
"enum": ["preset", "synced", "user-added", "ollama-discovery"],
"description": "模型来源。config-center 数据应统一为 preset"
}
},
"additionalProperties": false
}
}
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://desirecore.net/schemas/config-center/providers-index.schema.json",
"title": "ProvidersIndex",
"description": "compute/providers/_index.json 与 compute/coding-plans/_index.json 共用。指定 Provider 加载顺序。",
"type": "object",
"required": ["order"],
"properties": {
"description": { "type": "string" },
"order": {
"type": "array",
"description": "Provider 加载顺序,元素为对应 JSON 文件的 basename不含 .json",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://desirecore.net/schemas/config-center/service-map.schema.json",
"title": "ServiceMap",
"description": "compute/service-map.json默认服务到 (provider, model) 的映射。key 为服务类型chat/reasoning/...value 指向具体 Provider 和模型。",
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["modelName", "providerId"],
"properties": {
"modelName": {
"type": "string",
"description": "目标模型 modelName须与对应 Provider 的 models[].modelName 一致",
"minLength": 1
},
"providerId": {
"type": "string",
"description": "目标 Provider 的 id 字段",
"minLength": 1
}
},
"additionalProperties": false
}
}