Skip to content

发布 v0.6.2:统一操作失败提示,并修复 SSH 主机指纹确认框 - #49

Merged
massif-01 merged 33 commits into
RMinte-AI:mainfrom
massif-01:fix/clear-user-prompts
Oct 12, 2026
Merged

massif-01 merged 33 commits into
RMinte-AI:mainfrom
massif-01:fix/clear-user-prompts

Conversation

@massif-01

@massif-01 massif-01 commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

合并即发布 v0.6.2

本 PR 同时是 0.6.2 的发布 PR。合并到 main 后,GitHub Actions 会自动构建、通过 CI Gate,然后创建标签 v0.6.2 并发布 Release(附 TianShanOS.bin、www.bin 等),Release 正文取自 docs/releases/v0.6.2.md,汇总 #47、#48、#49 三个 PR 的更新。

  • 本 PR 把 version.txt 升到 0.6.2,加入中文发布说明和验证记录(docs/releases/v0.6.2-validation.md),README 版本说明和静态资源 ?v= 标识同步为 0.6.2。
  • 依据(已用真实取值演练):tools/ci/check_profile.py 判定“上游仓库 main 推送”有发布资格(fork 推送、PR 事件没有);tools/ci/check_release_state.py 取标签 = v + version.txt,该版本尚无 Release 才发布;tools/check_release.py 要求标签、version.txt、编译版本一致,且中文说明不少于 100 字。
  • 请不要提前手动推 v0.6.2 标签:标签由 Action 打在合并后的提交上。提前打在别的提交上,预检会因“标签与构建提交不一致”拒绝发布,并且不会移动标签。
  • v0.6.2 的标签和 Release 目前都不存在。
  • 仓库 build/ 下的默认刷机镜像已更新为 0.6.2:从干净检出的提交 774c3a97 用 ESP-IDF 5.5.2 构建(版本 0.6.2+774c3a97.10130158,主固件 2,226,000 字节,3 MiB 分区剩 29%,构建不改动任何已提交文件)。build/firmware-manifest.json 记录版本、被构建提交和每个文件的 SHA-256。新增 tools/make_firmware_manifest.py 生成清单(--verify 可核对,带未提交修改的构建会被拒绝)。Release 附件由 Actions 另行构建,字节会和这里不同。
  • 建议用 Squash and merge(与 修复自动化删除保护并实现规则配置包导入 #47、ci: 完善回归门槛与构建后 Web 验证 #48 一致)。

与 #47、#48 的关系

#47、#48 已合并(squash)。本分支原本是在 #47 之上做的,直接合并最新 main 会因为 squash 产生大量假冲突,所以按真实的共同祖先(#47 的头提交 bdc987d9)做了三方合并:我方改动与 #48 改动没有重叠的文件,结果干净。合并后 Files changed 只剩本 PR 自己的改动(130 个文件,+7,066 / −2,438,测试占大头)。Commits 标签页里会出现 #47 原来的 9 个提交(上游是 squash 合并,SHA 不同),可以忽略。

概述

用户点按钮后操作失败时,WebUI 现在统一用「动作+对象失败:原因。下一步。」说话(英文:Failed to X “对象”: reason. Next step.)。固件只返回一个固定的原因代号,网页按当前语言把它翻成句子;设备确实说不清原因时,诚实写「设备没有返回具体原因」并指向「终端」页的系统日志,不编造原因。写操作遇到断线或没有应答时,不再冒充“失败”,而是写「…的结果未确认」并提醒“先核对状态、不要重复提交”。

另外包含两件独立的事:一个 SSH 缺陷修复(主机指纹确认框弹不出),以及一次无入口旧代码的清理。想先看效果,读 tests/fixtures/failure-texts/*.json(189 个失败场景,中英文各一句,就是用户真正看到的话,当文档读即可)。

为什么做

原来的失败提示很难让人定位问题。全量盘点(约 410 条弹出提示、170 条行内提示、790 条接口错误文案)后,典型问题是:

  • 手动触发规则被拒有 6 种原因,全部显示 Failed to trigger rule;SSH 连接失败(密码错、端口不通、DNS、超时…)统统落成 SSH connection failed (code=…)。
  • 规则保存、启停失败时把内部代号(capacity、storage_failed…)直接当文案;启停失败时还写“已启用/已禁用”。
  • 大量提示只有“保存失败”“操作失败”,不说对象、不说下一步;设备的英文原文被直接拼进句子。
  • 页面自己 throw new Error(设备给的文字),设备的真实原因被当成“页面出错”丢掉。

改后的句子(节选,均来自上述黄金清单):

场景 改后
手动触发被拒 触发规则“夜间关灯”失败:这条规则没有开启“允许手动执行”。请在规则设置里打开“允许手动执行”。
登录 登录失败:用户名或密码不对。请检查后重新输入。
打开 NAT 切换 NAT 失败:设备还没有连上上级 WiFi。请先让设备连上 WiFi,再开启 NAT。
主机指纹变了 从“root@192.0.2.8:22”撤销密钥“lab-key”失败:这台主机的指纹和之前登记的不一样。可能是服务器重装了,也可能有人在冒充它;请先到「安全」页核对指纹。
写操作无应答 重启设备的结果未确认。请先刷新核对当前状态,不要重复提交。
批量删除 删除结果:1 项成功,2 项失败或未确认。
/sdcard/b.txt: 删除文件或文件夹的结果未确认。请先刷新核对当前状态,不要重复提交。
/sdcard: 删除文件或文件夹失败:根目录不能删除。请只删除里面的文件或文件夹。

句子怎么拼

failure.format:{动作+对象}:{原因}。{下一步}(英文各有自己的标点)。三部分各有来源:

部分 来源
动作+对象 语言包 failure.ops.<组>.<操作>.{failed,unknown,blocked,done},每个按钮自己的,带对象名
原因 / 下一步 语言包 failure.reasons.<代号>.{why,hint};代号由固件返回(或旧固件的英文原文经对照表映射)
拿不到原因时 按错误类别说话;都不认识就是 no_detail。设备的英文原文永远不进句子,只进折叠的「详情」

js/api.js 的 describeFailure({op, params, result|error, token, endpoint, write}) 按固定顺序选原因:明确代号 → 错误类型(网络/超时/格式)→ 页面自己的错误(page_error)→ 设备代号(目录里有且参数齐才用)→ 旧英文原文对照 → 数字类别兜底 → no_detail。write: true 的操作拿不到应答时是“结果未确认”,读取/测试则说“无法连接”。页面自己发现的问题用 pageFailure(代号, 参数),设备给的原因必须用 requireApiSuccess/ApiOperationError 带上,不要 throw new Error(设备文字)。失败提示条停留 8 秒、带折叠的「详情」(原因代号、接口、设备原文、阶段、时间);成功提示 3 秒。做法和新增一条提示的步骤见 docs/repair/failure-messages/README.md。

具体改动

网页(components/ts_webui/web)

  • 新增统一目录:183 个原因、31 组 285 条操作句子(中英文逐条对应,占位符一致)。
  • 全站迁移:登录与改密、服务与重启、OTA、文件、网络/WiFi、风扇、LED、证书与密钥、配置包、自动化、SSH、规则包导入等页面共 278 个调用点(128 个操作)改走统一目录。展示方式有四种:提示条 reportFailure、页面内文字 failureText/failureNote、对话框结果框 packBox、批量结果列表。
  • 顺带修了迁移中发现的问题:命令页“读日志/查进程”失败被显示成“无输出”;登录失败在表单和提示条里各显示一遍;文件列表失败错套“上传结果不受影响”;密钥生成弹窗曾丢失失败阶段与清理结果。
  • 发版文件:sdkconfig 补上 修复自动化删除保护并实现规则配置包导入 #47 新增配置项(配置包签发根,默认空)的默认值,保证从提交出来的检出构建不会改动已跟踪文件;build/ 默认刷机镜像与清单更新到 0.6.2(见上)。
  • 清理:删除 50 个无入口的旧界面函数及 27 条 window.X = X 导出,并删除 198 条因此无人使用的语言包旧键(只清被这次迁移换掉的那批,v0.6.1 原有的其他无用键没动)。

固件(15 个文件,相对 #47 +526/−221;处理函数的业务逻辑不变,只加字段和代号;行为上的改动只有 HTTP 失败响应的序列化(见下面 SSH 一节)和少数失败的数字类别)

  • 公共:ts_api_result_reason(result, 类别, 代号, 详情);配置包失败用 ts_api_pack.h 的 pack_failure();ts_ssh_connect_reason() 把 SSH 失败分成 DNS/端口被拒/不可达/超时/握手/主机指纹/认证等阶段。
  • 手动触发被拒 10 种原因(含待删除的规则)、自动化引擎/动作模板/数据源/SSH/配置包的失败各自返回具体代号;config.pack.verify/apply 与上传校验多回 reason;TS_RULE_REJECT_PENDING_DELETE(待重启才移除的规则被拒绝执行)。
  • 数字错误码尽量保持“最近类别”;少数失败的类别更准确了(例如配置包导出无权限不再是 INTERNAL)。

SSH 主机指纹确认框(独立缺陷,见 docs/repair/ssh-fingerprint/RESULT.md)

  • 现象:“测试连接”遇到新主机(1002)/指纹变化(1001)时,前端靠 data.fingerprint 弹确认框,但 HTTP 通用失败分支丢掉了 data,确认框永远弹不出。
  • 修复:失败响应保留 result.data(没有 data 的响应字节不变)。加固:在构建失败响应的每个分配点注入失败的遍历测试发现,写 code 时分配失败会发出缺 code 的残缺 JSON,现改为 HTTP 500(成功分支有同类写法,未改)。
  • 一个可选的单独提交(7b899a07,可丢弃):指纹变化时的确认框改为明确的安全警告,显示新旧指纹,默认焦点在“取消”。
  • 确认只接受用户核对过的指纹;取消既不信任也不重试。

与 #47 的整合:规则包导入原有的 28 句“一个代号一句话”的单独表并入统一目录并改成“原因+下一步”(新增 22 个原因,如 wrong_device、signature_invalid、dependency_missing、overwrite_required…);删除保护的弹窗是 #47 评审过的设计,保留不动;规则“待重启/待删除”时的快捷操作和触发给出各自的原因。

用户可见的变化

  • 所有迁移过的失败提示的措辞和结构(见上);英文同步。
  • 失败提示条变长到 8 秒并带「详情」折叠;成功提示 3 秒不变。
  • 登录失败只在表单里显示一次。
  • 密钥生成、文件批量操作等处的失败原因更具体。
  • 没有改动:存储格式、接口名称。业务行为上的改动只有上面列出的缺陷修复(SSH 指纹确认框、读日志失败显示原因等)。版本号升到 0.6.2。

兼容性

  • 新网页 + 旧固件:旧固件只发英文原文;网页内置 54 条原文对照和 4 个前缀匹配,其余按错误类别说话,原文进折叠详情,不会出现“说明暂时无法显示”。有对应的页面场景测试(old-firmware-text 等)。
  • 旧网页 + 新固件:按代码推断仍能用——数字错误码保持最近类别,旧网页按类别说话;“详细原因”一行会露出原因代号而不是英文句子。未做专门测试。 WebUI 与固件最好一起升级(发布说明里已写明)。
  • 失败响应现在多带 data(ts_webui_api.c 通用失败分支)。按字段名审过:目前有 24 个处理函数可能在失败时带 data,字段名里没有密码/私钥/令牌类(其中数据源“测试连接”会带回用户自己请求的远端响应体 body)。WebUI 之外的调用方若假设失败响应没有 data,需要知道这一点。

验证(已合并最新 main,本地在 macOS 上)

命令 结果
npm test --prefix tests/prompts 133 项全过
npm run test:browser --prefix tests/prompts(真实 Chrome,15 个文件,与 CI 同样的单次调用) 720 项全过;其中 flows 覆盖 189 个失败场景(中英文各一遍),含 system 组、阻断删除弹窗、规则包导入
npm run test:xterm --prefix tests/prompts 4 项全过
bash tests/runtime/run_all.sh、run_rule_pack.sh 退出 0,44 项 + 8 项 PASS(含规则包 C 测试、SSH 指纹 HTTP 序列化、线上字节夹具)
tests/ws_subscriptions/:run_operation_adapter.sh、run_host.sh、run_reviewer.sh、run_f1_f2.sh 全过(其中两个测试原来按子串 ssh_output 判断“输出帧”,被新的原因代号误判,已改成按帧的 type 字段判断)
tests/fan_monitor、tests/fan_review、tests/fan_decay 全过
证书回归 bash tests/certificate/run_host.sh(本地用 ESP-IDF 自带的 cmake 编译 MbedTLS) 全过。上一个提交 25b7808e 的 CI 正是在这一步失败:test_ui.cjs 按名字从页面脚本里取已被删除的 certError;已改为直接运行生产的 describeFailure/packBox 并断言,类 HTML 的设备文字只以转义形式出现在折叠详情里
python3 -m unittest discover -s tests/ci、tests/release/test_release.py、test_web_minify.py 全过;发布判定用真实取值演练过(见上)
ESP-IDF v5.5.2、esp32s3 完整编译(干净检出 774c3a97) 通过,0 警告;版本 0.6.2+774c3a97.10130158,镜像 2,226,000 字节,应用分区剩 29%;tools/make_firmware_manifest.py --verify 通过
tools/ci/check_web_artifacts.py(--build-dir、--web-root)+ 成品 WebUI 冒烟/缺资源负例 全过:www.bin 与刷写映射一致,13 个成品资源语法和 gzip 通过
体积 整个 web 目录 gzip 约 0.66 MiB(分区 3 MiB);相对 #47 合入后的 main,web gzip 约 +14 KB

额外的检查(本 PR 新增,已在测试里保留):failure-catalog.test.cjs 新增“每个调用点都给足句子里的占位值”检查(故意去掉一个参数会失败)和“目录里没有无人使用的操作/原因”检查;对页面脚本做过“使用了未声明标识符”扫描,相对 main 新增 0 个。

没有验证的 / 已知限制

  • 没有真机验证:SSH 失败阶段的判断(lwIP errno)、各提示的实际手感、新的指纹警告弹窗,都只在主机上用生产代码加模拟件验证过。
  • 本地只在 macOS(clang) 上跑;Linux/gcc 由本 PR 的 CI 验证(上一轮在证书回归失败,原因和修复见验证表)。
  • 页面级场景测试覆盖 189 个场景;批量迁移的页面里,其余调用点靠静态检查(操作存在、占位值齐全、无未声明标识符)保证,没有逐个场景跑。
  • 刻意没改:终端页的终端输出、纯输入校验提示(“请输入…”)、成功/信息提示、远端脚本运行摘要、删除保护弹窗(修复自动化删除保护并实现规则配置包导入 #47 的设计)。
  • 未做视觉走查(新的行内提示在窄屏下的换行等)。

风险与回滚

提交一览(本 PR 自己的提交)

提交 内容
e068f151 fix: 失败响应保留 data,修复 SSH「测试连接」弹不出主机指纹确认框
7b899a07 feat: 指纹变化时的确认框改为明确的安全警告(可选,单独提交)
85d84740 refactor: 删除 50 个无入口的旧界面函数及其残留
46303b29 feat(firmware): 手动触发被拒和 SSH 连接失败时,如实报告具体原因
fed29205 feat(webui): 失败提示按"动作:原因。下一步。"重写——规则启停保存、快捷操作、SSH 测试连接(样板间)
08f05c03 feat(firmware): 自动化引擎、动作模板、数据源失败时如实报告具体原因
ca8cbf68 feat(webui): 自动化页失败提示迁到统一目录(引擎、规则、动作模板、数据源)
a52346df feat(firmware): 数据源连接测试与配置包导入导出失败时如实报告具体原因
767eb533 feat(webui): 自动化页其余失败提示迁到统一目录(连接测试、配置包、选择列表)
87b716f9 feat(firmware): SSH 主机、密钥、指令、执行失败时如实报告具体原因
75f0ab4e feat(webui): SSH 主机、密钥、指令页失败提示迁到统一目录
0e5be43b feat(firmware): 配置包校验、应用、导出失败时如实报告具体原因代号
e5954435 feat(webui): 全站操作失败提示迁到统一目录(登录与改密、服务与重启、OTA、文件、网络、风扇、LED、证书与密钥、配置包)
3a63ee4a feat(webui): 合并后补齐上游新功能的提示语(规则配置包导入、规则待重启/待删除、模板缺失)走统一目录
474bec11 fix(webui): 验证中发现并修复的问题
9db1b3ce test: 跟上合并与提示语迁移——新增页面场景测试、调用点参数检查,修正钉着旧句子的断言
61df8d5e test: 补上“待删除规则被拒绝”的引擎、线上字节与页面用例;去掉一处行尾空格
5f376439 fix(webui): 文件选择器读取目录失败时保留设备给出的原因
25b7808e 发布 v0.6.2:版本号、中文发布说明与验证记录
5ae368b6 test: 证书页测试跟上新的失败提示方式
774c3a97 chore: 同步 sdkconfig 新增配置项的默认值;新增 build/ 清单生成脚本
6b518bb2 发布 v0.6.2:更新 build/ 默认刷机镜像到 0.6.2

当前头提交:6b518bb2。


🤖 Generated with Claude Code

massif-01 and others added 30 commits October 9, 2026 19:46
SSH 主机页「测试连接」调用 ssh.exec。目标是新主机(1002)或指纹变化(1001)时,
固件把指纹放进 result->data,同时返回非 OK;HTTP 出口的通用失败分支只输出
{code,error} 并释放 data,页面取不到指纹就跳过确认框,用户只看到「连接失败」,
无法在该入口信任新主机,也无法核对并接受指纹变化。

- ts_webui_api.c:通用失败分支同样输出 result.data;没有 data 的失败响应不变。
  同一处顺带加固:code / error 写不进去(内存分配失败)时改发 HTTP 500,
  不再发出缺 code 的残缺 JSON(原有漏洞,由下面的分配失败遍历发现)。
  固件的指纹校验没有改动:确认必须带回当前连接上的指纹,否则继续拒绝,
  校验失败时不发凭据、不执行命令。
- 新增 tests/runtime/test_ssh_fingerprint_wire.py:运行生产的 ssh.exec / ssh.test
  处理函数和 HTTP 响应分支,检查线上字节,并存为契约夹具
  tests/fixtures/ssh-fingerprint;并在构建失败响应的每个分配点注入失败,
  结果只能是完整响应或 HTTP 500。
- 新增 tests/prompts/ssh-fingerprint.browser.test.cjs:把夹具原样回放给真实页面,
  中英文检查确认框内容、取消不信任不重试、确认只发送用户看到的指纹、
  确认时设备换密钥则不信任且不循环。
- test_keystore.py:原先钉着「其他接口的失败响应不带 data」,按新决定改为断言带上
  (只改了这一条断言和模拟字段名,共 2 行);密钥生成原有的失败阶段、清理结果、
  序列化失败返回 500 等测试全部保留,并逐一破坏生产代码确认它们仍会报错。
- docs/repair/ssh-fingerprint/RESULT.md:根因、修复与验证记录。

验证(未接触真机):运行时全套 41 项、静态 122 项、浏览器 249 项、发布检查通过;
ESP-IDF 5.5.2 完整编译通过。还原修复后运行时测试断言失败;夹具换成旧格式后
浏览器测试失败。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
修复「测试连接」确认框之后,指纹变化(1001)和新主机(1002)弹出的是同一个框:
不说指纹变了,也不显示已登记的旧指纹,用户容易当成普通确认点过去。

- 指纹变化时:标题「安全警告:主机指纹不匹配」,说明可能原因(中间人攻击、服务器重装、
  IP 被分配给了别的服务器),同时显示已登记指纹和当前指纹,确认按钮为「更新主机密钥」,
  默认焦点在「取消」,回车等于取消。
- 新主机的确认框不变。
- 文案沿用已有的 securityPage.* 语言包条目,没有新增翻译。
- index.html 中 app.js 的缓存版本串提升为 0.6.1-lpmu6-keygen2-fp1。
- 浏览器测试补充:标题、按钮文字、两个指纹、默认焦点、回车不接受;去掉本提交后这些断言失败。

验证(未接触真机):静态 122 项、浏览器 249 项、运行时 41 项、发布检查通过。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
app.js 删除确认没有任何入口的旧函数 50 个(约 900 行):LED 旧控制与滤镜、
旧 SSH 主机管理、旧自动化变量页、旧动作行等;同时删除它们的 27 条 window
导出、因此失去用途的 selectedFilter / allVariables 变量,以及再也打不开的
「主机指纹不匹配」旧弹窗及其处理函数。app.js 减少约 47 KB(gzip 约 8 KB)。

确认方式:语法树可达性分析加全文搜索(含字符串里的函数名、window.xxx、
模板里的 onclick);删除后重新分析无人调用函数为 0;静态 122 项、浏览器
模拟 232 项与删除前一致。handleModalLogMessage 经 window 在运行时被调用,
不在删除范围内。

input-repair 浏览器测试原先直接调用旧矩阵页的 applySelectedFilter,
改为在现行的弹窗滤镜路径上验证同样的零值保留。macOS 27 对照清单与说明
文档同步。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
样板间 B、C 需要固件多说一点;只加字段、不改任何业务,旧界面照常工作。

快捷操作点击后被拒(automation.rules.trigger):
- 引擎里新增 ts_rule_trigger_ex(),除返回值外还报告拒绝原因:引擎未在接收执行请求、
  规则被禁用、不允许手动执行、配置未加载、需要恢复、配置正在保存、规则正在执行、
  服务状态不允许启动、规则刚被修改。原来这些全被压成同一句 "Failed to trigger rule"。
  ts_rule_trigger() 行为不变(内部调用 _ex)。
- API 把原因代号写进 message(如 engine_not_accepting、execution_busy);
  忙碌类仍用 BUSY 错误码,旧界面还能说"设备正忙"。规则不存在、其他未知原因的返回不变。

SSH 连接失败(ssh.exec / ssh.test):
- 客户端记录连接停在哪一步:域名解析、端口被拒、主机不可达、超时、握手、主机指纹、
  认证(ts_ssh_get_fail_stage);错误文字 "SSH connection failed (code=N)" 与所有返回值不变。
- ssh.exec 的连接失败改用原因代号(ssh_dns_failed、ssh_port_refused、ssh_host_unreachable、
  ssh_connect_timeout、ssh_handshake_failed、ssh_auth_failed、ssh_connect_failed),
  data 里带 stage 和客户端原文 detail;ssh.test 保持原来的成功信封,另加 reason 和 stage。

测试(主机上运行生产代码,未接触真机):
- 引擎:逐一覆盖每种拒绝原因;未知规则不报原因;ts_rule_trigger / _ex 两个入口。
- SSH 客户端:逐一模拟每个失败阶段,并确认原有返回值与错误文字不变。
- test_failure_wire.py:规则保存/启停的回复和触发被拒经生产的 HTTP 响应链,线上字节存为
  tests/fixtures/failure-wire;test_ssh_fingerprint_wire.py 增加每种 SSH 连接失败的线上字节。
  夹具与代码不一致时测试失败,浏览器测试用同一批字节回放。
- 逐一破坏(原因搞错、原因丢失、阶段搞错),这些测试都会报错。
- test_stop_protocol 沿用的是 test_engine 的前半截,桩件原文未动,新开关用宏叠加。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
三个样板间,作为后续铺开到全部页面的范本:
- 规则:启用 / 禁用 / 保存。写出规则名而不是内部 id;原因是人话("它正在执行中"),
  不再先说"提交的信息不符合要求"再跟内部代号;失败了不再写"已启用";成功说"已启用规则“x”",
  而不是 "…: ok";结果未确认单独成句并提示不要重复提交。
- 快捷操作:原来一句"当前不允许启动:请检查启用、手动权限及服务状态"塞了 5 种可能,
  现在卡片知道自己为什么不能启动(规则已禁用、不允许手动执行、引用未解析、详情读取失败、
  服务运行中/启动中/核验中/停止中/未确认已停止),分别说;点击后被固件拒绝的 9 种也各说各的。
  "请等待几秒后再触发其他模型"改为完整句子"刚刚触发过其他快捷操作,请等几秒再试。"
- SSH 测试连接:按连接停在哪一步说(解析不了主机名、对方拒绝连接、无法到达、超时、
  无法建立 SSH 加密连接、主机拒绝了用户登录);取消确认框是中性提示而不是"连接失败";
  确认时对方换了指纹会明确说没有信任它。

公共部分:
- 语言包新增 failure 目录:拼句格式、各操作名称、约 50 个"原因 + 下一步"(中英文)。
- api.js describeFailure():按原因代号取句子;查不到的代号按错误类别诚实地说,
  设备英文原文只放进折叠的"详情";缺参数时降级;绝不出现"说明暂时无法显示"。
- app.js 新的失败提示条:停留 8 秒,鼠标悬停或展开详情时暂停,有"详情"折叠(原因代号、接口、
  设备原文、阶段、时间)和关闭按钮;不会被随后的普通成功提示冲掉。其他页面的提示不受影响。
- runtimeSaveError 唯一的使用者被替换,已删除。

测试:
- failure-catalog:两种语言一一对应;每条文字符合写作规范(不带机器代号、中文里无多余英文、
  原因无句末标点、下一步是完整句子);固件会发出的每个原因代号必须有对应说法(从 C 源码读取)。
- failure-messages:提示条行为;每种语言下的标准句子;规则、快捷操作的真实页面流程,
  回放固件线上字节;SSH 连接失败 7 种 + 老固件无指纹 + 确认时换指纹。
- 受影响的旧测试同步:快捷操作被阻止、SSH 取消、规则保存的 Node 版测试;
  静态检查把 failure 目录整族纳入。

验证(未接触真机):静态 127、浏览器 305、运行时 43 项、发布检查通过;ESP-IDF 5.5.2 完整编译通过。

补充:详情只在有可看的内容时出现(页面自己判断出的拒绝没有接口、设备原文,就没有「详情」链接);
× 与「详情」同一行;新网页配旧固件(只发英文原文)时仍按错误类别诚实地说;
写操作没拿到设备回答一律说"结果未确认"。写法与新增方法见 docs/repair/failure-messages/README.md。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
只加字段、不改业务:失败时 message 换成固定的原因代号(网页按语言翻译),
数字错误码按"最近类别"选,旧网页仍能按类别说话。

- 引擎:启动/暂停被拒时按引擎当时状态报 engine_paused / engine_not_running /
  engine_unavailable;停止超时 engine_stop_timeout;重载读配置失败
  config_file_invalid(原始错误名放 data.detail);内存不足 no_memory
- 动作模板:数量满 template_capacity、重复 ID template_exists、LED 滤镜参数
  不合法 led_filter_invalid;服务绑定保护沿用 service_delete_protected
- 动作测试:没跑成功时报 action_failed / action_timeout,动作自己的输出放
  data.detail(折叠详情里看);找不到模板用 NOT_FOUND
- 数据源创建:表满 source_capacity;管理器未就绪 engine_unavailable。
  原先 INVALID_STATE 被说成"该 ID 已存在",其实 ts_source_register 对已有 ID
  是更新而不是拒绝,这句话是错的

结果拼装抽成小函数(fail_with / engine_refusal_token / action_template_write_reply /
action_execute_failure / source_register_reply),主机测试用生产代码 + 生产
HTTP 响应链生成线上字节存进 tests/fixtures/failure-wire/,漂移即失败。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
按"动作+对象失败:原因。下一步。"重写,成功也说清做了什么、对谁:
- 引擎四个按钮、规则(手动触发/删除/读取)、动作模板(保存/测试/读取/删除)、
  数据源(启用/禁用/删除/创建)及三个列表的读取失败;列表读取失败不再被当成"空列表"
- 保存动作模板的必填项校验改为贴在输入框旁并自动定位(空的是变量名还是值分别提示)
- 规则编辑器里"没有动作模板"和"动作模板列表读不出来"是两回事,分别说
- 页面自己的脚本出错单独说"页面出了问题"并把脚本报错放进详情,不再冒充设备失败;
  写操作的网络中断一律说"结果未确认"(引擎启停等是用 GET 发出的写操作)
- 英文必填提示改成通顺的句子;详情里"设备原文"改称"原始信息"(网络错误文字不是设备说的)

顺带修复:数据源的启用/禁用开关一直先去查"同名规则",查不到就报错,请求从未发给设备
(v0.5.2 起;规则才需要版本检查)。现在直接请求 sources.enable/disable,并有回归测试。

测试:新增 tests/prompts/flows.browser.test.cjs,用真实页面 + 生产固件的线上字节逐场景
回放,中英文句子存进 tests/fixtures/failure-texts/automation.json(评审文案看这份);
pagekit.cjs 抽出公共的页面脚手架;目录测试改为读取每个调用点真正需要的文案,
并检查目录里没有无人使用的条目。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
只加字段、不改业务:失败时 message 换成固定的原因代号,数字错误码按"最近类别"。

- 配置包(.tscfg):新增 ts_config_pack_reason(),13 种结果各有自己的代号
  (不是为这台设备生成的 / 签名没通过 / 内容损坏 / 已过期 ……),库里的英文原句放进
  data.detail;automation.{sources,rules,actions}.{export,import} 六个接口改用它。
  导出权限不足 pack_not_allowed、设备证书不可用 device_cert_unavailable;
  写文件失败统一 storage_failed(errno 文字放详情)
- 规则配置包:验证和预览可以,真正导入一直返回 "source_read_only: encrypted rule
  import requires a recoverable writer"——这是功能没做完,不是"只读来源",现在如实报
  rule_import_unavailable(原文放详情)
- 数据源连接测试(REST / WebSocket / Socket.IO 代理):连不上 proxy_connect_failed、
  对方回了 HTTP 错误 proxy_http_error(状态码数字 + 回应开头放 data)、WebSocket
  连接出错/超时/没收到消息 ws_*、Socket.IO 没按协议回应/等不到事件 sio_*;
  原来都是写死的英文句子

主机测试用生产代码 + 生产 HTTP 响应链生成线上字节,存进 tests/fixtures/failure-wire/。
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 数据源「测试」:REST / WebSocket / Socket.IO 的失败说清是哪个地址、为什么、怎么办;
  测试不改任何东西,所以断线说"无法连接到设备"而不是"结果未确认"(write:false)
- 配置包导出 / 验证 / 导入(数据源、规则、动作模板):失败写进对话框结果框,详情放提示;
  对象叫法放进 failure.kinds,英文句中小写;"请先选择文件"说明要选的是 .tscfg 配置包
- 变量 / 图片 / 主机的选择列表读取失败不再被当成"空列表";SD 卡未挂载单独一句
- 设备回的数字(如 HTTP 状态码)可以填进原因文案里的 {status}
- 英文表单提示补冠词、改成通顺的句子

测试:新增 41 个场景,中英文句子存进 tests/fixtures/failure-texts/automation.json。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
只加字段、不改业务:失败时 message 换成固定的原因代号,数字错误码按"最近类别",旧网页仍按类别说话。

- 新增公共函数 ts_api_result_reason(result, 类别, 代号, 详情)(原先只在自动化接口里有)和私有头
  ts_api_pack.h(配置包失败的统一回复,自动化与 SSH 两边共用)
- 主机指纹:保存/核对失败 known_host_failed;新主机 ssh_host_key_unknown、指纹变化
  ssh_host_key_changed(数字码 1002/1001 不变)——页面里其他调用 ssh.exec 的地方也能说清
- ssh.exec / ssh.test / ssh.copyid / ssh.revoke:登录信息不全 ssh_config_invalid、会话创建失败
  no_memory、密钥库没有这把密钥 key_not_found、公钥格式错 pubkey_invalid、服务器拒绝部署
  ssh_deploy_failed(服务器的 stderr 放详情)、撤销前检查/撤销失败、命令没执行成功
  ssh_exec_failed;连接失败沿用上一批的阶段代号
- ssh.exec_stream / ssh.cancel:另一条命令在跑 ssh_exec_busy、没能开始 ssh_start_failed、
  没有正在执行的命令 ssh_no_session、取消没送达 ssh_cancel_failed
- 主机 / 指令数量满 host_capacity / command_capacity;密钥重名 key_exists;服务不允许启动
  service_not_admissible
- 配置包导入导出(主机、指令)改用共用回复;写文件失败 storage_failed
- 连接失败的阶段映射挪进 SSH 客户端库(ts_ssh_connect_reason),WebSocket 的命令执行错误也用它:
  ssh_exec_error 消息现在带原因代号 + stage + detail;输出送达不完整 / 结果送达失败改成代号

主机测试按新代号更新(指纹相关的线上字节夹具随之改变)。
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 安全页:已知主机指纹、主机记录、撤销公钥、删除/导出密钥、部署/撤销密钥的失败都说清
  "对谁做什么失败:原因。下一步。";部署/撤销里缺哪项就把光标放到哪项;主机列表读不出来时
  说明原因,不再只写"加载失败";撤销成功但没能移除本机记录单独说
- 指令页:保存/删除指令、执行(含 WebSocket 推回来的错误)、取消、启动服务、查看日志;
  辅助命令(读日志/查进程)失败不再显示成"无输出";必填项贴在输入框旁;ID 规则一次说全
- 配置包导出(SSH 主机、指令)失败写进对话框;导出句式改成"导出“名称”(种类)失败",
  中文里的英文种类(SSH 主机)前后不再粘连
- 行内失败提示现在也有「详情」链接,和提示条一致("点详情可以看到…"在哪里都成立);
  浏览器拒绝写剪贴板单独说明;列表过期(找不到这台主机/指令)说明是页面过期

测试:新增 53 个场景,中英文句子存进 tests/fixtures/failure-texts/ssh.json 和 ssh-commands.json;
几个旧测试跟着新句子更新。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Persist complete versioned rule sets with one selector, exact-byte acceptance records, target-local revisions and restart-pending guards. Validate signing authority, recipient identity and dependencies without publishing imported rules at runtime.

Fix datasource toggle routing and add production crypto, migration, fault-recovery, engine/API and bilingual browser coverage.
Classify active/saved union states and block new admission after confirmed
pending deletion while preserving existing runtime objects and leases.
Keep ordinary rule loading compatible, retain published source identity,
and scope strict dependencies to active and saved package versions.
Share effective-action/proposed-object validation for direct/reference SSH,
internal-first host fallbacks and unsupported Webhook actions.

Add the directed A1-A3, B1-B3 and C1-C3 production-path regressions and
bilingual pending-delete/error UI checks. Preserve v3 storage, encrypted
trust and save-only/reboot activation. Record red/green evidence and
unverified device/deployment prerequisites in R1-R3-review.md.
Preserve both reviewed histories, PR README/banner changes and existing
ordinary-reference deletion diagnostics. Keep package dependency guards
in the checked removal path and retain new-reference validation on saves.

Use the same recursive SSH binding gate for nested cold operations and
nonblocking edit admission with binding-before-transaction lock order.
Adapt production-path fixtures without weakening their assertions.

Validated combined runtime suite, exact HTTP fixtures, 33 unit/static
checks, 58 bilingual browser cases and isolated ESP-IDF 5.5.2 build.
Device acceptance remains unexecuted; see INTEGRATION.md for evidence.
冲突已按“保留上游新逻辑、失败提示走统一目录”解决:删除保护弹窗保留;规则配置包的验证/导入失败、
快捷操作被拒(待删除/未生效)、规则保存失败(模板已不存在)改由 failure 目录出句子。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 规则配置包验证/导入失败、结果未确认改由 failure 目录出句子(原因 22 条新增;旧的 rulePack.errors 整张表删除)
- 快捷操作:待删除/未生效的规则被拒时说明原因
- 固件:config pack 新结果码(时间未确认、未配置信任、签名者无权限、凭证变化)映射为原因代号;引擎拒绝待删除规则时给出 rule_pending_delete
- describeFailure 读取错误自带的 data(上游让 ApiOperationError 带上 data)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 44 个自动转换留下的“保存失败/设置失败”式提示改成“动作+对象”,并补上“结果未确认”的说法
- 设备原因不再被页面自己抛的 Error 吞掉:OTA 上传/服务器、证书、配置包都带原因代号(pageFailure)
- 登录失败只在表单里显示一次;目录读取失败不再套用“上传结果不受影响”;更新检查不再重复“检查更新失败”
- 密钥生成弹窗保留失败阶段与清理结果;规则保存缺少动作模板时说出模板名
- 折叠详情里的“接口”统一写成 storage.mkdir 这种形式;错误自带 data 时也显示阶段与原始信息
- 删除 198 条已无人使用的语言包旧键和 1 条已不存在的原因(规则导入已实现)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- failure-catalog:新增“每个调用点都给足句子里的占位值”检查;一次列出全部问题
- flows:新增 system 组(登录、重启、服务、文件、网络、OTA、证书、配置包)和阻断删除弹窗、规则包导入场景;黄金句子重生成
- 上游测试适配:rule-pack/delete-reference 用 describeFailure 取句子;runtime 测试补桩与抽取清单
- ws 适配测试:补 ts_ssh_connect_reason 桩;帧类型按 type 字段判断,不再被原因代号里的 ssh_output 误判

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 引擎:删除待重启的规则,手动触发给出 TS_RULE_REJECT_PENDING_DELETE(优先于“正在保存/正在执行”),自动评估保持静默
- 线上字节:新增 wire-trigger-rule-pending-delete.json,并让“每个拒绝原因都有代号”的循环覆盖到它
- 页面:触发该规则的句子写入黄金清单;快捷操作卡片在“待删除/还没生效”时各自给出原因

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-prompts

RMinte-AI#47、RMinte-AI#48 在上游是以 squash 方式合并的,分支里保留的是它们原来的提交,直接合并会产生大量假冲突。
这里按正确的共同祖先(RMinte-AI#47 的头提交 bdc987d)做三方合并:我方改动与 RMinte-AI#48 改动没有重叠的文件,结果干净无冲突,
合并后相对 origin/main 的差异就只剩本 PR 自己的改动。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- version.txt 升到 0.6.2;新增 docs/releases/v0.6.2.md(汇总 RMinte-AI#47、RMinte-AI#48、RMinte-AI#49)和 v0.6.2-validation.md
- 合并到上游 main 后由 Actions 自动创建标签 v0.6.2 并发布 Release(check_release_state.py 的 main 路径)
- README 版本说明、静态资源 ?v= 标识同步为 0.6.2
- tests/ws_subscriptions/test_f1_f2.c:按帧的 type 字段判断“输出帧”,不再被原因代号里的 ssh_output 字样误判

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@massif-01 massif-01 changed the title 统一操作失败提示:说清哪里失败、为什么、下一步做什么(含 SSH 主机指纹确认修复) 发布 v0.6.2:统一操作失败提示,并修复 SSH 主机指纹确认框 Oct 12, 2026
massif-01 and others added 3 commits October 13, 2026 01:55
CI 的证书回归按名字从页面脚本里取 certError 并断言设备文字原样显示;迁移后该函数已删除,失败提示改由
failure 目录生成。测试改为:直接运行 api.js 里生产的 describeFailure 与 app.js 的 packBox/failureHtml,
断言句子来自目录、设备给的类 HTML 文字只以转义形式出现在折叠详情里、断线按“结果未确认”处理且只提交一次、按钮恢复。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- RMinte-AI#47 新增的配置包签发根(CONFIG_TS_CONFIG_PACK_SIGNING_ROOT_SHA256,默认空)还没写进已跟踪的 sdkconfig,
  每次构建都会把它补上;这里先提交,保证从提交出来的干净检出构建不会改动已跟踪文件
- tools/make_firmware_manifest.py:发版时把干净构建的 6 个镜像/映射文件复制到 build/ 并生成 firmware-manifest.json,
  并检查构建版本与 version.txt、被构建提交一致(HASH 后带 d 的未提交修改构建会被拒绝);--verify 核对 build/ 与清单

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 从干净检出的提交 774c3a9 用 ESP-IDF 5.5.2 构建;版本 0.6.2+774c3a97.10130158,主固件 2,226,000 字节
- build/firmware-manifest.json 记录版本、被构建提交、源码输入哈希和 6 个文件的 SHA-256(tools/make_firmware_manifest.py 生成,--verify 通过)
- 验证记录补充:镜像来源与更新方法、证书回归已在本地跑通

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@massif-01
massif-01 merged commit 88dd980 into RMinte-AI:main Oct 12, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant