fix: 保持 Registry 客户端 Schema 信任摘要兼容 (#9)

This commit is contained in:
2026-09-22 00:56:56 +08:00
committed by GitHub
parent 7728203ed6
commit 33f3ae4a8a
13 changed files with 52 additions and 54 deletions

View File

@@ -43,6 +43,5 @@
"path": "/api/v1", "path": "/api/v1",
"name": "AnythingLLM API" "name": "AnythingLLM API"
} }
], ]
"license": "MIT"
} }

View File

@@ -44,6 +44,5 @@
"path": "/v1", "path": "/v1",
"name": "Dify API" "name": "Dify API"
} }
], ]
"license": "Dify Open Source License (Apache-2.0 with additional conditions)"
} }

View File

@@ -44,6 +44,5 @@
"path": "/api", "path": "/api",
"name": "LobeChat API" "name": "LobeChat API"
} }
], ]
"license": "LobeHub Community License"
} }

View File

@@ -43,6 +43,5 @@
"path": "/api/v1", "path": "/api/v1",
"name": "n8n REST API" "name": "n8n REST API"
} }
], ]
"license": "Sustainable Use License; enterprise files separately licensed"
} }

View File

@@ -43,6 +43,5 @@
"path": "/api", "path": "/api",
"name": "Open WebUI API" "name": "Open WebUI API"
} }
], ]
"license": "Open WebUI License (BSD-3-Clause with branding conditions)"
} }

View File

@@ -45,6 +45,5 @@
"path": "/", "path": "/",
"name": "OpenClaw Gateway" "name": "OpenClaw Gateway"
} }
], ]
"license": "MIT"
} }

View File

@@ -45,6 +45,5 @@
"path": "/api/v1", "path": "/api/v1",
"name": "RagFlow API" "name": "RagFlow API"
} }
], ]
"license": "Apache-2.0"
} }

View File

