Files
drouter/_dev/t-109-node-front.py
liuzhuohua 309be8b0a5 feat(snapshot): 快照可单独上锁 + 备注事后可改,全部并入紧急救援通道页
用户反馈的四件事一次做完:
1. 给快照加保护(单份上锁,自动清理一律跳过)
2. 说明「手动快照受保护不被清理」,并讲清它与上锁的区别
3. 快照备注事后可改(此前只能在创建时填,存下来就只能看不能改)
4. 列表从「配置快照与回滚」卡片搬进「紧急救援通道」,回滚入口不再弹残缺弹窗

protected 与 keep_manual 是两件事,必须分开:
- keep_manual 是全局策略,按 tag.startswith('auto') 猜来源
- protected 是用户对这一份的显式意图,跨策略生效
判据只能加在删除分支上,混进 auto 算法的话,受保护的自动快照
会开始占 keep_count 配额,把别的自动快照挤掉 —— 用户设「保留 5 份」
却只剩 4 份,且没有任何提示。

上锁不等于删不掉:删受保护的那份时后端回 needs_force,前端弹二次确认
后带 force 重试。审计里标注「受保护,已强制删除」,否则事后只看得到
「删了快照」,看不出删的是用户特意保下来的那份。

meta 用 .tmp + os.replace 原子写 —— meta 是 prune 唯一的判据来源,
写到一半被打断会留下解析不了的残文件,之后那份快照既不会被清理,
也上不了锁(读不到就当没保护)。

备注消毒:控制字符一律换成空格而不是删掉。删掉会把「甲 BEL 乙」
变成「甲乙」,两个词粘成一坨,而用户看到的备注莫名其妙。

真机验收(40 项全绿)当场抓到两个本地回归漏掉的真 bug:
- 备注消毒把制表符删掉而不是换空格:'a\\nb\\tc' 变成 'ab c'
  本地判据只写「不含换行和制表符」,恰好放过 —— 「没有危险字符」
  和「内容没被偷偷改掉」是两件事
- web 层无条件写 'protected': b.get('protected'),
  payload 里这个键永远在(值可能是 None),helper 的
  `if 'protected' in p` 恒成立,「不传就取反」从 HTTP 路上彻底失效
两个都补了反向验证(注入后必须观察到红)。

顺带修掉一个既存缺陷:t-107.py 里的 js_code_only 一直在吃 app.js
的真代码 —— 用 re.sub 块注释优先、再剥行注释,而 app.js 8222 行有一句
讲「怎么防止 CSS 注释被提前闭合」的行注释,注释里写着块注释的起止符
字面量,于是被当成真注释开头一路吃到文件尾。
证据:块注释起始 169 次而结束 170 次,数量不等就是配对错位一格。
HEAD 版同样中招(163/164),所以不是本轮引入。
换成 _dev/jsstrip.py 的词法扫描版(正确处理模板串的插值、正则字面量、
prev 记 token 尾词而不是一个字符),并加 node 侧 V8 交叉验证。
2026-10-03 14:22:14 +08:00

104 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把两个 node 侧的前端检查器纳入 python 回归。
为什么要有这么一个壳,而不是直接改 run-static.py 去调 node:
1. run-static.py 的收集规则是 `glob('t-*.py')` + EXCL 黑名单。
让 node 检查器**以 t-*.py 的身份出现**,就不用给 run-static.py
加第二套收集逻辑(那是两条会各自漂移的路径 —— 以后再加一个
node 检查器时,十有八九只记得改其中一处)。
2. `devtools/check-jsstrip.js` 需要一份「剥完注释的 app.js」当输入,
而那份产物只能由 python 生成(node 自己跑 python 在 Windows 上
EBUSY,spawnSync 会被信号杀掉、rc=null)。所以顺序必须是
**先 python 备料,再 node 验收** —— 这个顺序约束也只有放在
python 侧才表达得自然。
用法:
python _dev/t-109-node-front.py
python _dev/run-static.py t-109 # 跟全量回归一起跑
"""
import glob
import os
import re
import shutil
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
NODE = os.environ.get('NODE_BIN') or shutil.which('node') or 'node'
# 要串行跑的 node 检查器。**顺序有意义**:jsstrip 的产物是
# check-jsstrip.js 的输入,所以它虽然只是「备料」,也放在同一串里,
# 免得有人在只跑单个检查器时得到「产物不存在」的假红。
NODE_CHECKS = [
('check-jsstrip.js', '剥注释器(词法扫描版)的 V8 交叉验证'),
('check-snap-render.js', '快照列表内嵌渲染 + 备注/上锁/二次确认交互'),
]
def run(cmd, what):
try:
p = subprocess.run(cmd, capture_output=True, text=True,
encoding='utf-8', errors='replace', timeout=180)
return p.returncode, (p.stdout or '') + (p.stderr or '')
except FileNotFoundError:
# 没装 node 不是代码的错。别让一个环境问题把整轮回归判红。
return 127, '%s:找不到可执行文件 %r\n' % (what, cmd[0])
except subprocess.TimeoutExpired:
return 124, '%s:超时(180s)\n' % what
def main():
ok_all, bad_all = 0, []
# ---- 步骤 1:跑 jsstrip.py 生成 .jsstrip-appjs.out ----
# 这一步自己会跑一遍 44 项 selftest,产物是副产品。
rc, out = run([sys.executable, os.path.join(HERE, 'jsstrip.py')],
'jsstrip.py')
m = re.findall(r'通过 (\d+) 项', out)
if rc == 0:
print('[OK] jsstrip.py 自检%s 项(并写出 .jsstrip-appjs.out)'
% ((' ' + m[-1]) if m else ''))
else:
print('[NG] jsstrip.py 自检失败 rc=%s' % rc)
for line in out.splitlines():
if line.strip().startswith('·') or 'Traceback' in line:
print(' ' + line.rstrip())
bad_all.append('jsstrip.py')
# ---- 步骤 2:逐个跑 node 检查器 ----
for name, desc in NODE_CHECKS:
f = os.path.join(ROOT, 'devtools', name)
if not os.path.isfile(f):
print('[NG] %-24s 文件不存在:devtools/%s' % (name, name))
bad_all.append(name)
continue
rc, out = run([NODE, f], name)
m = re.findall(r'通过 (\d+) 项', out)
n = int(m[-1]) if m else 0
if rc == 0:
ok_all += n
print('[OK] %-24s %s(%d 项)' % (name, desc, n))
else:
print('[NG] %-24s %s rc=%s' % (name, desc, rc))
for line in out.splitlines():
if '[NG]' in line or line.strip().startswith('·') \
or 'Error' in line:
print(' ' + line.rstrip())
bad_all.append(name)
print()
print('=' * 60)
print('通过 %d 项,失败 %d 项' % (ok_all, len(bad_all)))
for b in bad_all:
print(' · %s' % b)
print('=' * 60)
return 1 if bad_all else 0
if __name__ == '__main__':
sys.exit(main())