全栈安全加固、性能提速与架构演进:连接池与沙箱止血、双路混合检索、多版本归档及前端分包治理

- 第一批(安全加固与生命周期止血):隔离测试宿主机密钥与 .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:
ThearchyHelios
2026-11-13 14:51:36 +08:00
parent 09949fc8ac
commit eddeded2a4
106 changed files with 7178 additions and 3913 deletions
+6 -6
View File
@@ -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)
+3 -3
View File
@@ -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 = '') => {
+3 -4
View File
@@ -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 })
+6 -22
View File
@@ -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
View File
@@ -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
+142
View File
@@ -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))
}
+31 -1
View File
@@ -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 "完成:"