mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:41:59 +00:00
全栈安全加固、性能提速与架构演进:连接池与沙箱止血、双路混合检索、多版本归档及前端分包治理
- 第一批(安全加固与生命周期止血):隔离测试宿主机密钥与 .env 泄漏;修复 SQLite 取消查询挂死、:memory: 连接池报错与 EngineCache 世代竞态;补齐运行与评测删除时的 Checkpointer 线程、Artifact 磁盘文件与无主附件回收;加固三套沙箱路径越界、进程组清理与 Seatbelt 数据目录隔离,封堵 SSRF IPv4-mapped IPv6 元数据绕过与响应体 OOM;修复推理模型多轮 reasoning_content、结构化输出 Token 漏计与缓存折扣计费 - 第二批(数据库/集群/问数提速与首屏分包):补齐核心表索引与 SQLite WAL 调优,列表接口按需 defer 大列,收敛启动扫描并打通跨进程 slot 槽位唤醒;优化跨源全局选表、哈希桶数字绑定、流式 CSV 导入及多方言分析算子;剥离首屏静态引入的 @xyflow/react 与全局样式(首屏主包降至 461.59 kB),补充流式 rAF 攒批、Store 精细订阅与表单无障碍绑定 - 第三批(架构演进与工程自动化闭环):记录库超挂接阈值历史版本自动增量归档至单库多表(解除 SQLite 10 库 ATTACH 硬上限);重构知识库为词面 BM25 与全集合归一化向量矩阵真·双路独立召回并支持游标分批重建;升级多方言感知 SQL 模板词法分析(E'...' / $$...$$ / q'[...]' / # / 嵌套注释)与 Decimal / bool / NaN 参数校验,消除 ledger 层对 api 路由层的反向依赖;拆分 3101 行 CaliberDrafts 页面为 7 个领域子模块,消除检查脚本本机路径硬编码并新增 GitHub Actions CI 门禁与三批回归测试套件
This commit is contained in:
@@ -52,13 +52,13 @@ import {
|
||||
readFileSync, readdirSync, rmSync, writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { createServer } from 'node:net'
|
||||
import { homedir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { resolveChrome, resolvePython } from './lib/env.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = dirname(HERE)
|
||||
const SYSTEM_CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
||||
const SYSTEM_CHROME = resolveChrome()
|
||||
|
||||
// 先跑快的、不开浏览器的,再按页面从外壳往里跑;最后是连真数据的两项
|
||||
const ORDER = [
|
||||
@@ -189,8 +189,8 @@ if (unknown.length) {
|
||||
process.exit(2)
|
||||
}
|
||||
const names = picked.length ? ORDER.filter((n) => picked.includes(n)) : ORDER
|
||||
// 纯静态的几项不用起服务
|
||||
const STATIC = new Set(['tokens', 'copy'])
|
||||
// 纯静态或内存转译单测项不用起前后端服务
|
||||
const STATIC = new Set(['tokens', 'copy', 'trace', 'decode'])
|
||||
const needsServers = names.some((n) => !STATIC.has(n))
|
||||
|
||||
// ---------------------------------------------------------------- 收尾
|
||||
@@ -421,11 +421,11 @@ async function warmUp(stacks) {
|
||||
let stacks = []
|
||||
if (stackCount > 0 && needsServers) {
|
||||
const dataSrc = process.env.CHECK_DATA_SRC && resolve(process.env.CHECK_DATA_SRC)
|
||||
const python = process.env.CHECK_PYTHON ?? join(homedir(), 'miniforge3', 'envs', 'agentlab', 'bin', 'python')
|
||||
const python = resolvePython({ requireBackend: true })
|
||||
const problems = []
|
||||
if (!dataSrc) problems.push('--stacks 要知道拿哪份数据:用 CHECK_DATA_SRC=<数据目录> 指定(里面有 agentlab.db 的那一层)')
|
||||
else if (!existsSync(join(dataSrc, 'agentlab.db'))) problems.push(`CHECK_DATA_SRC=${dataSrc} 里没有 agentlab.db`)
|
||||
if (!existsSync(python)) problems.push(`找不到后端解释器 ${python},用 CHECK_PYTHON 指定`)
|
||||
if (!python) problems.push('找不到可用的后端 Python 解释器,请用 CHECK_PYTHON 指定')
|
||||
if (problems.length) {
|
||||
for (const p of problems) console.error(`✗ ${p}`)
|
||||
process.exit(2)
|
||||
|
||||
@@ -54,13 +54,13 @@ import { homedir, tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { chromium } from '../frontend/node_modules/playwright-core/index.mjs'
|
||||
import { resolveChrome, resolvePython } from './lib/env.mjs'
|
||||
|
||||
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
|
||||
const API = process.env.AGENTLAB_API ?? 'http://localhost:8000/api'
|
||||
const CHROME = process.env.CHROME_PATH ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
||||
const CHROME = resolveChrome()
|
||||
const BACKEND = join(dirname(dirname(fileURLToPath(import.meta.url))), 'backend')
|
||||
const PYTHON = [process.env.CHECK_PYTHON, join(homedir(), 'miniforge3/envs/agentlab/bin/python')]
|
||||
.find((p) => p && existsSync(p))
|
||||
const PYTHON = resolvePython({ requireBackend: true })
|
||||
|
||||
let failed = 0
|
||||
const check = (name, cond, detail = '') => {
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
// 确实该保留的写在 scripts/copy-allow.json:每条写明文件、原文片段、规则和理由。白名单里有一条
|
||||
// 已经对不上任何命中(原文改掉了、挪走了),也算不通过:白名单只收现在还需要的例外。
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
||||
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join, relative } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { resolvePython } from './lib/env.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = dirname(HERE)
|
||||
@@ -332,8 +332,7 @@ for p, rel in files:
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
`
|
||||
|
||||
const PYTHON = [process.env.CHECK_PYTHON, join(homedir(), 'miniforge3/envs/agentlab/bin/python'), 'python3']
|
||||
.find((p) => p && (p === 'python3' || existsSync(p)))
|
||||
const PYTHON = resolvePython()
|
||||
|
||||
function backStrings(root, only = []) {
|
||||
const r = spawnSync(PYTHON, ['-c', PY_EXTRACT, root, ...only], { encoding: 'utf8', maxBuffer: 256 << 20 })
|
||||
|
||||
@@ -9,38 +9,22 @@
|
||||
// 跑之前前端得起着(./scripts/dev.sh),默认连 5273。对别的实例(比如一份沙箱拷贝)跑时
|
||||
// 带上地址:AGENTLAB_WEB=http://localhost:<前端端口> node scripts/check-decode.mjs
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { createTsLoader, ROOT } from './lib/env.mjs'
|
||||
|
||||
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
|
||||
const root = new URL('..', import.meta.url).pathname
|
||||
const root = `${ROOT}/`
|
||||
const fixtures = JSON.parse(
|
||||
readFileSync(`${root}frontend/src/run/__tests__/fixtures.json`, 'utf8'))
|
||||
readFileSync(join(ROOT, 'frontend/src/run/__tests__/fixtures.json'), 'utf8'))
|
||||
|
||||
// decode.ts 在运行时 import 了 lib/format、lib/terms。data: URL 里的模块没法解析
|
||||
// "/src/..." 这种路径,所以把依赖也各自转成 data: URL 再替换进去(同 check-trace)
|
||||
const loaded = new Map()
|
||||
async function load(path) {
|
||||
if (loaded.has(path)) return loaded.get(path)
|
||||
const res = await fetch(`${WEB}${path}?t=${Date.now()}`)
|
||||
if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`)
|
||||
let code = await res.text()
|
||||
const deps = new Set([...code.matchAll(/from\s+["'](\/src\/[^"'?]+)(?:\?[^"']*)?["']/g)]
|
||||
.map((m) => m[1]))
|
||||
for (const dep of deps) {
|
||||
const url = await load(dep)
|
||||
const pattern = new RegExp(`(["'])${dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\?[^"']*)?\\1`, 'g')
|
||||
code = code.replace(pattern, JSON.stringify(url))
|
||||
}
|
||||
const url = 'data:text/javascript;base64,' + Buffer.from(code).toString('base64')
|
||||
loaded.set(path, url)
|
||||
return url
|
||||
}
|
||||
const load = await createTsLoader(WEB)
|
||||
|
||||
let mod, synthetic
|
||||
try {
|
||||
mod = await import(await load('/src/run/decode.ts'))
|
||||
synthetic = await import(await load('/src/run/__tests__/synthetic.ts'))
|
||||
} catch (e) {
|
||||
console.error(`✗ 拿不到转译结果(${WEB})——前端没起?先跑 ./scripts/dev.sh\n ${e.message}`)
|
||||
console.error(`✗ 加载前端解码器模块失败(${WEB}):\n ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
+6
-22
@@ -9,31 +9,15 @@
|
||||
// 跑之前前端得起着(./scripts/dev.sh),默认连 5273。对别的实例(比如一份沙箱拷贝)跑时
|
||||
// 带上地址:AGENTLAB_WEB=http://localhost:<前端端口> node scripts/check-trace.mjs
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { createTsLoader, ROOT } from './lib/env.mjs'
|
||||
|
||||
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
|
||||
const root = new URL('..', import.meta.url).pathname
|
||||
const root = `${ROOT}/`
|
||||
const fixtures = JSON.parse(
|
||||
readFileSync(`${root}frontend/src/run/__tests__/fixtures.json`, 'utf8'))
|
||||
readFileSync(join(ROOT, 'frontend/src/run/__tests__/fixtures.json'), 'utf8'))
|
||||
|
||||
// trace.ts 在运行时 import 了 decode.ts 和 derive.ts。data: URL 里的模块没法
|
||||
// 解析 "/src/..." 这种路径,所以把依赖也各自转成 data: URL 再替换进去
|
||||
const loaded = new Map()
|
||||
async function load(path) {
|
||||
if (loaded.has(path)) return loaded.get(path)
|
||||
const res = await fetch(`${WEB}${path}?t=${Date.now()}`)
|
||||
if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`)
|
||||
let code = await res.text()
|
||||
const deps = new Set([...code.matchAll(/from\s+["'](\/src\/[^"'?]+)(?:\?[^"']*)?["']/g)]
|
||||
.map((m) => m[1]))
|
||||
for (const dep of deps) {
|
||||
const url = await load(dep)
|
||||
const pattern = new RegExp(`(["'])${dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\?[^"']*)?\\1`, 'g')
|
||||
code = code.replace(pattern, JSON.stringify(url))
|
||||
}
|
||||
const url = 'data:text/javascript;base64,' + Buffer.from(code).toString('base64')
|
||||
loaded.set(path, url)
|
||||
return url
|
||||
}
|
||||
const load = await createTsLoader(WEB)
|
||||
|
||||
let trace, derive, decode
|
||||
try {
|
||||
@@ -41,7 +25,7 @@ try {
|
||||
derive = await import(await load('/src/run/derive.ts'))
|
||||
decode = await import(await load('/src/run/decode.ts'))
|
||||
} catch (e) {
|
||||
console.error(`✗ 拿不到转译结果(${WEB})——前端没起?先跑 ./scripts/dev.sh\n ${e.message}`)
|
||||
console.error(`✗ 加载前端运行态模块失败(${WEB}):\n ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const { emptyTrace, foldEvent, finalizeTrace, project, isSettled } = trace
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, extname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
export const ROOT = dirname(dirname( dirname(fileURLToPath(import.meta.url)) ))
|
||||
const FRONTEND = join(ROOT, 'frontend')
|
||||
|
||||
/**
|
||||
* 跨平台解析可用的 Python 解释器:
|
||||
* 优先尊重 CHECK_PYTHON、当前激活的虚拟环境 / Conda 环境、仓库内 .venv,
|
||||
* 再回退到常见开发路径与系统 PATH 中的 python3 / python,并实际试跑确认可执行。
|
||||
*/
|
||||
export function resolvePython({ requireBackend = false } = {}) {
|
||||
const isWin = process.platform === 'win32'
|
||||
const venvBin = (dir) => (isWin ? join(dir, 'Scripts', 'python.exe') : join(dir, 'bin', 'python'))
|
||||
const candidates = [
|
||||
process.env.CHECK_PYTHON,
|
||||
process.env.VIRTUAL_ENV && venvBin(process.env.VIRTUAL_ENV),
|
||||
process.env.CONDA_PREFIX && (isWin ? join(process.env.CONDA_PREFIX, 'python.exe') : venvBin(process.env.CONDA_PREFIX)),
|
||||
venvBin(join(ROOT, '.venv')),
|
||||
venvBin(join(ROOT, 'backend', '.venv')),
|
||||
join(homedir(), 'miniforge3', 'envs', 'agentlab', 'bin', 'python'),
|
||||
join(homedir(), 'miniconda3', 'envs', 'agentlab', 'bin', 'python'),
|
||||
'/opt/homebrew/bin/python3',
|
||||
'/usr/local/bin/python3',
|
||||
'python3',
|
||||
'python',
|
||||
].filter(Boolean)
|
||||
|
||||
const probe = requireBackend
|
||||
? 'import sys, app; assert sys.version_info >= (3, 12)'
|
||||
: 'import sys; assert sys.version_info >= (3, 12)'
|
||||
const cwd = requireBackend ? join(ROOT, 'backend') : ROOT
|
||||
|
||||
const seen = new Set()
|
||||
for (const bin of candidates) {
|
||||
if (seen.has(bin)) continue
|
||||
seen.add(bin)
|
||||
if ((bin.includes('/') || bin.includes('\\')) && !existsSync(bin)) continue
|
||||
const res = spawnSync(bin, ['-c', probe], { cwd, stdio: 'ignore' })
|
||||
if (res.status === 0) return bin
|
||||
}
|
||||
return process.env.CHECK_PYTHON ?? 'python3'
|
||||
}
|
||||
|
||||
/** 跨平台解析 Chrome / Chromium 可执行文件路径 */
|
||||
export function resolveChrome() {
|
||||
if (process.env.CHROME_PATH) return process.env.CHROME_PATH
|
||||
const candidates = [
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/usr/bin/google-chrome',
|
||||
'/usr/bin/google-chrome-stable',
|
||||
'/usr/bin/chromium',
|
||||
'/usr/bin/chromium-browser',
|
||||
'/snap/bin/chromium',
|
||||
]
|
||||
for (const p of candidates) {
|
||||
if (existsSync(p)) return p
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
function resolveTsFile(fromDir, spec) {
|
||||
const base = spec.startsWith('/src/')
|
||||
? join(FRONTEND, spec.slice(1))
|
||||
: resolve(fromDir, spec)
|
||||
const tries = extname(base)
|
||||
? [base]
|
||||
: [base, `${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')]
|
||||
for (const candidate of tries) {
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
throw new Error(`找不到模块 ${spec}(自 ${fromDir})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 check-trace / check-decode 提供前端 TS 模块加载器:
|
||||
* - 若本地或指定的 AGENTLAB_WEB 开发服务器可达,直接拉取其转译结果;
|
||||
* - 若开发服务器未启动(如 CI、离线环境或单跑脚本),自动回退到 Vite 内置的 Oxc 内存转译,无需启服务或绑端口。
|
||||
*/
|
||||
export async function createTsLoader(webUrl = process.env.AGENTLAB_WEB ?? 'http://localhost:5273') {
|
||||
let serverOnline = false
|
||||
if (process.env.CHECK_OFFLINE !== '1') {
|
||||
try {
|
||||
const r = await fetch(`${webUrl}/`, { signal: AbortSignal.timeout(400) })
|
||||
serverOnline = r.ok
|
||||
} catch {
|
||||
serverOnline = false
|
||||
}
|
||||
}
|
||||
|
||||
if (serverOnline) {
|
||||
const loaded = new Map()
|
||||
const loadFromWeb = async (modPath) => {
|
||||
if (loaded.has(modPath)) return loaded.get(modPath)
|
||||
const res = await fetch(`${webUrl}${modPath}?t=${Date.now()}`)
|
||||
if (!res.ok) throw new Error(`${modPath} → HTTP ${res.status}`)
|
||||
let code = await res.text()
|
||||
const deps = new Set([...code.matchAll(/from\s+["'](\/src\/[^"'?]+)(?:\?[^"']*)?["']/g)].map((m) => m[1]))
|
||||
for (const dep of deps) {
|
||||
const url = await loadFromWeb(dep)
|
||||
const pattern = new RegExp(`(["'])${dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\?[^"']*)?\\1`, 'g')
|
||||
code = code.replace(pattern, JSON.stringify(url))
|
||||
}
|
||||
const url = 'data:text/javascript;base64,' + Buffer.from(code).toString('base64')
|
||||
loaded.set(modPath, url)
|
||||
return url
|
||||
}
|
||||
return loadFromWeb
|
||||
}
|
||||
|
||||
const viteEntry = pathToFileURL(join(FRONTEND, 'node_modules', 'vite', 'dist', 'node', 'index.js')).href
|
||||
const { transformWithOxc } = await import(viteEntry)
|
||||
const loaded = new Map()
|
||||
|
||||
const loadLocalFile = async (absPath) => {
|
||||
if (loaded.has(absPath)) return loaded.get(absPath)
|
||||
const raw = readFileSync(absPath, 'utf8')
|
||||
const lang = absPath.endsWith('.tsx') ? 'tsx' : 'ts'
|
||||
const transformed = await transformWithOxc(raw, absPath, { lang, sourcemap: false })
|
||||
let code = transformed.code
|
||||
const dir = dirname(absPath)
|
||||
const specs = new Set(
|
||||
[...code.matchAll(/(?:import|export)\s+(?:[^"']*?\s+from\s+)?["']((?:\.\.?\/|\/src\/)[^"'?]+)(?:\?[^"']*)?["']/g)]
|
||||
.map((m) => m[1]),
|
||||
)
|
||||
for (const spec of specs) {
|
||||
const depAbs = resolveTsFile(dir, spec)
|
||||
const depUrl = await loadLocalFile(depAbs)
|
||||
const escaped = spec.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const pattern = new RegExp(`(["'])${escaped}(\\?[^"']*)?\\1`, 'g')
|
||||
code = code.replace(pattern, JSON.stringify(depUrl))
|
||||
}
|
||||
const url = 'data:text/javascript;base64,' + Buffer.from(code).toString('base64')
|
||||
loaded.set(absPath, url)
|
||||
return url
|
||||
}
|
||||
|
||||
return async (modPath) => loadLocalFile(resolveTsFile(FRONTEND, modPath))
|
||||
}
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
用法:./scripts/package-docker.sh [--platform linux/arm64|linux/amd64] [--version <版本>] [--out <目录>] [--gzip] [--no-microvm]
|
||||
[--no-postgres]
|
||||
[--no-postgres] [--keep <数量>]
|
||||
|
||||
--platform <平台> 目标机的平台。默认按本机架构取 linux/<arch>。64 位系统的 ARM 单板机是 linux/arm64,
|
||||
普通 PC 和服务器是 linux/amd64。和本机架构不同时 docker 用模拟构建,会慢很多。
|
||||
@@ -18,6 +18,7 @@ usage() {
|
||||
--no-postgres 不带内置 PostgreSQL 的镜像(postgres-image.tar,约 160 MB)。只用 SQLite 或外接 PostgreSQL 时用不上;
|
||||
不带的包在目标机上 install --postgres 要先 docker pull 到同一个标签。默认带上,标签取 deploy.sh 里的
|
||||
POSTGRES_IMAGE(只在那一处定)。
|
||||
--keep <数量> 在产物目录里最多保留最近几份同架构的离线包(默认取 AGENTLAB_KEEP_RELEASES,未设时为 3;传 0 不清理旧包)。
|
||||
-h, --help 显示这段说明。
|
||||
|
||||
产物:<out>/agentlab-<版本>-linux-<arch>.tar,里面有 image.tar、postgres-image.tar、deploy.sh、agentlab.env、README.md、
|
||||
@@ -34,6 +35,7 @@ OUT="$ROOT/release"
|
||||
GZIP=""
|
||||
WITH_MICROVM=1
|
||||
WITH_POSTGRES=1
|
||||
KEEP_RELEASES="${AGENTLAB_KEEP_RELEASES:-3}"
|
||||
need_value() { [ $# -ge 2 ] && [ -n "$2" ] || { echo "$1 后面要跟一个值"; echo; usage; exit 2; }; }
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -46,11 +48,16 @@ while [ $# -gt 0 ]; do
|
||||
--gzip) GZIP=1 ;;
|
||||
--no-microvm) WITH_MICROVM=0 ;;
|
||||
--no-postgres) WITH_POSTGRES=0 ;;
|
||||
--keep) need_value "$@"; KEEP_RELEASES="$2"; shift ;;
|
||||
--keep=*) KEEP_RELEASES="${1#--keep=}" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "认不出的参数:$1"; echo; usage; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
case "$KEEP_RELEASES" in
|
||||
''|*[!0-9]*) echo "--keep 需要是非负整数:$KEEP_RELEASES"; exit 2 ;;
|
||||
esac
|
||||
|
||||
# ---- 平台 ----
|
||||
if [ -z "$PLATFORM" ]; then
|
||||
@@ -183,6 +190,29 @@ if [ -n "$GZIP" ]; then
|
||||
if command -v pigz >/dev/null 2>&1; then pigz -c "$TAR" > "$TAR.gz"; else gzip -c "$TAR" > "$TAR.gz"; fi
|
||||
fi
|
||||
|
||||
# ---- 4. 清理旧离线包 ----
|
||||
if [ "$KEEP_RELEASES" -gt 0 ]; then
|
||||
SEEN_STEMS=" $NAME "
|
||||
KEPT_COUNT=1
|
||||
for OLD_FILE in $(ls -1t "$OUT"/agentlab-*-linux-"$ARCH".tar "$OUT"/agentlab-*-linux-"$ARCH".tar.gz 2>/dev/null || true); do
|
||||
[ -f "$OLD_FILE" ] || continue
|
||||
BASE_NAME="$(basename "$OLD_FILE")"
|
||||
STEM="${BASE_NAME%.gz}"
|
||||
STEM="${STEM%.tar}"
|
||||
case "$SEEN_STEMS" in
|
||||
*" $STEM "*) ;;
|
||||
*)
|
||||
SEEN_STEMS="$SEEN_STEMS$STEM "
|
||||
KEPT_COUNT=$((KEPT_COUNT + 1))
|
||||
;;
|
||||
esac
|
||||
if [ "$KEPT_COUNT" -gt "$KEEP_RELEASES" ] && [ "$STEM" != "$NAME" ]; then
|
||||
echo "==> 清理旧离线包 $OLD_FILE(保留最近 $KEEP_RELEASES 份)"
|
||||
rm -f "$OLD_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
size() { du -h "$1" | awk '{print $1}'; }
|
||||
echo
|
||||
echo "完成:"
|
||||
|
||||
Reference in New Issue
Block a user