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 |
|
jkooe
|
451d44af72
|
fix(release): 版本号已钉在树中时不再卡在「nothing to commit」
发版脚本无条件执行 `git commit`。v2.1.0 的流程是先把版本号作为
`chore(release)` 提交进树、之后才发版 —— 此时 `git add -A` 后暂存区为空,
`git commit` 以 "nothing to commit" 非零退出,发版停在最后一步,且报错来自
git 而非发布逻辑,容易误判为工作区问题。
改为:暂存区为空则跳过 commit,标签与 Release 照常创建。
向后兼容 —— 版本号需要变更时行为完全不变。
|
2026-10-27 00:00:26 +08:00 |
|
jkooe
|
2d5be044ee
|
chore(privacy): 交付流水线文档去掉其他项目名
`docs/DELIVERY-PIPELINE.md:10` 原以三个真实项目名举例说明「原方案面向 Web/容器
服务」,属审计基线的树内 PII 之一(存量,非本批次引入)。公开仓库不宜暴露项目
组合,改为中性表述「同类多项目实践」,论证语义不变。
注:该处 PII 已存在于既有历史 blob(2026-10-17 起),彻底清除需重写整段历史,
属独立决策,不在本批次范围。
|
2026-10-26 21:11:43 +08:00 |
|
jkooe
|
9baf3b0443
|
fix(cli): rollback 指纹校验容忍 12/64 位两种口径
QA 独立验证路由的 P0 缺陷(tests/unit/qa-v210-supplement.test.mjs C⑦ 红):
- 写后校验(rollback.ts:462):back.hash 是 64 位流式 sha256,而老 LKG 记录的
expectHash 只有 12 位(无 hash_full)→ 严格 === 必不成立 → 老记录回滚永远
被判「写后校验失败」并用回滚源还原,向后兼容路径实际不可用。
- 重启复核(rollback.ts:519):新 LKG 记录带 hash_full(64 位)时,p.buildHash
是 12 位 → 严格 === 必不成立 → 回滚其实成功却被误报「90s 内未复核到回滚版本」
并以退出码 1 结束(--backup 逃生门同样命中此缺陷)。
修法:新增纯函数 hashMatchesEither(短的一方是长的一方前缀即命中),仅用于
「校验/复核」方向;挑回滚源仍用单向 hashMatches —— hash_full 在手时不许前缀
糊弄(C⑥ 钉死),两条语义各有其守卫。
README 单测数 476 → 477(新增 hashMatchesEither 用例,docs-numbers 守卫强制耦合)。
|
2026-10-26 21:00:26 +08:00 |
|
jkooe
|
dadd861fdd
|
test: QA 补充用例(代理重定向/NO_PROXY 组合、看门狗边界与恢复日志、rollback 双匹配路径、README 数字精度)
- A:httpGet 重定向链经代理逐跳绝对形态+共享预算;maxRedirects 上限边界;
NO_PROXY 命中直连/端口不匹配不误短路/miss+死代理大声报错(agent-proxy 已有
单跳经代理与直连重定向预算,组合形态空缺)
- B:看门狗宽限期精确边界(livedMs === STARTUP_GRACE_MS 按运行期不计数);
无指纹匹配时恢复日志须带回候选指纹且 install-dir 不动
- C:pickBackupByHash 在 hash_full 在场时不许仅凭 12 位前缀命中;
编排层老 LKG(无 hash_full)端到端前缀回滚 —— 后者当前为红,是路由给
工程师的 P0 缺陷(rollback.ts:462 全量 64 位指纹与 12 位 targetHash 严格
比较,详见缺陷单),修复后自绿
- D:README e2e 拆分口径(34 通用 + 16 Windows 专属按文件分别核对);
命令速查必须含 nodeagent rollback [--dry-run]
- README 单测数 467 → 476(docs-numbers 守卫强制耦合,非手滑)
|
2026-10-26 01:22:55 +08:00 |
|
jkooe
|
a7827ba986
|
docs: 增量 PRD / ARCH 与设计图入库(v2.1.0 全面完善)
本轮「全面完善」的四份设计产出此前后在 git 外(untracked)——
阶段 3 实现所依据的需求/架构/类图/时序图随收口一并入库,
使「代码 ← 设计」的引用链在仓库内闭环(代码注释已按 ARCH 任务号引用)。
|
2026-10-26 01:01:32 +08:00 |
|
jkooe
|
0074d09f08
|
docs: v2.1.0 收口回填(README/CHANGELOG/DEVELOPMENT/ROADMAP + 数字守卫)
- README:数字刷新为实测(52 能力 · 42 MCP · 467 单测 · 50 e2e · 23 Rust);
补 na_audit_l2 工具表项、看门狗三端接入段;删「控制台安装包只作为
CI artifact」过期行(9f95560 起已随 Release 发布)。
- CHANGELOG:[未发布] 定稿 v2.1.0 全部主题(rollback / 看门狗 /
na-gui 清理 / e2e 与文档收口),与 A 线已回填段共存无重复。
- DEVELOPMENT:新增 §7.5 selfupdate 代理出网机制(NA_PROXY→环境变量→
系统代理、PAC/socks5 明确报错、回环恒直连、计划任务环境变量告诫);
§7.4 补 rollback/看门狗双通道分工表与「为什么运行期不做自动回滚」
三问引述。
- ROADMAP:新增第十节 —— v2.1.0 已落地项划掉、P2 登记(控制台自更新
v2.2.0 / Linux auditd / macOS ESF / macOS 安装器 / guard 日志轮换)。
- tests/unit/docs-numbers.test.mjs(新,6 例):README 五个数字与可
复现实测口径钉死(manifest 条目 / defs.ts 工具 / node --test 实测 /
e2e 源码 / Rust 属性),「增能力不改 README」即红。单测数实测经
NODE_TEST_CONTEXT 剥离 + NA_DOCS_NUMBERS_CHILD 递归防护;注入故障
(52→53)实测 2 处守卫变红、恢复全绿。
|
2026-10-26 01:01:19 +08:00 |
|
jkooe
|
b7f44f7a64
|
feat(scripts): na-gui 陈旧计划任务白名单清理(ARCH B-P1-6)
历史版本在被控端上注册过 na-gui 等旧名计划任务,漏删会留下「端口被占、
新版本起不来」的幽灵任务。误删用户自己的任务则是比漏删严重得多的
生产事故 —— 故只按白名单删。
- scripts/lib/scheduled-tasks.mjs(新):TASK_WHITELIST(nodeagent /
na-gui / nodeagent-console,只增不改)+ planTaskCleanup 纯函数
(查询结果注入,白名单外一律不删、精确匹配大小写敏感、重复名去重)。
- scripts/uninstall.ps1:卸载末尾按白名单逐个注销(-ccontains 存在性
检查 + 动作进日志)。
- scripts/control.ps1:新增 cleanup 子命令(ValidateSet + Clear-StaleTasks),
Uninstall-Agent 复用同一白名单;control.cmd 菜单加第 8 项
(CRLF 安全插入,纯 ASCII 不变)。
- tests/unit/na-gui-cleanup.test.mjs(新,10 例):白名单命中才删 /
白名单外零动作 / 空列表 / 重复名 / 脏输入 + ps1 源码守卫与跨端
一致性守卫(白名单 mjs 与 ps1 漂移即红 —— 注入故障实测 2 处变红,
恢复后全绿)。
|
2026-10-26 01:01:05 +08:00 |
|
jkooe
|
654d694ff6
|
test(e2e): windows.mjs 补 L2 status/collect/verify 三条(T05)
字段与 manifest system.audit.l2 的 returns_schema(四 action 属性并集)
逐字对齐:status 全字段断言(platform/supported/auditpol/
legacy_policy_disabled/cmdline_logging/sysmon_installed/file/file_exists);
collect 断 since/limit 回显、written<=collected、信任边界 note 必返;
verify 断 ok/checked。
collect 对非提权环境(读 Security 日志需管理员)skip 容错 —— 与 winget
缺失同一哲学,不把「环境没条件」伪装成「功能坏了」。
Mac 上不跑 windows.mjs:node --check + 静态自证(参数名/能力名/字段
与 manifest returns_schema 逐字对齐),不假跑。e2e 总数 33 → 50。
|
2026-10-26 01:00:50 +08:00 |
|
jkooe
|
6152a68b4a
|
test(e2e): 补 L2 平台门控端到端用例(T04)
T04 把 audit-l2.ts 的平台门控从裸 throw 改成统一 assertWindows 后,
需要端到端证据:非 Windows 被控端上 system.audit.l2 必须明确失败
(result.status=failed + E_UNSUPPORTED_PLATFORM),不得静默走别的
分支、也不得报成误导性错误;Windows 上 status 可用(双平台各验一边)。
第一版按 assert.rejects 写,实测不会红 —— 能力执行失败走
result.status=failed(resolve),只有协议层错误才 reject。
已按 client 真实行为改为断言 failed + error.name(按 name 不按 code)。
e2e 34/34 全绿
|
2026-10-26 00:38:01 +08:00 |
|
jkooe
|
fbd42e1c3b
|
docs(changelog): [未发布] 回填本轮提交(出网修复 / P0 三修 / L2 三端暴露)
- selfupdate 代理出网(P0-1,db65bd1):裸 fetch 不读代理 → net/proxy.ts 移植;
回环恒直连;错误分类移植;maxBytes 双闸
- LKG 内容指针(A-P0-3):hash_full + backup_glob,lkgHashMatches 不猜
- 架构审查三个 P0(e864486 / d106214):validate 原型链 / 限速桶 key /
describeArgs 截断
- L2 三端暴露(b09ca68):MCP na_audit_l2 + 控制台 L2 分区 + agent.l2 类型
同时修正两处过期记述(代码已变、文字没跟上):
- 「刻意不做自动回滚」→ 写清两档边界:启动期自动恢复已由看门狗补上,
运行期自动回滚仍刻意不做(与 B 线 docs/DELIVERY-PIPELINE.md 同款改法)
- L2 小节「CLI 已有入口」→ 补「v2.1.0 起 MCP 与控制台亦暴露」
|
2026-10-26 00:31:08 +08:00 |
|
jkooe
|
5a2c5f9c55
|
feat: 三端暴露 L2 系统级审计(MCP na_audit_l2 + 控制台 L2 分区 + agent.l2 类型)
决策「L2 同时暴露 MCP+控制台」。此前 system.audit.l2 只存在于
被控端实现里:MCP 无工具(模型看不到)、控制台无入口无类型
(用户点不到)、审计事件写成 agent.start(与进程生命周期混类筛不出)。
MCP(41 → 42 个工具):
- defs.ts 在 na_audit 后新增 na_audit_l2(action: status/enable/
collect/verify + since/limit,additionalProperties: false)
- index.ts TOOL_TO_CAPABILITY 映射 CapabilityNames.AuditL2,
分发走 default 直通(参数名与被控端一致,无需改写)
- mcp-tools 守卫同步:41→42、BASELINE_SEQ_SHA 重算、
新增「na_audit_l2 必须紧随 na_audit」顺序断言
控制台:
- conn.ts WINDOWS_ONLY_CAPS += system.audit.l2(非 Windows 被控端
上按钮禁用并明示原因,不搞「点了才报错」)
- types/index.ts 新增 AuditL2Result(四 action 属性并集,对齐
manifest returns_schema)+ AuditType += agent.l2;
AuditEntry 同步补 args_bytes/args_truncated(A-P0-4 新字段)
- AuditView.vue 新增 L2 卡片:查状态/拉取采集/校验 L2 链/启用策略
(NPopconfirm 二次确认 + 管理员权限提示);信任边界如实说明
(采集器与 agent 同权限,防篡改依赖转发出机,不夸大本地副本)
agent.l2 审计类型四端同源:
- protocol audit-types.ts AUDIT_TYPES(真源)+= agent.l2
- agent audit.ts AuditType 联合同步(AssertExact 编译期守卫兜底)
- audit-l2.ts 两处写入点 agent.start → agent.l2(L2 策略操作与
进程生命周期无关,混类会污染两边统计与筛选)
守卫加强(console-contract 平台门控):
- audit-l2.ts 平台门控由裸 throw 改用统一 assertWindows
(错误信息格式统一 + 可被守卫扫到)
- 守卫扫描范围 +audit-l2.ts、模式 +assertWindows —— 覆盖面扩大;
否则把 system.audit.l2 加进 WINDOWS_ONLY_CAPS 反而报红
- mcp-tools 正则盲区修复:原 na_[a-z_]+ 匹配不了带数字的工具名,
na_audit_l2 会整个漏扫(新增了工具两个守卫却无觉)→ 放至 [a-z0-9_]+
新增 tests/unit/console-l2.test.mjs 5 例:前端字段 ⊆ returns_schema /
契约关键字段 ⊇ 前端渲染 / MCP 工具存在且映射 / agent.l2 四端同源 /
界面纪律(信任边界文案 + enable 二次确认 + 平台门控)
故障注入实测:审计类型退回 agent.start、WINDOWS_ONLY_CAPS 删项 → 3 处红;恢复 → 绿
单测 451/451、e2e 33/33、typecheck 全绿
|
2026-10-26 00:28:44 +08:00 |
|
jkooe
|
aad137f04a
|
fix(startup-guard): 强制向被看守进程注入 NODEAGENT_HOME = --data-dir
全局一致性审查发现:Windows 计划任务的 New-ScheduledTaskAction 无法
携带环境变量,若安装进程持有进程级 NODEAGENT_HOME,agent 运行时的
agentDir()(LKG/状态文件所在地)会与守卫的 --data-dir 漂移 ——
守卫读不到 LKG/状态文件(安全退化为 no-lkg 放行,但看门狗白养)。
由 spawnEnv 在 spawn 时把 data-dir 显式注入被看守进程,三端行为与
supervisor 环境解耦且一致(Linux unit / macOS plist 的
EnvironmentVariables 本就是同值,双保险,幂等无害)。
新增 spawnEnv 单测;真机验证子进程可见注入值。
|
2026-10-26 00:17:15 +08:00 |
|
jkooe
|
560b3c44e4
|
fix(agent): 限速桶 key 改能力名 + describeArgs 截断(A-P0-2 / A-P0-4)
架构审查两个 P0:
A-P0-2 限速桶 key 用错值(server.ts:364):
原实现 `allowByRate(clientId, limit, String(limit))` 把**限速值**
当桶名——ACL 支持按能力限速(精确 > glob > 全局),配
「screen.capture ≤5/min、app.install ≤5/min」两条同值规则时
两者 key 都是 `clientId|5` 共用一个桶:截图打满后装软件被误伤,
且两者总量仍按 5/min(漏过)。同时 rateBuckets 只增不删 +
psk 模式 client_id 自报 → 外部可枚举任意 client_id 造键 →
长期运行内存无界增长。
- 抽到 util/rate-limit.ts:key = `${认证身份 ?? 远端地址}:${能力名}`
(未认证时退化按远端地址,不用空串把所有人混为一桶)
- 过期键清扫(滑动窗口边界与旧实现逐字节一致:now - t < 60_000
才算窗口内)+ 容量上限淘汰最旧(Map 插入序 = 时间序)
- e2e limited 客户端改配两个同限速值能力(各 2/min),
打满 system.info 后 system.audit.list 必须仍有完整配额
A-P0-4 describeArgs 大参数同步序列化阻塞事件循环(audit.ts:172):
原实现每次 invoke 全量 JSON.stringify + sha256。fs.write 的 data
可达数 MB——同步序列化 + 哈希 ≈ 数十毫秒级 CPU 且阻塞事件循环,
期间 GUI 注入、事件推送、保活 ping 全部延迟。
- 超过 64KB 只哈希前 64KB(按**字节**切:多字节字符下按字符切
会得到非确定性边界),总长记 args_bytes、截断记 args_truncated
- 口径从「全量哈希」变为「前 64KB 哈希」只影响审计摘要,
不影响任何安全属性(摘要用途是比对与防泄密,不是完整性证明)
测试(均故障注入实测会红):
- rate-limit 6 例:桶名是能力名 / 身份进 key / 不限速不建桶 /
过期键清扫(59.999s 窗口内、恰好 60s 出窗)/ 容量上限 /
同值多能力各占一桶
- audit-args-digest 6 例:小参数口径不回退 / 大参数截断口径 /
按字节切(字符切口径必须不同)/ 尾部不进摘要 / 端到端入链
校验通过 / logArgs 默认关不产预览
- e2e 注入验证:桶名退回 String(rateLimit) → v3 场景红;恢复 → 绿
单测 445/445、e2e 33/33、typecheck 全绿
|
2026-10-26 00:14:17 +08:00 |
|
jkooe
|
68ea3cd20e
|
fix(protocol): validate 拒检原型链键(A-P0-1,in → Object.hasOwn)
架构审查 A-P0-1:validate.ts:69 用 `key in schema.properties` 判
「附加属性是否允许」,而 `in` 沿原型链查找——JSON 里写
`"constructor"` / `"toString"` / `"__proto__"` 等键会命中
Object.prototype 成员被判为「schema 已声明」从而跳过拒绝,
fail-open(与 server.ts B-03 同源复发:那个修的是 ACL clients,
这个修的是 returns_schema 校验)。
- 改为 `Object.prototype.hasOwnProperty.call(schema.properties, key)`,
只认真实声明的属性
- 补 2 个「注入即红」用例:原型键必须被判 additional property;
真实 manifest 能力 system.agent.update 同样拒原型键、合法调用照过
- 故障注入实测:改回 `in` → 2 例红;恢复 → 全绿
安全属性收紧(原行为是漏放),无任何放宽。
|
2026-10-26 00:14:00 +08:00 |
|
jkooe
|
cb468df7da
|
feat(agent): 看门狗自动恢复 LKG —— 启动连续失败 N 次从 LKG 恢复(决策 R-1)
scripts/startup-guard.mjs + scripts/lib/startup-guard-core.mjs(新增):
- 被控端启动连续失败 N 次(默认 3)自动从 LKG 指纹匹配的备份恢复入口脚本
- 30s 启动宽限期 + 端口探活,避免计划内重启与 supervisor 重启撞车被误计数
- 无 LKG / 无指纹匹配只记日志放行,绝不擦除 install-dir
- 恢复成功或服务已就绪均归零失败计数;被控端启动成功后 resetStartupGuardState 归零
(apps/agent/src/index.ts 为本任务对 agent 的唯一改动点)
三端 supervisor 接入:
- Windows install.ps1:计划任务包一层 guard,RestartCount 3→10(保证阈值可达)
- Linux install-linux.sh:systemd unit 的 ExecStart 包一层 guard
- macOS launchd:无安装器,docs/DEVELOPMENT.md §7.4 给出完整接法
docs:DELIVERY-PIPELINE §6 由「不做自动回滚」收窄为
「手动 rollback + 看门狗自动恢复 + 边界明示」,附录 C 同步;
DEVELOPMENT.md 新增 §7.4 三端 supervisor 接入说明。
|
2026-10-26 00:09:56 +08:00 |
|
jkooe
|
1249c90725
|
feat(agent): selfupdate 代理出网 + LKG 内容指针 + L2/AgentUpdate 契约补全
P0-1 被控端自更新境内必然失败(裸 fetch 不读代理):
- 新增 apps/agent/src/net/proxy.ts + index.ts:sidecar 已验证出网方案的
TS 移植(NA_PROXY → 环境变量 → 系统代理;PAC/socks 明确报错不静默降级;
重定向链共享超时预算;CONNECT 自管 abort;TLS 默认校验证书)。
只依赖 node 内置模块,不 import 项目内任何模块(依赖严格单向)
- selfupdate.download 改走代理;成功/失败均带回 via/proxy/proxy_source/
outbound_notes 诊断;错误分类同款移植 sidecar main.mjs:146-165
(ECONNRESET 不得报成「请求超时」——真机教训)
- httpGet 增 maxBytes 双闸(content-length 预检 + 流式中断),
保住「64MB 上限」安全属性
- 回环目标恒直连(CI/dev 机 HTTP_PROXY 不得误送自建镜像进代理——
e2e 实踩:代理另一侧的 localhost 不是本机 → 404)
A-P0-3 LKG 只有指纹没有内容指针:
- LkgRecord 只增 hash_full(64 位全量)+ backup_glob(.bak*)两字段;
新增 lkgHashMatches(全量优先、老记录回落 12 位前缀,不猜)
- selfupdate 备份保留最近 3 个(planBackupCleanup 纯函数,最旧先删)
P1-4 system.audit.l2 returns_schema 空壳 → 四 action 属性并集补全;
AgentUpdate returns 增 via/proxy/proxy_source/outbound_notes 四个只增字段
安全属性逐条不变:sha256 强制校验 / 先校验后落盘 / 写后校验失败回滚 /
TLS 默认校验证书 / 只接受 http(s)
测试:新增 agent-proxy 17 例 + selfupdate-backup-cleanup 8 例 +
lkg 新增 4 例;单测 403/403、e2e 33/33、typecheck 全绿
|
2026-10-25 23:58:30 +08:00 |
|
jkooe
|
6b2fa29710
|
feat(cli): nodeagent rollback —— 从 LKG 回滚被控端(ARCH T03 / PRD P0-2)
- 新增 apps/cli/src/cmd/rollback.ts:读 LKG → 指纹比对定位备份 →
覆盖写入口(同 deploy 姿势)→ 写后校验(失败用回滚源还原)→
受控重启 → probeOnce 复核 build.hash == LKG(90s 墙钟)
- 纯函数 planRollback/pickBackupByHash/hashMatches/backupNameRegex +
流式 hashRemoteFile(client 注入,可假测);hash_full 全量优先、
老记录回落 12 位前缀(A-P0-3/R-2 向后兼容)
- --dry-run 只读零写;--backup 逃生门直指备份路径;退出码 0/1/2 分明
- HELP/README 写明能力圈边界:RPC 命令需被控端可达,「完全起不来」
走看门狗自动恢复或带外恢复;不提供自动回滚(DELIVERY-PIPELINE §6)
- 19 条单测:无 LKG 零调用 / already-lkg 零写 / 写后校验失败自动还原 /
无匹配零写 / dry-run 只读 / 多候选 mtime 新→早 / Windows 路径与元字符
|
2026-10-25 23:45:02 +08:00 |
|
jkooe
|
a75c5c9bc5
|
chore(release): 版本统一至 v2.1.0 + CHANGELOG 回填未发布提交
- 经 scripts/lib/version-write.mjs 写入全部 11 处(8 package.json +
tauri.conf.json + Cargo.toml/lock),无散点遗漏、无 skipped
- CHANGELOG [未发布] 段补入 v2.0.3..HEAD 全部提交主题:
L2 系统级审计(7d781d1)、交付流水线(2bf45fd)、Linux 适配(00992e5)、
质量门禁与 CI(141f479 等 4 个)、console-release(9f95560);
并修正「安装包尚未接入 Release」的过期记述
- version-write 单测 12/12 绿(含仓库版本号一致性守卫)
|
2026-10-25 23:37:52 +08:00 |
|
jkooe
|
3680218010
|
fix(ci): typecheck 前改为全量构建 —— 只建 protocol 不够
## 上一版没修透
先只加 `pnpm --filter @nodeagent/protocol build`,CI 换了个错法:
`apps/cli` 报 `Cannot find module '@nodeagent/client'`。
依赖链是 **`protocol → client → cli / mcp / agent`**,逐层都要 dist/*.d.ts。
逐个补依赖是打地鼠,下次加包还会漏 —> 直接用 `pnpm -r build` 把整条链建出来。
## 顺带修掉一个顺序矛盾
原工作流是 `typecheck` 在前、`pnpm -r build` 在后,而后者注释写着
「单测 import 的是 dist/,必须先构建」。既然后面无论如何都要 build,
不如提前一次到位 —— 现在 build 在 typecheck 之前,只保留一份。
**注入验证(模拟 CI 全新检出)**:把 `packages/*/dist`、`apps/*/dist` 全部移走 →
`pnpm -r build` → `pnpm typecheck` **零错误**;`pnpm test:unit` **357 项全绿**。
|
2026-10-17 23:24:36 +08:00 |
|
jkooe
|
141f479e4a
|
fix(ci): 让质量门禁真正生效 —— 修两处「一直红所以等于没有」,并清零高危漏洞
## 发现
今天 8 次质量门禁运行**全部失败**(从今天第一个 commit 起就没绿过)。
一道长期红的门禁等于没有门禁 —— 没人会看它,也就保护不了任何东西。
两处独立原因:
### ① SCA:audit 打到了镜像
仓库 `.npmrc` 指向 `registry.npmmirror.com`(加速用),但**镜像不实现 audit 接口**,
`pnpm audit` 必报 `ERR_PNPM_AUDIT_ENDPOINT_NOT_EXISTS`。
→ 显式加 `--registry=https://registry.npmjs.org`。
### ② typecheck:缺 build 顺序
各包 import 的是 `@nodeagent/protocol` 的**类型产物**(dist/*.d.ts)。
CI 全新检出没有 dist,直接 typecheck 集体报
`TS2307: Cannot find module '@nodeagent/protocol'` —— 而本地因为 dist 还在,看不出来
(典型「本地绿 / CI 红」)。
→ 在 typecheck 前插入 `pnpm --filter @nodeagent/protocol build`(只建这一个包,够用且快)。
**注入验证**:把 `packages/protocol/dist` 移走 → 复现 TS2307 → 执行新增的 build 步骤 →
typecheck 零错误。(先证明诊断对,再证明修复有效。)
## audit 通了以后暴露 2 个 high(之前被「假绿」掩盖)
1. **node-forge <=1.4.0**(路径 `apps/agent > selfsigned > node-forge`)
上游**无修复版本**(patched_versions: None)→ 只能移除依赖。
`selfsigned` 5.x 已整体改用 `@peculiar/x509` + pkijs,node-forge 被彻底删掉。
- 升级带来两处不兼容:`generate` 改为**异步**、`days` 改为 `notAfterDate`
- `ensureCert()` 随之改 async(唯一调用点在 async 的 `main()` 里)
2. **@modelcontextprotocol/sdk <1.31.0**(OAuth 凭据可能被发往 MCP 服务器指定的授权服务器)
→ 升到 `^1.32.1`
## 证书不能只看编译通过(权威本上一条真机教训)
用临时数据目录起真被控端实测:
- 证书 `notBefore=2026-10-17 / notAfter=2036-10-14`(10 年,换算正确)
- key.pem 权限仍为 600
- 客户端握手成功:**52 项能力**,验收 **13 通过 / 0 失败**
- 审计:`No known vulnerabilities found`
验证:`pnpm typecheck` 通过;`pnpm test:unit` **357 项全绿**;
`pnpm install --frozen-lockfile` 通过(lock 与 manifest 一致)。
|
2026-10-17 23:21:51 +08:00 |
|
jkooe
|
9f95560268
|
build(ci): 控制台安装包接入 Release —— 修「修复发了但没人拿得到」
## 问题
`scripts/release.mjs` 只产出 agent / cli / mcp,`apps/console` 从来没接进去。
后果很具体:今天修的 6 个真机缺陷(转圈、状态卡死、退出确认、复用、断管、ws/wss)
**只存在于 CI artifact**,而 artifact retention 是 7 天。
用户从 Release 页面下载到的,永远是旧控制台。
## 做法
1. `console-windows.yml` 加 `workflow_call:`,并新增 `publish` job:
- `if: startsWith(github.ref, 'refs/tags/v')` —— 只有 tag 才发布,日常分支构建跳过
- 下载两架构产物 → 断言安装包数 ≥ 4(exe+msi × x64/arm64),少了直接报红,
**绝不静默少发一个架构**
- `gh release upload --clobber` 挂到 `release.mjs` 建好的那个 Release
- release.mjs 在本地跑、与本工作流并行,故先轮询等 Release 出现(最多 10 分钟);
等不到就给 `::warning::` 并跳过,不阻塞流水线
2. 新增 `console-release.yml`:只认 `push: tags: ['v*']`,`uses:` 调用上面那个工作流
## 为什么不直接把 tags 写进 console-windows.yml
它带 `paths` 过滤。`paths` 与 `tags` 并存时,tag 推送会被路径判定挡掉而**静默不触发** ——
失败形态是「什么都没发生」,最难察觉。拆成「日常验证走分支 + 发布只认 tag」最稳。
验证:PyYAML 解析通过;triggers / jobs 结构正确;shell 转义已核对。
(tag 发布路径需真打一次 tag 才能端到端验证,见下文待办)
|
2026-10-17 23:12:44 +08:00 |
|
jkooe
|
240c52c44f
|
fix(console): 「连接本机」必失败 —— 地址硬编码 ws:// 无视 tls
## 皇上实测
点「连接本机」→ 界面显示**连接失败**。
## 根因(协议层,不是鉴权)
`agent.rs` 里拼监听地址时**硬编码** `ws://127.0.0.1:{port}`,
完全没看同一个函数里刚解析出来的 `tls` 字段:
```rust
"url": format!("ws://127.0.0.1:{port}"), // ← tls = true 时这是错的
```
而被控端 `agent.json` 默认 `tls: true`(只接受 TLS)。于是控制台用
**明文 WebSocket** 去连一个**只监听 TLS** 的端口:服务端在等 TLS ClientHello,
收到的却是 HTTP GET,握手当场失败。
**鉴别特征**:被控端审计里连一条记录都没有(根本没进到鉴权层),
所以「密钥对不对」根本轮不到 —— 这类故障看审计有无 `auth.success` 即可一刀切开:
- 有记录 → 问题在鉴权/ACL
- 无记录 → 问题在协议/网络层(本次就是)
## 修法
- `url` 改为按 tls 选择:`if tls { "wss" } else { "ws" }`
- 顺带修两处同类默认值(同一个坑):
- `stores/conn.ts` 兜底地址 → `wss://`
- `NodesView` 远程节点输入默认值与占位 → `wss://`
## 守卫
新增单测 `连接地址应跟随tls开关而非硬编码ws`:分别写 tls=true/false 的
agent.json,断言得到 `wss://` 与 `ws://`。防止将来又被"简化"回硬编码。
验证:`cargo test --lib` **22 项全绿**;`vue-tsc --noEmit` 通过。
|
2026-10-17 22:15:17 +08:00 |
|
jkooe
|
0782d901a3
|
fix(console): 消除两处同步阻塞调用 —— 修「按钮一直转圈、PID 不再更新」
## 真机现象
被控端明明在跑,界面上「启动/停止/重启」三个按钮**同时**转圈不结束,
PID 停在旧值(显示 30160,实际已是 22948),点导航也不切页。
只能重开控制台。
## 根因一(臣上一版自己引入的)
`looks_like_agent()` 用 `std::process::Command::output()` 调 PowerShell:
**同步阻塞 + 无超时**。而这台机器上 PowerShell/CIM 本身就慢甚至偶发卡死
(实测 `Get-ScheduledTask` 2.0s、`Get-NetTCPConnection` 1.7s,
另有一次 CIM 查询直接 30s 超时)。
它在 `start()` 里被调用(复用判定),一旦卡住就**永久占住运行时线程** →
`run()` 的 finally 里的 `refresh()` 永远走不到 → `busy` 卡死、状态不再更新。
连带 sidecar 的请求也拿不到执行机会,表现为「重连中」但审计里没有任何握手记录。
改:`looks_like_agent` 改 async + `tokio::process` + **3 秒超时**。
超时/失败一律**判为「不是被控端」**(拒绝接管)—— 宁可让用户看到「端口被占用」
去手工排查,也不能误杀别人的进程。
## 根因二
`autostart::is_enabled()` 每次 `agent_status` 都会跑一次 `schtasks /Query`
(实测约 2 秒)。每刷新一次界面白等 2 秒,叠加后同样表现为转圈不结束。
改:加 5 秒 TTL 缓存(自启是低频状态),并在 `set()` 后 `invalidate_cache()`,
保证用户点完开关立刻看到新值而不是 5 秒前的旧值。
验证:`cargo build` / `cargo clippy` 干净;`cargo test --lib` **21 项全绿**。
|
2026-10-17 21:55:44 +08:00 |
|
jkooe
|
9af6e0f73a
|
fix(agent): stdout 断管容错 —— 修「退出控制台后被控端假死」
## 真机现象(两次踩到,都能复现)
退出/重启控制台后,被控端**端口仍然 OPEN,但所有 RPC 全部超时**(连握手都不回),
持续数分钟不自愈,只能再重开一次控制台。外部看像是「进程活着,但哑了」。
## 根因
被控端由控制台 spawn 时,stdout/stderr 是**管道**而非终端。控制台一退出
(尤其是本就要支持「保留被控端」这个场景),管道**读端消失**,而 agent 仍在往死管道写日志:
- Node 对未监听 'error' 的 stream 会把 `EPIPE` **抛成未捕获异常** → 进程退出;
- 写端在无读者时还可能**阻塞**,把事件循环一起卡住 —— 端口保住了(OS 层 socket 还在),
但 JS 再也不执行。这正是「端口 OPEN 却全员超时」的形态。
全仓此前**没有任何** stdout/stderr 错误处理,grep 可证。
## 做法
- 给 `process.stdout` / `process.stderr` 挂 'error' 监听,吞掉 EPIPE / DESTROYED;
- 一旦判定通道已断,**短路后续写**(包一层 console.log / console.error),
不再往死管道里堆字节;
- 任何写异常都只置标志、不抛 —— 日志通道故障不该打死常驻服务。
日志不丢:审计走文件(audit.log),控制台在时照常收管道输出。
宁可少一行 stdout,也不能让常驻服务因为「没人看日志」而假死。
## 与「复用已运行实例」的关系
上一提交做了「下次启动复用孤儿被控端」,但如果孤儿本身已被断管卡死,
复用就只是把一个哑掉的进程认领回来。**这条修复才让复用真正有意义。**
|
2026-10-17 21:23:20 +08:00 |
|
jkooe
|
00659165b1
|
fix(console): 状态永远停在「启动中」—— refresh 被 catch 跳过
## 真机症状
被控端明明在跑(点「启动」后端答「**被控端已在运行**」、端口在听、验收 15/15 全通),
界面却一直显示「启动中」,「停止 / 重启」永久灰着 —— 用户完全无法操作。
## 根因(两条更新通道同时失效)
1. `run()` 把 `await refresh()` 写在 **try 里**:
```ts
try { const r = await fn(); await refresh(); return r; }
catch (e) { ... } // ← 动作一抛错,refresh 就被跳过
finally { busy = false }
```
那一次 `start()` 抛了「已在运行」,于是 refresh 被跳过,界面停在旧值。
2. `na-agent-state` 推送在 **WebView 就绪前**发出,前端 `bind()` 的监听还没挂上 → 事件丢失。
3. `LocalServiceView` 的 `onMounted` 只调 `agent.bind()`,而 `bind()` 有 `bound`
守卫 —— 第二次进页面直接 return,**不会刷新**。所以切页也救不回来。
后端状态其实是对的(`inner.state` 已是 running),纯前端未同步。
## 修法
- `run()`:`refresh()` 移入 **finally** —— 成败都同步状态。
`refresh()` 自身已有 try/catch,不会掩盖原始错误。
- `LocalServiceView.onMounted`:显式再 `await agent.refresh()`,
绕过 `bind()` 的 bound 守卫,每次进页面都拉一次(最便宜的状态自愈)。
- `snapshot()`:加状态自愈兜底 —— `STARTING` 且已有 PID 且超 2 秒,就按客观事实
校正为 `RUNNING`(拿得到 PID 是进程已起的硬证据);`STOPPING` 且无 PID 则校正为 `STOPPED`。
## 自愈的三条单测(含反向守卫)
- 已起 + 超 2 秒 → 必须 `running`
- 刚起(<2 秒)→ **不得**提前自愈(否则真实启动失败会被伪装成运行中)
- 无 PID → 不得自愈(进程根本没起来,不能装成 running)
验证:`cargo build` / `cargo test --lib`(17 项全绿)/ `vue-tsc --noEmit` 均通过。
|
2026-10-17 21:09:13 +08:00 |
|
jkooe
|
704fb8cfc9
|
feat(console): 退出前确认是否保留被控端 + 下次启动复用已运行实例
## 问题(皇上实测)
1. 退出控制台后仍有一个被控端在跑 —— 因为 `kill_on_drop(false)`,
agent 成了孤儿继续监听端口。用户以为关了窗口就干净了。
2. 下次启动时该孤儿占着 8765,`start()` 直接报「端口已被占用」,
明明有个健康实例却用不上。
## 需求 1:退出前确认
托盘「退出」与「关 X(未开启最小化到托盘时)」两条路都会留下孤儿,
故两条都必须问,否则从 X 走就绕过了确认。
- Rust:`quit` 不再直接 `app.exit(0)`,改为 emit `na-quit-requested` + 唤出窗口
- Rust:新增 `quit_app(keep_agent)` 命令 —— true 直接退出(保留 agent),
false 先 `stop()` 再退出
- 前端:新增 `QuitGuard.vue`(挂在 `n-dialog-provider` 内),三选一:
保留被控端退出 / 停止被控端退出 / 关闭弹窗即取消
为什么不加 `tauri-plugin-dialog`:前端已有 `n-dialog-provider`,
不必新增依赖,视觉也与全应用一致。
## 需求 2:复用已运行实例(adopt)
`start()` 遇到端口占用时不再一刀切报错,而是先分清占用者:
- 占用者**确实是** nodeagent 被控端(`looks_like_agent`:命令行含 `agent.mjs`)
→ **复用**:记 PID、置 RUNNING、`external=true`,返回 `adopted:true`
- 否则(用户自己的服务 / 别的应用)→ 仍报「端口已被占用,且占用者不是被控端」
—— 绝不误接管别人的进程
配套:
- `Inner` 增 `external` 标记;`snapshot()` 暴露给前端
- `stop()` 支持外部实例(无 Child 句柄,走 `stop_pid()` → taskkill /T /F),
并**等待端口真正释放**再返回,否则 restart 会误判「还活着」又复用同一个 PID
- `restart()` 无需改动:stop 收掉外部实例 + 600ms + start 自己 spawn
→ 天然把控制权收回本控制台
- 前端:状态栏加「已复用外部实例」标签
## 顺带修复(同一类问题)
上一提交把「检查更新」从全局 `busy` 拆出后,发现 `applyUpdate` 也占全局 `busy`
(下载 RPC 超时 330s),一并迁到 `updateBusy`。
验证:`cargo build` / `cargo clippy` 干净;`vue-tsc --noEmit` 通过。
|
2026-10-17 20:18:32 +08:00 |
|
jkooe
|
618ebc986f
|
fix(console): 更新检查不再锁死整页按钮(全局 busy 串台)
真机现象:点「检查更新」后,启动/停止/重启等**所有**按钮一起转圈,
持续两分半,看起来像「点什么都卡死」。
## 根因(两个缺陷叠加)
1. **`busy` 是全页共享的**:`LocalServiceView` 里每个按钮都是 `:loading="busy"`。
2. **`check_update` 是网络操作且超时很长**:出网 120s / RPC 150s。
而这两个值**不能缩短** —— `sidecar.rs` 的注释与编译期不变式记录了真机实测:
跟随两次重定向的耗时出现过 2.0 / 10.0 / 39.7 / 68.6 / >109s,
回落到 60s 会让 68.6s 那次变成「假失败」。
故超时保持不动,**只修隔离**。
于是任何一次「检查更新」都会把全页按钮锁死 —— 更新检查本是
**只读、可等待、与启停无关**的操作,不该拖累其他按钮。
## 修法
- 新增 `updateBusy`,只服务「检查更新 / 更新被控端」
- 全局 `busy` 只留给毫秒级的启停与设置
- `applyUpdate` 同步迁移(下载 RPC 超时 330s,占用全局 busy 更糟)
- 视图里检查更新按钮与更新按钮改绑 `updateBusy`
## 刻意不改的
- `UPDATE_CHECK_TIMEOUT_MS` 保持 120s、RPC 150s —— 见上述实测,缩短会退回「假失败」
- `busy` 仍保留全局语义(启停确实应互斥)
验证:`vue-tsc --noEmit` 通过;`pnpm --filter nodeagent-console build` 通过。
|
2026-10-17 19:54:04 +08:00 |
|
jkooe
|
c7d596e087
|
fix(ci): quality-gate 补 pnpm 版本(首次运行 10 秒即失败)
首次推送后 Quality Gate 立即失败,两个 job 都在 pnpm/action-setup 步骤报:
Error: No pnpm version is specified.
根因:本仓库其他三个工作流(console-windows / rust / windows-e2e)
都显式写了 `with: version: 12`,臣新加的这份漏了 —— 而 package.json
里没有 `packageManager` 字段,action 无从推断版本。
修:两处 pnpm/action-setup 均补 `version: 12`,与既有工作流保持一致。
教训:新增 CI 工作流不能只照抄步骤名,**版本/参数要与既有工作流对齐**;
否则门禁自己先挂,比没有门禁更误导(看起来"有 CI",其实从未真正跑过检查)。
|
2026-10-17 19:17:39 +08:00 |
|
jkooe
|
630ea7dd7a
|
chore(security): 精确忽略 4 处测试占位密钥误报
全局 pre-push 钩子的 `gitleaks git` 全历史扫描报 4 处命中,全部位于测试文件:
tests/e2e/run.mjs sec-e2e-key-0123456789abcdef0123456789
fs-key-0123456789abcdef0123456789abcdef
e2e-test-key-0123456789abcdef0123456789abcdef
tests/unit/hmac.test.mjs test-key-0123456789abcdef
逐一核对:均为明显的递增占位值(含 0123456789abcdef 模式),
**均不等于被控端真实 PSK,均不含 64 位十六进制串** → 判定误报。
## 为什么用 .gitleaksignore 而非 .gitleaks.toml
- 顶层 `[allowlist].paths = ['tests/**']` 会对**所有规则**放行,
于是 private-key / aws-access-token 等在测试目录下也失效 ——
而「为造样本把真实私钥贴进 fixture」恰是最常见的泄露方式。
- `[[rules]]` 重定义 generic-api-key 会**替换**内置规则(gitleaks 要求重写 regex),
等于自己维护一份正则,更容易漏。
- 指纹格式 `<commit>:<file>:<rule>:<line>` 只忽略这一条,其余照旧。
## 验证(不是「让 CI 变绿」)
- 加配置后全历史重扫:`no leaks found`
- 故障注入:在 tests 目录新造一条**未列入忽略**的密钥
(`apiKey = 'sk-live-…'`)→ **仍被检出**(leaks found: 1)
→ 证明忽略是精确的,没有按路径削弱规则
|
2026-10-17 19:03:56 +08:00 |
|
jkooe
|
7d781d132e
|
feat(audit): L2 系统级审计兜底 —— 三端设计 + Windows 落地
## 它补的是 L1 结构上记不到的那块
L1 是 agent 自己记的,只看得见经过自己 RPC 的调用。攻击者绕过 agent
直接跑系统命令、或干脆把 agent 停掉 —— L1 全部盲区。L2 采集操作系统
自己的审计事件,补的正是这块。设计见 docs/AUDIT-L2.md。
## 必须先说清的信任边界(不夸大作用)
采集器与 agent 同进程同权限 —— 攻击者能停 agent 就能停采集器。
故本地 audit-l2.log 只是一份**归一化副本**,防篡改价值有限。
真正的独立性来自**把记录转发出机**(与 SECURITY.md §4.8 锚定同一思路)。
本实现不自动转发,只提供导出接口;这一点已写入文档与能力描述。
## 三端
- Windows:**落地**。选内置 auditpol 而非 Sysmon —— Sysmon 是第三方驱动,
需单独分发、需重启生效、版本行为有差异;内置策略零依赖即可立起来。
检测到装了 Sysmon 则并用,未装不尝试下载。
- status:查策略是否启用,并提示两个关键开关
* SCENoApplyLegacyAuditPolicy —— 不设它,组策略旧式类别策略会盖掉
subcategory,表现为「auditpol 显示已启用但事件日志里没有 4688」
* ProcessCreationIncludeCmdLine_Enabled —— 不开则 4688 只有进程名
- enable:开启进程创建 4688 / 登录 4624,4625 / 进程终止 4689(需管理员)
- collect:Get-WinEvent 拉取 → 归一化 → 独立落盘(带哈希链)
- verify:L2 链完整性校验
- Linux:仅设计(auditd,需预装),未落地
- macOS:仅设计,ESF 需 Apple entitlement 且必须是签名 System Extension,
个人自签无法启用 —— 不写假装能用的代码
## 落盘与 schema
独立文件 audit-l2.log,不与 L1 混 —— 混了就无法区分「agent 记的」与
「OS 记的」。记录含 ts(OS 事件时间)、src、event_id(保留原始便于核对)、
kind(归一化类别)、collected_at(采集时刻,与 ts 差过大说明曾离线)。
## 实测
- 新增 8 条单测,含两条故障注入:篡改内容检出「哈希不符」、
删中间条目检出「链断裂」
- 既有契约守卫两次报红并已被修复:manifest 新增 lkg 字段 → SystemInfo 接口缺声明;
新增 system.audit.l2 → 能力清单断言过期(51→52)
- 全量单测 349 → 357 全绿
|
2026-10-17 03:24:17 +08:00 |
|
jkooe
|
2bf45fdf98
|
feat(pipeline): 交付流水线规范落地 —— 门禁 + LKG 回滚锚点
将《代码审计 → 上线 完整交付方案》v1.0 适配到 nodeagent,产出
docs/DELIVERY-PIPELINE.md,并落地其中可执行的部分。
## 为什么要适配而不是照搬
原方案面向 Web/容器服务(有服务端、有镜像、有多环境)。nodeagent 是
跑在用户机器上的跨机接管框架,无服务端、无容器、无多环境。照搬会产生
「跑得很好但守不住任何东西」的假门禁。故每个阶段给三档判定:
适用 / 改造后适用 / 不适用(但必须给替代,不允许只写 N/A)。
关键替换:
- 容器镜像扫描 → 交付物 PE 架构断言(arm64 包误装 x64 node.exe 的真实事故)
- DAST → 协议健壮性测试(本项目对外输入面是 WebSocket RPC,非 HTTP)
- dev/staging/prod → 真机矩阵(Win x64/arm64、macOS、Linux)
- 金丝雀 → 设备分批自更新(先一台试点,latest.json 灰度)
- Prometheus → system.metrics(闭环率/P95/拦截率)+ 发布后复核清单
## 本次落地
1. 覆盖率门禁 scripts/ci/check-coverage.mjs
- 全量**只观测不阻断**(实测基线 66.9%,照搬 80% 全量不现实)
- 变更文件行覆盖率 ≥70% 阻断;单测 import 的是 dist/,需 src→dist 映射
- 无覆盖数据的变更文件不阻断,单列供人工判断(算 0% 会让重构无法合入)
2. 提交信息校验 scripts/ci/check-commit-msg.mjs
- 项目实际已在用 Conventional Commits,但无强制 → 变成会红的门禁
- 不引 commitlint+husky:本项目只需类型白名单,几十行即可且 CI 本地都能跑
3. .github/workflows/quality-gate.yml:gitleaks + pnpm audit + typecheck + 覆盖率 + 提交规范
4. CI artifact retention-days 30 → 7(公开可下载,留存越久暴露面越大)
5. LKG(最后已知可用版本)apps/agent/src/lkg.ts
- 只在**启动成功后**才写,不在更新写完时写(否则等于把未验证版本当已知可用)
- 暴露为 system.info.lkg,matches=false 表示当前版本尚未被证明能启动
- 控制端据此暂缓向其余设备放量
## 刻意不做的事:自动回滚
远程设备上无法区分「更新坏了」与「网络抖动」;自动回滚可能形成回滚循环;
被控端自作主张改自己会让控制端失去掌控。故 LKG 只提供确定的回滚目标,
决策权留给控制端。已在文档中写明取舍理由。
## 实测证据
- 覆盖率门禁三条故障注入均 exit=1(阈值100 / 全量99 / lcov缺失),正常 exit=0
- 提交规范反例 3 类均红,Merge 与无 scope 正确豁免,历史 20 条提交一次通过
- LKG 新增 8 条单测(含篡改 hash 必须检出 matches=false)
- 既有契约测试在新增 lkg 字段后立刻报红(SystemInfo 缺声明),补接口后转绿
—— 这本身是门禁有效的实证
- 全量单测 341 → 349 全绿
|
2026-10-17 03:13:18 +08:00 |
|
jkooe
|
590f62e968
|
docs(changelog): 回填「检查更新」修复的真机复验与守卫口径
CHANGELOG 与 docs/console-plan.md 的该节写于「15 用例 / 248KB」时点(随后被隐私脱敏
提交一并带走),已落后于代码,且当时「经代理成功」那一组受宿主线路拥塞未取到。
本次回填三处:
- 用例数 15 → **16**,补第 15 条守卫(错误映射:连接被中途重置不得报成「请求超时」)
及其注入验证行。
- 补记**第三个缺陷**:ECONNRESET 被误归 abort 类 → 真机耗时 82.9s 却报「请求超时
(120000ms 内未完成)」,把「对端掐断连接」这条最有价值的信息抹掉。
- 产物 248KB → **249KB**;真机复验表补 D/E 两组(Win11 ARM64 VM,用随包 node.exe 直跑
dist/sidecar.mjs,两侧 sha256 一致):
· D 对照(不配系统代理、不给任何环境变量)→ 30877ms 失败,但报真实原因 socket hang up,
data 带 via=直连 / timeout_ms / 可操作提示;
· E 实验(只设 WinINET 系统代理、仍不给任何环境变量)→ **成功 28883ms**,
via_proxy=system、日志「(来源 system)」、退出码 0。
实验组走通的正是本次新增的系统代理发现;其 28.9s 耗时同时反证旧 20s 内部超时必然失败。
复验后 VM 的 ProxyEnable/ProxyServer 已原样还原。
无代码改动。
|
2026-10-13 22:47:36 +08:00 |
|
jkooe
|
b34e8c73e0
|
fix(console): 让「检查更新」在境内真的能用 —— 自持出网 + 系统代理发现
真机症状:控制台点「检查更新」必报 `检查更新失败:fetch failed`。
上一轮曾把此项归因为「环境网络问题」而收工;本轮按「即使环境本身不通,
代码也不该让用户无从自救」重新定位,确认为**产品缺陷**。
三条根因(均经隔离实测)
- Node 内建 fetch(undici)默认不读任何代理环境变量。`NODE_USE_ENV_PROXY=1` 可开,
未开时 env 被**完全忽略** —— `env -i` 对照实验:带 flag 得 `fetch failed`(证明 flag 生效),
不带得 `timeout`(证明 env 被忽略)。
- 系统代理(WinINET / macOS 网络设置)Node 亦不读;而 `fetch(url,{dispatcher})` 走不通 ——
dispatcher 必须是 undici 的 Dispatcher 实例,Node 未把 undici 暴露为可导入模块,
整包 vendored 会撑胖 sidecar 且其依赖 .wasm 解析器,esbuild 单文件打包有风险。
- 内部超时 20s 对真实重定向链必然失败:`latest.json` → 版本化 URL → CDN,
实测耗时 2.0 / 10.0 / 39.7 / 68.6 / >109 秒。
修法
- 新增 `apps/console/sidecar/proxy.mjs`(自持出网,零新依赖):https 目标走 `CONNECT host:443`
隧道后叠 TLS(隧道返回的 socket 已是 TLS,须用 `http.request` 承载,用 `https.request` 会二次握手;
且**不可把 URL 对象当第一参**,否则会覆盖手搭的代理字段退化成直连);http 目标走绝对形态请求行;
含 NO_PROXY 匹配、系统代理发现(`reg query` / `scutil --proxy`,PAC 不可解析时记 note 而非静默)、
socks5 等看不懂的形态一律记 note。
- `check_update` / `download` 全部改走 `httpGet`,整条重定向链共享一个超时预算与 AbortController。
- 内外超时成对下发:`sidecar.rs` 新增 `UPDATE_CHECK_TIMEOUT_MS`/`_RPC_TIMEOUT_SECS` 与
`UPDATE_DOWNLOAD_TIMEOUT_MS`/`_RPC_TIMEOUT_SECS` 两组常量 + **编译期不变式**
(`const _: () => assert!(...)`),保证「出网超时 < RPC 超时」——否则用户只会看到
丢掉真实原因的「check_update 超时」假失败。订正 6 处因超时改动而变陈旧的注释。
- 失败信息带 `url` + `via`/`proxy`/`proxy_source`/`timeout_ms`/`hint`;界面新增「出网」行,
并修掉重复前缀(`检查更新失败:检查更新失败:…`)。
顺带修掉两个真机暴露的缺陷
- **隧道/TLS 阶段的 abort 无人受理**:`createConnection` 回调返回前外层请求尚未建立,
外层 `signal`/`timeout` 无人受理 → 真机实测配 75s 超时却挂了 109s。
- **`ECONNRESET` 被归入 abort 类** → 报成「请求超时」(真机:耗时 82.9s 却报「120000ms 内未完成」)。
改为只认真 abort,并新增网络错误翻译(ENOTFOUND/EAI_AGAIN/ECONNREFUSED/ECONNRESET/EPIPE)。
守卫与验证
- 新增 `tests/unit/sidecar-proxy.test.mjs`(16 用例,四段:纯函数 / 真发请求 / 端到端
spawn sidecar 跑 JSON-RPC / 形态守卫)。夹具用本机真 `scutil --proxy` 与真 `reg query` 输出,
地址改为 RFC 5737 文档段,不写内网 IP、不写工具实名。
- 守卫落在「最终下发的请求」与「产物字节」上,逐条**注入故障实测会红、还原后逐字节一致**:
① `check_update` 退回裸 `fetch` → 代理未收到该次请求 → 红
② 去掉 `isBypassed` 后缀匹配 → 红
③ 出网/RPC 超时改回反序 → 红 / `error[E0080]`
④ 去掉隧道 abort 挂载 → 红(`实际 4002ms`)
⑤ `ECONNRESET` 塞回 abort 类 → 红
- 全量:`unit` **341/341**、`rust` **18/18**、`clippy -D warnings` 干净、`vue-tsc` 退出码 0。
未覆盖项(如实记档)
- https 经 CONNECT 隧道叠 TLS 无离线自动化用例:`selfsigned` 只在 `apps/agent` 包内,
测试进程解析不到 → 无法离线造受信任 TLS 站点;该路径仅真机验证。
- 真机「经代理取 GitHub 成功」的那一组未取到(宿主两条代理线路当时都取不到 GitHub),
属环境而非产品缺陷;错误信息已可操作(含 `via`/`proxy_source`/`hint`)。
- 同类缺陷**第三处未改**:`apps/agent/src/capabilities/selfupdate.ts` 仍是裸 `fetch`
(属被控端变更,改则需升 PATCH)。
注:本项为上壳变更,不触发 release patch;`apps/console` 至今未接入 `release.mjs`。
|
2026-10-13 22:41:02 +08:00 |
|
jkooe
|
b9236dfaf1
|
chore(privacy): 公开仓库隐私脱敏 —— 清除可定位到个人与设备的字面量
按 security-reports/ 的隐私审查台账逐项收口。纯脱敏与表述泛化,
不含任何业务逻辑变更;实测结论与排查步骤一字未动。
P1(可定位到具体设备/个人)
- scripts/verify-b2.mjs:NA_URL 去掉硬编码缺省值(原指向一台真实机器),改为必填。
这是全仓库唯一「拿着就能连到某台真机」的常量。
- 测试账户名 pdverify → na-test(docs/VM-VERIFICATION.md 7 处、docs/console-plan.md 1 处)
- 虚拟机与宿主的真实网段 → RFC 5737 文档保留段 192.0.2.x(含 CLOSE_WAIT 记录)
- 跨项目名与私人称呼 → 中性表述(docs/PRD.md、docs/DEVELOPMENT.md、docs/console-plan.md 等)
P2(低风险设备指纹)
- 虚拟化产品及其 CLI / 访客工具名 → 中性表述(12 个文件 + 1 个工作流注释)
- 测试目录 C:\nodeagent-test → C:\na-test
- NAS 设备型号与系统名 → NAS 设备 / NAS 系统(docs/DEPLOY-LINUX.md、scripts/install-linux.sh、README.md)
- 真机 CPU 型号、真机物理分辨率、代理软件品牌与监听端口、杀软品牌 → 泛化
- 单元测试示例段去掉真机 IP 尾八位:acl-v11 / net-scripts 的 fixture 与断言同步更新
- .gitignore 补 config.json / agent.json(含真实密钥与节点地址,绝不入库)
- 顺带移除混入技术文档的「不构成投资建议」字样(与文档主题无关)
验证:pnpm test:unit 340/340 通过;独立扫描器对 235 个跟踪文件做 17 类模式
全量扫描,命中 0。
|
2026-10-13 22:35:40 +08:00 |
|
jkooe
|
00992e5c1c
|
feat(linux): Linux 被控端适配 —— 安装器 + systemd 自启 + 平台守卫 + 部署文档
面向「服务器型被控端」(命令/状态/文件/审计;无 GUI),实测环境 Debian 12(fnOS 底座)。
## 新增
- `scripts/install-linux.sh`:一键安装器(对齐 Windows `install.ps1` 的语义)
· 自动获取 Node 运行时(系统无 Node 22+ 时下载 linux-<arch>,走 npmmirror)
· **systemd 服务**(系统级 / 用户级+linger / `--no-service` 三种模式),开机自启
· 幂等:复用既有密钥(并自动备份 agent.json);卸载保留配置(`--purge` 才删)
· `--node-id/--port/--allow-input` 等参数;缺 `ip`/`ifconfig` 时 IP 展示多级回退
- `apps/agent/src/util/platform.ts`:**统一平台守卫**
· `assertPlatform(cap, ...allowed)` / `assertGuiPlatform` / `assertWindows`
· 抛 `E_UNSUPPORTED_PLATFORM` 并带上「支持哪些平台」,让"未实现"不再伪装成"参数错/环境缺依赖"
- `docs/DEPLOY-LINUX.md`:部署指南 + **实测能力清单** + 故障排查表
## 修改
- `screen.ts` / `record.ts` / `clipboard.ts` / `audio.ts` / `input.ts`:入口接入平台守卫。
修复前的表现是**误导性错误**(`screen.capture` 在 Linux 上报 `region 宽高必须 ≥ 1`,
像是参数没传对),现在是 `screen.capture 未在 Linux 上实现(仅支持 Windows / macOS)`。
- `scripts/verify.mjs`:v2.1 / v2.3 补 `E_UNSUPPORTED_PLATFORM → skip`
(v2.2 早已处理;此前把"平台不支持"记成失败)
- `README.md`:文档区链入 DEPLOY-LINUX
## 实测(Docker · Debian 12)
- 安装流程端到端:装文件 → 写配置(600) → 提取密钥 → 启动监听 → **幂等复装密钥不变** ✓
- 验收:**9 通过 / 1 失败 / 6 跳过**(对比 macOS 15/0/1)
· 1 失败 = 容器缺 `procps`(`process.list` 返回空),非代码问题;NAS 自带该包
· 6 跳过 = GUI 类能力(平台诚实拒绝)
- 单元测试 325 全绿;7 包构建通过
## 途中修正的三个"差点误判"
1. **Alpine 误判**:`spawn /bin/bash ENOENT` 是**镜像没 bash**,不是 Linux 不支持
2. **`set -e` 静默退出**:`LAN_IP=$(ip ...)` 在无 `ip` 的环境失败会**直接终止脚本**
(`2>/dev/null` 把原因吞了,表现为"跑到一半就没了")→ 已改多级回退
3. **slim 镜像缺 procps** → `process.list` 空列表,被误读为"Linux 特性"
|
2026-10-13 21:15:59 +08:00 |
|
jkooe
|
c9e34b93a2
|
docs(changelog): 补黑窗修复的真机复验结果
- sidecar 在点击那一刻被拉起(pid 4056 @ 20:52:58),命令行两侧均无 \\?\;再次点击
复用存活实例、不重建,符合设计
- 黑窗消失:MainWindowTitle -like '*nodeagent-console*' 返回空(修复前命中一个
WindowsTerminal 窗口,标题正是 …\nodeagent-console\node.exe)
- 附带记档(与本次修复无关的环境问题):该次点击时 VM 连不上 GitHub,
界面报 "fetch failed",同机 Invoke-WebRequest 21.5s 无法连接;更早一次网络通时
已成功返回结果,故功能本身已验证
CHANGELOG 不在 console-windows.yml 的 paths 里,本次提交不触发新构建。
|
2026-10-13 21:05:52 +08:00 |
|
jkooe
|
9384b64c42
|
fix(console): sidecar 补 CREATE_NO_WINDOW(不再弹常驻黑窗)
真机复验 verbatim 修复时暴露的既有缺陷:点「检查更新」后屏幕上多出一个不消失的
黑窗(WindowsTerminal 承载,标题即 node.exe 路径)。
根因:控制台自身是 GUI 子系统,spawn 控制台子系统程序(node.exe)时 Windows 会
新建控制台,必须 CREATE_NO_WINDOW 抑制。AgentManager::start 早有此标志,
sidecar_command 漏了。修复 verbatim 前 sidecar 秒退、黑窗一闪而过;
sidecar 常驻后它才留在屏幕上。
修法:sidecar_command 补 creation_flags;常量提为 sidecar::CREATE_NO_WINDOW
由两处共用,避免值漂移。
本项无自动化守卫(如实记档):creation_flags 只有 setter 没有 getter,
断言不到最终值;扫源码属源码级检查,违反铁律 1。靠真机截图对照把关。
clippy 零告警,test:rust 18/18(行为零变化的注释/常量整理 + 一处新标志)。
|
2026-10-13 20:41:02 +08:00 |
|
jkooe
|
2437b0e863
|
fix(console): unverbatim 用 to_str 而非 to_string_lossy(不损坏非 UTF-8 路径)
to_string_lossy 会把非法字节替换成 U+FFFD 再重建路径 —— 那是损坏路径,
比不净化更糟:原本只是 node 报 EISDIR,改完变成指向一个不存在的目录。
改为 to_str():非 UTF-8 → None → 原样返回(Windows 上是含未配对代理项的路径,
Unix 上是含非法字节的文件名),绝不悄悄改写。
新增守卫 unverbatim_对非_utf8_的_verbatim_路径必须原样返回不得损坏(#[cfg(unix)]):
构造必须同时含 \\?\ 前缀与非法字节才有区分力 —— 不带前缀时新旧实现都走
「原样返回」那一步,测不出差异。
注入故障(退回 to_string_lossy)实测该守卫如期变红;还原后 sha256 逐字节一致。
pnpm test:rust 18/18(+1),clippy 零告警。
|
2026-10-13 20:21:51 +08:00 |
|
jkooe
|
56f64e1cc1
|
docs(console): 把 verbatim 路径的证据精确到「脚本参数」这一维
真机二维四格实验(ProcessStartInfo,精确对应 Command::new(prog).arg(script)):
G1 plain-prog + plain-script -> exit 0,正常
G2 plain-prog + VERBATIM-script -> exit 1,EISDIR lstat 'C:'
G3 VERBATIM-prog + plain-script -> exit 0,正常
G4 VERBATIM-prog + VERBATIM -> exit 1,EISDIR lstat 'C:'
结论:炸的是「脚本参数」带 verbatim,不是「可执行文件」带 verbatim。
旁证:真机上被控端与 sidecar 的 program 都带 \\?\ 却长期存活。
因此改准注释口径:净化 script 是关键修复,净化 node 属防御性
(守住「凡交给外部进程的路径都干净」这条单一纪律,不靠记位置)。
行为零变化(纯注释 + CHANGELOG)。
|
2026-10-13 20:16:07 +08:00 |
|
jkooe
|
eec76ed91e
|
fix(console): 交给 node 的路径去掉 Windows verbatim 前缀(真机 sidecar 秒退根因)
真机现象(Windows 11 ARM64,控制台 v2.0.3):点「检查更新」立刻报
「sidecar 已退出(stdout 关闭),请重试」—— sidecar 在请求进行中真的退出了。
真机 A/B(同一个 node.exe、同一份 sidecar.mjs,只换脚本参数写法):
C:\…\sidecar.mjs → 2.5 秒返回结果,进程常驻
\\?\C:\…\sidecar.mjs → 0.47 秒退出,exit 1,零 JS 执行
根因:Tauri 的 resource_dir() 在 Windows 上返回 canonicalize 后的 verbatim 路径,
把它当参数交给 node,node 启动期 realpathSync 把 `\\?\C:` 拆成 `C:` →
EISDIR ... lstat 'C:' → exit 1,一行 JS 都来不及执行。
本地/CI/开发模式全测不出:开发期走 CARGO_MANIFEST_DIR 的普通路径,只有打包后才暴露。
修法:
- 新增 unverbatim():剥 \\?\UNC\ → \\、剥 \\?\ → 无前缀,纯字符串处理,全平台可调用
- 新增 sidecar_command():唯一把路径交给 node 的地方,单测断言其最终 program/args
- resolve_node / resolve_sidecar / resolve_bundled_agent 全部出口加闸
- Sidecar::spawn 与 AgentManager::start 进程边界对 node/script/data_dir 三条路径净化
- autostart::app_exe() 净化(该路径进计划任务 -Execute)
回归:新增 3 条守卫;注入故障(去掉 sidecar_command 的 unverbatim)实测第三条如期变红,
还原后 sha256 逐字节一致。pnpm test:rust 17/17(14 → +3),clippy 零告警。
|
2026-10-13 20:11:03 +08:00 |
|
jkooe
|
6adf11c85a
|
fix(test): 两条守卫在 Windows 上必红(CRLF 与路径分隔符)
CI run 38380306200(提交 73ed06f)Windows 单元测试 319/321,macOS 全绿。
① 仓库内全部版本号互一致 → 报 undefined apps/console/src-tauri/Cargo.lock
根因:actions/checkout 在 Windows 默认 core.autocrlf=true → CRLF,
而 Cargo.lock 的版本提取把 \n 写死。**同一隐患也在生产代码里**:
version-write.mjs 另有一条 name…\nversion… 正则读旧值,CRLF 下返回
undefined → 把 Cargo.lock 记入 skipped 并跳过 → 发版后滞留旧版本
(正是本模块要消灭的分叉,且不报错)。
修法:读旧值收敛到 readCargoTomlVersion / setCargoLockVersion().before
单一真源,删掉重复正则;新增 splitLineEol() 拆「正文+行尾」后改写
(不能写 ("".*)$ 兜行尾 —— JS 的 . 不匹配 \r)。
② W-01 根因守卫 → 把唯一真源自己判成违规(util\quote.ts)
根因:file.slice(dir.length+1) 在 Windows 得到反斜杠路径,与白名单
的 util/quote.ts 比不中 → 守卫自己错。
修法:新增 relPosix() 归一成 /。
新增 4 条守卫:Cargo.lock / Cargo.toml 的 CRLF 纯函数用例、
CRLF 工作区上真跑 writeVersionEverywhere 再读回文件、
以及用 path.win32 形态字符串在 macOS 上直接测 Windows 分支。
注入故障实测 4 条如期变红;test:unit 325/325(321 + 4)。
|
2026-10-13 18:57:04 +08:00 |
|
jkooe
|
73ed06f5f1
|
fix(console): 修复 sidecar 退出后被复用(Windows 更新超时根因)
现象(Win11 ARM64 真机):点「检查更新」等满 45 秒报「check_update 超时」。
对照:同一台机器手工拉起 sidecar 跑同一请求 6.989s 成功;现场 node.exe
只有被控端一个 —— 控制台的 sidecar 进程根本不存在。
根因:ensure_sidecar 只判「缓存里有没有实例」,不判「那个实例还活着没」。
往已退出进程的管道写,在 Windows 上未必立刻报错(缓冲区吞下即返回成功),
于是请求既不报错也永远等不到响应,只能干等到 request 的超时。
修法:
- Sidecar 增 dead: Arc<AtomicBool>,三处置死(stdout EOF / 写入或 flush 失败
/ shutdown 主动关闭);stdout 关闭时唤醒所有在等的请求,让它们立刻失败。
- 新增 is_alive():dead / child.try_wait() / 已被 take 走,拿不准一律按不可用。
- request() 入口先判存活,不可用即秒失败。
- ensure_sidecar 取到缓存实例先判存活,已退出则丢弃重建。
- 新增 3 条单测(含「对已退出的 sidecar 发请求必须秒失败」——给 30s 超时但
断言耗时 < 5s,因为修复前的表现正是干等到超时)。注入故障实测守卫变红。
- pnpm test:rust 14/14;test:unit 321/321。
属控制台上壳变更,不触发 release patch;但需重建控制台安装包才能让用户拿到修复。
|
2026-10-13 17:59:49 +08:00 |
|
jkooe
|
79969e9d5d
|
chore(release): v2.0.3
|
2026-10-13 17:36:54 +08:00 |
|
jkooe
|
a1c51d62a0
|
docs(changelog): 记入本批三类改动,并统一版本叙述为 v2.0.3
- 安全加固批次:18 条实机复现缺陷(含两条 P0 的完整根因与修法、崩溃链共同根因)
- 控制台单实例保护(症状:Windows 端两个被控端 / 旧服务端在跑时控制台起不来)
- 发版脚本漏改 Cargo 清单导致的版本号分叉
- 版本叙述统一:注释里原先散落 v2.0.4,与下一版号不符,全部改为 v2.0.3
⚠️ 本批含被控端变更,发版时 `pnpm release patch`(→ v2.0.3)必须执行。
|
2026-10-13 17:16:51 +08:00 |
|
jkooe
|
bce11514a9
|
fix(release): 发版脚本漏改 Cargo 清单导致版本号分叉
release.mjs patch --dry-run 实测:只改 8 个 package.json + 1 个 tauri.conf.json,而
apps/console/src-tauri/{Cargo.toml,Cargo.lock} 里**同样存着 2.0.2** —— 发版后必然滞留,
与 tauri.conf.json 悄悄分叉,且**不报错**(Tauri 打包以 tauri.conf.json 为准)。
- 版本写入抽成 scripts/lib/version-write.mjs(纯函数、可测),release.mjs 调用它。
抽出来的理由:埋在脚本里时单测只能断言「源码里出现过某个字符串」—— 那种守卫注入
故障都不会变红(铁律:守卫要落在最终产物字节上)。
- Cargo.toml **只在 [package] 段**改(非 package 段里也可能有独占一行的 version = "…");
Cargo.lock **按 [[package]] 块 + crate 名定位**(全文盲替换会改坏 thiserror 2.0.21
这类依赖版本)。
- release.mjs 的「脏文件白名单」改为取自**实际改写结果**(changed),不再手写文件名列表 ——
手写的那份一旦与写入逻辑漂移,要么拦下本该放行的(发版卡死),要么放过不该放行的
(静默推走改动)。
- 新增 tests/unit/version-write.test.mjs(9 条):在临时目录铺**真实文件**并 git init/git add
(versionTargets 走 git ls-files,不真进索引就会假绿),跑完读回文件断言;另含一条
**仓库版本号一致性守卫**,发版漏项会直接变红。
- 注入 4 处故障实测:编排漏 Cargo / Cargo.toml 全文盲替换 / Cargo.lock 按位置盲改 /
仓库版本分叉 —— 全部变红,还原后全绿。
|
2026-10-13 17:16:46 +08:00 |
|
jkooe
|
530bb4fca7
|
fix(console): 加单实例保护,修「Windows 端起了两个被控端」
真机现象两桩:① Windows 上同时跑着两个被控端;② 旧的服务端还在跑时控制台起不来。
根因同一个:控制台没有单实例锁,再启动一次就是第二个进程 → 第二个 AgentManager
→ 抢同一个 8765 端口 → 后起的那个起不来。另有一条陈旧的 na-gui 计划任务指向测试
部署目录,也在抢同一个端口。
- 引入 tauri-plugin-single-instance(**必须注册为第一个插件**)。
- 第二次启动**不是**静默退出,而是「聚焦既有窗口 + 发 na-second-instance 事件」让前端
刷新状态。这是刻意的:自启动实例是**无窗口**的,若第二次启动静默退出,用户双击图标
只会看到「什么都不发生」—— 那比缺陷本身更难排查。带自启参数(AGENT_ONLY_FLAG)时跳过显示。
- 端口占用的报错补上「怎么找出占用者」:netstat -ano | findstr :<port> /
lsof -nP -iTCP:<port> -sTCP:LISTEN,并新增一条单测。
|
2026-10-13 17:16:41 +08:00 |
|
jkooe
|
ae02252a1d
|
fix(agent): 修复 18 条实机复现缺陷(2×P0 / 5×P1 / 10×P2 / 1×P3)
一轮「静态审查 + 真机实测」的完整普查。基线 v2.0.2 / ad90634 上 unit 291 + e2e 30
全绿 —— 也就是说 18 条**全部落在既有覆盖之外**。报告见 bug-report-2026-10-13.html。
P0 两条:
- W-01:window.focus 把 JSON.stringify(title) 插进 PowerShell **双引号**串(PS 里 `\` 不是
转义符、双引号串还会展开 $(...))→ 命令注入。真机实测最险的是它**静默**:接口返回的是
「正常业务错误」E_EXECUTION_FAILED: 未找到匹配的窗口,而 payload 已经把文件创建出来了。
新增 apps/agent/src/util/quote.ts 作 psLit/shLit 单一真源,14 处注入点收敛(window.focus /
uiaFind / ocrFind / system.ts 计划任务参数 / ps-helper.ts 的 -File 裸拼 …)。
- A-01:ACL 省略 clients 时 aclPolicy.clients.find 抛错,而这段落在 if (hasAcl) 块**之外**
无条件执行 → 配置少写一个字段,每条 invoke 都崩掉被控端。
⚠️ 静态审查当时判它是「fail-open 放行」,实测是崩溃(更严重)。
崩溃链共同根因:ws.on('message') 用 void handleMessage() 且无 .catch,全仓又没有任何
process.on('unhandledRejection') —— Node ≥22 遇未处理 rejection 直接终止进程。两条畸形报文
(hello {protocol:123} / auth {hmac:1})就能在**认证之前**打死被控端(verifyNonce 有 try/catch,
psk 默认路径的 verifyHmac 没有)。index.ts 新增进程级兜底:日志每次打、审计按 10s 限流
(否则畸形报文可高频刷爆 audit.log),且明确不退出进程(可用性优先,取舍写在注释里)。
其余 15 条:
- clients: [] → fail-open 全放行(判据改为「对象是否存在」而非「数组长度」)
- macOS screen.find 的 text 拼 bash → $(...) 实测执行(走 shLit)
- 控制台审计筛选项与协议 enum 漂移,除 invoke 外全 E_PARAM_INVALID
→ 新增 packages/protocol/src/audit-types.ts 作唯一真源,manifest enum 由它派生 + 编译期双向守卫
- monitor.start 的 target 注入、monitor id 路径穿越(读/删)→ psLit + guardPath
- keys{} + client_id:"constructor" 崩 → hasOwnProperty(原型链取值)
- 事件订阅跨控制端可见/可删、event.watch 绕开 fs_roots → 有状态资源落 owner + 统一白名单
- screen.capture 的 region 无上界 → assertRegionSane() 且**提前到枚举显示器之前**(fail fast)
- ipMatchesCidr('8.8.8.8','10.0.0.0/') → true(空掩码被 Number('') === 0 当 /0)
- CLI --timeout 静默失效(strict:false 下未声明选项被当 boolean,值掉进 positionals)
- 拒绝类事件不写审计 → 补 request.rejected / agent.error
回归:unit 321/321(+21)、e2e 33/33(+3 组硬化用例)、rust 11/11。
e2e 新增用例的姿势:每场景**独占端口 + 独立临时 NODEAGENT_HOME**(首版复用端口,
连上了上一块的残留实例,把对照组误判成「实现有缺陷」)。
新增守卫均按铁律注入故障实测:确认会变红、还原后复核一致。
|
2026-10-13 17:16:36 +08:00 |
|
jkooe
|
ad90634a18
|
fix(test): 兜底守卫的片段排除在 Windows 上失效(跨平台路径分隔符)
CI(Windows E2E,run 38166982975)在「单元测试」步骤红:
not ok 102 - 兜底:capabilities 下命中坐标信号的新文件必须引用 PS_DPI_AWARE
- apps\agent\src\capabilities\win-ps\prelude-image.ts(命中 "CopyFromScreen")
而本机 macOS 全绿 —— 又是一例「本地全绿 ≠ Windows 绿」。
根因:`rel = relative(ROOT, full)` 在 Windows 返回 `apps\agent\…`,而排除规则写的是
`rel.includes('capabilities/win-ps/')`(**硬编码 "/")→ 在 Windows 上恒为 false
→ 片段文件被误报成违规。排除本身是**对的**(prelude-* / body-* 由 WIN_HELPER_PRELUDE
在最前面统一注入 DPI 感知,其序位已由第 5 条断言),错的只是判断依赖了平台分隔符。
修法:
- 新增 `toPosix()`:把任意平台路径归一为 posix 形态(附注释说明为何必须显式归一);
- 把「是否排除」收敛成纯函数 `isWinPsFragment(relPath, name)`,**函数内自带归一化** ——
调用方传什么形态都正确,且可在本地直接断言;
- `walk` 内改用归一化后的 `rel` 并调用该函数。
新增守卫(第 9 条):直接喂 Windows 反斜杠路径断言排除逻辑,把这类差异钉进
「本地可验」的范围。已用 `path.win32.relative()` 在本地**真实复现** CI 场景
(修复前 false → 误报;修复后 true → 正确排除),并注入故障实测该守卫会红。
测试:dpi 守卫 9/9;pnpm test:unit 291/291。
|
2026-10-12 03:17:44 +08:00 |
|
jkooe
|
02887e265b
|
fix(agent): 图形能力在高 DPI(200% 缩放)下坐标错位
真机(Parallels 27 + Win11 ARM64,主机 200% 缩放、物理 3600×2020)暴露:
被控端 PowerShell 助手进程**默认 DPI 不感知**,同一进程里并存两套坐标系 ——
| 调用 | 不感知时 |
|---|---|
| Screen.Bounds / VirtualScreen | 逻辑 1800×1010 |
| CopyFromScreen | 按物理裁剪 → 缺省只截到屏幕左上 1/4 |
| SetCursorPos | 被系统 ×2 |
| UIA(screen.find) | 物理(与进程是否感知无关) |
净效果:screen.find 给出的物理矩形**不能**直接喂 input.mouse.*,必须人工除以 scale
(上一轮靠这个临时绕法才点中主题按钮 —— 能绕,但属用户侧的隐式约定,必须消灭)。
- 新增 apps/agent/src/util/dpi.ts:PS_DPI_AWARE(SetProcessDPIAware 前置段)。
- 注入全部坐标相关脚本:screen.ts / input.ts / window.ts(WIN_HELPER_PRELUDE,覆盖
window.list 与 window.focus)/ record.ts;并把它从 win-ps/prelude-ocr.ts 的 try{} 里
移出 —— 放在 try 内一旦抛错就被 catch 吞掉,助手会静默退回不感知。
- screen.ts 显式改用 $d.Bounds.Width(已是物理),scale 仅作参考。
- 新增守卫 tests/unit/dpi-awareness.test.mjs(8 项):断言**实际下发的脚本文本**,
而非「源码里出现过常量」—— 首版按源码扫描,注入故障实测不会变红;改为断言
buildInputPsScript / buildListDisplaysScript / buildCaptureScript / WIN_HELPER_PRELUDE
的脚本串后,二次注入故障精准变红。
真机终验(零换算):
- screen.info 3600×2020,与裸 GetSystemMetrics 一致;
- input.mouse.move(x,y) 读回物理光标 == (x,y)(4 处抽样全中);
- screen.find 物理矩形中心**直接** click → 主题单选 UIA selected 正确翻转且可逆。
文档:VM-VERIFICATION.md 新增 §10(坐标口径缺陷本体 / 复验配方 / 输入注入失效的逐条
排除与判决证据 / 两条 prlctl 通道的语义差异);console-plan.md 补 ARM64 真机安装冒烟与
界面实点记录(含新发现 P2:控制台无单实例保护)。
⚠️ CHANGELOG 备注:本批**属被控端变更**,与上一节「本轮不升版本号」的结论不同 ——
发版必须升 PATCH(→ 2.0.3),否则 agent.mjs 会谎报一个并不包含本修复的版本。
测试:dpi 守卫 8/8;pnpm test:unit 290/290。
|
2026-10-12 03:11:34 +08:00 |
|
jkooe
|
c20e71bddc
|
docs: 补齐 Windows 端控制台文档(含三次 run 的失败-修复-实证全过程)
README / CHANGELOG / docs/console-plan.md 三处同步:
- README:补 Windows 双架构安装包实测体积、第三条断言(解开安装包查内容)、
以及实测解出的 node.exe 体积与 PE Machine。
- CHANGELOG:新增「Windows 端控制台安装包」整节,含双架构理由、
为什么用原生 ARM runner、cfg!(windows) 分支首次被编译器看到、
以及三次 run 的完整过程(体积断言写错 → `&` 调 GUI 程序不等待 → 双绿)。
- docs/console-plan.md:断言表 2 条 → 3 条;新增三节实测记录;
产物形态表补实测体积;8.2 指标验收的「Win 安装包 < 40MB(待实测)」补上实测值。
另修复:`## 界面观感约定(2026-10-11)` 标题被上一轮编辑误吞
(正文直接从「两轮纯界面调整」开始),已恢复。
留下两条可迁移教训:
1. 体积阈值属于「中间逻辑自洽」式断言 —— 判据要落在产物内容上;
且压缩容器与未压缩目录的体积不可直接比。
2. `msiexec.exe` / NSIS 安装包是 GUI 子系统程序,PowerShell `&` 调用
既不等待也不设 $LASTEXITCODE → 必须 Start-Process -Wait -PassThru。
|
2026-10-12 00:20:50 +08:00 |
|
jkooe
|
e616e80819
|
fix(ci): 取安装器退出码改用 Start-Process -Wait,& 调 GUI 程序不等待也不设退出码
第二个 run(38152850819)两 job 仍同点红,但已是另一处原因,且同样是
「静默失效」:日志里 `msiexec /a 失败(退出码 )` —— $LASTEXITCODE 是空的,
而整步从开始到抛错只过了 0.8 秒。
根因:msiexec.exe 与 NSIS 安装包都是 **GUI 子系统程序**,PowerShell 用 `&`
调用时**既不等待其结束、也不设置 $LASTEXITCODE**(于是 $null -ne 0 判真,
在安装器还没跑完时就当场抛错)。arm64 那条没有 MSI、直奔 NSIS,
失败形态一模一样,正是同一根因的第二个样本。
修法:
- 两处都改 Start-Process -Wait -PassThru,读 $p.ExitCode(确定性等待)。
- 参数拼成**单个字符串**传入,避免 -ArgumentList 数组被 PowerShell 自动加引号
而破坏 NSIS `/D=` 的「必须原样、不加引号、且是最后一个参数」语义。
- 新增 Wait-PayloadRoot:安装器可能「自己先退出、由子进程继续装」,
给最多 30 秒收敛时间再判定「找不到 node.exe」,避免竞态误报。
- 打印安装器实际退出码,便于下次一眼看出是哪一类失败。
|
2026-10-12 00:05:01 +08:00 |
|
jkooe
|
71f2cd2597
|
fix(ci): 控制台 Windows 断言改为解开安装包查内容,不再用体积阈值
首个 run(38151366534)两个 job 双双失败,且失败点完全一致:
第 14 步「断言安装包存在且体积合理」。构建链路本身全绿 ——
tauri build(msi 34.2MB / nsis 19.5~23.2MB)、产物自举
(打包内 node v22.23.3 真跑起打包内 sidecar)都成功,
是守卫自己错了。
错在哪:断言写成「安装包体积 > 50MB」,理由是「含 Node 运行时约 80MB」。
这把**未压缩的目录形态**(.app ≈ 92MB)当成了**压缩安装包的体积** ——
LZMA 下 node.exe 自身就能压到 1/3~1/4,阈值必然误杀。
更根本的是:体积阈值属于「中间逻辑自洽」式断言,包大不等于
node.exe 真在里面,且压缩率会随版本漂。
改为解开安装包、查内容(msi 走 msiexec /a 管理安装,nsis 走
/S 静默安装),一次钉死三件事:
① node.exe / sidecar.mjs / agent.mjs 三样随包资源必须都在;
② node.exe 的 PE Machine 必须等于目标架构(最终产物的架构断言,
对治「arm64 包里装的是 x64 node.exe」那类只在真机暴露的事故);
③ node.exe 解压后 > 40MB,挡住占位文件。
附带收益:真装一遍本身就是安装器冒烟。
另加 timeout-minutes: 8,避免静默安装万一卡住把 job 拖满 60 分钟。
|
2026-10-11 23:48:29 +08:00 |
|
jkooe
|
1471708f35
|
ci: 新增 Windows 控制台安装包构建(x64 + arm64 双架构)
在此之前,控制台的 Windows 侧只被验证到「prepare:node + 自举」(见 windows-e2e),
**从未真跑过 tauri build**。而 Rust 里那些 cfg!(windows) 分支(taskkill 收进程树、
PowerShell 计划任务、node.exe 路径解析)在 macOS 上只被类型检查、
从未被编译成 Windows 目标,更没有产物级验证 —— 本工作流是它们的第一道真闸。
为什么必须双架构
----------------
被控端发布包出过「arm64 包里装的是 x64 node.exe」的事故:在 ARM Windows 上
**能 listen、但 TLS 握手不完成**,且 macOS 侧 100% 复现不出。
控制台同理 —— 它内含 node 运行时,且要用它**托起本机被控端**(即 TLS 服务端)。
故 x64 / arm64 必须各出一套。
为什么用原生 ARM runner 而不是交叉编译
------------------------------------
`windows-11-arm` 是 GitHub 的 ARM64 Windows runner(公共仓库免费,本仓库为 public),
在它上面 arm64 是**原生构建**。若改走 x64 机器交叉编译,需要 VS 的 ARM64 工具链,
且 prepare-console.mjs 的自检要**真执行** node.exe(x64 机器跑不了 arm64 二进制),
会逼着引入「跳过自检」这类削弱验证的开关 —— 宁可等原生 runner。
两条断言是必需项,不是顺手检查
----------------------------
1. **构建机架构 ⇄ 目标架构**:ARM runner 镜像里确实存在 x64 模拟的组件
(社区实测 python 即为 AMD64)。一旦 Node 也是模拟版,process.arch 会是 x64
→ 把 x64 的 node.exe 打进 arm64 安装包 → 真机上能启动、TLS 握手不完成,而 CI 全绿。
2. **随包 node.exe 的 PE Machine**:与 pack.mjs 的双架构校验同源思路 ——
不只看「脚本跑完了」,而是直接读最终要打进那个包的二进制头。
中间逻辑再自洽,也可能因上游给了错架构的二进制而产出错包。
另断言安装包体积 > 50MB:含 Node 运行时(约 80MB),体积过小意味着运行时没打进去
—— 那种包能装、但一启动就找不到 node。
产物:x64 出 msi + nsis(企业批量部署 + 普通双击各一),arm64 只出 nsis
(WiX 的 arm64 MSI 支持面较窄,不值得为它引入不确定性)。
|
2026-10-11 23:25:16 +08:00 |
|
jkooe
|
456e452b30
|
test(console): 补两条「静默失效」守卫 —— CSS 变量闭合 + 首帧底色一致
主题改动引入了一类新的静默失效:**拼错的 CSS 变量名不报错**。
`var(--na-sunkenTypo)` 类型检查过、构建过、运行也不抛 —— 颜色只是落回继承值,
界面「看着对但就是不对」,与本项目历来吃亏的失效模式同源。故补守卫:
7. 扫 src/** 全部 var(--na-*) 使用点,与 cssVars() 的键集合比对
8. index.html 的首帧兜底色 ⇄ 调色板 bg
第 8 条是因为首帧防闪脚本是全应用**唯一允许的色值重复处**(其执行时点早于任何模块加载,
重复无法消除);既然允许重复,就必须有东西盯着它,否则改调色板忘了同步
→ 深色用户每次启动闪一帧白。
注入故障实测两条都会红:
var(--na-sunken) → var(--na-sunkenTypo) 第 7 条红
var(--na-sunken) → var(--na-sunkenx) 第 7 条红
index.html #f4f4f5 → #ffffff 第 8 条红
复原后 8/8 绿,文件逐字节还原。
第 7 条顺带暴露过一次**守卫自身漏报**:正则原写 [a-z0-9-]+,遇到 `sunkenTypo`
这种含大写的拼错会在 `T` 处停下导致整个模式不匹配 —— 写错的变量名反而逃过检查。
放宽到 [^)\s,;]+ 后才真正生效。
教训:**注入故障不只验证守卫有没有,还要验证它覆盖了目标形态**。
单测 280 → 282。
|
2026-10-11 14:34:41 +08:00 |
|
jkooe
|
4e6eb32929
|
style(console): 配色收敛为「简约白 / 高级灰」两套中性灰阶主题
原配色是深色 + 品牌青绿(#63e2b7)+ naive-ui 默认绿主色 —— 颜色种类多。
改为两套**只用中性灰阶**的主题,侧栏底部一行切换,选择存 localStorage:
简约白 页底 #f4f4f5 / 侧栏 #fafafa / 卡片 #ffffff
高级灰 页底 #1d1f22 / 侧栏 #181a1d / 卡片 #24262a
几处刻意的选择:
1. 白主题页底**不是纯白**。卡片本身是白的,两者同色就只能靠 1px 分线撑层次,
整页发「糊」;压一档浅灰后层次为「页底 < 侧栏 < 卡片」,且仍是白为主。
2. 主色位(naive-ui 的 primary)改**中性灰** —— 主按钮、开关激活态、进度条、
勾选框全吃这一个值。原来它们是绿色/青绿,正是「配色不简单」的主因。
3. 语义色(成功/警告/错误/信息)**降饱和而非去掉**。这是运维工具,
「运行中 / 出错」必须一眼可辨,全部灰化会导致误读。故它们只出现在徽章与
告警这类**小面积**上,底色观感仍是「白 + 灰」。
4. **色值只定义一份**:src/theme/index.ts 的 Palette 既生成 naive-ui 的
themeOverrides,又由 App.vue 运行期注入成 --na-* CSS 变量。
若「TS 调色板」与「CSS 写死色值」各存一份,换主题时会**一半新一半旧、
不报错、类型也抓不到** —— 正是本轮要消灭的失效模式。
为此清掉散在 8 个文件里的 17 处写死色值(日志三流着色、代码块底色、
节点选中边框、历史悬停…),复扫后自定义样式里硬编码色为 0。
5. 主题**只存 localStorage**,不落 Rust 侧 settings:那套 Settings 是给
「仅服务模式」(无窗口、常驻托盘)也生效的偏好用的;主题是纯图形偏好,
没有窗口时毫无意义 —— 为一件纯前端的事去动 Rust 命令面与契约守卫不划算。
6. index.html 内联一小段脚本做**首帧防闪**(否则深色用户每次启动闪一帧白)。
此处**刻意重复 2 个底色值**,已在 index.html 与 theme/index.ts 两处注明
「改底色时同步」—— 因为在它执行之前没有任何模块可依赖;
除此之外全应用再无第二份色值。
验证:vue-tsc 0 错 | 契约守卫 6/6 | 单测 280/280 | .app 重打通过(92.22 MiB)|
两套主题均以真实采集数据回填渲染核对 —— 截图脚本顺带读回 --na-bg 与 body 背景,
确认主题真的切过去了,而不是「看起来像」。
|
2026-10-11 14:29:50 +08:00 |
|
jkooe
|
591e44721a
|
style(console): 界面一律不使用图标 —— 导航改纯文字,并关闭内置插画/图标
触发原因是一次真实的渲染事故:侧栏那些 emoji 图标在非系统字体栈的渲染器里
退化成**豆腐块**(本机截图对比可见)。试过给图标指定 Apple Color Emoji、
全局字体栈加 !important、乃至 font-variant-emoji —— 分别无效或更糟
(最后一招把 ASCII 数字也拆成了 emoji 呈现)。而「哪套字体栈一定带 emoji」
跨平台并不成立,与其赌字形,不如不用。
emoji 图标 → 路由表 meta.icon 字段整体移除,侧栏改纯文字
n-empty 空态插画 → 16 处 :show-icon="false"(空态里的插画比图标更「图形」,
分布在 9 个页面,是最容易漏掉的一类)
n-alert 类型图标 → 7 处(naive-ui 的 showIcon 默认 true,不显式关就在)
n-popconfirm 询问图标 → 3 处
夹在文案里的 ⚠ → TerminalView 的「高权限操作,请谨慎」保留纯文字
严重程度不靠图标也能读:n-alert 仍由 type 的底色/边框区分。
顺带清掉随包的模板残留资产:
public/vite.svg、public/tauri.svg —— public/ 下的文件会被整体复制进产物,
此前等于把 Vite / Tauri 的 logo 一并发出去了(且 index.html 的 favicon 正指向它);
src/assets/vue.svg —— 无人引用;
index.html 的 <title> 还是模板默认的「Tauri + Vue + Typescript App」。
原生窗口标题取自 tauri.conf.json("nodeagent 控制台"),故此前不影响观感,
但与 dev 态浏览器标签页不一致,一并改齐。
「不属界面图标、刻意未动」的三类写明在 CHANGELOG 里:
托盘图标(菜单栏没图标就点不到,关窗后应用无法寻回,属功能必需)、
被控端 CLI 自身打印的文本标记(⚠️/ℹ️/✅,是程序输出内容,非界面元素)、
控件的结构性部件(下拉箭头、折叠箭头、输入框清除钮、表格排序箭头)。
验证:vue-tsc 0 错 | 契约守卫 6/6 | 单测 280/280 | 前端产物内已无任何 svg 与 logo 字样 |
macOS .app 重打通过(92.21 MiB,前端在无 public/ 下亦正常构建)。
|
2026-10-11 13:50:49 +08:00 |
|
jkooe
|
2528f6b709
|
docs: 归档「控制台去命令行化」—— CHANGELOG / console-plan / README
CHANGELOG:新增 [未发布] 章节,并按「刻意不升版本号」写明理由 ——
本轮新增的全是控制台功能,被控端能力集合未变;而 bundle-agent.mjs 会把根
package.json 的 version 注入 agent.mjs(__AGENT_VERSION__),
一旦为控制台功能升号,被控端就会谎报一个它并未包含的版本。故维持 2.0.2。
console-plan:新增「本机服务托管」章节,含需求逐条落点对照表、
消灭命令行窗口的三条措施、四条实测踩出来的约束、播种防降级的规则、
「不靠单一信号判实例在跑」的交叉判定、以及仍需图形环境人工实点的清单。
README:控制台页面表 7 → 9(补「本机服务」与「服务日志」),
打包命令改为 pnpm --filter nodeagent-console bundle,
体积数字修正 —— 原文写的 .app ≈ 6.8 MiB 是 Node sidecar 方案之前的旧数,
现为 .app 90.8 MiB / .dmg 33.78 MiB,并补「无需预装 Node」「自启与图形能力
要求登录用户会话」两条注意事项。
|
2026-10-11 11:42:27 +08:00 |
|
jkooe
|
ad569d4e6d
|
ci: 控制台资源断言 2→3,并补 @nodeagent/agent 构建前置
控制台随包分发的资源由 2 份增至 3 份(sidecar.mjs / node / agent.mjs):
- rust.yml 的资源项断言同步改为 3,且不再只数个数 ——
改为断言目标名确实包含 sidecar.mjs 与 agent.mjs、以及 node/node.exe 二者任一。
只数个数的话,「三份但错了一份」照样过。
顺序依赖(本地验证发现不了的一类):
prepare:node 现在还要 esbuild 打包被控端,故 @nodeagent/agent 的 dist 成为新的
前置产物。本地验证时 dist 早就在,会把这层依赖完全掩盖 —— 故在 steps 里显式写全
构建步骤,并加注释说明为什么顺序不能调。
rust.yml 的 paths 触发器补 scripts/lib/bundle-agent.mjs 与 apps/agent/**:
改这两处同样会让控制台打包结果变化,不触发就等于零覆盖。
windows-e2e.yml 同步:Windows 分支现在也覆盖 agent.mjs 的准备与解析。
|
2026-10-11 11:42:14 +08:00 |
|
jkooe
|
2eb6a6f8bb
|
feat(console): 本机被控端托管 —— 启停/重启/日志/自更新/托盘/开机自启
目标:双击应用即用,全程不碰命令行。
要求 落点
不再打开命令行窗口 agent.rs 的 CREATE_NO_WINDOW + 自启拉控制台而非裸 node
启动/停止/重启 AgentManager 五态状态机 + 界面按钮 + 托盘菜单
检查更新 check_update / download / apply_update
日志功能 stdout/stderr 双泵 + 环形缓冲 + 实时推送 + 过滤/导出
后台运行 托盘常驻,关窗最小化而非退出
开机自启 macOS LaunchAgent / Windows 计划任务
新增「本机服务」页(/local,默认首页)与「服务日志」页(/logs)。
默认路由由 /nodes 改为 /local:打开应用先看到本机被控端的启停面板,
而不是先要求用户填一个连接地址。
四条实测踩出来的关键约束(均已在代码里注明原因):
1. 退出监控用轮询 try_wait,绝不把 Child 交给 wait()
—— wait() 会取得 Child 所有权,此后 stop() 再也拿不到它去 kill。
2. 日志游标用独立单调 seq,不用位置下标
—— 环形缓冲淘汰旧行后位置下标会漂移,前端增量拉取静默错位(不报错)。
3. start() / restart() 的接收者必须是 self: &Arc<Self>
—— 监控任务要持自持引用。
4. macOS gui/<uid> 的 uid 运行时 id -u 现取,不硬编码 501。
播种(seeding)防降级:只有「随包版本 > 已安装版本」才覆盖数据目录里的脚本,
且版本比较按数值逐段(2.0.10 > 2.0.9)。若写成「同版本才跳过」,
用户自更新过被控端后下次开控制台会被静默降回旧版。
「实例是否在跑」不靠单一信号:管理器状态与 TCP 端口探测交叉判定,
以区分「本控制台托管的实例」与「手工安装的非托管实例」并给出不同处置。
消灭黑窗有三条措施,第 2 条最易漏:Rust 的 std::process::Command 默认不设
CREATE_NO_WINDOW,于是「界面上点一下启动」= 屏幕上闪一个控制台窗口。
控制台现在随包分发被控端脚本(资源 2 份 → 3 份:sidecar.mjs / node / agent.mjs),
用户装完控制台本机被控端即已就绪。
新增 tests/unit/console-commands.test.mjs(6 用例)钉住三类「不报错的失效」:
invoke ⇄ generate_handler! 双向、emit ⇄ listen 双向(这类失效会让界面永不刷新且零报错)、
bundle 资源名 ⇄ Rust resolve_* 字面量、sidecar 方法名、路由表 ⇄ 实际 view 文件。
已注入故障实测会红(改命令名与事件名 → 当场 2 条失败),非空转守卫。
验证:cargo check 0 错 0 警 / cargo test --lib 10·10 通过(含真起 node 子进程)/
clippy -D warnings 通过 / vue-tsc 通过 / 单测 280·280(274 → 280)/
verify-bundle 真 spawn 打包内 node 跑通打包内 sidecar。
⚠️ 托盘、关窗最小化、开机自启、界面点按钮仍需图形环境人工实点。
|
2026-10-11 11:41:56 +08:00 |
|
jkooe
|
a0be43d76c
|
refactor(pack): 被控端 esbuild 配置抽为单一实现
`agent.mjs` 现在有两个消费方:
1. scripts/pack.mjs → Windows 便携包 / 自更新载荷(Release 资产)
2. scripts/prepare-console.mjs → 桌面控制台的随包资源(控制台要能托起本机被控端)
若各写一份 esbuild 配置就是「两套真相」——改了一处漏另一处时构建过、单测过,
只在真机产物里暴露(本项目已多次吃过这类亏)。故配置只此一份。
bundle-agent.mjs 导出 repoRoot / repoVersion / gitShortCommit / bundleAgent,
沿用原有全部约束:JS API(pnpm 隔离布局下 .bin/esbuild 在 Windows 不存在)、
不注入构建时间(否则每次打包字节不同,指纹比对失真)、
define 用 JSON.stringify 得纯双引号。
本次对 pack.mjs 的输出无语义变化(同一份 options 原样迁移)。
|
2026-10-11 11:41:38 +08:00 |
|
jkooe
|
9ae1326be2
|
docs(readme): 版本基线更新到 v2.0.2
v2.0.0(10-10)那条基线已过期。现基线:v2.0.2(2026-10-11,资产修复 ——
v2.0.1 的 arm64 包内实为 x64 node),51 项能力 · 274 单测(本地实跑全绿)。
|
2026-10-11 10:45:48 +08:00 |
|
jkooe
|
1ab9ed22c7
|
docs(vm): 图形能力实测归档 + §9 复验配方
- §7 图形能力行:⏳ 待登录态 → ✅ **登录态下全通**(真实桌面截图 1800×1010 / window.list /
screen / audit verify),并注明「会话 0 的失败是身份问题,非产品缺陷」
- 修正重复的 §7 编号(→ §8),复位一处错位的表格行
- §6 补三条排查项:x64 Node 的 Prism 现象、`screen.capture` 的会话身份、
**微软账户不支持传统 autologon**(MSA 凭据走 CloudAP,`LogonUser` 恒返回 1326)
- 新增 §9「图形能力复验配方」:本地账户 autologon → 计划任务 Interactive 启动 →
三件核对 → 证书指纹变更的处置 → 验证完的安全清理
|
2026-10-11 10:32:48 +08:00 |
|
jkooe
|
5e6231493e
|
fix(pack): Windows 打包条目带目录前缀 —— 与 macOS 产物结构不一致
CI 在 f85acad 上红了:`✗ zip 内未找到条目 node.exe: release/nodeagent-win-x64.zip`
(而本地 macOS 全绿 —— 方向恰好相反)。根因是两平台用了不同打包器:
- macOS/Linux: `cd <dir> && zip -r <out> .` → 条目是裸名 `node.exe`
- Windows : `Compress-Archive -Path '<dir>\*'` → 条目带前缀 `<dir>/node.exe`
两处一起修:
1. `scripts/lib/zip-pe-arch.mjs`:条目名匹配改用 **basename**,兼容两种打包方式。
已用构造的带前缀 zip 实测通过,并注入错架构验证守卫仍会红。
2. `scripts/pack.mjs`:Windows 分支改用 .NET `ZipFile::CreateFromDirectory`
—— 它打包**目录内容**,条目同样是裸名,从根上消除两平台产物结构差异。
否则 CI 校验的产物与发布产物**不同构**,校验本身的价值打折;
且「解压后双击 install.cmd」的说明会因多一层目录而失配。
教训:**「本地全绿」≠「Windows 绿」** —— 平台差异只有真 runner 能暴露(第 N 次实证)。
|
2026-10-11 10:32:41 +08:00 |
|
jkooe
|
f85acad5db
|
chore(release): v2.0.2
|
2026-10-11 10:18:28 +08:00 |
|
jkooe
|
f558e3e7df
|
chore(release): 交付物架构断言 + CHANGELOG 归置 v2.0.2
已公开发布的 nodeagent-win-arm64.zip 内装的其实是 x64 的 node.exe
(两包内 node.exe 字节完全相同,PE Machine 均为 0x8664),该包在 ARM Windows 上
能启动、能 listen,却 TLS 握手不完成 —— 被控端实际不可用,而 macOS 侧 100% 复现不出。
- 新增 scripts/lib/zip-pe-arch.mjs:纯内置 zlib 手解 zip、读 PE 头判架构。
不依赖 unzip / Expand-Archive(Windows runner 上没有 unzip)。
含 CLI 自检入口,已注入故障实测会红(arm64 包去要 x64 期望 → 退出码 1)。
- release.mjs:生成资产前调用之,不符即中止发版 —— 拦在 tag/push 之前,
是「交付物本身」的断言,与 pack.mjs 的「流程内部」断言分两层。
- windows-e2e.yml:把原先内联的 PowerShell 校验实现改为复用同一模块,
消除两套实现各自漂移的可能。
- CHANGELOG:[未发布] 归入 v2.0.2。
|
2026-10-11 10:17:30 +08:00 |
|
jkooe
|
87b74fd015
|
docs(vm): B5 按实测改写 —— arm64 包原生可用 / win-x64 包 TLS 握手不通
原 B5 的假设是「x64 包在 ARM 上也许能跑」,本轮实测证伪:能启动、能 listen,
但 TLS 握手不完成(E_TIMEOUT),被控端实际不可用。补对照数据与复验步骤;
同步更新检测表、前置准备(③④ 改用 arm64 包)、结论标注规则与新增 §7 实测记录。
|
2026-10-11 09:48:10 +08:00 |
|
jkooe
|
b2404291b0
|
fix(pack): arm64 包内实为 x64 node —— 缓存名不含架构 + 补 PE 架构断言
release.mjs 先打 x64 时会把运行时缓存为 node-runtime.zip(该名不含架构),
随后打 arm64 时误判「已缓存」直接复用 → arm64 包内 node.exe 实为 x86-64
(两份公开包内 node.exe 字节完全相同,sha256 同为 fdddbf45…,PE Machine=0x8664)。
该包在 ARM Windows 上能启动、能 listen,但 TLS 握手不完成(控制端只见 E_TIMEOUT),
且 macOS 侧完全复现不出。
- pack.mjs: 缓存名改为 node-runtime-<arch>.zip(按架构隔离)
- pack.mjs: 解包后断言 node.exe 的 PE Machine 与 --arch 一致(防呆 C,已注入故障实测会红)
- ci: windows-e2e 补「双架构包 PE 架构校验」,直读最终 zip 内 node.exe 的 PE 头
(此前 CI 只打 x64 且只验 x64,arm64 打包零覆盖 —— 该 bug 必然逃过 CI)
- docs: CHANGELOG 记录 ARM64 真机对照实测
真机实测(ARM64 Windows 虚拟机,同机同参对照):
arm64 → TLS 握手成功 + PSK + 51 项能力两端一致 + exec/审计全通
x64 → E_TIMEOUT(复现「能 listen 但 TLS 握手不完成」)
|
2026-10-11 09:42:32 +08:00 |
|
jkooe
|
e816c1fd39
|
fix(ci): Windows 上补 sidecar:build —— 新加的步骤漏了前置产物
本轮新增的「控制台 Node 运行时(Windows 分支)与自举验证」一步在真实 runner 上失败:
✗ 未找到 D:\a\nodeagent\nodeagent\apps\console\sidecar\dist\sidecar.mjs
根因:prepare-console.mjs 的前置检查要求 sidecar 已构建,而该步被排在了构建 sidecar 之前
(rust.yml 里恰好已有「构建 sidecar」在前,所以那边没暴露)。
教训(本地跑过 ≠ CI 跑得过,又一例):本地验证时 sidecar/dist 早已存在,
把「步骤之间的产物依赖」这一层完全掩盖了。**新增 CI 步骤时,应逐条核对
「该步依赖的产物由哪一步生成、顺序是否确实在其后」,而不是在已有产物的本地环境里跑一遍就认为通过。**
|
2026-10-11 09:00:09 +08:00 |
|
jkooe
|
a6be917637
|
docs: 记录控制台 Node 运行时分发与体积口径调整
- console-plan.md:
· §8.2 打包体积指标口径由「Mac .app < 30MB」改为「**分发件** Mac .dmg < 40MB」
—— 原数字是 Tauri + Rust client 时代定的(当时应用内不含 JS 运行时);
改 sidecar 方案后应用必须自带 Node 运行时,.app 因此到 90.8MB,但用户下载的 dmg 仅 33.8MB。
已明确标注「口径调整,待确认」。附本机 Electron 运行时本体 207MB 的实测对照。
· 新增「Node 运行时分发」节:做法、三处实测约束(编译期契约 / strip 须重签 / 映射须对齐
resolve_node)、验证方式(verify-bundle 真跑产物)、实测体积。
· 「已知待办」由两条收敛为一条(剩 GUI 端到端人工验证,需图形环境)。
- CHANGELOG.md:新增 [未发布] 节,记录运行时分发功能与两处规避(SIGKILL 137、编译期契约)。
|
2026-10-11 08:55:45 +08:00 |
|
jkooe
|
8664ec2926
|
ci: 覆盖 Node 运行时准备与「产物自举」(Linux + Windows 双覆盖)
新增 runtime 准备脚本后,两件事此前零覆盖:脚本本身在 Windows 上的分支、
以及「打包产物能不能真跑」。补上:
- rust.yml:prepare:node 真跑(验证脚本可用 + 产物自检)→ 断言注入片段声明两份资源
→ `verify-bundle --runtime` 真 spawn 运行时跑 sidecar 走一次 JSON-RPC。
paths 触发加上 scripts/prepare-console.mjs。
注:Linux 上不走 strip 分支(macOS 专属),故「strip 必须 adhoc 重签」由 macOS 本机实测覆盖。
- windows-e2e.yml:新增一步跑 prepare:node + verify-bundle --runtime —— 该脚本在 Windows
上走的是「复制 node.exe、无 strip」的另一条分支,此前从未被真实 runner 验证过。
|
2026-10-11 08:55:38 +08:00 |
|
jkooe
|
22bc732334
|
feat(console): 控制台随包分发 Node 运行时(目标机器无需预装 Node)
控制台的 sidecar 是 .mjs,此前只能在装了 Node 的机器上跑(resolve_node 会回退系统 node)。
新增 scripts/prepare-console.mjs 把 Node 运行时准备为 Tauri resource,随包分发给最终用户。
- prepare-console.mjs:复制本机 node → apps/console/src-tauri/runtime/,macOS 上 strip -x
(108MB→86MB)**并 adhoc 重签**,再生成注入片段 tauri.bundle.gen.json;带幂等(同源跳过)
与**产物自检**(真跑一次 node,失败即删除产物并报错,避免坏产物被静默复用)。
- apps/console/scripts/verify-bundle.mjs:验证「打包产物能否自举」——真 spawn 打包内的 node
去跑打包内的 sidecar 并走一次真实 JSON-RPC 握手。
- package.json:新增 prepare:node / verify:bundle,bundle = sidecar:build → prepare:node →
tauri build --config → verify:bundle(一条命令产出可分发件并自证可用)。
- src-tauri/.gitignore:忽略 /runtime/(~86MB)与生成的注入片段(生成物不入库)。
三处实测踩出来的约束(均已写进脚本注释与 console-plan.md):
1. **macOS 上 strip 会使 Mach-O 代码签名失效 → 内核直接 SIGKILL(exit 137)**,且无任何有用报错。
实测对照:原样复制 ✓ / strip 不重签 ✗(137) / strip + `codesign --force --sign -` ✓。
故 strip 之后的 adhoc 重签是**必要配套**,不是可选优化。
2. **Node 运行时不得声明进默认 tauri.conf.json** —— bundle.resources 是编译期契约
(tauri-build 校验声明的文件必须存在),声明「运行期才就绪」的资源会让 cargo check/test
在 build.rs 阶段直接失败(2026-10-10 已踩)。故一律走 `tauri build --config` 注入。
3. **resource 映射必须与 resolve_node 对齐** —— `runtime/node → node` 使产物落在
Contents/Resources/node,正是 src-tauri/src/sidecar.rs 里 resolve_node 查找的路径。
实测(macOS arm64):.app **90.8 MiB**(原 6.75 MiB,增量即运行时 86MB)/
.dmg **33.78 MiB**(分发件,仍低于 40MB 目标);应用进程实测可启动且无崩溃输出。
对照:本机实测 Electron 运行时本体(Electron Framework.framework)207MB —— 不到一半。
|
2026-10-11 08:55:31 +08:00 |
|
jkooe
|
4f4d8913b5
|
chore(release): v2.0.1
|
2026-10-11 08:02:19 +08:00 |
|
jkooe
|
f4ae20969a
|
chore(release): release.mjs 支持双架构(同时出 x64 + arm64 包)+ CHANGELOG v2.0.1
## release.mjs
- 打包阶段改为循环 `pack.mjs --arch x64|arm64`(原先只出 x64)
- 资产新增 `nodeagent-win-arm64.zip`;`latest.json` 新增 `zip_arm64` 字段
(**只增不减**,符合 VERSIONING 的兼容铁律;旧控制端忽略即可)
- RELEASE_NOTES 提示按架构选包(x64 包在 ARM Windows 上 TLS 握手不通)
- 顺手修一处改名遗漏:`zipPath` → `zipX64` 后,产物存在性检查仍在用旧名(会 ReferenceError)
## CHANGELOG
补 v2.0.1 段:3 个修复(control.ps1 解析期错误 / 连接保活缺 pong / v26 清理阈值过粗)
+ 2 项新增(`pack --arch`、`verify-b2.mjs`)+ 新增文档 `VM-VERIFICATION.md` + 真机验证状态表
|
2026-10-11 08:00:52 +08:00 |
|
jkooe
|
7d13a48caa
|
docs(changelog): v2.0.0 的「已知限制」更新为「验证状态已补齐」(2026-10-11 VM 真机验证)
|
2026-10-11 07:53:42 +08:00 |
|
jkooe
|
40b7db16c1
|
docs(roadmap): §五/§九 的 7 处历史「未做」标注实际状态(避免误导为待办)
这些行是当时的计划快照,内容其实都已实现(screen.find 属性化、条件组合、方向二/三、ed25519 默认化
以及 v1.5 真机验证)。不改写历史结论,只更新事实以免读者误判为未完成。
|
2026-10-11 07:53:10 +08:00 |
|
jkooe
|
765ad08ee6
|
docs(roadmap): §九 台账 —— v1.5/v26 真机验证标记完成(VM Win11 ARM)
|
2026-10-11 01:13:36 +08:00 |
|
jkooe
|
fbcb8f8408
|
fix(v23): 连接保活补 pong 检测(治僵尸连接占名额)+ B2 三防护真机验证通过
## 真机发现(Parallels Win11 ARM,B2 验证过程中撞到)
心跳只发 `ping()` 而**不检查 pong**,且 ws 库默认不会因「ping 无 pong」断开 ——
客户端进程已死(TCP 未发 FIN)时,连接会一直挂在 `states` 里占 `max_connections` 名额。
叠加「有任务/订阅时清扫保守跳过」的既有策略 → `states.size` 虚高 → **把正常控制端挡在外面**
(本次实测就把自己锁在外面了)。
## 改法
- `attachConnection` 里加 `ws.on('pong')`:**客户端回应也算活跃**(更新 lastActiveAt)
- 心跳里:若某连接静默超过 `idle_timeout_ms * 2` → `ws.terminate()`(标准 ws 保活)
→ 僵尸连接不会再永久占位
## B2 三场景真机验证(全部通过)
- ① **握手封禁**:配 `max_attempts=3` → 3 次错误密钥被拒后,**正确密钥也被拒** ✓
- ② **连接上限**:配 `max_connections=6` → 第 1~6 个成功、**第 7 个被拒** ✓
- ③ **空闲断开**:配 `idle_timeout_ms=20000` → 静默 **40 秒**被断开 ✓
(符合 20s 阈值 + 30s 清扫周期)
- 新增 `scripts/verify-b2.mjs`(三模式:idle / maxconn / ban),全部走**真实连接**
## 验证过程中另修的两个自测问题(都记进文档)
1. 脚本默认 URL 写成了 `ws://` —— 被控端 `tls:true`,**必须 wss**(否则 socket hang up)
2. 测 `max_connections` 时值太小会**把自己锁在外面**;且被拒连接会被计入握手失败形成恶性循环
→ 结论:测试要留余量,并给自己配 `auth_ban.whitelist`
## 收尾
测试用的 `security` 段已用**备份恢复**(`agent.json.before-b2`);pong 修复已 `deploy` 到 VM
(指纹 abca2b8168f0)。
|
2026-10-11 01:13:12 +08:00 |
|
jkooe
|
ab7a2b147b
|
fix(v26): 自重启任务清理阈值改为「本 agent 启动时刻」(真机实测 1 小时粗阈值仍会累积)
## 真机发现(Parallels Win11 ARM)
restart 两次后仍有 **2 个** selfrestart 任务(旧的 Ready 未被清)。诊断确认:
- 清理**条件**能选中它 ✓(非匹配问题)
- **手工删除**成功 ✓(非权限问题)
→ 算时间戳才明白:那是「1 小时」粗阈值的**必然结果** —— 旧任务创建于 00:44,
第二次 restart 在 00:56(仅隔 12 分钟),清理那一刻它**仍是 Running**(cmd 还持有上一次的 agent),
不满足「非 Running 或早于 1 小时」→ 保留。
**但规则本身是缺陷**:频繁 restart(间隔 < 1h)时旧任务照样累积 ——
v26 想治的僵尸只是从「永不清理」变成「1 小时后才清」。
## 改法(语义更精确)
```powershell
# 旧:$cut = (Get-Date).AddHours(-1) ← 粗略
# 新:$cut = (Get-Date).AddMilliseconds(-<agent uptime>) ← 「本 agent 的启动时刻」
```
凡在**我启动之前**就运行的任务,必然不是我的宿主(我的宿主刚刚才创建)→ 可安全删除。
## 验证(真机闭环)
1. `nodeagent deploy release/agent.mjs` → 指纹校验 + 备份 + 上传 + 重启 + 校验 ✓
(顺带把 deploy 机制也验了)
2. `restart` → **任务只剩 1 个**(Running,当前宿主)→ **旧任务被精确清理** ✓✓
3. 单测:断言必须是「agent 启动时刻」阈值,且**禁止回退到 AddHours(-1)**;
守卫 9/9 + 全量 274 绿;7 包构建通过
|
2026-10-11 00:50:51 +08:00 |
|
jkooe
|
047cc9efd2
|
docs(vm): 补 ARM64 包的解决方案(pack.mjs --arch arm64)
|
2026-10-11 00:32:54 +08:00 |
|
jkooe
|
d8f21f4e68
|
feat(pack): 支持 --arch(x64 / arm64)—— ARM Windows 上 x64 包不可用(真机实证)
## 背景(Parallels Win11 ARM 真机验证的结论)
x64 包(内含 x64 Node)在 ARM Windows 上:**能启动、能 listen 8765,但 TLS 握手 60s 不完成**
(Prism 模拟下协议层异常)→ 协议层完全不可用,且现象具有误导性(端口是通的)。
换同版本 ARM64 Node 后**完全正常**(握手成功、连接、51 项能力)。
## 改动
`scripts/pack.mjs` 新增 `--arch <x64|arm64>`(默认 x64,向后兼容):
- Node 下载 URL、包目录、产物名全部按架构生成
- 产出:`nodeagent-win-arm64.zip`(ARM Windows 用)/ `nodeagent-win-x64.zip`(默认)
- 加了架构校验(非法值直接报错)
## 验证
- `node --check` 语法通过
- 实跑 `node scripts/pack.mjs --arch arm64` → 产出 31.4 MB 包,
内含 ARM64 `node.exe`(85.6 MB 解压后)+ `agent.mjs`
|
2026-10-11 00:30:43 +08:00 |
|
jkooe
|
54f7f4c7ab
|
test(vm): Parallels Win11 ARM 真机验证 —— A 组 15/0 + v1.5 四项全过 + 锚点端到端通过
## 环境
Parallels 27 + Win11 ARM,VM 10.211.55.9(共享网络);被控端 `-NodeId win-arm -AllowInput`。
## ⭐ B5 结论(与预期不同,但更有价值)
| 组合 | 结果 |
|---|---|
| `win-x64` 包(内含 x64 Node) | ⚠️ **能启动、能 listen 8765,但 TLS 握手 60s 不完成** → 协议层不可用(Prism 下) |
| **换 ARM64 Node(同版本 v22.20.0)** | ✅ **完全正常**:握手成功、连接、51 项能力 |
× 结论:`nodeagent-win-x64.zip` 不能在 ARM Windows 上实用;ARM 上需 ARM64 Node。
## 验证结果
- **A 组(verify.mjs 17 项)**:✅ 15 通过 / 0 失败 / 1 跳过(29.5s)
- **B1 v1.5 四项**:✅ 全过 —— 字符串热键 / 预设别名(`copy→ctrl+c` 映射正确)/ 长按 /
后台定向投递(**explorer 命中 138 窗口**、WindowsTerminal 10 窗口)
→ **v1.5.0「未经真机就发版」的债,至此还清**
- **B4 锚点端到端**:✅ collect(62 条) → 上传云盘 → verify 判定「链已增长」+ 精确明细
- B2/B3:⏸ 阻塞(需 restart,而计划任务启动失败 —— 手工跑却完全正常)
## 本轮真机抓到的 2 个真 bug(都已修)
1. `control.ps1:100` 的 `$scheme:` 被 PowerShell 当 drive 引用 → **解析期错误 → 整个脚本不可用**
(stop/start/status 全废);已修 + 加守卫(扫全部 .ps1 的 `$ident:`)
2. `anchor-audit.mjs` 调用**不存在的** `audit head` 子命令 → 改为 `invoke system.audit.head`
(这种「能力存在但 CLI 无子命令」的错配,不真跑一次发现不了)
## 文档
`docs/VM-VERIFICATION.md` 补「实际验证结果」节 + 5 条新增排查项(安装/密钥/CLI 语法/计划任务)
|
2026-10-11 00:18:43 +08:00 |
|
jkooe
|
c8aba83589
|
fix(control): control.ps1 的 $scheme: 被当 drive 引用 → 整个脚本不可用(真机踩到)
## 现象(Parallels Win11 ARM 真机)
`control.ps1 -Action stop/start/status` 一律报:
所在位置 control.ps1:100 字符: 36
+ Write-Host " 协议 : $scheme://0.0.0.0:$Port"
变量引用无效。':' 后面的变量名称字符无效。
## 根因
PowerShell 把 `$scheme:` 解析为 **drive 引用**(如 `env:`),而 `scheme` 不是 drive → 语法错误。
且这是**解析期**错误 → **整个脚本无法运行**(不是只有那一行),stop/start/status 三个子命令全废。
修法:写成 `${scheme}://`(用花括号界定变量名)。
## 改动
- `scripts/control.ps1:100`:`$scheme://` → `${scheme}://`,并加注释说明为什么必须用花括号
(注释里刻意**不写**危险字面量 —— 否则会被新守卫命中,这点在验证时真踩到了)
- 新增守卫(`tests/unit/security-hardening.test.mjs`):扫全部 `.ps1/.psm1`,
禁止 `$ident:` 形式(白名单 `env`/`nuget`/`psitem`/`PSScriptRoot`)
## 验证
- 守卫 **9/9 绿**;全量单测 **274 绿**;ps1-encoding 保持通过(BOM 未被破坏)
- **注入故障实测**:把 `${scheme}` 回退成 `$scheme:` → 守卫**立即红**(失败数 1)→ 恢复后绿
(按项目纪律:没验证过会红的守卫等于没有)
- 全仓重扫同类模式:**仅此 1 处**
|
2026-10-11 00:05:42 +08:00 |
|
jkooe
|
3b05f5b373
|
fix(docs): VM 标注插错位置 —— 移到 §九 台账的正确条目之后
上一提交的锚点匹配过宽(匹配到版本台账表里的「ci: 补全 Windows 真机验证」行),
导致标注插在了错误位置。现改为精确锚点「清理策略**真机验证**」,并复核只出现一次。
|
2026-10-10 23:39:48 +08:00 |
|
jkooe
|
7eb773099f
|
docs(roadmap): 补上「VM 可补验证」标注(上一提交因文本未匹配而未生效)
|
2026-10-10 23:38:17 +08:00 |
|
jkooe
|
ba57d99806
|
docs: 新增 VM 验证清单(Parallels 27 + Win11 ARM)—— 真机不可用时的替代路径
## 背景
Windows 被控端离线导致 4 项验证卡住(v1.5 四项 / v23 三防护 / v26 清理 / 锚点端到端)。
Parallels Desktop 27 可跑 **Windows 11 ARM** 虚拟机(Apple silicon 只支持 ARM 版 Windows,
x86_64 仅 Pro/Business 的早期预览),可提前跑掉**架构无关**的验证项。
## 清单内容(docs/VM-VERIFICATION.md)
- **能补 / 不能补的矩阵**:协议层与输入层可验;**杀软交互、显示/DPI、性能指标不可替代**
- 前置准备:桥接网络(否则 UDP 广播发现测不了)、优先 ARM64 Node、run-portable/install 二选一
- **逐条命令**(均已对着源码核实参数):
- A 基础验收:直接复用既有 `scripts/verify.mjs`(17 项 + Markdown 报告)
- B1 v1.5 四项:`key press "ctrl+shift+esc"` / `copy` / `--hold 800` / `--route post --pid N`
(对应 args 的 hotkey / preset / hold_ms / route+target_pid 四个分支)
- B2 v23 三防护:`agent.json` 的 `security.auth_ban` 与 `idle_timeout_ms` + 三场景判定
- B3 v26 清理:`restart` 后 `schtasks /query`,**重点看"当前宿主任务必须仍在"**(否则即误杀)
- B4 锚点端到端:`anchor-audit.mjs collect` → 云盘 → `verify`
- B5 额外结论:**win-x64 包在 ARM Windows 上能否跑**(走 Prism;跑通则写进 README)
- **结论标注规则(强制)**:VM 验过的写「VM 已验」;杀软/显示/性能**不得**因 VM 通过就标已验;
并注明「VM 与真机架构不同(真机 x86_64),VM 通过不自动推出真机通过」
- **已知 VM 环境差异表**(排查先看):广播发现需桥接、坐标为逻辑验证、性能数字不可采信等
README 文档区与 ROADMAP §九 均已链入。
|
2026-10-10 23:38:02 +08:00 |
|
jkooe
|
28ce95f045
|
feat(audit): 审计锚点外部化落地 —— 判定逻辑收敛为 protocol 纯函数 + 控制端工具 + 云盘链路
## 为什么(一句话)
锚点的安全性完全来自「它在攻击者碰不到的地方」。与被审计日志同机的锚点等于没锚:
被控端一旦被 root,重写日志后顺手改掉锚点即可。
## 本次改动
### ① 判定逻辑收敛(消除「两份真相」)
把 `compareWithAnchors` 的三条判定抽到 **`packages/protocol/src/audit-anchor.ts`** 的
**纯函数 `judgeAnchors(head, anchors)`**(无 IO,不知路径):
- agent 端 `compareWithAnchors` 改为「读文件 + 调 judgeAnchors」
- 控制端脚本同样调用它
→ 同一份日志不可能出现「被控端说一致、控制端说被改」的分歧
- 附带 `parseAnchorsFromJsonl`(坏行跳过,一行坏数据不毁整份锚点)
- agent 侧类型改为 re-export,既有导入路径不变(**9 个原有锚点单测全绿,行为零变化**)
### ② 控制端工具 `scripts/anchor-audit.mjs`
- `collect`:拉链头 → 生成一行锚点 JSONL(并打印 `ANCHOR_JSON=` 便于 automation 取用)
- `verify`:读锚点文件(可从云盘下载)+ 拉链头 → 判定,`ok:false` 时**退出码 2** + 醒目告警
- 走 CLI 而非自己实现连接(复用其多设备配置/证书钉住/daemon,避免又一份实现)
- 支持 `--head-file`:**离线也能验证判定链路**(本轮即用它做了 fixture 验证)
### ③ 云盘链路
- 建 `…/Project/NodeAgent/anchors/`(与全量备份分开目录)
- 每日 automation 更新为「拉链头 → 上传云盘(一天一个带日期的文件)→ 简报」,
并规定每月首日额外做一次「下载历史锚点 → 链外比对」
## 验证
- **纯函数重构零行为变化**:原有 audit-anchor 单测 9/9 全绿;全仓 273 单测全绿;7 包构建
- **工具离线验证**(fixture):一致 → 「与锚点完全一致」退出码 0;
篡改 → 「链头哈希与锚点不一致」退出码 2;三个错误分支均给出明确提示
- 云盘:`anchors/` 目录创建成功(1/1 步),目录内容已复核
- ⚠️ 端到端(真拉链头 + 真上传)**待被控端上线**;脚本的 CLI 分支已做错误处理验证
|
2026-10-10 19:14:09 +08:00 |
|
jkooe
|
2fe717cb62
|
fix(ci): 修 clippy 的两个错误(含一个真实并发问题)+ 本地补齐 lint 验证
## CI 失败的真实原因(上一轮我误判为 resources,实为 clippy)
`-D warnings` 下两个错误:
1. **`clippy::await_holding_lock`(sidecar.rs,真问题)**:
`if let Some(child) = self.child.lock().unwrap().take() { child.kill().await }`
—— MutexGuard 存活到 if 块结束,**跨 await 持有 std Mutex**:不仅触发 lint,
真会阻塞 executor。已改为「先取出 child(锁随即释放),再 await kill」。
2. `clippy::doc_lazy_continuation`(lib.rs 文档注释):列表与后续段落之间缺空行。
## 我的验证方式错在哪(已修正)
上一轮我本地只跑 `cargo test`,**没跑 clippy** —— 而 CI 是 `clippy -D warnings` + `test`
两道闸。本地与 CI **标准不一致**,于是"本地绿、CI 红"。
本轮起:**本地按 CI 的完整命令集验证**(clippy --all-targets -- -D warnings → cargo test)。
## 验证
- `cargo clippy --all-targets -- -D warnings` → **零警告**
- `cargo test` → **3/3 通过**
|
2026-10-10 15:33:11 +08:00 |
|
jkooe
|
9bba53d5c0
|
fix(ci): 修 Rust Shell 工作流 —— resources 只声明构建期必产出的文件
CI 失败根因(本地已复现确认):`tauri-build`(build.rs) 会在**编译期校验**
`tauri.conf.json` 的 `bundle.resources` 声明文件是否存在。
我之前声明了两项:`sidecar.mjs`(CI 未生成)+ `resources/node`(Node 运行时未下载)
→ build.rs 直接失败("failed to run custom build command")。
修复:
- `tauri.conf.json` 的 resources **只留 sidecar.mjs**,并写明为何不能声明未就绪的文件
- `rust.yml` 在 clippy/test **之前**加「构建 sidecar」步骤(`pnpm --filter nodeagent-console sidecar:build`)
- console-plan.md:⑤ 不再把 node 记为已完成(未验证的不写成做完),
补「已知待办」(Node 运行时随包分发 + GUI 端到端人工验证)与「构建约束」说明
本地复现验证:删掉 `sidecar/dist/` → `cargo check` 复现同一错误;恢复 → 通过 ✓
|
2026-10-10 15:25:12 +08:00 |
|
jkooe
|
53bf5a0634
|
feat(console)!: Rust 壳改用 Node sidecar(选项 D 完成)—— 退役 crates/nodeagent-client
## 动机(消除「两套协议实现」)
原设计让 Rust 壳自己实现 client,与 TS 版(@nodeagent/client)并存 → 改一处漏一处时
构建过、单测过,只在真机暴露。现 Rust 壳只做**进程管理 + 消息转发**,协议实现只剩 TS 一份。
## ④ Rust 壳改造
- 新增 `src-tauri/src/sidecar.rs`(~290 行):Sidecar 管理器 —— spawn node、
行分隔 JSON-RPC 的请求/响应匹配(oneshot + 超时兜底)、通知回调(state/log/agent_event)、
`resolve_node`/`resolve_sidecar` 路径解析(env → Tauri resource → 开发期系统 node)
- 重写 `lib.rs` 为薄转发层:`ensure_sidecar` 懒启动;**5 个 command 签名与 3 个 event 名
(na-state/na-log/na-event)一律不变 → 前端零改动**;`get_capabilities`/`get_state`
读本地缓存以**保持同步签名**;`RunEvent::Exit` 时优雅 shutdown(kill_on_drop 兜底不留孤儿)
- `Cargo.toml`:移除 nodeagent-client 依赖、加 tokio;**版本 0.1.0 → 2.0.0**
(原先与 tauri.conf.json 的 2.0.0 不一致,属版本号统一范围)
## ⑤ 打包配置
- `tauri.conf.json`:`bundle.resources` 声明 sidecar.mjs 与 node;
`beforeBuildCommand` 串上 `sidecar:build`(否则 resource 缺失)
## ⑥ 退役
- 删除整个 `crates/`(12 文件,含 nodeagent-client 与其 probe 示例)
- `rust.yml` 由「Rust client 单测」改为「**Rust 壳单测(真跑 sidecar)**」:
装 Tauri 系统依赖 + Node + pnpm,跑 clippy `-D warnings` 与 `cargo test`
- `package.json`:`test:rust` 改指 console 壳;移除 `probe`
(其「对真机逐项核对字段」的用途已由 CLI `nodeagent invoke` 覆盖)
- 引用清理:README(数字与协议客户端说明)、console 前端两处注释、
console-plan(开头加「原方案已被取代」提示 + 补改造详情)、ROADMAP 历史描述加退役批注
## 验证
- **`cargo test` 3/3 通过**(真跑 Node sidecar 子进程:spawn/请求-响应/错误不崩)——
这是关键:sidecar 管理器**不依赖 Tauri GUI**,故核心逻辑可在 CI 真实验证,不必等人工开窗口
- `cargo check` 零警告;clippy 标准与 CI 一致
- 273 单测 / 7 包构建 全绿
- ⚠️ **仍需人工验证**:Tauri GUI 端到端(开窗口 → 连被控端 → 点界面),沙箱跑不了图形应用
|
2026-10-10 15:16:24 +08:00 |
|