11 Commits
Author SHA1 Message Date
jkooe 9b6b063e2b docs: 更新 README 单测计数为 483 2026-11-03 20:33:36 +08:00
jkooe cf51e0415a fix(scripts): 自重启任务纳入卸载清理 —— 修「僵尸任务反复拉起已删文件」
真机发现:`system.agent.restart` 每次重启注册 `nodeagent-selfrestart-<毫秒>`,
而 TASK_WHITELIST 只有 nodeagent/na-gui/nodeagent-console → 卸载后这类任务残留,
会持续尝试拉起已被删除的 agent.mjs。

但它不能当垃圾随手清:**Running 的那一个往往就是当前 agent 的父任务**
(删了会连带杀掉 agent)。故只纳入「卸载清理」判定集合,由调用方在停掉
agent 之后使用。

匹配用严格命名空间 `^nodeagent-selfrestart-\d{6,}$`(纯数字时间戳形态),
不做任何通配;`nodeagent-selfrestart-abc`/前缀不全/带额外后缀一律不认,
单测同时钉死「第三方任务(Adobe/OneDrive)绝不出现」。
2026-10-27 13:06:51 +08:00
jkooe c062d1cc8b fix(agent): ETIMEDOUT 给出「配代理」提示 —— 境内直连最高频失败形态无指引
真机(Windows 192.168.0.160)验证自更新出网时发现:
`connect ETIMEDOUT 20.205.243.166:443` 落到 describeNetError 的兜底分支,
只回一串 IP:端口超时 —— 而这正是境内直连 GitHub 最常见的形态,用户唯一
有效动作就是配代理,却完全没被告知。

修法:新增 ETIMEDOUT / EHOSTUNREACH / ENETUNREACH 分支,点明
「设置 NA_PROXY=<代理地址> 或配置系统代理后重试」。与 ECONNRESET 的
「代理或线路问题」保持区分(两类原因不同,单测钉死互不串味)。
真机复验:重新部署后同一命令已输出新提示。
2026-10-27 13:06:51 +08:00
jkooe 0b858bb343 docs(changelog): 记录本轮四项遗留修复 + 单测数 477→480
- CHANGELOG [未发布] 补:依赖彻底清零、看门狗日志轮换、L2 参数常显、
  e2e 子进程兜底清理;
- README 单测数同步 480(docs-numbers 守卫强制耦合)。
2026-10-27 08:04:33 +08:00
jkooe a769479a2a chore(deps): 剩余中低危清零 —— hono / ip-address / fast-uri 抬到修复版
上一轮把 3 个 high 压掉后,复扫仍余 6 条(2 low / 4 moderate),全部经
@modelcontextprotocol/sdk 传递:

- hono <4.13.11(serveStatic 二次解码绕过中间件)→ ^4.13.14;
- ip-address <=10.7.3(zone 标识符二次方耗时 DoS)→ ^10.7.5;
- fast-uri <3.1.9(scheme 伪造 / host 混淆 / 路径等价)→ ^3.1.9。

与 dependabot #3/#6/#7 同源,覆盖后那三个 PR 无增量,随本提交关闭。
当前 pnpm audit:No known vulnerabilities found。
2026-10-27 08:04:32 +08:00
jkooe 5e32d7fa55 test(e2e): 子进程统一登记 + 退出兜底 SIGKILL —— 修连跑偶发失败
套件有 8 处 spawn,各自 finally 只发 kill()(SIGTERM)就 process.exit(),
被控端可能来不及退出 → 下一轮固定端口仍被占用,表现为「连跑第二次偶发失败、
重跑又绿」这种最难查的形态。

做法:把 spawn 包一层统一登记(同名函数遮蔽,调用点零改动),进程退出时对
仍存活的子进程 SIGKILL。单点收口,将来新增 spawn 也不会漏。

实测:连跑两次 34/34,退出后 pgrep 无残留 agent 进程。
2026-10-27 08:04:27 +08:00
jkooe eebfb45f73 fix(console): L2 采集参数常显 —— 不再「必须先出一次结果」才出现输入框
「采集窗口(小时)」「条数上限」两个输入框原先嵌在 v-if="l2Rows.length" 的
容器里,于是想「先设 72 小时再采集」做不到,只能先按默认值点一次再改。

参数与结果没有生命周期关系,拆出容器常显;结果区仍由 l2Rows.length 控制。
2026-10-27 08:04:26 +08:00
jkooe 419f6dca13 fix(agent): 看门狗日志按 1MB×4 轮换 —— 修常驻被控端的无界增长
startup-guard.log 一直是 appendFileSync 无限追加。看门狗常驻被控端,
「坏版本 + supervisor 反复重启」时写得很快,磁盘写满会连带把 agent 的
审计写入一起拖死(看门狗自己就在同一台机器上)。

- core(零依赖纯函数):shouldRotate(严格大于阈值,NaN 不触发)+ rotationTargets;
- guard:超阈值先丢最旧 .N → 逐个后移 → 当前滚为 .1,滚动与写入均失败静默
  (日志问题绝不能反过来拦住被控端启动);
- 单测 +3:阈值边界(含恰好等于/Nan)、命名序列、真实滚存三轮(验证只留 3 份、
  当前日志重新开始、未超阈值只追加)。
2026-10-27 08:04:20 +08:00
jkooe 1a2d1c7825 fix(ci): 补挂安装包断言改为按架构校验 —— 修「每次必红」的漂移守卫
v2.1.0 的 Console Release 在 publish job 红:
`安装包数量不足(3 < 4),架构可能缺失`。

根因是**守卫与实现漂移**,不是产物缺失:build 矩阵是
x64 → msi+nsis(2 个)、arm64 → 仅 nsis(1 个,WiX 的 arm64 MSI 支持面窄,
刻意不出),合计恒为 3;而断言写死「两架构各一套(exe+msi)= 4 个」,
于是从 arm64 去掉 msi 那天起就**每次都红** —— 红门禁等于没有门禁。

改为按架构断言(x64 ≥2 / arm64 ≥1),缺失时精确报出是哪一架构;
矩阵再变也只需改一行期望值。

附带修一个只在 CI 爆的坑:`$dir)`(变量紧贴全角括号)在 set -u 下被 bash
当成变量名的一部分 → unbound variable,本地肉眼完全正常。变量引用统一改
${var} 形式,并用抽取工作流原文的方式在本地跑通正例/反例两个分支。
2026-10-27 00:36:09 +08:00
jkooe b6e58dbcc4 docs(changelog): v2.1.0 定稿 + 修正历史改写后的悬空 hash 引用
- `## [未发布]` 中已发布的 v2.1.0 段改为 `## [2.1.0] - 2026-10-26`;
  新开 [未发布] 记录发版脚本修复与依赖漏洞清零两条。
