From 33f3ae4a8ab58a4d149b04077c0a06adf89039a5 Mon Sep 17 00:00:00 2001 From: Yige Date: Tue, 22 Sep 2026 00:56:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=9D=E6=8C=81=20Registry=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=20Schema=20=E4=BF=A1=E4=BB=BB?= =?UTF-8?q?=E6=91=98=E8=A6=81=E5=85=BC=E5=AE=B9=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entries/anythingllm/manifest.json | 3 +-- entries/dify/manifest.json | 3 +-- entries/lobechat/manifest.json | 3 +-- entries/n8n/manifest.json | 3 +-- entries/open-webui/manifest.json | 3 +-- entries/openclaw/manifest.json | 3 +-- entries/ragflow/manifest.json | 3 +-- schemas/registry-entry.schema.json | 21 +-------------------- schemas/registry-entry.v3.schema.json | 21 +-------------------- scripts/catalog/static-metadata.mjs | 18 ++++++++++++++++++ scripts/catalog/static-metadata.test.mjs | 17 +++++++++++++++++ scripts/catalog/validator.mjs | 4 ++++ scripts/validate-registry.mjs | 4 ++++ 13 files changed, 52 insertions(+), 54 deletions(-) create mode 100644 scripts/catalog/static-metadata.mjs create mode 100644 scripts/catalog/static-metadata.test.mjs diff --git a/entries/anythingllm/manifest.json b/entries/anythingllm/manifest.json index e61a51e..7659d17 100644 --- a/entries/anythingllm/manifest.json +++ b/entries/anythingllm/manifest.json @@ -43,6 +43,5 @@ "path": "/api/v1", "name": "AnythingLLM API" } - ], - "license": "MIT" + ] } diff --git a/entries/dify/manifest.json b/entries/dify/manifest.json index 65c80d6..eb27ceb 100644 --- a/entries/dify/manifest.json +++ b/entries/dify/manifest.json @@ -44,6 +44,5 @@ "path": "/v1", "name": "Dify API" } - ], - "license": "Dify Open Source License (Apache-2.0 with additional conditions)" + ] } diff --git a/entries/lobechat/manifest.json b/entries/lobechat/manifest.json index 0216b39..417ec92 100644 --- a/entries/lobechat/manifest.json +++ b/entries/lobechat/manifest.json @@ -44,6 +44,5 @@ "path": "/api", "name": "LobeChat API" } - ], - "license": "LobeHub Community License" + ] } diff --git a/entries/n8n/manifest.json b/entries/n8n/manifest.json index 682684e..b55d596 100644 --- a/entries/n8n/manifest.json +++ b/entries/n8n/manifest.json @@ -43,6 +43,5 @@ "path": "/api/v1", "name": "n8n REST API" } - ], - "license": "Sustainable Use License; enterprise files separately licensed" + ] } diff --git a/entries/open-webui/manifest.json b/entries/open-webui/manifest.json index b292853..71af1f4 100644 --- a/entries/open-webui/manifest.json +++ b/entries/open-webui/manifest.json @@ -43,6 +43,5 @@ "path": "/api", "name": "Open WebUI API" } - ], - "license": "Open WebUI License (BSD-3-Clause with branding conditions)" + ] } diff --git a/entries/openclaw/manifest.json b/entries/openclaw/manifest.json index f1ca0c2..391d874 100644 --- a/entries/openclaw/manifest.json +++ b/entries/openclaw/manifest.json @@ -45,6 +45,5 @@ "path": "/", "name": "OpenClaw Gateway" } - ], - "license": "MIT" + ] } diff --git a/entries/ragflow/manifest.json b/entries/ragflow/manifest.json index 01abebd..723021c 100644 --- a/entries/ragflow/manifest.json +++ b/entries/ragflow/manifest.json @@ -45,6 +45,5 @@ "path": "/api/v1", "name": "RagFlow API" } - ], - "license": "Apache-2.0" + ] } diff --git a/schemas/registry-entry.schema.json b/schemas/registry-entry.schema.json index ee9dd96..cd472c1 100644 --- a/schemas/registry-entry.schema.json +++ b/schemas/registry-entry.schema.json @@ -196,26 +196,7 @@ }, "fullDesc": { "$ref": "#/definitions/nonEmptyString" }, "shortDesc": { "$ref": "#/definitions/nonEmptyString" }, - "stars": false, - "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":"此收录版本的上游许可名称;自定义附加条款不得省略。版本与许可核验依据见文档。"}, + "stars": { "type": "integer", "minimum": 0 }, "githubUrl": { "$ref": "#/definitions/httpsUrl" }, "install": { "type": "object", diff --git a/schemas/registry-entry.v3.schema.json b/schemas/registry-entry.v3.schema.json index eff3855..c57674c 100644 --- a/schemas/registry-entry.v3.schema.json +++ b/schemas/registry-entry.v3.schema.json @@ -109,26 +109,7 @@ "category": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, "shortDesc": { "$ref": "#/$defs/nonEmptyString" }, "fullDesc": { "$ref": "#/$defs/nonEmptyString" }, - "stars": false, - "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":"此收录版本的上游许可名称;自定义附加条款不得省略。版本与许可核验依据见文档。"}, + "stars": { "type": "number", "minimum": 0 }, "githubUrl": { "type": "string", "format": "uri" }, "install": { "$ref": "#/$defs/dockerInstall" }, "exposes": { "type": "array", "items": { "$ref": "#/$defs/expose" } } diff --git a/scripts/catalog/static-metadata.mjs b/scripts/catalog/static-metadata.mjs new file mode 100644 index 0000000..f6a34be --- /dev/null +++ b/scripts/catalog/static-metadata.mjs @@ -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 +} diff --git a/scripts/catalog/static-metadata.test.mjs b/scripts/catalog/static-metadata.test.mjs new file mode 100644 index 0000000..633309a --- /dev/null +++ b/scripts/catalog/static-metadata.test.mjs @@ -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') +}) diff --git a/scripts/catalog/validator.mjs b/scripts/catalog/validator.mjs index 96488f6..3837418 100644 --- a/scripts/catalog/validator.mjs +++ b/scripts/catalog/validator.mjs @@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs' import { isDeepStrictEqual } from 'node:util' import { basename, join, relative, resolve, sep } from 'node:path' import { validateJsonSchema } from './json-schema.mjs' +import { findStaticMetrics } from './static-metadata.mjs' export const CATALOG_SIDECAR_FILENAME = 'catalog-metadata.v1.json' @@ -222,6 +223,9 @@ export function validateRegistry(repoRoot, options = {}) { } const entry = readJson(manifestPath, diagnostics) 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 if (entry.type === 'docker-app') counts.dockerApps += 1 if (entry.type === 'native-app') counts.nativeApps = (counts.nativeApps ?? 0) + 1 diff --git a/scripts/validate-registry.mjs b/scripts/validate-registry.mjs index c27899e..0f3008e 100644 --- a/scripts/validate-registry.mjs +++ b/scripts/validate-registry.mjs @@ -2,6 +2,7 @@ import { lstat, readFile, readdir } from 'node:fs/promises' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import Ajv from 'ajv' +import { findStaticMetrics } from './catalog/static-metadata.mjs' const scriptDirectory = dirname(fileURLToPath(import.meta.url)) const rootArgumentIndex = process.argv.indexOf('--root') @@ -322,6 +323,9 @@ for (const directoryEntry of await readdir(entriesRoot, { withFileTypes: true }) } const manifest = await readJson(manifestPath) 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) if (manifest.id !== directoryEntry.name) {