Files
dsh_shenxian/scripts/ensure-anysearch-pool.mjs
admin 452924d89c feat(config): 涉密内容外置到配置目录(档案 140)
把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
2026-09-19 15:12:19 +08:00

128 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const cfg = require('../config/index.cjs')
/**
* ensure-anysearch-pool.mjs —— 把 AnySearch 插件投放进「功能插件」候选池(档案 64 / 65)
*
* 与门户 `POST /api/plugins/business`(admin 上传)走**同一条校验路径**:复用平台编译产物
* 导出的 `stageTgzArchive()`(列成员防路径穿越 → 解压 → `package.json` 校验 → 危险内容扫描),
* 再按同一「替换策略」(同名先删旧 tgz、旧文件无残留)落盘进 `<dataRoot>/business-plugins/`,
* 并 upsert `business_plugins` 行。
*
* 投放完成后,用户在实例「设置 → 功能管理」里自助**启用/禁用**;启用/禁用会由
* `syncWebProviderPatch()`(档案 65)自动维护 profile 的 web provider 托管段。
*
* 为什么需要它:门户上传需要浏览器的 admin 会话;本脚本用于无会话场景(如本次迁移),
* 产物与走门户完全一致。
*
* 用法:
* node scripts/ensure-anysearch-pool.mjs # 干跑(只打印)
* node scripts/ensure-anysearch-pool.mjs --apply # 执行投放
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs'
import { join } from 'node:path'
import Database from 'better-sqlite3'
const DATA_ROOT = cfg.dataRoot()
const ART_DIR = cfg.artifactDir()
const POOL_DIR = join(DATA_ROOT, 'business-plugins')
const DB_PATH = join(DATA_ROOT, 'dshs.db')
const PLUGIN_ROUTES = cfg.installPath('lib', 'web', 'routes', 'business-plugins.js')
const PREFIX = 'anysearch-dsh-'
const APPLY = process.argv.includes('--apply')
/** 取产物目录里版本号最大的 anysearch-dsh-*.tgz。 */
function pickArtifact() {
const cands = readdirSync(ART_DIR).filter((f) => f.startsWith(PREFIX) && f.endsWith('.tgz'))
if (cands.length === 0) throw new Error(`no ${PREFIX}*.tgz under ${ART_DIR}`)
const ver = (f) => f.slice(PREFIX.length, -4).split('.').map(Number)
cands.sort((a, b) => {
const va = ver(a); const vb = ver(b)
for (let i = 0; i < 3; i++) { const x = va[i] || 0; const y = vb[i] || 0; if (x !== y) return x - y }
return 0
})
return join(ART_DIR, cands[cands.length - 1])
}
const artifact = pickArtifact()
const size = statSync(artifact).size
console.log(`artifact = ${artifact}`)
console.log(`size = ${size} B`)
console.log(`pool dir = ${POOL_DIR}`)
console.log(`mode = ${APPLY ? 'APPLY' : 'DRY-RUN'}`)
if (!APPLY) {
console.log('\n(干跑结束;加 --apply 执行)')
process.exit(0)
}
// 1) 走平台自己的校验 + staging(与门户上传完全一致)
const { stageTgzArchive } = await import(PLUGIN_ROUTES)
const staged = stageTgzArchive(DATA_ROOT, readFileSync(artifact))
console.log(`staged = name=${staged.name} version=${staged.version} tgz=${staged.tgzName} files=${staged.fileCount}`)
// 1b) 安全检测裁决 —— 与上传接口同一套语义:**默认 fail-closed**,只有显式声明信任才放行。
const TRUST = process.argv.includes('--trust')
if (staged.blocked.length > 0) {
console.log(`\n⚠ 安全检测命中 P0 规则 ${staged.blocked.length} 处:`)
for (const b of staged.blocked) console.log(` - ${b.file} — ${b.why}`)
if (!TRUST) {
rmSync(staged.stage, { recursive: true, force: true })
console.log('\n默认拒绝投放(fail-closed)。逐条确认确属误报 / 风险可控后,加 --trust 重新执行;')
console.log('放行会写入 audit_log(trust_business_plugin),留痕「谁 / 何时 / 命中什么 / 理由」。')
process.exit(2)
}
console.log(' (--trust 已声明 → 继续投放并留痕)')
} else {
console.log('scan = clean(无 P0 命中)')
}
if (staged.warnings.length > 0) console.log(`scan = ${staged.warnings.length} 条 P1 告警(不阻断)`)
// 2) 替换策略落盘
mkdirSync(POOL_DIR, { recursive: true, mode: 0o755 })
const db = new Database(DB_PATH)
const existing = db.prepare('SELECT id, tgz_path FROM business_plugins WHERE id = ?').get(staged.name)
if (existing !== undefined && existsSync(existing.tgz_path)) {
rmSync(existing.tgz_path, { force: true })
console.log(`replaced = 已删除旧包 ${existing.tgz_path}`)
}
const dest = join(POOL_DIR, staged.tgzName)
renameSync(join(staged.stage, 'payload.tgz'), dest)
// 3) upsert 元数据行
const now = Date.now()
const admin = db.prepare("SELECT id FROM users WHERE username = 'admin'").get()
const uploadedBy = admin?.id ?? null
if (existing !== undefined) {
db.prepare(
'UPDATE business_plugins SET name=?, description=?, version=?, tgz_path=?, file_size=?, uploaded_by=?, updated_at=? WHERE id=?',
).run(staged.name, staged.name, staged.description, staged.version, dest, size, uploadedBy, now, staged.name)
} else {
db.prepare(
'INSERT INTO business_plugins (id, name, description, version, tgz_path, file_size, uploaded_by, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)',
).run(staged.name, staged.name, staged.description, staged.version, dest, size, uploadedBy, now, now)
}
// 留痕:与上传接口同一组 action(命中 P0 时另记一条 trust_business_plugin)
db.prepare('INSERT INTO audit_log (ts, actor, action, detail) VALUES (?, ?, ?, ?)').run(
now,
uploadedBy,
'upload_business_plugin',
JSON.stringify({ name: staged.name, version: staged.version, trustedOverride: staged.blocked.length > 0, via: 'ensure-anysearch-pool.mjs' }),
)
if (staged.blocked.length > 0) {
db.prepare('INSERT INTO audit_log (ts, actor, action, detail) VALUES (?, ?, ?, ?)').run(
now,
uploadedBy,
'trust_business_plugin',
JSON.stringify({ name: staged.name, version: staged.version, blocked: staged.blocked, reason: (process.env.TRUST_REASON ?? '').trim() }),
)
}
const rows = db.prepare('SELECT id, version, file_size, tgz_path FROM business_plugins ORDER BY id ASC').all()
db.close()
rmSync(staged.stage, { recursive: true, force: true })
console.log(`\n✓ 已投放:${dest}`)
console.log('候选池当前内容:')
for (const r of rows) console.log(` - ${r.id} @ ${r.version} (${r.file_size} B) → ${r.tgz_path}`)