- 历史改写(脱敏)后旧提交 hash 不再可达,文档里的引用会变成死链:
  CHANGELOG `efe4744` → `a75c5c9`;ROADMAP `efe4744`/`b09ca68`/`e864486`/`d106214`
  → `a75c5c9`/`5a2c5f9`/`68ea3cd`/`560b3c4`。全仓已无旧 hash 残留。
2026-10-27 00:27:17 +08:00
jkooe 5bacade667 chore(deps): 依赖漏洞清零 —— zod/body-parser 覆盖 + vite 升 8.3.5
Quality Gate 的「依赖漏洞扫描(SCA)」在 451d44a 上红:`pnpm audit
--audit-level=high` 报 3 个 high,全部不是本仓直接可控的代码:

- zod <4.6.6(GHSA-mrhx-c7xc-m3w5),路径 apps/mcp → @modelcontextprotocol/sdk;
- body-parser <2.4.0(GHSA-27r3-jxmh-p828),同上经 sdk→express;
- vite <8.3.3(GHSA-587h-7wmj-h5pm),控制台 devDependency。

处置:前两者用根 pnpm-workspace.yaml 的 overrides 抬到修复版(同 major patch 级);
vite 直接改依赖范围 ^8.3.5。实测 14 → 6 条(**0 high**),门禁转绿。

注意(真坑):pnpm 12 已不读 package.json 的 `pnpm` 字段,overrides 写在那里会被
**静默忽略**(install 报 "Already up to date" 但审计毫无变化)—— 必须写进
pnpm-workspace.yaml,理由与命令均落在文件注释里。
2026-10-27 00:26:55 +08:00
16 changed files with 608 additions and 86 deletions

No files matched your search

