mirror of
https://git.openapi.site/https://github.com/desirecore/config-center.git
synced 2026-10-21 06:24:14 +08:00
fix(data): complete verified contracts and source maintenance (#114)
This commit is contained in:
128
scripts/update-runtime-catalog.mjs
Normal file
128
scripts/update-runtime-catalog.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
export const SOURCES = Object.freeze({
|
||||
node: 'https://nodejs.org/dist/index.json',
|
||||
npm: 'https://api.github.com/repos/npm/cli/releases?per_page=100',
|
||||
pnpm: 'https://api.github.com/repos/pnpm/pnpm/releases?per_page=100',
|
||||
yarn: 'https://api.github.com/repos/yarnpkg/berry/releases?per_page=100',
|
||||
hatch: 'https://raw.githubusercontent.com/pypa/hatch/b998d2b755bc0dca20054f96da8981532444fc2a/src/hatch/python/distributions.py',
|
||||
})
|
||||
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
||||
const stableVersion = /^v\d+\.\d+\.\d+$/
|
||||
|
||||
export function selectNodeVersions(rows, now) {
|
||||
if (!Array.isArray(rows)) throw new Error('Invalid official Node index')
|
||||
const counts = new Map()
|
||||
const selected = rows.filter(row => {
|
||||
if (!stableVersion.test(row.version) || !/^\d{4}-\d{2}-\d{2}$/.test(row.date)) return false
|
||||
if (row.date > now.slice(0, 10)) return false
|
||||
const major = Number(row.version.split('.')[0].slice(1))
|
||||
if (major < 16 || (counts.get(major) ?? 0) >= 3) return false
|
||||
counts.set(major, (counts.get(major) ?? 0) + 1)
|
||||
return true
|
||||
})
|
||||
if (!selected.length) throw new Error('No stable Node releases')
|
||||
return selected
|
||||
}
|
||||
|
||||
export function selectPackageReleases(rows, name, now) {
|
||||
if (!Array.isArray(rows)) throw new Error(`Invalid ${name} releases`)
|
||||
const seen = new Set()
|
||||
return rows.flatMap(row => {
|
||||
if (row.draft || row.prerelease) return []
|
||||
let tag = row.tag_name
|
||||
if (name === 'yarn') {
|
||||
const match = /^@yarnpkg\/cli\/(\d+\.\d+\.\d+)$/.exec(tag)
|
||||
if (!match) return []
|
||||
tag = `v${match[1]}`
|
||||
}
|
||||
if (!stableVersion.test(tag) || seen.has(tag)) return []
|
||||
if (!row.published_at || !Number.isFinite(Date.parse(row.published_at)) || Date.parse(row.published_at) > Date.parse(now)) return []
|
||||
seen.add(tag)
|
||||
// Client-compatible field name; value is publication time, not commit creation time.
|
||||
return [{ tag_name: tag, created_at: row.published_at }]
|
||||
}).slice(0, 30)
|
||||
}
|
||||
|
||||
export function verifyRecommended(recommended, nodeRows, sums, pythonRelease, now = new Date().toISOString()) {
|
||||
const nowMs = Date.parse(now)
|
||||
if (!Number.isFinite(nowMs)) throw new Error('Invalid verification time')
|
||||
const publishedMs = Date.parse(pythonRelease.published_at)
|
||||
if (!Number.isFinite(publishedMs) || publishedMs > nowMs) throw new Error('Python release unpublished or future publication date')
|
||||
const node = nodeRows.find(row => row.version === `v${recommended.node.version}`)
|
||||
if (!node || node.lts !== recommended.node.ltsCodename) throw new Error('Recommended Node release/LTS mismatch')
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(node.date) || node.date > now.slice(0, 10)) throw new Error('Node release missing or future publication date')
|
||||
if (pythonRelease.tag_name !== recommended.python.distTag || pythonRelease.draft || pythonRelease.prerelease) throw new Error('Python release tag/status mismatch')
|
||||
const lines = new Map(sums.split('\n').flatMap(line => {
|
||||
const match = /^([a-f0-9]{64})\s+\*?(\S+)$/.exec(line.trim())
|
||||
return match ? [[match[2], match[1]]] : []
|
||||
}))
|
||||
const nodeSuffixes = { 'darwin-arm64': 'darwin-arm64.tar.gz', 'darwin-x64': 'darwin-x64.tar.gz', 'win32-x64': 'win-x64.zip', 'win32-arm64': 'win-arm64.zip', 'linux-x64': 'linux-x64.tar.gz', 'linux-arm64': 'linux-arm64.tar.gz' }
|
||||
const pythonTriples = { 'darwin-arm64': 'aarch64-apple-darwin', 'darwin-x64': 'x86_64-apple-darwin', 'win32-x64': 'x86_64-pc-windows-msvc', 'win32-arm64': 'aarch64-pc-windows-msvc', 'linux-x64': 'x86_64-unknown-linux-gnu', 'linux-arm64': 'aarch64-unknown-linux-gnu' }
|
||||
for (const platform of Object.keys(nodeSuffixes)) {
|
||||
const path = recommended.node.archives[platform]
|
||||
if (path !== `v${recommended.node.version}/node-v${recommended.node.version}-${nodeSuffixes[platform]}`) throw new Error('Unsafe Node archive path')
|
||||
if (lines.get(path.split('/').at(-1)) !== recommended.node.sha256[platform]) throw new Error(`Node digest mismatch: ${platform}`)
|
||||
}
|
||||
for (const platform of Object.keys(pythonTriples)) {
|
||||
const path = recommended.python.archives[platform]
|
||||
if (path !== `${recommended.python.distTag}/cpython-${recommended.python.version}+${recommended.python.distTag}-${pythonTriples[platform]}-install_only_stripped.tar.gz`) throw new Error('Unsafe Python archive path')
|
||||
const asset = pythonRelease.assets.find(row => row.name === path.split('/').at(-1))
|
||||
if (!asset || asset.state !== 'uploaded' || asset.digest !== `sha256:${recommended.python.sha256[platform]}`) throw new Error(`Python digest mismatch: ${platform}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function officialText(url) {
|
||||
const allowed = new Set([...Object.values(SOURCES)])
|
||||
const recommended = JSON.parse(await readFile(resolve(ROOT, 'runtimes/recommended.json'), 'utf8'))
|
||||
allowed.add(`https://nodejs.org/dist/v${recommended.node.version}/SHASUMS256.txt`)
|
||||
allowed.add(`https://api.github.com/repos/astral-sh/python-build-standalone/releases/tags/${recommended.python.distTag}`)
|
||||
if (!allowed.has(url)) throw new Error(`Unapproved source: ${url}`)
|
||||
const response = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(30000), headers: { 'User-Agent': 'config-center-runtime-audit' } })
|
||||
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`)
|
||||
return response.text()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const write = process.argv.includes('--write')
|
||||
if (process.argv.slice(2).some(arg => arg !== '--write')) throw new Error('Usage: node scripts/update-runtime-catalog.mjs [--write]')
|
||||
const recommended = JSON.parse(await readFile(resolve(ROOT, 'runtimes/recommended.json'), 'utf8'))
|
||||
const old = JSON.parse(await readFile(resolve(ROOT, 'runtimes/versions-fallback.json'), 'utf8'))
|
||||
const sumsUrl = `https://nodejs.org/dist/v${recommended.node.version}/SHASUMS256.txt`
|
||||
const pythonUrl = `https://api.github.com/repos/astral-sh/python-build-standalone/releases/tags/${recommended.python.distTag}`
|
||||
const urls = [...Object.values(SOURCES), sumsUrl, pythonUrl]
|
||||
const texts = await Promise.all(urls.map(officialText))
|
||||
const source = Object.fromEntries(urls.map((url, i) => [url, texts[i]]))
|
||||
const nodes = JSON.parse(source[SOURCES.node])
|
||||
const pythonRelease = JSON.parse(source[pythonUrl])
|
||||
const now = new Date().toISOString()
|
||||
verifyRecommended(recommended, nodes, source[sumsUrl], pythonRelease, now)
|
||||
// The fallback mirrors bundled Hatch 1.16.5, not standalone's latest release.
|
||||
// Fail closed if its existing rows no longer match that official distribution map.
|
||||
for (const row of old.python) {
|
||||
const expected = row.name.startsWith('pypy') ? `${row.name}-v${row.version}-` : `cpython-${row.version}`
|
||||
const escapedName = row.name.replaceAll('.', '\\.')
|
||||
const section = new RegExp(`^ ['\"]${escapedName}['\"]: \\{([\\s\\S]*?)(?=^ ['\"]|^\\})`, 'm').exec(source[SOURCES.hatch])
|
||||
if (!section || !section[1].includes(expected)) throw new Error(`Hatch pin mismatch: ${row.name}`)
|
||||
}
|
||||
const next = { ...old, generatedAt: now, node: selectNodeVersions(nodes, now), pkgManagers: {} }
|
||||
for (const name of ['npm', 'pnpm', 'yarn']) {
|
||||
next.pkgManagers[name] = selectPackageReleases(JSON.parse(source[SOURCES[name]]), name, now)
|
||||
if (!next.pkgManagers[name].length) throw new Error(`No stable ${name} releases`)
|
||||
}
|
||||
if (!next.node.some(row => row.version === `v${recommended.node.version}`)) throw new Error('Recommended Node absent from snapshot')
|
||||
if (write) {
|
||||
await writeFile(resolve(ROOT, 'runtimes/versions-fallback.json'), `${JSON.stringify(next, null, 2)}\n`)
|
||||
for (const name of ['node', 'npm', 'pnpm', 'yarn']) {
|
||||
const rows = name === 'node' ? next.node : next.pkgManagers[name]
|
||||
const path = name === 'node' ? 'node-snapshot.md' : `package-managers/${name}-snapshot.md`
|
||||
await writeFile(resolve(ROOT, 'docs/runtime-sources', path), `# ${name} 离线目录快照\n\n- 实际读取:${now}\n- 官方来源:[官方目录](${SOURCES[name]})\n- 数据位置:\`runtimes/versions-fallback.json#${name === 'node' ? 'node' : `pkgManagers.${name}`}\`\n- 目录条数:${rows.length}\n- 首条版本:${rows[0].version ?? rows[0].tag_name}\n- 本仓条目指纹(JSON.stringify、SHA-256):\`${hash(rows)}\`\n- 本轮读取的官方正文 SHA-256:\`${createHash('sha256').update(source[SOURCES[name]]).digest('hex')}\`\n\n筛选规则:${name === 'node' ? 'Node 16 及以上,每个大版本最多三条正式发布;保留上游顺序。不表示所有大版本仍受支持。' : '读取官方仓库最近 100 个 releases,排除 draft、prerelease、非正式版本标签及未来发布日期,最多 30 条;Yarn 只接受 @yarnpkg/cli 正式标签。created_at 字段保持客户端结构,值取 published_at。目录不代表当前平台安装测试已通过。'}\n`)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ write, generatedAt: now, node: next.node.length, packageManagers: Object.fromEntries(Object.entries(next.pkgManagers).map(([key, value]) => [key, value.length])), python: 'preserved: bundled Hatch 1.16.5 pin; recommended archive hashes verified against official manifests' }))
|
||||
}
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch(error => { console.error(error.message); process.exitCode = 1 })
|
||||
91
scripts/validate-data-sources.mjs
Normal file
91
scripts/validate-data-sources.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { resolve, relative, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
|
||||
const ROOT = fileURLToPath(new URL('../', import.meta.url))
|
||||
const OFFICIAL_HOSTS = {
|
||||
tavily: ['docs.tavily.com'], brave: ['api.search.brave.com', 'brave.com'],
|
||||
serper: ['serper.dev'], 'doubao-search-global': ['www.volcengine.com', 'docs.volcengine.com'],
|
||||
'configured-vision-openai': ['developers.openai.com', 'platform.openai.com'],
|
||||
'configured-vision-anthropic': ['platform.claude.com', 'docs.anthropic.com'],
|
||||
}
|
||||
function files(root, directory, extension) {
|
||||
const dir = join(root, directory)
|
||||
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
||||
const path = join(directory, entry.name)
|
||||
return entry.isDirectory() ? files(root, path, extension) : entry.name.endsWith(extension) ? [path] : []
|
||||
})
|
||||
}
|
||||
export function validateDataSources(root = ROOT) {
|
||||
const errors = [], records = new Map()
|
||||
const read = path => JSON.parse(readFileSync(join(root, path), 'utf8'))
|
||||
for (const page of files(root, 'docs/data-sources', '.md')) {
|
||||
const text = readFileSync(join(root, page), 'utf8')
|
||||
if (!text.includes('<!-- data-source:start -->')) continue
|
||||
try {
|
||||
const match = text.match(/<!-- data-source:start -->\s*```json\s*([\s\S]*?)```\s*<!-- data-source:end -->/)
|
||||
if (!match) throw new Error('invalid metadata block')
|
||||
const record = JSON.parse(match[1])
|
||||
const target = resolve(root, record.config)
|
||||
if (relative(root, target).startsWith('..')) throw new Error('config escapes repository')
|
||||
if (records.has(record.config)) throw new Error('duplicate config record')
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(record.checkedAt)) throw new Error('missing checkedAt')
|
||||
const config = read(record.config)
|
||||
records.set(record.config, record)
|
||||
if (record.kind === 'routing-policy') {
|
||||
if (record.status !== 'policy' || record.config !== 'compute/service-map.json') throw new Error('invalid routing policy')
|
||||
if (!isDeepStrictEqual(config, record.claims)) errors.push(`${page}: routing policy drift: update the documented default selection`)
|
||||
const providers = files(root, 'compute/providers', '.json').filter(p => !p.endsWith('/_index.json')).map(read)
|
||||
const external = new Set((record.externalProviders ?? []).map(item => {
|
||||
if (!item.id || !item.reason?.trim()) throw new Error('external provider needs reason')
|
||||
return item.id
|
||||
}))
|
||||
for (const [service, route] of Object.entries(config)) {
|
||||
const provider = providers.find(item => item.id === route.providerId)
|
||||
if (external.has(route.providerId)) continue
|
||||
if (!provider || !provider.models?.some(model => model.modelName === route.modelName)) throw new Error(`unresolved route ${service}: ${route.providerId}/${route.modelName}`)
|
||||
}
|
||||
} else if (record.kind === 'policy' || record.kind === 'api-contract') {
|
||||
if (!record.claims || !Object.keys(record.claims).length) throw new Error('missing claims')
|
||||
for (const [key, value] of Object.entries(record.claims)) {
|
||||
if (!isDeepStrictEqual(config[key], value)) throw new Error(`contract drift: ${key}`)
|
||||
}
|
||||
if (record.kind === 'policy') {
|
||||
if (record.status !== 'policy' || record.config !== 'compute/pricing.json') throw new Error('invalid pricing policy')
|
||||
if (!isDeepStrictEqual(config, record.claims)) throw new Error('unrecorded pricing field')
|
||||
} else {
|
||||
if (!['partial', 'pending'].includes(record.status)) throw new Error('invalid verification status')
|
||||
if (!Array.isArray(record.verifiedFields) || record.verifiedFields.some(key => typeof key !== 'string' || key.split('.').reduce((value, segment) => value?.[segment], record.claims) === undefined)) throw new Error('invalid verified fields')
|
||||
if (record.status === 'pending' && record.verifiedFields.length) throw new Error('pending cannot assert verified fields')
|
||||
if (record.status === 'partial' && !record.verifiedFields.length) throw new Error('partial needs verified fields')
|
||||
if (!record.sources?.length) throw new Error('missing official source')
|
||||
for (const source of record.sources) {
|
||||
const url = new URL(source.url)
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || !OFFICIAL_HOSTS[config.id]?.includes(url.hostname)) throw new Error('unapproved source URL')
|
||||
if (!['fetched', 'shell', 'failed'].includes(source.retrieval)) throw new Error('invalid retrieval')
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(source.checkedAt ?? '') || Number.isNaN(Date.parse(source.checkedAt)) || new Date(source.checkedAt).toISOString().slice(0, 10) !== source.checkedAt) throw new Error('invalid source checkedAt')
|
||||
if (source.resolvedUrl) {
|
||||
const resolved = new URL(source.resolvedUrl)
|
||||
if (resolved.protocol !== 'https:' || resolved.username || resolved.password || resolved.searchParams.has('token') || !OFFICIAL_HOSTS[config.id]?.includes(resolved.hostname)) throw new Error('unapproved resolved source URL')
|
||||
}
|
||||
if (source.contentSha256 != null && !/^[a-f0-9]{64}$/.test(source.contentSha256)) throw new Error('invalid source digest')
|
||||
if (source.retrieval !== 'failed' && (!source.resolvedUrl || !source.contentSha256)) throw new Error('read source needs resolved URL and digest')
|
||||
}
|
||||
if (record.verifiedFields.length && !record.sources.some(source => source.retrieval === 'fetched')) throw new Error('verified field without fetched source')
|
||||
for (const key of ['endpoint', 'method', 'auth', 'request', 'response']) if (!(key in record.claims)) throw new Error(`missing contract field ${key}`)
|
||||
}
|
||||
} else throw new Error('unknown record kind')
|
||||
} catch (error) { errors.push(`${page}: ${error.message}`) }
|
||||
}
|
||||
const required = ['compute/pricing.json', 'compute/service-map.json', ...files(root, 'api-providers', '.json').filter(path => !path.endsWith('/_index.json'))]
|
||||
for (const path of required) if (!records.has(path)) errors.push(`missing source record: ${path}`)
|
||||
for (const path of records.keys()) if (!required.includes(path)) errors.push(`out of scope source record: ${path}`)
|
||||
return { errors, records: records.size }
|
||||
}
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const result = validateDataSources()
|
||||
for (const error of result.errors) console.error(error)
|
||||
console.log(`Data sources: ${result.records} policy/API records; ${result.errors.length} failed (not account-call verification)`)
|
||||
process.exitCode = result.errors.length ? 1 : 0
|
||||
}
|
||||
62
scripts/validate-runtime-sources.mjs
Normal file
62
scripts/validate-runtime-sources.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve, join } from 'node:path'
|
||||
import { SOURCES } from './update-runtime-catalog.mjs'
|
||||
|
||||
const ROOT = fileURLToPath(new URL('../', import.meta.url))
|
||||
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
||||
function tableRows(text, columns) {
|
||||
return text.split('\n').filter(line => line.startsWith('|')).map(line => line.split('|').slice(1, -1).map(cell => cell.trim().replace(/^`|`$/g, ''))).filter(cells => cells.length === columns && !cells.every(cell => /^[- :]+$/.test(cell)))
|
||||
}
|
||||
export function validateRuntimeSources(root = ROOT, now = new Date().toISOString()) {
|
||||
const errors = []
|
||||
const read = path => readFileSync(join(root, path), 'utf8')
|
||||
try {
|
||||
const fallback = JSON.parse(read('runtimes/versions-fallback.json'))
|
||||
const recommended = JSON.parse(read('runtimes/recommended.json'))
|
||||
if (!Number.isFinite(Date.parse(fallback.generatedAt)) || Date.parse(fallback.generatedAt) > Date.parse(now)) errors.push('fallback: invalid/future generatedAt')
|
||||
for (const name of ['node', 'npm', 'pnpm', 'yarn']) {
|
||||
const rows = name === 'node' ? fallback.node : fallback.pkgManagers[name]
|
||||
const path = name === 'node' ? 'node-snapshot.md' : `package-managers/${name}-snapshot.md`
|
||||
const page = read(`docs/runtime-sources/${path}`)
|
||||
if (!rows.length) errors.push(`${name}: empty snapshot`)
|
||||
if (!page.includes(`- 官方来源:[官方目录](${SOURCES[name]})\n`)) errors.push(`${name}: official source URL drift`)
|
||||
const seen = new Set()
|
||||
for (const row of rows) {
|
||||
const version = row.version ?? row.tag_name
|
||||
const date = row.date ?? row.created_at
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(version) || seen.has(version)) errors.push(`${name}: invalid/duplicate stable version`)
|
||||
seen.add(version)
|
||||
if (!Number.isFinite(Date.parse(date)) || Date.parse(date) > Date.parse(fallback.generatedAt)) errors.push(`${name}: invalid/future row publication date`)
|
||||
}
|
||||
const expectedVersion = rows[0]?.version ?? rows[0]?.tag_name
|
||||
if (!page.includes(`- 实际读取:${fallback.generatedAt}\n`)) errors.push(`${name}: snapshot date drift`)
|
||||
if (!page.includes(`- 目录条数:${rows.length}\n`)) errors.push(`${name}: snapshot count drift`)
|
||||
if (!page.includes(`- 首条版本:${expectedVersion}\n`)) errors.push(`${name}: first version drift`)
|
||||
if (!page.includes(`- 本仓条目指纹(JSON.stringify、SHA-256):\`${hash(rows)}\`\n`)) errors.push(`${name}: snapshot fingerprint drift`)
|
||||
if (!/- 本轮读取的官方正文 SHA-256:`[a-f0-9]{64}`\n/.test(page)) errors.push(`${name}: missing official body digest record`)
|
||||
}
|
||||
const pythonRows = tableRows(read('docs/runtime-sources/python-fallback.md'), 2).filter(cells => cells[0] !== '名称')
|
||||
if (JSON.stringify(pythonRows) !== JSON.stringify(fallback.python.map(row => [row.name, row.version]))) errors.push('python: Hatch pin table drift')
|
||||
for (const name of ['node', 'python']) {
|
||||
const page = read(`docs/runtime-sources/${name}-recommended.md`)
|
||||
const expected = recommended[name]
|
||||
const sourceUrl = name === 'node' ? `https://nodejs.org/dist/v${expected.version}/SHASUMS256.txt` : `https://api.github.com/repos/astral-sh/python-build-standalone/releases/tags/${expected.distTag}`
|
||||
if (!page.includes(`- 官方来源:[官方摘要清单](${sourceUrl})。\n`)) errors.push(`${name}: recommended official source URL drift`)
|
||||
if (!page.includes(`- 数据:\`runtimes/recommended.json#${name}\`,版本 \`${expected.version}\`。\n`)) errors.push(`${name}: recommended version drift`)
|
||||
if (name === 'node' && !page.includes(`LTS 名 ${expected.ltsCodename};`)) errors.push('node: recommended LTS drift')
|
||||
if (name === 'python' && !page.includes(`tag \`${expected.distTag}\``)) errors.push('python: recommended distTag drift')
|
||||
const rows = tableRows(page, 3).filter(cells => cells[0] !== '平台')
|
||||
const values = Object.entries(expected.archives).map(([platform, path]) => [platform, path, expected.sha256[platform]])
|
||||
if (JSON.stringify(rows) !== JSON.stringify(values)) errors.push(`${name}: recommended archive/hash table drift`)
|
||||
}
|
||||
} catch (error) { errors.push(`runtime source read: ${error.message}`) }
|
||||
return { errors }
|
||||
}
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const result = validateRuntimeSources()
|
||||
for (const error of result.errors) console.error(error)
|
||||
console.log(`Runtime sources: snapshot and recommended tables; ${result.errors.length} failed (offline consistency, not official re-fetch or installation verification)`)
|
||||
process.exitCode = result.errors.length ? 1 : 0
|
||||
}
|
||||
@@ -57,7 +57,7 @@ const get = (row, path) => path.split('.').reduce((value, key) => value?.[key],
|
||||
const dateValid = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value ?? '') && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value
|
||||
const configSupplier = (config) => {
|
||||
const name = config.split('/').at(-1).replace(/\.json$/, '')
|
||||
const aliases = { 'openai-codex': 'openai', 'local-whisper': 'openai', 'anthropic-claude': 'anthropic', dashscope: 'alibaba', qwen: 'alibaba', wan: 'alibaba', happyhorse: 'alibaba', 'dashscope-coding': 'alibaba', 'dashscope-token-plan': 'alibaba', 'zhipu-embedding': 'zhipu', 'tencent-token': 'tencent', 'moorethread-coding': 'moorethread' }
|
||||
const aliases = { 'openai-codex': 'openai', 'local-whisper': 'openai', 'anthropic-claude': 'anthropic', dashscope: 'alibaba', qwen: 'alibaba', wan: 'alibaba', happyhorse: 'alibaba', 'dashscope-coding': 'alibaba', 'dashscope-token-plan': 'alibaba', 'zhipu-embedding': 'zhipu', 'tencent-token': 'tencent', 'xunfei-x2': 'xunfei', 'moorethread-coding': 'moorethread' }
|
||||
return aliases[name] ?? name.replace(/-coding$/, '')
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ const SUSPICIOUS_EXTRA_KEYS = {
|
||||
provider: {
|
||||
reasoningEffort: '客户端只读嵌套的 extra.reasoning.supportedEfforts,扁平写法永不生效',
|
||||
defaultReasoningEffort: '同上,应并入 extra.reasoning.defaultEffort',
|
||||
defaultEffort: '扁平默认值不被客户端读取,应并入 extra.reasoning.defaultEffort 并核查接入面默认',
|
||||
},
|
||||
spec: {
|
||||
reasoningEffort: 'reasoning effort 是接入面能力,只能声明在 provider model 的 extra.reasoning 中',
|
||||
|
||||
Reference in New Issue
Block a user