Showing Posts From
Node.js
NVM 安装与配置:macOS/Linux 和 Windows 环境变量设置,npm 找不到的排查
macOS / Linux 配置 安装 nvm 后,需要在 Shell 配置文件中初始化脚本,否则每次新开终端都找不到 nvm 命令。 在 ~/.zshrc(zsh)或 ~/.bashrc(bash)末尾添加: export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"重新加载: source ~/.zshrc # 或 source ~/.bashrc验证: nvm -vWindows(nvm-windows)配置 nvm-windows 安装后需要两个系统环境变量: NVM_HOME=C:\Users\<用户名>\AppData\Roaming\nvm NVM_SYMLINK=C:\Program Files\nodejs并在 Path 中加入: %NVM_HOME% %NVM_SYMLINK%验证(重开终端后): nvm version node -v npm -v查看当前配置: nvm root # nvm 安装目录 where nvm # 命令位置常用命令 # 安装指定版本 nvm install 22 nvm install 20.11.0# 查看已安装版本 nvm list# 切换版本 nvm use 22# 设置默认版本(新终端自动激活) nvm alias default 22# 查看当前使用的版本 nvm current node -vnpm 找不到的排查 原因一:还没安装 Node nvm list # 如果为空,先 install nvm install 22 nvm use 22原因二:版本未激活 nvm current # 输出 none 或系统版本 nvm use 22原因三:没有设置 default alias 新开终端后 nvm 不会自动激活任何版本: nvm alias default 22设置后新终端自动使用该版本。 原因四:Windows PATH 配置错误 where node where npm echo %NVM_SYMLINK%如果 where node 返回空或旧路径,检查 NVM_SYMLINK 是否指向 nvm 创建的 nodejs 符号链接目录,并确认 %NVM_SYMLINK% 在 PATH 中。 项目级 Node 版本锁定 在项目根目录创建 .nvmrc: 22进入目录后执行: nvm use # 自动读取 .nvmrc 切换版本配合 .nvmrc 可以保证团队成员使用相同的 Node 版本。
NVM 环境配置与 npm 找不到的排查
macOS / Linux 上配 nvm 先确认 nvm 安装位置: echo $NVM_DIR通常是 ~/.nvm。在 ~/.zshrc(zsh)或 ~/.bashrc(bash)末尾加: export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"生效并验证: source ~/.zshrc nvm -vWindows 上配 nvm-windows 先看 nvm 目录: nvm root通常输出: C:\Users\<你>\AppData\Roaming\nvm系统环境变量里加两条: NVM_HOME = C:\Users\<你>\AppData\Roaming\nvm NVM_SYMLINK = C:\Program Files\nodejs再把 Path 里追加: %NVM_HOME% %NVM_SYMLINK%新开一个 CMD 窗口,验证: nvm version node -v npm -v常用命令速查 nvm install 22 # 装 Node 22 nvm list # 已装版本 nvm use 22 # 切换版本 nvm alias default 22 # 设默认(macOS/Linux) nvm current # 当前使用版本which node 有输出,但 npm not found 碰到这种情况: $ which node /Users/you/.nvm/versions/node/v22.21.1/bin/node$ which npm npm not found有六种可能,从概率高到低排: 1. 只装了 nvm 没装 Node nvm list # 一片空 nvm install 22 nvm use 222. 版本没激活 nvm current # 显示 none / N/A nvm use 223. PATH 没指向当前 Node echo $PATH # 看有没有 .nvm/versions/node/v22.x.x/bin4. Node 目录里 npm 文件真的没了 ls -lah /Users/you/.nvm/versions/node/v22.21.1/bin/正常应该有 node、npm、npx、corepack。只有 node 说明安装被中断过,重装: nvm uninstall 22 nvm install 225. Windows:nvm-windows 软链坏了(最常见) dir "C:\Program Files\nodejs"看到目录是空的或者链接指向失效版本,重新 nvm use: nvm use 22.19.0之后 where node / where npm 都会有结果。 6. Homebrew 装的 nvm 没加载环境 nvm use default # 或 nvm install --lts一句话总结 nvm 有问题时按这个顺序自检:nvm list → nvm current → which node / which npm → 检查 bin 目录是否完整。95% 的问题都是"没 use 版本"或"软链坏了"。
npm cb() never called 别再瞎清缓存:先看 Node 和 npm 匹不匹配
老项目上手,npm install 直接崩: npm ERR! cb() never called!npm ERR! This is an error with npm itself. Please report this error at: npm ERR! <https://npm.community>网上一搜全是"清缓存、删 node_modules、换镜像"这种模板答案。真去清了,然后再跑就变成: ERROR: npm v11.17.0 is known not to run on Node.js v14.21.3. This version of npm supports the following node versions: `^20.17.0 || >=22.9.0`SyntaxError: Unexpected token '&&='这才是真面目——Node 版本和 npm 版本对不上。 症状识别 看到这两种就是版本冲突:cb() never called(npm 内部逻辑走到不该走的分支) SyntaxError: Unexpected token (npm 用了新语法,老 Node 不认识,比如 &&= 是 Node 15+ 才有的)不是包坏了、不是 registry 挂了、也不是缓存的锅。 Node 与 npm 的兼容表Node 版本 兼容的 npmNode 14 npm 6 ~ 8Node 16 npm 7 ~ 8Node 18 npm 9 ~ 10Node 20 npm 10 ~ 11Node 22 npm 10 ~ 11跨太多档就会互相不认。npm 11 明确要求 Node >= 20.17 或 >= 22.9。 正确修法 方案一:直接升 Node(推荐) 用 nvm: nvm install 22 nvm use 22 nvm alias default 22 node -v npm -vNode 22 + npm 10/11,是现在最省事的组合。 方案二:必须用老 Node,就降 npm 比如项目锁死 Node 14: npm install -g npm@6 # 或 npm install -g npm@8但注意——大部分现代依赖已经放弃了 Node 14,长远还是得升。 方案三:项目里锁定引擎(推荐) package.json 里加: { "engines": { "node": ">=20.17" } }结合 nvm use 和 .nvmrc: echo "22" > .nvmrc nvm use # 自动读 .nvmrc新人 clone 下来 nvm use 就切对了。 如果只是普通 cb() never called Node/npm 版本没问题,仍然报的情况下才走"缓存派"步骤: npm cache clean --force rm -rf node_modules package-lock.json npm install --legacy-peer-deps顺带换个镜像加速: npm config set registry https://registry.npmmirror.com一句话总结 看到 cb() never called 别急着删 node_modules,先 node -v && npm -v 对一下兼容表。绝大多数是 Node 版本跟不上 npm。
npm ERR! cb() never called 修复:npm 与 Node.js 版本不兼容的根因
npm ERR! cb() never called! 看起来像 npm 内部崩溃,但真正的根因通常是 npm 版本与当前 Node.js 版本不兼容。 最常见根因:版本不匹配 错误表现: ERROR: npm v11.17.0 is known not to run on Node.js v14.21.3. This version of npm supports the following node versions: `^20.17.0 || >=22.9.0`.SyntaxError: Unexpected token '&&=' at wrapSafe (internal/modules/cjs/loader.js:1029:16)npm 11 内部使用了 &&= 逻辑赋值运算符,这是 ES2021 特性,Node.js 14 不支持,导致 npm 自身启动时语法解析失败。 解决方案:切换 Node.js 版本 # 查看当前版本 node -v npm -v# 用 nvm 切换到兼容版本 nvm install 22 nvm use 22# 验证 node -v # v22.x.x npm -v # 10.x.x 或更高通用修复流程 如果版本兼容但仍然报错: 1. 升级 npm npm install -g npm@latest2. 清理缓存重装 npm cache clean --force rm -rf node_modules rm package-lock.json npm installmacOS 还可以删除整个 npm 缓存目录: rm -rf ~/.npm3. 切换国内镜像 国内网络拉包失败时: npm config set registry https://registry.npmmirror.com npm install4. 老项目依赖冲突 npm install --legacy-peer-depsnpm 与 Node.js 版本对照npm 版本 最低 Node.js 要求npm 11 Node.js 20.17 / 22.9+npm 10 Node.js 18+npm 9 Node.js 14.17+npm 8 Node.js 12+用 nvm 安装 LTS 版本可以拿到对应版本的 npm: nvm install --lts nvm use --lts
JavaScript 动态执行代码:new Function、eval 与 Node.js vm 沙箱
三种方式对比方式 能访问外部作用域 安全性 推荐场景eval() 是 低 几乎不推荐new Function() 否 中 浏览器/Node 通用vm.runInContext() 否(隔离) 高 Node.js 执行用户代码new Function(推荐) // 无参数 const fn = new Function("return 1 + 2"); console.log(fn()); // 3// 带参数 const add = new Function("a", "b", "return a + b"); console.log(add(1, 2)); // 3// 多行代码 const code = ` const x = 10; return x * 2; `; console.log(new Function(code)()); // 20new Function 创建的函数只能访问全局作用域,不能访问创建它时的局部变量——这是它比 eval 更安全的原因。 eval(不推荐) const result = eval("1 + 2"); // 3eval 可以访问当前闭包内的变量,是潜在的代码注入入口,大多数代码规范禁止使用。 注入上下文:with(data) 在表单引擎、规则配置等场景中,需要让动态代码访问对象属性: function runExpression(expr, data) { return new Function("data", `with(data){ return ${expr} }`)(data); }runExpression("price * count", { price: 10, count: 2 }); // 20 runExpression("amount > 100", { amount: 200 }); // truewith(data) 把 data 的属性提升为词法作用域,表达式中可以直接使用属性名。 执行用户脚本(带函数体) 从数据库加载 JS 函数字符串并执行: const scriptStr = ` function check(order) { return order.amount > 100; } `;// 提取函数体,传入参数执行 const fn = new Function("order", ` ${scriptStr} return check(order); `);console.log(fn({ amount: 200 })); // true异步版本:AsyncFunction // 获取异步函数构造器 const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;const code = ` const res = await Promise.resolve(123); return res; `;const fn = new AsyncFunction(code); fn().then(console.log); // 123Node.js vm 沙箱(服务端执行用户代码) vm 模块提供真正的上下文隔离,适合服务端运行不受信任的代码: const vm = require("vm");// 创建隔离上下文 const context = { a: 1, b: 2, result: 0 }; vm.createContext(context);vm.runInContext("result = a + b", context); console.log(context.result); // 3设置超时,防止死循环: try { vm.runInContext( "while(true){}", context, { timeout: 1000 } // 1秒超时 ); } catch (e) { console.log("超时:", e.message); }完整执行流程: function safeExec(code, data, timeoutMs = 500) { const ctx = { ...data, __result: undefined }; vm.createContext(ctx); vm.runInContext( `__result = (function(){ ${code} })()`, ctx, { timeout: timeoutMs } ); return ctx.__result; }safeExec("return a * b", { a: 3, b: 4 }); // 12vm 模块的局限 vm 不是完全安全的沙箱,能被构造特定代码逃逸(prototype chain escape)。对安全要求更高的场景,使用 vm2 库(已停止维护)或 isolated-vm(推荐): import ivm from "isolated-vm";const isolate = new ivm.Isolate({ memoryLimit: 64 }); const ctx = await isolate.createContext(); const result = await ctx.eval("1 + 2");
漏洞管理平台架构:main.db + 每应用独立 SQLite 的多库设计
数据库拆分设计 /database main.db ← 用户、管理员、应用列表 java.db ← Java 漏洞、CVE、POC、EXP mysql.db ← MySQL 漏洞、CVE、POC、EXP redis.db nginx.db每个应用独立 SQLite 的优点:某应用数据损坏不影响整个系统 可单独备份或迁移到 PostgreSQL 漏洞数据量大时不会拖慢其他应用的查询main.db 表结构 -- 用户表 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password TEXT, -- bcrypt hash role TEXT, -- superadmin / admin / user status INTEGER, -- 0=禁用 1=正常 created_at DATETIME );-- 应用注册表 CREATE TABLE applications ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, -- "java" db_name TEXT, -- "java.db" db_path TEXT, -- "/database/java.db" created_at DATETIME );应用数据库(每个应用独立) CREATE TABLE vulnerabilities ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT, severity TEXT, -- critical/high/medium/low affected_version TEXT, fixed_version TEXT, created_at DATETIME );CREATE TABLE cves ( id INTEGER PRIMARY KEY AUTOINCREMENT, cve_id TEXT, -- CVE-2024-12345 cvss REAL, description TEXT );CREATE TABLE pocs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_path TEXT, -- /uploads/java/uuid.py description TEXT );CREATE TABLE exps ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_path TEXT, description TEXT );-- 多对多关联 CREATE TABLE vulnerability_pocs (vuln_id INT, poc_id INT); CREATE TABLE vulnerability_exps (vuln_id INT, exp_id INT); CREATE TABLE vulnerability_cves (vuln_id INT, cve_id INT);动态创建应用数据库 添加应用时,后端自动创建 SQLite 文件并初始化表: // NestJS service async createApplication(name: string) { const dbPath = path.join("database", `${name}.db`); const db = new Database(dbPath); // 初始化表结构 db.exec(` CREATE TABLE IF NOT EXISTS vulnerabilities (...); CREATE TABLE IF NOT EXISTS cves (...); CREATE TABLE IF NOT EXISTS pocs (...); CREATE TABLE IF NOT EXISTS exps (...); `); db.close(); // 记录到 main.db await this.mainDb.run( "INSERT INTO applications (name, db_name, db_path) VALUES (?, ?, ?)", [name, `${name}.db`, dbPath] ); }RBAC 三角色权限角色 权限superadmin 全部权限,管理用户和管理员admin 管理漏洞/CVE/POC/EXP,不能添加管理员user 只能查询漏洞和下载 POC/EXPJWT 鉴权: // NestJS Guard @Injectable() export class RolesGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const { user } = context.switchToHttp().getRequest(); const requiredRoles = this.reflector.get<string[]>("roles", context.getHandler()); return requiredRoles.includes(user.role); } }文件安全 POC/EXP 文件禁止通过静态目录暴露: ❌ GET /uploads/java/poc.py (直接静态访问) ✅ GET /api/download/poc/:id (JWT 鉴权后返回文件流)@Get("download/poc/:id") @UseGuards(JwtAuthGuard) async downloadPoc(@Param("id") id: string, @Res() res: Response) { const poc = await this.pocService.findById(id); res.download(poc.file_path, poc.name); }上传文件名用 UUID 避免路径遍历: const filename = `${uuidv4()}${path.extname(file.originalname)}`;版本匹配建议 不要简单字符串比对,用 semver 范围查询: import semver from "semver";function isVulnerable(version: string, affected: string): boolean { // affected: ">=8.0.0 <8.0.31" return semver.satisfies(version, affected); }"8.0.9" < "8.0.31" 用字符串比较会得到错误结果(字典序 9 > 3),必须用 semver 数字化比较。 推荐技术栈层 推荐后端框架 NestJS 或 Next.js App RouterORM Prisma(SQLite 支持好,类型安全)鉴权 JWT + bcrypt前端 Vue3 + Element Plus 或 shadcn/ui部署 PM2 + Nginx 反代
Cheerio 解析 HTML 表格:动态定位列索引
HTML 表格的列顺序经常变动,硬编码列索引(cells[2])在结构改变后立刻失效。正确做法是先扫描表头行确定目标列的实际位置,再按索引取数据。 核心思路 1. 找到目标表格 2. 读取表头行,确定"销售"列的索引 3. 遍历数据行,按列索引取值 4. 匹配产品名称,验证数值范围完整实现 import * as cheerio from "cheerio";class PriceParser { constructor() { this.validationRules = { gold: { min: 400, max: 1000 }, // 元/克 silver: { min: 3, max: 20 }, }; } parsePrices(html) { const $ = cheerio.load(html); const result = { goldPrice: null, silverPrice: null }; $("table").each((_, table) => { const rows = $(table).find("tr"); if (rows.length < 2) return; // 扫描表头行,定位"销售"列 let saleColumnIndex = -1; rows.first().find("th, td").each((i, cell) => { if ($(cell).text().trim().includes("销售")) { saleColumnIndex = i; return false; // break } }); if (saleColumnIndex < 0) return; // 遍历数据行 rows.each((rowIndex, row) => { if (rowIndex === 0) return; // 跳过表头 const cells = $(row).find("td, th"); if (!cells.length) return; const productName = $(cells[0]).text().trim(); // 匹配黄金9999 if ( (productName === "黄金9999" || productName === "黄金 9999") && !result.goldPrice ) { result.goldPrice = this.extractPrice( $(cells[saleColumnIndex]).text().trim(), this.validationRules.gold ); } // 匹配白银(排除含"黄金"的行) if ( productName === "白银" && !productName.includes("黄金") && !result.silverPrice ) { result.silverPrice = this.extractPrice( $(cells[saleColumnIndex]).text().trim(), this.validationRules.silver ); } }); }); return result; } extractPrice(text, { min, max }) { const match = text.match(/(\d+\.?\d*)/); if (!match) return null; const price = parseFloat(match[1]); if (price < min || price > max) return null; // 超出合理范围 return price.toFixed(2); } }// 使用 const parser = new PriceParser(); const { goldPrice, silverPrice } = parser.parsePrices(html);关键点解析 动态列定位:用 .includes("销售") 而不是固定索引,表格新增或删除列时代码无需修改。 数值范围验证:validationRules 定义合理价格区间,过滤掉格式异常或明显错误的数据(如抓到表格里的其他数字)。 产品名精确匹配:productName === "黄金9999" 而不是 .includes("黄金"),防止把"黄金饰品9999"也匹配进来。!productName.includes("黄金") 在白银行再加一层保护。 多语言/别名的处理 如果表格里的产品名不统一(黄金9999、黄金 9999、Au9999 混用): const GOLD_NAMES = new Set(["黄金9999", "黄金 9999", "Au9999", "足金9999"]);if (GOLD_NAMES.has(productName) && !result.goldPrice) { ... }调试技巧 遇到解析不到数据时,先打印表格 HTML 确认结构: $("table").each((i, table) => { console.log(`Table ${i}:`, $.html(table).slice(0, 500)); });再检查表头文本是否有空格或全角字符: rows.first().find("th, td").each((i, cell) => { console.log(`Header ${i}: "${$(cell).text().trim()}"`); });页面改版后表格结构变化,这两步能快速定位问题所在。
GLIBC 版本不兼容:node_sqlite3.node 报 GLIBC_2.38 not found 的解决方法
Error: /lib64/libm.so.6: version `GLIBC_2.38' not found (required by node_modules/sqlite3/build/Release/node_sqlite3.node)典型场景:本地 Ubuntu 24 编译后,将整个 node_modules 上传到 CentOS 7 / Rocky 8 / 老 Ubuntu 服务器,因为 glibc 版本不匹配,native addon 加载失败。 确认系统 glibc 版本 ldd --version # ldd (GNU libc) 2.17 ← 远低于 2.38sqlite3 native addon 编译时用了 GLIBC_2.38 的符号,但服务器的 /lib64/libm.so.6 只支持到 2.17,因此 dlopen 失败。 解决方案 方案 1:服务器重新编译(推荐) 在目标服务器上重新安装或重编译 native 模块: # 进入项目目录 cd /your/project# 先删除已有 node_modules rm -rf node_modules package-lock.json# 重新安装(会在当前服务器编译 .node 文件) npm install# 或者只重编译 sqlite3 npm rebuild sqlite3如果编译报错,需要先安装编译工具链: CentOS / RHEL: yum groupinstall "Development Tools" -y yum install python3 gcc gcc-c++ make -yUbuntu / Debian: apt install build-essential python3 make g++ -y然后再执行 npm rebuild sqlite3。 方案 2:不要跨系统上传 node_modules 根本原因是 native addon 的 .node 文件是平台相关的二进制,不能跨系统复用。正确的部署流程: # 本地:只上传源码,不上传 node_modules git push # 或 scp 项目文件,排除 node_modules# 服务器:安装 npm install将 node_modules/ 加入 .gitignore 和 rsync --exclude。 方案 3:改用 better-sqlite3 sqlite3 的 native binding 经常出现编译问题。better-sqlite3 维护更活跃且通常编译更顺畅: npm uninstall sqlite3 npm install better-sqlite3API 略有不同(同步而非回调),迁移成本视代码量而定。 为什么 GLIBC 版本不能降级升级 glibc 是系统核心库,版本升级风险极高(可能导致系统无法启动),降级几乎不可能。唯一可靠的方式是在目标环境重新编译,或用 Docker 把运行环境固定下来: FROM node:18-bullseye-slim WORKDIR /app COPY package*.json ./ RUN npm install COPY . . CMD ["node", "server.js"]容器化后 glibc 版本由镜像固定,不再受宿主机影响。
Node 报 GLIBC_2.38 not found 的根因和四种解法
宝塔服务器启动 Node 项目,直接崩: Error: /lib64/libm.so.6: version `GLIBC_2.38' not found (required by /www/wwwroot/yourproject/server/node_modules/sqlite3/build/Release/node_sqlite3.node) code: 'ERR_DLOPEN_FAILED'这是典型的 glibc 版本不匹配。sqlite3.node 是个 native 二进制,编译时链接的 glibc 是 2.38,服务器 glibc 只有 2.17 或 2.28,加载不了。 先确认服务器 glibc 版本 ldd --versionCentOS 7 通常是 2.17,Rocky 8 / 老 Ubuntu 是 2.28。而 Ubuntu 24 / Debian 13 的 glibc 已经到 2.38+,本地编译的东西拿过去自然跑不动。 场景还原 99% 是这么造成的:本地新系统 npm install 打包整个项目(包括 node_modules)上传到服务器 服务器上直接 node index.js 崩node_modules/sqlite3/build/Release/node_sqlite3.node 是在本机 Ubuntu 24 编译的,扔到 CentOS 7 上当然报 GLIBC。 四种解法 方案一:服务器重新编译(推荐) cd /www/wwwroot/yourproject/server rm -rf node_modules package-lock.json npm install或者只重编译原生模块: npm rebuild sqlite3编译失败通常是缺工具链: # CentOS / RHEL yum groupinstall "Development Tools" -y yum install python3 gcc gcc-c++ make -y# Ubuntu / Debian apt install build-essential python3 make g++ -y方案二:本地别上传 node_modules 正确的部署流程:本地只上传 package.json + package-lock.json 服务器 npm install.gitignore 里加上 node_modules/,部署脚本里 rsync --exclude=node_modules/,这类问题就彻底没了。 方案三:换更省心的 SQLite 驱动 老牌 sqlite3 依赖 node-gyp 编译,每次升 Node 都可能炸。换成:better-sqlite3(同步 API、性能更好) libsql(Cloudflare 生态)npm install better-sqlite3大多数场景 API 迁移不复杂,还能避开 native 编译地狱。 方案四:升级系统 glibc(不推荐) CentOS 7 手动装高版本 glibc 有极高概率把系统弄崩,别碰。要新 glibc 就直接升系统或者上 Docker——用官方 Node 镜像编译,产物就是那个环境的 glibc。 一条经验 Node 项目部署,永远不要把 node_modules 传上服务器。 让服务器自己 npm install,很多 native 模块问题从此消失。
Node 内置 SQLite 报 statement has been finalized 的原因
Node 22 开始有了内置的 node:sqlite,同步 API 用起来非常顺。但启动时挂: ExperimentalWarning: SQLite is an experimental feature Failed to start server Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) ... code: 'ERR_INVALID_STATE'这个错的字面意思很明确:你在一个已经 finalize() 过的 prepared statement 上继续调用 all() / get() / iterate()。 三种最常见的成因 1. 手动过早 finalize const stmt = db.prepare("SELECT * FROM devices"); try { return stmt.all(); } finally { stmt.finalize(); }看上去正确。但如果 stmt.all() 返回的是迭代器(iterate())而不是数组(all()),外层还在懒消费,finalize 一走就崩。用 all() 拿到具体数组的写法没这个问题。 2. 用了 using 自动 finalize(Node 22 的新语法) export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.iterate(); // ← 迭代器,出了作用域 stmt 就被 finalize 了 }using 借用了 TC39 的 Explicit Resource Management 提案,出作用域时会自动调 [Symbol.dispose](),也就是 finalize()。返回迭代器给外部继续用,外部一 next() 就炸。 规则:using + iterate() 不能混,要么用 all() 一次拿全部再返回,要么调用方也 using。 3. 模块级缓存了 stmt // 模块顶部 const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); }看着挺合理——避免每次 prepare 的开销。但代码其它地方可能:单元测试用 db.close() 关掉了数据库(stmt 一起 finalize) 有热重载/HMR 重新 import 了模块(旧 stmt 引用还留着) 显式调用了 readStmt.finalize() 想清理,然后忘了之后再走这个函数就 ERR_INVALID_STATE。 推荐写法 每次现 prepare,让 GC 管理: export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 立刻取完,不返回 iterator }现代 SQLite 的 prepare 开销很小,不需要为性能提前优化。 要缓存的话,配套复用而不是复用 + finalize: const cache = new Map<string, ReturnType<typeof db.prepare>>();function s(sql: string) { let stmt = cache.get(sql); if (!stmt) { stmt = db.prepare(sql); cache.set(sql, stmt); } return stmt; }// 用: s("SELECT * FROM device_states").all();不在缓存生命周期外手动 finalize。数据库 close 时统一 cache.clear()。 要用 using 就一次性拿完数据: export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 数组,可以安全跨作用域 }一句话总结 statement has been finalized = 你正在用一个死掉的 stmt。别返回迭代器给作用域外、别在模块级缓存又手动 finalize。改成"每次 prepare + all()"最省心。
Node.js SQLite statement has been finalized 错误:原因与修复
Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) { code: 'ERR_INVALID_STATE' }这个错误来自 Node.js 22+ 的内置 node:sqlite 模块,对已经调用了 finalize() 的 StatementSync 对象再次操作时抛出。 原因一:全局缓存 Statement 被意外 finalize // 错误写法:模块级共享 statement const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); // 若 readStmt 被 finalize 则崩溃 }如果在某处调用了 readStmt.finalize(),后续所有使用都会报错。 原因二:using 关键字自动释放 Node.js SQLite 支持 using 声明(Explicit Resource Management),离开作用域后自动调用 finalize(): // 错误:返回迭代器后 stmt 已被 finalize function getRows() { using stmt = db.prepare("SELECT * FROM logs"); return stmt.iterate(); // 离开函数后 stmt 自动 finalize,迭代器失效 }修复:每次使用时重新 prepare // 正确写法:每次都 prepare,不缓存 export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); const result = stmt.all(); stmt.finalize(); return result; }或者更简洁,不手动调用 finalize(Node.js 会在 GC 时自动释放): export function readDeviceStates() { return db.prepare("SELECT * FROM device_states").all(); }需要性能优化时:用对象包装 如果 prepare 开销较大,可以在模块初始化时准备,但确保 finalize 只在明确不再使用时调用: class DeviceRepository { private readStmt: StatementSync; constructor(private db: DatabaseSync) { this.readStmt = db.prepare("SELECT * FROM device_states"); } getAll() { return this.readStmt.all(); } close() { this.readStmt.finalize(); this.db.close(); } }生命周期由对象管理,close() 明确释放资源。 ExperimentalWarning 的处理 (node:2436) ExperimentalWarning: SQLite is an experimental featurenode:sqlite 在 Node.js 22 中是实验性功能,可以用以下方式消除警告: node --no-experimental-warnings server.js或在代码中: process.removeAllListeners('warning');生产环境建议关注 Node.js 版本更新,等待 SQLite 模块稳定。
Chrome CDP 远程调试:--remote-debugging-port 与 Node.js Hook 框架
CDP 远程调试 启动浏览器并开放调试端口 # Chrome chrome.exe --remote-debugging-port=9222# Edge(建议指定独立 user-data-dir) msedge.exe --remote-debugging-port=9222 --user-data-dir=D:\edge_debug获取 WebSocket 调试地址 浏览器启动后,访问: http://127.0.0.1:9222/json返回当前所有 Tab 的信息,其中 webSocketDebuggerUrl 是关键字段: { "id": "ABC123", "title": "test page", "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ABC123" }连接 DevTools 前端 devtools://devtools/bundled/devtools_app.html?ws=127.0.0.1:9222/devtools/page/ABC123整体链路:--remote-debugging-port 开放 CDP 端口 → /json 获取 ws 地址 → DevTools 前端通过 ws 接入。 Node.js Hook 三件套 在 Node 程序里注入以下 hook 可以拦截运行时数据流。 Hook JSON.parse(记录解析数据) const fs = require('fs'); const _parse = JSON.parse;JSON.parse = function (text, reviver) { const result = _parse.call(this, text, reviver); // 只保存含 "raw" key 的对象 if (result && typeof result === 'object' && 'raw' in result) { fs.appendFileSync('./json_log.txt', JSON.stringify({ time: Date.now(), data: result }) + '\n'); } return result; };过滤条件也可以改为 /"raw"\s*:/.test(text) 在原始字符串层面筛选,减少反序列化开销。 Hook TextDecoder.decode(截获二进制转字符串) const { TextDecoder } = require('util'); const _decode = TextDecoder.prototype.decode;TextDecoder.prototype.decode = function (input, options) { const result = _decode.call(this, input, options); console.log('[TextDecoder]', result); return result; };适合抓 WebSocket 二进制帧、CDP 协议数据等场景。注意 Buffer.toString('utf-8') 走的是另一条路径,需要单独 hook Buffer.prototype.toString。 Hook AES 加密(抓密钥与明文) const crypto = require('crypto'); const _createCipheriv = crypto.createCipheriv;crypto.createCipheriv = function (algorithm, key, iv, options) { console.log('[AES key]', { algorithm, key: key?.toString?.('hex'), iv: iv?.toString?.('hex') }); const cipher = _createCipheriv.call(this, algorithm, key, iv, options); const _update = cipher.update; cipher.update = function (data) { console.log('[AES plaintext]', data?.toString?.() ?? data); return _update.apply(this, arguments); }; return cipher; };Hook createDecipheriv 的结构完全对称,可以抓解密后的明文。 注意事项JSON.parse 被局部引用绕过时(const p = JSON.parse; p(...)),改 global.JSON.parse 更彻底 大流量场景建议加过滤条件(text.length < 5000)避免日志刷屏 Node.js 版本 >= 11 才有全局 TextDecoder,旧版用 require('util').TextDecoder
SyntaxError: Unexpected identifier '目':文本被贴进 JS 文件
看到这个报错,几乎不用想: 题 目 AI辅助的全方位科研管理与创作平台 ^ SyntaxError: Unexpected identifier '目' at wrapSafe (node:internal/modules/cjs/loader:1637:18)Node 把这一行当代码解析了,而中文字符不是合法 JS 标识符,直接崩。 真正原因 八成是你想读的文件内容被复制粘贴到 JS 源码里了。你的 .js 文件现在长这样: const fs = require('fs') rd = fs.readFileSync("1.txt") console.log(rd.length)题 目 AI辅助的全方位科研管理与创作平台 // ← 这一行是裸文本裸文本既不是字符串(没引号)也不是注释(没 // 或 /* */),Node 只能当代码解析,然后崩在第一个中文字符上。 正确姿势 JS 文件里只留代码,文本内容留在 1.txt 里: // index.js const fs = require("fs");const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length);# 1.txt 题 目 AI辅助的全方位科研管理与创作平台顺手补:readFileSync 不传 encoding 的坑 fs.readFileSync("1.txt") 返回的是 Buffer,不是字符串: const rd = fs.readFileSync("1.txt"); console.log(rd.length); // 字节数,中文会算错想拿字符长度就要传编码: const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length); // 字符数 console.log(Buffer.byteLength(text, "utf8"));// 字节数一个中文字符在 UTF-8 里是 3 字节,字符长度和字节长度会差好几倍——涉及"长度"就要先想清楚要哪种。 触类旁通:类似的 SyntaxError 同样是"文件混了不该混的东西",报错方向不同:报错 通常原因Unexpected identifier '目' 中文文本被贴进 .jsUnexpected token '<' HTML 页面被当 JS 加载(一般是路径错)Unexpected token 'export' ESM 语法进了 CJS 文件(改 .mjs 或加 "type": "module")Unexpected token '?' / ?? / &&= 老 Node 不认新语法,升级 NodeInvalid or unexpected token 指向 文件带了 UTF-8 BOM,重存为无 BOM一句话总结 看到 Unexpected identifier '中文'——先打开源码文件,把混进去的文本挪出去。readFileSync 记得传 "utf8"。
Node.js SyntaxError: Unexpected identifier:非 ASCII 字符混入代码的排查
中文字符导致 SyntaxError SyntaxError: Unexpected identifier '目' at wrapSafe (node:internal/modules/cjs/loader:...)报错指向中文字符"目",不是代码写法问题,而是文本内容被误粘贴进了 JS 文件: // 错误:文本直接出现在代码里 const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length);题 目 AI辅助的全方位科研管理与创作平台 // ← 这行是数据,不是代码最后一行是从 1.txt 复制过来的内容,Node 解析时把它当代码执行,"题"和"目"之间有空格被当作两个标识符。 修复方法:删除误粘贴的文本,JS 文件只保留代码逻辑。 readFileSync 的编码参数 // 不指定编码:返回 Buffer const buf = fs.readFileSync("1.txt"); console.log(buf.length); // 字节数(中文 UTF-8 每字 3 字节)// 指定 utf8:返回字符串 const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length); // 字符数(JavaScript 的 UTF-16 码元数) console.log(Buffer.byteLength(text)); // UTF-8 字节数中文字符用 buf.length 统计结果是字节数(通常是字符数的 3 倍),必须指定 "utf8" 才能拿到字符串后再用 .length。 用 Map 统计数组重复项 Map 是计数/去重的高效结构: const arr = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];const counter = new Map(); for (const item of arr) { counter.set(item, (counter.get(item) ?? 0) + 1); } // Map { 'apple' => 3, 'banana' => 2, 'cherry' => 1 }找出出现超过 1 次的元素: const duplicates = [...counter.entries()] .filter(([, count]) => count > 1) .map(([key]) => key); // ['apple', 'banana']一遍遍历即可,时间复杂度 O(n),比嵌套 filter/indexOf 更高效。 去重(保留唯一值) 用 Set 更简洁: const unique = [...new Set(arr)]; // ['apple', 'banana', 'cherry']如果同时需要计数和去重,先建 Map 再从 Map 的 key 拿唯一值: const unique = [...counter.keys()];
JSC 文件分析:V8 字节码、bytenode 产物与字符串提取
JSC 文件的三种类型 .jsc 不是一种统一格式,常见有三类:类型 本质 能否反编译bytenode 生成 V8 字节码 基本不能还原源码JavaScriptCore(iOS/WebKit) JSCore 缓存 无稳定通用工具微信小程序 V8 字节码或混淆加密体 几乎不可能还原核心结论:JSC 是编译产物,不是源码压缩。变量名、结构、注释已丢失,不存在完美反编译。 View8 报错原因 RuntimeError: Failed to detect version for file index.jscView8 要先检测 V8 版本,失败通常是这几个原因:文件不是 V8 cache——bytenode 生成的 .jsc 结构与 V8 code cache 不同 .jsc 被加了 header、base64 或 zip 包装 V8 版本超出 View8 支持范围(通常只支持 8.x~11.x)用 Python 快速判断: with open("index.jsc", "rb") as f: print(f.read(32).hex())V8 code cache 开头有固定的 magic bytes。如果头部是乱码或压缩特征,View8 直接放弃。 Python 提取可读字符串 不管是哪种 JSC,都可以先提取二进制文件中的可读字符串,判断是否有逆向价值: import rewith open("index.jsc", "rb") as f: data = f.read()strings = re.findall(rb"[ -~]{6,}", data)seen = set() for s in strings: text = s.decode("utf-8", errors="ignore") if text not in seen: seen.add(text) print(text)正则 [ -~]{6,} 匹配长度 >= 6 的 ASCII 可读字符串。 结果判断:出现 /api/、token、userInfo 等 → 文件未加密,有分析价值 全是乱码 → 已加密或压缩,需换思路bytenode JSC 的运行时分析 bytenode 产物无法静态反编译,但可以在 Node.js 中加载后 hook: require("bytenode");// hook 所有函数调用 const originalCall = Function.prototype.call; Function.prototype.call = function(...args) { if (this.name) { console.log("[call]", this.name, args.slice(0, 2)); } return originalCall.apply(this, args); };require("./index.jsc");或者用 Node.js inspect 调试: node --inspect-brk -e "require('bytenode'); require('./index.jsc')"然后用 Chrome DevTools 连接 chrome://inspect,可以打断点、查看调用栈。 三种方案对比方案 成功率 适用场景Python strings 提取 高(未加密) 快速判断有无价值运行时 hook 高 bytenode 产物Node inspect 调试 高 有 require 入口静态 V8 字节码分析 低 仅作辅助对于加密 JSC(AES/RC4 加密,或微信小游戏自定义 VM),以上方法均不适用,需要先逆向解密逻辑。