+33 -4
View File
@@ -352,11 +352,40 @@ jobs:
- name: 列出待发布文件(先看清再上传)
run: |
set -euo pipefail
echo "--- dist 实际内容 ---"
find dist -type f | sort | head -40
echo "--- 安装包 ---"
find dist -type f \( -name '*.exe' -o -name '*.msi' \) -printf '%10s %p\n' | sort -k2
n=$(find dist -type f \( -name '*.exe' -o -name '*.msi' \) | wc -l)
echo "共 $n 个安装包"
# 两架构各一套(exe+msi)= 4 个;少一个说明产物不全,宁可红也不要静默少发
if [ "$n" -lt 4 ]; then echo "::error::安装包数量不足($n < 4),架构可能缺失"; exit 1; fi
# 期望值必须与上面的 build 矩阵一致:
# x64 → msi + nsis(2 个)
# arm64 → 仅 nsis(1 个,WiX 的 arm64 MSI 支持面窄,刻意不出)
# 合计 3 个。
#
# ⚠️ 这里曾写死「两架构各一套(exe+msi)= 4 个」,与矩阵漂移后**每次必红** ——
# 红门禁等于没有门禁(本项目反复踩的同一类坑:断言与实现各改各的)。
# 故改为**按架构**断言,并在缺失时报出具体是哪个架构,便于一眼定位。
fail=0
for spec in "x64:2" "arm64:1"; do
arch=${spec%%:*}; want=${spec##*:}
dir="dist/nodeagent-console-windows-$arch"
# ⚠️ 变量引用一律用 ${var} 形式:`$dir)` 这种「$var + 全角括号」在
# set -u 下会被 bash 当成变量名的一部分 → "unbound variable",
# 而写法在本地肉眼完全正常(本地实测复现过)。
if [ ! -d "${dir}" ]; then
echo "::error::缺少 ${arch} 的产物目录(${dir})—— 该架构构建失败或 artifact 名称变更"
fail=1
continue
fi
got=$(find "${dir}" -type f \( -name '*.exe' -o -name '*.msi' \) | wc -l)
echo " ${arch}: ${got} / ${want}"
if [ "${got}" -lt "${want}" ]; then
echo "::error::${arch} 安装包不足(${got} < ${want})—— 产物缺失,或矩阵已变更但本断言未同步"
fail=1
fi
done
[ "${fail}" -eq 0 ] || exit 1
echo "✓ 两架构安装包齐全"
- name: 挂到对应 Release
env:
+56 -1
View File
@@ -7,7 +7,62 @@
## [未发布]
> 本段为 **v2.1.0** 定稿段(版本统一见 `efe4744`):selfupdate 代理出网、LKG 内容指针、
### 修复 —— 发版脚本在版本号已钉在树中时卡死(`451d44a`)
`scripts/release.mjs` 无条件执行 `git commit`。当版本号已经作为 `chore(release)`
提交进树(v2.1.0 的实际流程)时,`git add -A` 后暂存区为空,`git commit` 以
`nothing to commit` 非零退出,整个发版停在最后一步,且报错来自 git 而非发布逻辑
(极易误判为工作区问题)。改为:暂存区为空则跳过 commit,标签与 Release 照常创建。
### 安全 —— 依赖漏洞清零(`pnpm audit --audit-level=high` 由 3 high → 0)
Quality Gate 的「依赖漏洞扫描(SCA)」在 `451d44a` 上为红,全部来自
`@modelcontextprotocol/sdk` 的传递依赖与本仓 devDependency:
- `zod <4.6.6`(GHSA-mrhx-c7xc-m3w5)→ 根 `pnpm-workspace.yaml` 加 overrides `^4.6.6`;
- `body-parser <2.4.0`(GHSA-27r3-jxmh-p828)→ overrides `^2.4.0`;
- `vite <8.3.3`(GHSA-587h-7wmj-h5pm)→ 控制台 devDependency `^8.3.5`。
⚠️ pnpm 12 起 `package.json` 的 `pnpm` 字段不再被读取(设置迁移到
`pnpm-workspace.yaml`)—— 写在那里会被**静默忽略**,与「一直红所以等于没有」
是同一类失败形态,故覆盖项写在 workspace 文件里并注明原因。
### 安全 —— 依赖漏洞彻底清零(中低危一并处理)
在上一节之后复扫仍余 6 条(2 low / 4 moderate),全部经 MCP SDK 传递:
- `hono <4.13.11`(serveStatic 路径二次解码绕过中间件)→ overrides `^4.13.14`;
- `ip-address <=10.7.3`(zone 标识符二次方耗时 DoS)→ overrides `^10.7.5`;
- `fast-uri <3.1.9`(scheme 伪造 / host 混淆 / 路径等价 4 条)→ overrides `^3.1.9`。
三条与 dependabot #3/#6/#7 同源,覆盖后那三个 PR 已无增量(随提交一并关闭)。
当前 `pnpm audit` 输出 **No known vulnerabilities found**。
### 修复 —— 看门狗日志无界增长(常驻被控端,写满磁盘会拖死 agent)
`startup-guard.log` 一直是 `appendFileSync` 无限追加。看门狗本身常驻被控端,
「坏版本 + supervisor 反复重启」时写得很快,磁盘满会连带把 agent 的审计写入拖死。
现按 **1MB × 4 份**(当前 + `.1`…`.3`)轮换:超阈值先丢最旧、再逐个后移、当前滚为 `.1`。
轮换与写入**双双失败静默** —— 日志问题绝不能反过来拦住被控端启动。
判断逻辑放 `startup-guard-core.mjs` 纯函数(`shouldRotate` / `rotationTargets`),
并补 3 条单测(阈值边界含 NaN、命名序列、真实滚存三轮)。
### 修复 —— 控制台 L2 采集参数必须先出结果才出现
`AuditView.vue` 里「采集窗口 / 条数上限」两个输入框原先在
`v-if="l2Rows.length"` 容器内 —— 用户想「先设 72 小时再采集」做不到,只能先按
默认值点一次再改。参数与结果无生命周期关系,已拆出容器常显。
### 修复 —— e2e 连跑偶发失败(固定端口被上一轮残留占用)
套件有 8 处 `spawn`,各自 `finally` 只发 `kill()`(SIGTERM)就 `process.exit`,
被控端可能来不及退出 → 下一轮固定端口仍被占,表现为「连跑第二次偶发失败,重跑又绿」。
改为**包一层 spawn 统一登记**子进程(调用点零改动),进程退出时对存活者 `SIGKILL`;
单点收口,将来新增 spawn 也不会漏。实测连跑两次 34/34、退出后无残留 agent 进程。
## [2.1.0] - 2026-10-26
> 本段为 **v2.1.0** 定稿段(版本统一见 `a75c5c9`):selfupdate 代理出网、LKG 内容指针、
> 三个架构审查 P0 修复、L2 三端暴露,以及 rollback / 看门狗 / na-gui 清理 / 收口。
### 新增 —— 控制端 `nodeagent rollback`:从 LKG 回滚被控端(ARCH T03 / PRD P0-2)
+1 -1
View File
@@ -168,7 +168,7 @@ nodeagent --node win audit anchor --note "每日例行" # 追加一个锚点
> 真正发挥价值的前提:把锚点文件指向**另一台机器 / 网盘同步目录**。
> 本机文件被整盘重写时,锚点也随之消失 —— 那就等于没锚。
**52 项能力** · **42 个 MCP 工具** · **477 项单元测试** · **50 项端到端用例**(34 通用 + 16 Windows 专属) · **23 项 Rust 壳单测**(真跑 Node sidecar)(CI 在真实 Windows / Linux runner 上验证)
**52 项能力** · **42 个 MCP 工具** · **483 项单元测试** · **50 项端到端用例**(34 通用 + 16 Windows 专属) · **23 项 Rust 壳单测**(真跑 Node sidecar)(CI 在真实 Windows / Linux runner 上验证)
> 关键里程碑:**GUI 语义**(`window.list` + `screen.find`,UIA 找不到自动降级 OCR)
> 让 AI 从「看得到画面但读不懂界面」变成「按名字取坐标点下去」。
@@ -102,6 +102,12 @@ export function describeNetError(err: unknown, timeoutMs: number): string {
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED') {
return `${msg}(域名解析失败或目标拒绝连接;若配了代理,请核对代理地址与端口)`;
}
// 2026-10-27 真机补:境内直连 GitHub 的**最常见**失败形态就是 TCP 层 ETIMEDOUT
// (不是我们自己的 timeoutMs,也不是 ECONNRESET)。原先它落到下面兜底分支,
// 用户只看到一串 IP:443 超时,不知道该干什么 —— 而这时唯一有效的动作就是配代理。
if (code === 'ETIMEDOUT' || code === 'EHOSTUNREACH' || code === 'ENETUNREACH') {
return `${msg}(连接超时/网络不可达;若目标在境外或需要代理,请设置 NA_PROXY=<代理地址> 或配置系统代理后重试)`;
}
return msg;
}
+1 -1
View File
@@ -29,7 +29,7 @@
"@tauri-apps/cli": "^2",
"@vitejs/plugin-vue": "^6.0.7",
"typescript": "~6.0.3",
"vite": "^8.0.16",
"vite": "^8.3.5",
"vue-tsc": "^3.3.5"
}
}
+24 -20
View File
@@ -297,27 +297,31 @@ onMounted(() => {
本地副本防篡改价值有限,真正的独立性依赖把记录<strong>转发出机</strong>。
</n-alert>
<!-- 采集参数始终可见:早前它在 `v-if="l2Rows.length"` 里面,
必须先出一次结果才出现输入框 —— 用户想「先设窗口 72h 再采集」做不到,
只能先点一次默认参数再改。参数与结果的生命周期无关,故拆开。 -->
<n-space align="center" style="margin-bottom: 8px">
<n-text depth="3" style="font-size: 12px">采集窗口(小时)</n-text>
<n-input-number
v-model:value="l2SinceHours"
:min="1"
:max="720"
:step="1"
size="small"
style="width: 110px"
/>
<n-text depth="3" style="font-size: 12px">条数上限</n-text>
<n-input-number
v-model:value="l2Limit"
:min="1"
:max="5000"
:step="100"
size="small"
style="width: 120px"
/>
</n-space>
<n-space v-if="l2Rows.length" vertical size="small">
<n-space align="center">
<n-text depth="3" style="font-size: 12px">采集窗口(小时)</n-text>
<n-input-number
v-model:value="l2SinceHours"
:min="1"
:max="720"
:step="1"
size="small"
style="width: 110px"
/>
<n-text depth="3" style="font-size: 12px">条数上限</n-text>
<n-input-number
v-model:value="l2Limit"
:min="1"
:max="5000"
:step="100"
size="small"
style="width: 120px"
/>
</n-space>
<n-descriptions :column="2" size="small" label-placement="left" bordered>
<n-descriptions-item v-for="row in l2Rows" :key="row.label" :label="row.label">
{{ row.text }}
+3 -3
View File
@@ -457,15 +457,15 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md>
| 项 | 状态 | 落点 |
|---|---|---|
| 版本统一 v2.1.0 + CHANGELOG 骨架 | ✅ | `efe4744` |
| 版本统一 v2.1.0 + CHANGELOG 骨架 | ✅ | `a75c5c9` |
| selfupdate 代理出网(NA_PROXY→env→系统代理) | ✅ | `apps/agent/src/net/proxy.ts` |
| LKG 内容指针(`hash_full` / `backup_glob`) | ✅ | `apps/agent/src/lkg.ts` |
| 控制端 rollback(ARCH T03) | ✅ | `apps/cli/src/cmd/rollback.ts` |
| 看门狗启动自动恢复(决策 R-1) | ✅ | `scripts/startup-guard.mjs` + 三端 supervisor 接入 |
| L2 三端暴露(MCP `na_audit_l2` + 控制台 + `agent.l2`) | ✅ | `b09ca68` |
| L2 三端暴露(MCP `na_audit_l2` + 控制台 + `agent.l2`) | ✅ | `5a2c5f9` |
| na-gui 陈旧计划任务白名单清理 | ✅ | `scripts/lib/scheduled-tasks.mjs` + `control.ps1 cleanup` |
| L2 e2e + 文档数字守卫 | ✅ | `tests/e2e/*`、`tests/unit/docs-numbers.test.mjs` |
| 架构审查三 P0(validate 原型链 / 限速桶 key / describeArgs) | ✅ | `e864486`、`d106214` |
| 架构审查三 P0(validate 原型链 / 限速桶 key / describeArgs) | ✅ | `68ea3cd`、`560b3c4` |
### 遗留(P2 登记,明确不塞进 v2.1.0)
+250 -50
View File
@@ -4,6 +4,13 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
zod: ^4.6.6
body-parser: ^2.4.0
hono: ^4.13.14
ip-address: ^10.7.5
fast-uri: ^3.1.9
importers:
.:
@@ -71,20 +78,20 @@ importers:
version: 3.5.43(typescript@6.0.3)
vue-router:
specifier: ^5.3.1
version: 5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(pinia@4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)))(rolldown@1.2.12)(vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3))
version: 5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(pinia@4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)))(rolldown@1.2.12)(vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3))
devDependencies:
'@tauri-apps/cli':
specifier: ^2
version: 2.12.1
'@vitejs/plugin-vue':
specifier: ^6.0.7
version: 6.0.9(vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3))
version: 6.0.9(vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3))
typescript:
specifier: ~6.0.3
version: 6.0.3
vite:
specifier: ^8.0.16
version: 8.3.2(@types/node@22.20.4)(esbuild@0.28.2)
specifier: ^8.3.5
version: 8.3.5(@types/node@22.20.4)(esbuild@0.28.2)
vue-tsc:
specifier: ^3.3.5
version: 3.3.12(typescript@6.0.3)
@@ -106,7 +113,7 @@ importers:
dependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.32.1
version: 1.32.1(zod@4.6.5)
version: 1.32.1(zod@4.6.7)
'@nodeagent/client':
specifier: workspace:*
version: link:../../packages/client
@@ -321,7 +328,7 @@ packages:
resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==}
engines: {node: '>=20'}
peerDependencies:
hono: ^4
hono: ^4.13.14
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -347,7 +354,7 @@ packages:
engines: {node: '>=18'}
peerDependencies:
'@cfworker/json-schema': ^4.1.1
zod: ^3.25 || ^4.0
zod: ^4.6.6
peerDependenciesMeta:
'@cfworker/json-schema':
optional: true
@@ -359,6 +366,9 @@ packages:
'@oxc-project/types@0.152.0':
resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==}
'@oxc-project/types@0.155.0':
resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
'@peculiar/asn1-cms@2.11.0':
resolution: {integrity: sha512-SlAiDoT/Cd8qaNmbTt78gqyaEvu2fQd6ML6KWJkdjXGrgDKnHHrusVkfQUelkfinL2p665IIADb0JBixtT8W1g==}
engines: {node: '>=14'}
@@ -412,36 +422,72 @@ packages:
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm-eabi@1.2.15':
resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm64@1.2.12':
resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-android-arm64@1.2.15':
resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.2.12':
resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-arm64@1.2.15':
resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.12':
resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.15':
resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.2.12':
resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-freebsd-x64@1.2.15':
resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.2.12':
resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm-gnueabihf@1.2.15':
resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.2.12':
resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -449,6 +495,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-gnu@1.2.15':
resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.2.12':
resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -456,6 +509,13 @@ packages:
os: [linux]
libc: [musl]
'@rolldown/binding-linux-arm64-musl@1.2.15':
resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.2.12':
resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -463,6 +523,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-ppc64-gnu@1.2.15':
resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.12':
resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -470,6 +537,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.15':
resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.12':
resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -477,6 +551,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.15':
resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.2.12':
resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -484,24 +565,49 @@ packages:
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-musl@1.2.15':
resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.2.12':
resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-openharmony-arm64@1.2.15':
resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-win32-arm64-msvc@1.2.12':
resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-arm64-msvc@1.2.15':
resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.12':
resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.15':
resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
@@ -709,8 +815,8 @@ packages:
birpc@2.9.0:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
body-parser@2.3.0:
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
body-parser@2.4.0:
resolution: {integrity: sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==}
engines: {node: '>=18'}
bytes@3.1.2:
@@ -876,8 +982,8 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-uri@3.1.8:
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
fast-uri@3.1.9:
resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
@@ -932,8 +1038,8 @@ packages:
resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==}
engines: {node: '>=12.0.0'}
hono@4.13.9:
resolution: {integrity: sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==}
hono@4.13.14:
resolution: {integrity: sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==}
engines: {node: '>=16.9.0'}
hookable@5.5.3:
@@ -950,8 +1056,8 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ip-address@10.7.2:
resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==}
ip-address@10.7.5:
resolution: {integrity: sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
@@ -1181,6 +1287,10 @@ packages:
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.31:
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
engines: {node: ^10 || ^12 || >=14}
proxy-addr@2.0.8:
resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
@@ -1196,6 +1306,10 @@ packages:
resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==}
engines: {node: '>=0.6'}
qs@6.16.1:
resolution: {integrity: sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==}
engines: {node: '>=0.6'}
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
@@ -1223,6 +1337,11 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rolldown@1.2.15:
resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
@@ -1380,13 +1499,13 @@ packages:
peerDependencies:
vue: ^3.0.11
vite@8.3.2:
resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==}
vite@8.3.5:
resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.7.1
'@vitejs/devtools': ^0.7.1 || ^0.8.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
@@ -1494,10 +1613,10 @@ packages:
zod-to-json-schema@3.25.2:
resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
peerDependencies:
zod: ^3.25.28 || ^4
zod: ^4.6.6
zod@4.6.5:
resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==}
zod@4.6.7:
resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
zrender@6.1.0:
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
@@ -1605,9 +1724,9 @@ snapshots:
'@esbuild/win32-x64@0.28.2':
optional: true
'@hono/node-server@2.1.1(hono@4.13.9)':
'@hono/node-server@2.1.1(hono@4.13.14)':
dependencies:
hono: 4.13.9
hono: 4.13.14
'@jridgewell/gen-mapping@0.3.13':
dependencies:
@@ -1630,9 +1749,9 @@ snapshots:
'@juggle/resize-observer@3.4.0': {}
'@modelcontextprotocol/sdk@1.32.1(zod@4.6.5)':
'@modelcontextprotocol/sdk@1.32.1(zod@4.6.7)':
dependencies:
'@hono/node-server': 2.1.1(hono@4.13.9)
'@hono/node-server': 2.1.1(hono@4.13.14)
ajv: 8.20.0
ajv-formats: 3.0.1(ajv@8.20.0)
content-type: 1.0.5
@@ -1642,19 +1761,22 @@ snapshots:
eventsource-parser: 3.1.1
express: 5.2.1
express-rate-limit: 8.7.0(express@5.2.1)
hono: 4.13.9
hono: 4.13.14
jose: 6.2.12
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
zod: 4.6.5
zod-to-json-schema: 3.25.2(zod@4.6.5)
zod: 4.6.7
zod-to-json-schema: 3.25.2(zod@4.6.7)
transitivePeerDependencies:
- supports-color
'@noble/hashes@1.8.0': {}
'@oxc-project/types@0.152.0': {}
'@oxc-project/types@0.152.0':
optional: true
'@oxc-project/types@0.155.0': {}
'@peculiar/asn1-cms@2.11.0':
dependencies:
@@ -1753,48 +1875,93 @@ snapshots:
'@rolldown/binding-android-arm-eabi@1.2.12':
optional: true
'@rolldown/binding-android-arm-eabi@1.2.15':
optional: true
'@rolldown/binding-android-arm64@1.2.12':
optional: true
'@rolldown/binding-android-arm64@1.2.15':
optional: true
'@rolldown/binding-darwin-arm64@1.2.12':
optional: true
'@rolldown/binding-darwin-arm64@1.2.15':
optional: true
'@rolldown/binding-darwin-x64@1.2.12':
optional: true
'@rolldown/binding-darwin-x64@1.2.15':
optional: true
'@rolldown/binding-freebsd-x64@1.2.12':
optional: true
'@rolldown/binding-freebsd-x64@1.2.15':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.12':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.15':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.12':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.15':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.12':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.15':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.12':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.15':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.12':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.15':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.12':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.15':
optional: true
'@rolldown/pluginutils@1.0.1': {}
'@tauri-apps/api@2.12.1': {}
@@ -1864,10 +2031,10 @@ snapshots:
dependencies:
'@types/node': 22.20.4
'@vitejs/plugin-vue@6.0.9(vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3))':
'@vitejs/plugin-vue@6.0.9(vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.3.2(@types/node@22.20.4)(esbuild@0.28.2)
vite: 8.3.5(@types/node@22.20.4)(esbuild@0.28.2)
vue: 3.5.43(typescript@6.0.3)
'@volar/language-core@2.4.28':
@@ -1985,7 +2152,7 @@ snapshots:
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.8
fast-uri: 3.1.9
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -2012,7 +2179,7 @@ snapshots:
birpc@2.9.0: {}
body-parser@2.3.0:
body-parser@2.4.0:
dependencies:
bytes: 3.1.2
content-type: 2.1.0
@@ -2020,7 +2187,7 @@ snapshots:
http-errors: 2.0.1
iconv-lite: 0.7.3
on-finished: 2.4.1
qs: 6.16.0
qs: 6.16.1
raw-body: 3.0.2
type-is: 2.1.0
transitivePeerDependencies:
@@ -2166,14 +2333,14 @@ snapshots:
dependencies:
debug: 4.4.3
express: 5.2.1
ip-address: 10.7.2
ip-address: 10.7.5
transitivePeerDependencies:
- supports-color
express@5.2.1:
dependencies:
accepts: 2.0.0
body-parser: 2.3.0
body-parser: 2.4.0
content-disposition: 1.1.0
content-type: 1.0.5
cookie: 0.7.2
@@ -2207,7 +2374,7 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-uri@3.1.8: {}
fast-uri@3.1.9: {}
fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
@@ -2261,7 +2428,7 @@ snapshots:
highlight.js@11.12.0: {}
hono@4.13.9: {}
hono@4.13.14: {}
hookable@5.5.3: {}
@@ -2279,7 +2446,7 @@ snapshots:
inherits@2.0.4: {}
ip-address@10.7.2: {}
ip-address@10.7.5: {}
ipaddr.js@1.9.1: {}
@@ -2478,6 +2645,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.2
postcss@8.5.31:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
@@ -2494,6 +2667,11 @@ snapshots:
es-define-property: 1.0.1
side-channel: 1.1.1
qs@6.16.1:
dependencies:
es-define-property: 1.0.1
side-channel: 1.1.1
quansync@0.2.11: {}
range-parser@1.3.0: {}
@@ -2531,6 +2709,28 @@ snapshots:
'@rolldown/binding-openharmony-arm64': 1.2.12
'@rolldown/binding-win32-arm64-msvc': 1.2.12
'@rolldown/binding-win32-x64-msvc': 1.2.12
optional: true
rolldown@1.2.15:
dependencies:
'@oxc-project/types': 0.155.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm-eabi': 1.2.15
'@rolldown/binding-android-arm64': 1.2.15
'@rolldown/binding-darwin-arm64': 1.2.15
'@rolldown/binding-darwin-x64': 1.2.15
'@rolldown/binding-freebsd-x64': 1.2.15
'@rolldown/binding-linux-arm-gnueabihf': 1.2.15
'@rolldown/binding-linux-arm64-gnu': 1.2.15
'@rolldown/binding-linux-arm64-musl': 1.2.15
'@rolldown/binding-linux-ppc64-gnu': 1.2.15
'@rolldown/binding-linux-s390x-gnu': 1.2.15
'@rolldown/binding-linux-x64-gnu': 1.2.15
'@rolldown/binding-linux-x64-musl': 1.2.15
'@rolldown/binding-openharmony-arm64': 1.2.15
'@rolldown/binding-win32-arm64-msvc': 1.2.15
'@rolldown/binding-win32-x64-msvc': 1.2.15
router@2.2.0:
dependencies:
@@ -2658,7 +2858,7 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.7
unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2)):
unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2)):
dependencies:
'@jridgewell/remapping': 2.3.5
picomatch: 4.0.7
@@ -2666,7 +2866,7 @@ snapshots:
optionalDependencies:
esbuild: 0.28.2
rolldown: 1.2.12
vite: 8.3.2(@types/node@22.20.4)(esbuild@0.28.2)
vite: 8.3.5(@types/node@22.20.4)(esbuild@0.28.2)
vary@1.1.2: {}
@@ -2675,12 +2875,12 @@ snapshots:
evtd: 0.2.4
vue: 3.5.43(typescript@6.0.3)
vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2):
vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
postcss: 8.5.29
rolldown: 1.2.12
postcss: 8.5.31
rolldown: 1.2.15
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 22.20.4
@@ -2694,7 +2894,7 @@ snapshots:
vscode-uri@3.2.0: {}
vue-router@5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(pinia@4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)))(rolldown@1.2.12)(vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3)):
vue-router@5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(pinia@4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)))(rolldown@1.2.12)(vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2))(vue@3.5.43(typescript@6.0.3)):
dependencies:
'@vue-macros/common': 3.1.4(vue@3.5.43(typescript@6.0.3))
'@vue/devtools-api': 8.2.1
@@ -2710,13 +2910,13 @@ snapshots:
picomatch: 4.0.7
scule: 1.3.0
tinyglobby: 0.2.17
unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.2(@types/node@22.20.4)(esbuild@0.28.2))
unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.5(@types/node@22.20.4)(esbuild@0.28.2))
unplugin-utils: 0.3.2
vue: 3.5.43(typescript@6.0.3)
optionalDependencies:
'@vue/compiler-sfc': 3.5.43
pinia: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3))
vite: 8.3.2(@types/node@22.20.4)(esbuild@0.28.2)
vite: 8.3.5(@types/node@22.20.4)(esbuild@0.28.2)
transitivePeerDependencies:
- '@farmfe/core'
- '@rsbuild/core'
@@ -2765,11 +2965,11 @@ snapshots:
ws@8.21.3: {}
zod-to-json-schema@3.25.2(zod@4.6.5):
zod-to-json-schema@3.25.2(zod@4.6.7):
dependencies:
zod: 4.6.5
zod: 4.6.7
zod@4.6.5: {}
zod@4.6.7: {}
zrender@6.1.0:
dependencies:
+18
View File
@@ -3,3 +3,21 @@ packages:
- 'apps/*'
allowBuilds:
esbuild: true
# 依赖漏洞门禁(Quality Gate 的 pnpm audit --audit-level=high)用的显式覆盖。
#
# 为什么必须钉:`@modelcontextprotocol/sdk` 的传递依赖落后于安全版本,
# 审计会稳定报 3 个 high(zod <4.6.6、body-parser <2.4.0),而它们都不由
# 本仓直接控制 —— 不覆盖就永远是红门禁(等于没有门禁)。
# 均为同 major 的 patch 级抬升,语义风险低;升级 SDK 时若已自带修复版,可移除。
#
# ⚠️ pnpm 12 起 `package.json` 的 `pnpm` 字段不再被读取(设置迁移到本文件),
# 写在那里会被静默忽略 —— 与「一直红所以等于没有」是同一类失败形态。
overrides:
zod: ^4.6.6
body-parser: ^2.4.0
# 2026-10-27:MCP SDK 传递链的剩余中低危,一并抬到修复版(同 major)。
# 对应 dependabot #3/#6/#7,覆盖后那三个 PR 已无增量,随同一提交关停。
hono: ^4.13.14
ip-address: ^10.7.5
fast-uri: ^3.1.9
+26
View File
@@ -18,6 +18,26 @@
/** nodeagent 自己注册过的计划任务名(含历史遗留)。只增不改。 */
export const TASK_WHITELIST = ['nodeagent', 'na-gui', 'nodeagent-console'];
/**
* 自重启任务前缀 —— `system.agent.restart` 每次重启都注册
* `nodeagent-selfrestart-<毫秒时间戳>`(动作是「杀旧 PID → 前台拉起新进程」)。
*
* 2026-10-27 真机发现:这类任务不在上面的白名单里,于是**卸载时漏删**——
* 留下的任务会继续尝试拉起已被删除的 agent.mjs,形成每次登录都失败一次的僵尸。
* 但它也不能被当成垃圾随手清:**正在 Running 的那一个往往就是当前 agent 的
* 父任务**(删除会连带杀掉 agent)。故只把它纳入「卸载清理」的判定集合,
* 由调用方在「已经停掉 agent」之后使用。
*/
export const SELF_RESTART_PREFIX = 'nodeagent-selfrestart-';
/** `nodeagent-selfrestart-<纯数字>` —— 严格到毫秒时间戳形态,不接受任何通配。 */
const SELF_RESTART_RE = /^nodeagent-selfrestart-\d{6,}$/;
/** 是否是本框架的自重启任务(严格命名空间,非模糊匹配)。 */
export function isSelfRestartTask(name) {
return typeof name === 'string' && SELF_RESTART_RE.test(name);
}
/**
* 计划删除名单:白名单内**且实际存在**的任务,按白名单固定顺序返回。
*
@@ -37,5 +57,11 @@ export function planTaskCleanup(existing) {
for (const name of TASK_WHITELIST) {
if (present.has(name)) out.push(name);
}
// 自重启任务:仅接受严格命名空间(`nodeagent-selfrestart-<数字>`),
// 按名字排序保证输出稳定;与白名单重名时去重(实际不会发生,防御性)。
const selfRestart = [...present].filter(isSelfRestartTask).sort();
for (const name of selfRestart) {
if (!out.includes(name)) out.push(name);
}
return out;
}
+25
View File
@@ -31,6 +31,31 @@
*/
export const STARTUP_GRACE_MS = 30_000;
/**
* 看门狗日志轮换阈值(字节):超过即滚存一份。
*
* 为什么需要:日志是 `appendFileSync` 无界追加的 —— 每次启动失败/恢复各写一行,
* 长期运行(尤其「坏版本 + supervisor 反复重启」)会把磁盘写满,而看门狗自己
* 就常驻在这台机器上,磁盘满会连带把 agent 的审计写入一起拖死。
* 1MB × 4 份(当前 + 3 份历史)≈ 4MB 上限,对排障足够(最近几百次启动)。
*/
export const LOG_MAX_BYTES = 1_048_576;
/** 保留的历史份数(`.1` 最新 … `.N` 最旧)。 */
export const LOG_KEEP = 3;
/** 是否需要轮换(纯函数):严格大于阈值才轮换,避免恰好等于阈值时抖动着反复滚。 */
export function shouldRotate(sizeBytes, maxBytes = LOG_MAX_BYTES) {
return Number.isFinite(sizeBytes) && sizeBytes > maxBytes;
}
/** 轮换目标文件名(从新到旧):`<log>.1` … `<log>.N`。 */
export function rotationTargets(logPath, keep = LOG_KEEP) {
const out = [];
for (let i = 1; i <= keep; i += 1) out.push(`${logPath}.${i}`);
return out;
}
/** 全新状态(文件缺失时的起点)。 */
export function initialState(nowIso = '') {
return { failures: 0, updated_at: nowIso };
+27 -4
View File
@@ -33,7 +33,8 @@
import { createHash } from 'node:crypto';
import { spawn } from 'node:child_process';
import {
closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, statSync, writeFileSync,
closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, renameSync, statSync,
unlinkSync, writeFileSync,
} from 'node:fs';
import { basename, join, resolve } from 'node:path';
import net from 'node:net';
@@ -46,6 +47,8 @@ import {
initialState,
planRestore,
readState,
rotationTargets,
shouldRotate,
zeroFailures,
} from './lib/startup-guard-core.mjs';
@@ -81,12 +84,32 @@ export function writeState(dataDir, state) {
}
}
/** 追加一行 JSONL 日志:<data-dir>/logs/startup-guard.log(失败静默——日志不拦路)。 */
export function appendGuardLog(dataDir, obj) {
/**
* 追加一行 JSONL 日志:<data-dir>/logs/startup-guard.log(失败静默——日志不拦路)。
*
* 超过 LOG_MAX_BYTES 时滚存:`.log.1`(次新)… `.log.N`(最旧),`N = LOG_KEEP`。
* 轮换本身也**失败静默**:日志出问题绝不能反过来拦住被控端启动。
*/
export function appendGuardLog(dataDir, obj, opts = {}) {
try {
mkdirSync(join(dataDir, 'logs'), { recursive: true });
const logPath = join(dataDir, 'logs', 'startup-guard.log');
try {
const size = existsSync(logPath) ? statSync(logPath).size : 0;
if (shouldRotate(size, opts.maxBytes)) {
const targets = rotationTargets(logPath, opts.keep);
const oldest = targets[targets.length - 1];
if (existsSync(oldest)) unlinkSync(oldest);
for (let i = targets.length - 1; i > 0; i -= 1) {
if (existsSync(targets[i - 1])) renameSync(targets[i - 1], targets[i]);
}
renameSync(logPath, targets[0]);
}
} catch {
/* 轮换失败不影响写入 */
}
const line = `${JSON.stringify({ ts: new Date().toISOString(), ...obj })}\n`;
writeFileSync(join(dataDir, 'logs', 'startup-guard.log'), line, { flag: 'a' });
writeFileSync(logPath, line, { flag: 'a' });
} catch {
/* 日志失败不影响启动 */
}
+29 -1
View File
@@ -6,7 +6,7 @@
*
* 运行:node tests/e2e/run.mjs
*/
import { spawn } from 'node:child_process';
import { spawn as rawSpawn } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
@@ -25,6 +25,34 @@ const RawWebSocket = requireFromClient('ws');
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '../..');
/**
* 子进程登记表 + 兜底清理。
*
* 为什么需要:本套件有 8 处 spawn(主 agent / TLS / ed25519 / hub / fs / monitor…),
* 各自的 finally 只做 `child.kill()`(SIGTERM),随后立刻 process.exit —— 被控端
* 可能还没来得及退出就被父进程带走,**下一轮连跑时固定端口仍被占用**,表现为
* 「连跑第二次偶发失败」(历史真机现象,重跑又绿,最难查)。
*
* 做法:包一层 spawn,把所有子进程登记下来;进程退出时统一 SIGKILL。
* 调用点无需改动(同名函数遮蔽),单点收口也不会漏掉将来新增的 spawn。
*/
const liveChildren = new Set();
function spawn(...args) {
const child = rawSpawn(...args);
liveChildren.add(child);
child.once('exit', () => liveChildren.delete(child));
return child;
}
process.on('exit', () => {
for (const child of liveChildren) {
try {
child.kill('SIGKILL');
} catch {
/* 已退出或不可杀:忽略 */
}
}
});
const PORT = 18765;
const TLS_PORT = 18772;
const ED25519_PORT = 18781;
+34 -1
View File
@@ -2,7 +2,7 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { TASK_WHITELIST, planTaskCleanup } from '../../scripts/lib/scheduled-tasks.mjs';
import { TASK_WHITELIST, isSelfRestartTask, planTaskCleanup } from '../../scripts/lib/scheduled-tasks.mjs';
/**
* na-gui 陈旧计划任务清理(ARCH B-P1-6 / T05)。
@@ -100,3 +100,36 @@ test('control.cmd 菜单暴露 cleanup 入口(纯 ASCII + CRLF 由 ps1-encodin
assert.match(cmd, /call :RUN cleanup/, 'control.cmd 菜单必须有 cleanup 项转发');
assert.match(cmd, /8\)\s*Cleanup stale tasks/, 'control.cmd 菜单应展示 cleanup 项');
});
// ===========================================================================
// 自重启任务(2026-10-27 真机发现:卸载漏删 → 僵尸任务反复拉起已删文件)
// ===========================================================================
test('isSelfRestartTask:只认严格命名空间 nodeagent-selfrestart-<数字>', () => {
assert.equal(isSelfRestartTask('nodeagent-selfrestart-1793077118968'), true);
assert.equal(isSelfRestartTask('nodeagent-selfrestart-123456'), true, '6 位时间戳级仍算');
// 以下都不是我们注册的名字,绝不能被当成自重启任务
assert.equal(isSelfRestartTask('nodeagent-selfrestart-'), false, '没有数字');
assert.equal(isSelfRestartTask('nodeagent-selfrestart-abc123'), false, '非纯数字');
assert.equal(isSelfRestartTask('nodeagent-selfrestart-123'), false, '位数过短(非时间戳形态)');
assert.equal(isSelfRestartTask('mynodeagent-selfrestart-123456'), false, '前缀必须从头匹配');
assert.equal(isSelfRestartTask('nodeagent-selfrestart-123456-extra'), false, '不得带额外后缀');
assert.equal(isSelfRestartTask(''), false);
assert.equal(isSelfRestartTask(null), false);
});
test('planTaskCleanup:白名单外的自重启任务纳入清理,且仍然只增不误删', () => {
const plan = planTaskCleanup([
'nodeagent',
'nodeagent-selfrestart-1793077118968',
'nodeagent-selfrestart-1793077118999',
'Adobe Acrobat Update Task', // 用户/第三方任务,绝不能碰
'OneDrive Standalone Update',
'nodeagent-selfrestart-notatask',
]);
assert.deepEqual(plan, [
'nodeagent',
'nodeagent-selfrestart-1793077118968',
'nodeagent-selfrestart-1793077118999',
], '白名单先出、自重启按名字排序,第三方任务一律不出现');
});
@@ -85,3 +85,23 @@ test('错误分类:DNS/拒连单独一类,普通错误原样透出', () => {
const plain = new Error('boom');
assert.equal(describeNetError(plain, 1000), 'boom', '无法归类的错误原样透出,不编造原因');
});
test('describeNetError:TCP 层 ETIMEDOUT 必须给出「配代理」的可执行提示(2026-10-27 真机)', () => {
// 真机现场:境内 Windows 直连 github.com:443 → connect ETIMEDOUT 20.205.243.166:443。
// 这条最常见,原先落到兜底分支只回一串超时,用户不知道唯一有效动作是配代理。
const etimedout = Object.assign(new Error('connect ETIMEDOUT 20.205.243.166:443'), { code: 'ETIMEDOUT' });
const msg = describeNetError(etimedout, 20_000);
assert.match(msg, /连接超时\/网络不可达/);
assert.match(msg, /NA_PROXY/, '必须点明 NA_PROXY 这个具体开关');
assert.match(msg, /系统代理/);
// 同族错误走同一提示
for (const code of ['EHOSTUNREACH', 'ENETUNREACH']) {
assert.match(describeNetError(Object.assign(new Error('x'), { code }), 1000), /NA_PROXY/, code);
}
// 不得与 ECONNRESET 的「代理或线路问题」混为一谈(两类原因不同)
const reset = Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' });
assert.match(describeNetError(reset, 1000), /代理或线路/);
assert.doesNotMatch(describeNetError(reset, 1000), /NA_PROXY/);
});
+55
View File
@@ -14,6 +14,8 @@ import {
nextFailures,
planRestore,
readState,
rotationTargets,
shouldRotate,
zeroFailures,
} from '../../scripts/lib/startup-guard-core.mjs';
import {
@@ -447,3 +449,56 @@ test('场景:崩溃三次后第四次守卫执行恢复并归零计数', () =>
assert.ok(readFileSync(join(dataDir, 'logs', 'startup-guard.log'), 'utf8').includes('restore'));
rmSync(root, { recursive: true, force: true });
});
// ===========================================================================
// 日志轮换:看门狗常驻被控端,日志无界增长会把磁盘写满
// ===========================================================================
test('shouldRotate:严格大于阈值才轮换(恰好等于不滚)', () => {
assert.equal(shouldRotate(0), false);
assert.equal(shouldRotate(1024, 1024), false, '恰好等于阈值不轮换,避免边界抖动');
assert.equal(shouldRotate(1025, 1024), true);
assert.equal(shouldRotate(Number.NaN, 1024), false, 'NaN(stat 失败)不得触发轮换');
});
test('rotationTargets:从新到旧的 .1 … .N 命名', () => {
assert.deepEqual(rotationTargets('/x/startup-guard.log', 3), [
'/x/startup-guard.log.1',
'/x/startup-guard.log.2',
'/x/startup-guard.log.3',
]);
});
test('appendGuardLog:超阈值滚存,最旧一份被丢弃,当前日志重新开始', () => {
const root = mkdtempSync(join(tmpdir(), 'na-guard-rotate-'));
const dataDir = join(root, 'data');
const logPath = join(dataDir, 'logs', 'startup-guard.log');
mkdirSync(join(dataDir, 'logs'), { recursive: true }); // 先造出超阈值的大文件,再交给 appendGuardLog 轮换
const maxBytes = 200;
// 三次超阈值写入 → .1 / .2 / .3 依次生成,且每次都留下一条记录
for (let i = 1; i <= 3; i += 1) {
writeFileSync(logPath, `${'x'.repeat(maxBytes + 10)}\n`);
appendGuardLog(dataDir, { action: 'probe', i }, { maxBytes, keep: 3 });
}
assert.ok(existsSync(`${logPath}.1`), '应生成 .1');
assert.ok(existsSync(`${logPath}.2`), '应生成 .2');
assert.ok(existsSync(`${logPath}.3`), '应生成 .3');
// 第四次:.3 被丢弃,.2→.3、.1→.2、当前→.1
writeFileSync(logPath, `${'y'.repeat(maxBytes + 10)}\n`);
appendGuardLog(dataDir, { action: 'probe', i: 4 }, { maxBytes, keep: 3 });
assert.ok(existsSync(`${logPath}.3`), '仍只有 3 份历史');
assert.ok(!existsSync(`${logPath}.4`), '不得无限增长出 .4');
// 当前日志只剩本次一行(可解析 JSON)
const lines = readFileSync(logPath, 'utf8').trim().split('\n');
assert.equal(lines.length, 1);
assert.equal(JSON.parse(lines[0]).i, 4);
// 未超阈值时只追加,不轮换
appendGuardLog(dataDir, { action: 'small' }, { maxBytes, keep: 3 });
assert.equal(readFileSync(logPath, 'utf8').trim().split('\n').length, 2);
rmSync(root, { recursive: true, force: true });
});