build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
134 lines
5.6 KiB
JavaScript
134 lines
5.6 KiB
JavaScript
#!/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」才能抓到。')
|