@@ -196,26 +196,7 @@
}, },
"fullDesc": { "$ref": "#/definitions/nonEmptyString" }, "fullDesc": { "$ref": "#/definitions/nonEmptyString" },
"shortDesc": { "$ref": "#/definitions/nonEmptyString" }, "shortDesc": { "$ref": "#/definitions/nonEmptyString" },
"stars": false, "stars": { "type": "integer", "minimum": 0 },
"views": false,
"viewCount": false,
"visits": false,
"visitCount": false,
"downloads": false,
"downloadCount": false,
"rating": false,
"ratings": false,
"ratingCount": false,
"averageRating": false,
"popularity": false,
"usageCount": false,
"installCount": false,
"installedCount": false,
"activeUsers": false,
"installStatus": false,
"healthScore": false,
"resourceUsage": false,
"license": {"type":"string","minLength":1,"description":"此收录版本的上游许可名称;自定义附加条款不得省略。版本与许可核验依据见文档。"},
"githubUrl": { "$ref": "#/definitions/httpsUrl" }, "githubUrl": { "$ref": "#/definitions/httpsUrl" },
"install": { "install": {
"type": "object", "type": "object",

View File

@@ -109,26 +109,7 @@
"category": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, "category": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
"shortDesc": { "$ref": "#/$defs/nonEmptyString" }, "shortDesc": { "$ref": "#/$defs/nonEmptyString" },
"fullDesc": { "$ref": "#/$defs/nonEmptyString" }, "fullDesc": { "$ref": "#/$defs/nonEmptyString" },
"stars": false, "stars": { "type": "number", "minimum": 0 },
"views": false,
"viewCount": false,
"visits": false,
"visitCount": false,
"downloads": false,
"downloadCount": false,
"rating": false,
"ratings": false,
"ratingCount": false,
"averageRating": false,
"popularity": false,
"usageCount": false,
"installCount": false,
"installedCount": false,
"activeUsers": false,
"installStatus": false,
"healthScore": false,
"resourceUsage": false,
"license": {"type":"string","minLength":1,"description":"此收录版本的上游许可名称;自定义附加条款不得省略。版本与许可核验依据见文档。"},
"githubUrl": { "type": "string", "format": "uri" }, "githubUrl": { "type": "string", "format": "uri" },
"install": { "$ref": "#/$defs/dockerInstall" }, "install": { "$ref": "#/$defs/dockerInstall" },
"exposes": { "type": "array", "items": { "$ref": "#/$defs/expose" } } "exposes": { "type": "array", "items": { "$ref": "#/$defs/expose" } }

View File

@@ -0,0 +1,18 @@
/** Publication-only policy. Keep the byte-pinned authored client schema unchanged. */
const dynamicFields = new Set([
'stars', 'views', 'viewCount', 'visits', 'visitCount', 'downloads', 'downloadCount',
'rating', 'ratings', 'ratingCount', 'averageRating', 'popularity', 'usageCount',
'installCount', 'installedCount', 'activeUsers', 'installStatus', 'installedVersion',
'healthScore', 'resourceUsage', 'lastHealthCheck',
])
export function findStaticMetrics(value, prefix = '') {
if (!value || typeof value !== 'object' || Array.isArray(value)) return []
const findings = []
for (const [key, child] of Object.entries(value)) {
if (dynamicFields.has(key)) findings.push(prefix + key)
if (['metrics', 'statistics', 'stats', 'metadata'].includes(key)) {
findings.push(...findStaticMetrics(child, prefix + key + '.'))
}
}
return findings
}

View File

@@ -0,0 +1,17 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { findStaticMetrics } from './static-metadata.mjs'
test('metrics are rejected without interpreting examples or configuration parameters as analytics', () => {
assert.deepEqual(findStaticMetrics({ stars: 99, metrics: { downloads: 3 }, installStatus: 'installed' }), ['stars', 'metrics.downloads', 'installStatus'])
assert.deepEqual(findStaticMetrics({ description: 'downloads files', toolCount: 13, configSchema: { properties: { rating: { type: 'number' } } } }), [])
})
test('Registry 4.1 keeps the exact published client schema identity', () => {
const version = readFileSync(new URL('../../SCHEMA_VERSION', import.meta.url), 'utf8').trim()
assert.equal(version, '4.1.0')
const bytes = readFileSync(new URL('../../schemas/registry-entry.schema.json', import.meta.url))
assert.equal(createHash('sha256').update(bytes).digest('hex'), '767e103a66b4f8f722d1eb4d9073c218536e8f551ef43ac766dcd1be8f88fc62')
})

View File

@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { isDeepStrictEqual } from 'node:util' import { isDeepStrictEqual } from 'node:util'
import { basename, join, relative, resolve, sep } from 'node:path' import { basename, join, relative, resolve, sep } from 'node:path'
import { validateJsonSchema } from './json-schema.mjs' import { validateJsonSchema } from './json-schema.mjs'
import { findStaticMetrics } from './static-metadata.mjs'
export const CATALOG_SIDECAR_FILENAME = 'catalog-metadata.v1.json' export const CATALOG_SIDECAR_FILENAME = 'catalog-metadata.v1.json'
@@ -222,6 +223,9 @@ export function validateRegistry(repoRoot, options = {}) {
} }
const entry = readJson(manifestPath, diagnostics) const entry = readJson(manifestPath, diagnostics)
if (!entry) continue if (!entry) continue
for (const field of findStaticMetrics(entry)) {
diagnostics.push(diagnostic('error', 'static-metric', relativeFile(root, manifestPath), '$.' + field, 'Dynamic metrics must not be published as static data'))
}
counts.totalEntries += 1 counts.totalEntries += 1
if (entry.type === 'docker-app') counts.dockerApps += 1 if (entry.type === 'docker-app') counts.dockerApps += 1
if (entry.type === 'native-app') counts.nativeApps = (counts.nativeApps ?? 0) + 1 if (entry.type === 'native-app') counts.nativeApps = (counts.nativeApps ?? 0) + 1

View File

@@ -2,6 +2,7 @@ import { lstat, readFile, readdir } from 'node:fs/promises'
import { dirname, join, relative, resolve } from 'node:path' import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import Ajv from 'ajv' import Ajv from 'ajv'
import { findStaticMetrics } from './catalog/static-metadata.mjs'
const scriptDirectory = dirname(fileURLToPath(import.meta.url)) const scriptDirectory = dirname(fileURLToPath(import.meta.url))
const rootArgumentIndex = process.argv.indexOf('--root') const rootArgumentIndex = process.argv.indexOf('--root')
@@ -322,6 +323,9 @@ for (const directoryEntry of await readdir(entriesRoot, { withFileTypes: true })
} }
const manifest = await readJson(manifestPath) const manifest = await readJson(manifestPath)
if (!manifest) continue if (!manifest) continue
for (const field of findStaticMetrics(manifest)) {
addError(`entries/${directoryEntry.name}/manifest.json: dynamic field ${field} cannot be published as static data`)
}
manifests.push(manifest) manifests.push(manifest)
if (manifest.id !== directoryEntry.name) { if (manifest.id !== directoryEntry.name) {