Files

133 lines
5.6 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* 插件兼容性预检 v2 —— PoC(只读)
*
* A. 平台内置 @deepseek-ai/* 的「真实导出符号」清单(判据来源)
* B. 候选池每个 tgz:解包 → 依赖声明 + bundle 内 import 语句
* C. 逐条比对 → 判定「兼容 / 不兼容 / 需装后复核」
*
* 用法:node plugin-compat-check.mjs
*/
import { readFileSync, existsSync, readdirSync, mkdtempSync, rmSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { execFileSync } from 'node:child_process'
const DSH_ROOT = process.env.DSH_ROOT || '/usr/local/lib/node_modules/@deepseek-ai/dsh'
const NESTED = join(DSH_ROOT, 'node_modules', '@deepseek-ai')
const POOL = process.env.POOL || '/var/lib/dshs/business-plugins'
/* ---------- A. 平台 exports 真值 ---------- */
const platform = new Map() // pkgName -> { version, keys:Set }
for (const name of readdirSync(NESTED)) {
if (name.startsWith('.')) continue
const pj = join(NESTED, name, 'package.json')
if (!existsSync(pj)) continue
const meta = JSON.parse(readFileSync(pj, 'utf8'))
let entry = null
for (const cand of ['lib/index.js', 'dist/index.js', 'index.js']) {
const f = join(NESTED, name, cand)
if (existsSync(f)) { entry = f; break }
}
let keys = null
if (entry) { try { keys = new Set(Object.keys(await import(entry))) } catch { /* ignore */ } }
platform.set(meta.name, { version: meta.version, keys })
}
console.log(`[A] 平台内置 @deepseek-ai 包 ${platform.size} 个(其中有导出符号清单的 ${[...platform.values()].filter((v) => v.keys).length} 个)`)
const llm = platform.get('@deepseek-ai/dsh-llm')
console.log(` · dsh-llm@${llm?.version} 导出 ${llm?.keys?.size ?? 0} 个符号;含 assertNever? ${llm?.keys?.has('assertNever') ? '✅' : '❌(= anysearch 崩溃判据成立)'}`)
/* ---------- B+C. 逐个 tgz 预检 ---------- */
const IMPORT_RE = /(?:import|export)\s*(?:\*\s*as\s*[\w$]+|\{([^}]*)\})\s*from\s*["'](@deepseek-ai\/[^"'/]+(?:\/[^"']*)?)["']/g
function scanFileImports(file, base) {
const text = readFileSync(file, 'utf8')
const hits = []
let m
IMPORT_RE.lastIndex = 0
while ((m = IMPORT_RE.exec(text))) {
const spec = m[1] || ''
// 具名导入:取每个符号(去掉 as 别名)
const named = spec.split(',').map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter((s) => s && !s.startsWith('type '))
hits.push({ pkg: m[2], named, file: file.slice(base.length + 1) })
}
return hits
}
function walkJs(dir, base, out = []) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name)
if (e.isDirectory()) { if (e.name !== 'node_modules') walkJs(p, base, out) }
else if (/\.(js|mjs|cjs)$/.test(e.name)) out.push(p)
}
return out
}
const tgzs = existsSync(POOL) ? readdirSync(POOL).filter((f) => f.endsWith('.tgz')) : []
console.log(`\n[B] 候选池 tgz ${tgzs.length} 个:${tgzs.join(', ') || '(无)'}\n`)
for (const tgz of tgzs) {
const full = join(POOL, tgz)
const tmp = mkdtempSync(join(tmpdir(), 'compat-'))
console.log('━'.repeat(72))
console.log(`📦 ${tgz} (${(statSync(full).size / 1024).toFixed(0)} KB)`)
try {
execFileSync('tar', ['-xzf', full, '-C', tmp], { stdio: 'pipe' })
} catch (e) {
console.log(` ✗ 解包失败: ${String(e.message).slice(0, 100)}`)
rmSync(tmp, { recursive: true, force: true })
continue
}
// package.json
const pkgRoot = join(tmp, 'package')
const pj = join(pkgRoot, 'package.json')
let name = tgz, version = '?'
const deps = {}
if (existsSync(pj)) {
const meta = JSON.parse(readFileSync(pj, 'utf8'))
name = meta.name || name
version = meta.version || '?'
Object.assign(deps, meta.dependencies || {}, meta.peerDependencies || {})
}
console.log(` 包名 ${name}@${version}`)
// 依赖里的 @deepseek-ai
const dsDeps = Object.entries(deps).filter(([k]) => k.startsWith('@deepseek-ai/'))
if (dsDeps.length) {
console.log(` 声明的 @deepseek-ai 依赖 ${dsDeps.length} 个:`)
for (const [d, rng] of dsDeps) {
const p = platform.get(d)
const verdict = !p ? '⚠️ 平台无此包' : (p.version === rng.replace(/^[\^~=]/, '') ? '✅ 版本一致' : `⚠️ 平台为 ${p.version},插件要求 ${rng}`)
console.log(` ${d} ${rng} → ${verdict}`)
}
} else {
console.log(' 未声明任何 @deepseek-ai 依赖')
}
// bundle 内 import
const files = walkJs(pkgRoot, pkgRoot)
const hits = files.flatMap((f) => scanFileImports(f, pkgRoot))
console.log(` 扫描 ${files.length} 个 js 文件,命中 ${hits.length} 条 @deepseek-ai 导入`)
const problems = []
for (const h of hits) {
const p = platform.get(h.pkg)
if (!p || !p.keys) continue
const missing = h.named.filter((n) => n && !p.keys.has(n))
if (missing.length) problems.push({ ...h, missing })
}
if (problems.length) {
console.log(' 🔴 不兼容命中:')
for (const pr of problems) console.log(` ${pr.pkg} 缺少导出 ${pr.missing.join(', ')} ← ${pr.file}`)
} else if (hits.length) {
console.log(' 🟢 静态可判部分:全部符号在平台包中存在')
} else {
console.log(' ⚪ 静态无法判定(插件未直接 import 平台包 → 需「装后扫 node_modules」复核)')
}
rmSync(tmp, { recursive: true, force: true })
}
console.log('\n' + '━'.repeat(72))
console.log('说明:静态判定只覆盖「插件自身 bundle 里的 import」。若插件把平台 API 的调用藏在依赖包内')
console.log(' (如 anysearch 的 @deepseek-ai/dsh-tool-web),必须做「装后扫 profile node_modules」才能抓到。')