#!/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」才能抓到。')