← 返回 Skill 列表
extension
分类: 数据与分析无需 API Key

DeepSeek 公式粘贴净化器(剪贴板)

DeepSeek / KaTeX 网页复制到 Obsidian 后,公式「重复两遍、空格丢失、定界符缺失、分数塌成数字、完全不渲染」的根治工具。 常驻监听剪贴板,在你 Ctrl+C 之后约 0.16 秒把内容改写成干净 Markdown。优先从 KaTeX 的 annotation 里取出唯一可信的 TeX 源码;annotation 不在时(网页上拖选复制,mathml 层被 CSS clip 成 1px 而掉出选区)则按 KaTeX 的排版几何把视觉层反推回 LaTeX —— 分子分母靠 top 偏移判断、上下标靠 pstrut 高度判断,\frac / 上下标 / 根号 / cases / 矩阵 / \binom / \vec 都能还原。随后重建 $ / $$ 定界符、丢掉重复的视觉层、抹掉 HTML 格式让 Obsidian 走纯文本路径。检测不到公式就一个字节都不动。

person作者: user_26bbdea0hubcommunity

DeepSeek / KaTeX 复制粘贴净化器

把「网页上复制的公式」在进入剪贴板的那一刻就修好,而不是指望下游编辑器抢救。

适用平台:Windows(依赖 Win32 剪贴板 API)。零第三方依赖,Python 3.8+ 即可。

当前版本 v1.4.1。改完脚本后,务必用 --restart(= 启动监听.bat)重启监听 —— 旧进程会占着单例锁,不重启等于新代码没上,同一个 bug 会反复复现(见陷阱 r)。

| 版本 | 关键修复 | |---|---| | 1.0 | 从 annotation 提取 TeX 源码、重建定界符、抹掉 HTML | | 1.1 | 中文控制台 emoji 崩溃 / stderr 被 DEVNULL 吞 / 互斥体假活(引入心跳) | | 1.2 | 网页复制异步覆盖剪贴板(守护窗口)/ 启动补处理 / 写回读回校验 | | 1.3 | 判据漏「HTML 已丢的重复裸文本」导致静默跳过 + 自愈 + 版本握手 + 僵尸清理 | | 1.4 | 框选复制时 annotation 根本不在剪贴板里(mathml 被 clip 成 1px)→ 视觉层反推:分数不再塌成 "79",上下标 / 根号 / cases / 矩阵 / \binom / \vec 都能还原 | | 1.4.1 | SkillHub 拒收 .bat(报「不允许的文件类型」)→ 发布包里不再放启动器,改为 --make-shortcuts 在本地生成(内容与原版逐字节一致) |


1. 先判断是不是这个病

用户贴了截图说「复制过去格式有问题」时,对照这三条:

| 现象 | 判定 | |---|---| | 公式出现两遍,且没有空格(15x+8y=4000015x+8y=40000) | ✅ 就是这个病 | | 公式是裸文本,Obsidian 里不渲染 | ✅ 同上 | | $y$、$$ 这些定界符在原文里看不见 | ✅ 同上(说明只拿到视觉层) | | 界面按钮文字(复制、ds-icon)混进了正文 | ✅ 附带症状,本 skill 一并清理 |

如果用户是先粘到纯文本编辑器再转,或者用的是 Word/PPT,这个工具仍然可用 (它只输出干净的 Markdown),但收益不如 Obsidian 场景明显。


2. 病根(三层叠加,缺一层都不会这么难看)

  1. 公式在 DOM 里有两份。

    <span class="katex-mathml">   无障碍 MathML,内含
        <annotation encoding="application/x-tex">TeX 源码</annotation>
    <span class="katex-html">     真正显示的字形,被拆成一堆细碎 span
    

    浏览器「选择 + 复制」把两份都写进 text/html。Obsidian 的 HTML→Markdown 转换器不认识 KaTeX,于是两份都变成了文本 → 公式重复两遍。

  2. 定界符从来没进过剪贴板。 $...$ / $$...$$ 只存在于 annotation 的属性值里,不在任何文本节点中。 所以粘过去必然是裸文本,渲染器无从下手。

  3. 空格是 CSS margin 摆出来的,不是字符。 katex-html 里的 15x + 8y = 40000 转成文本时空格全丢 → 15x+8y=40000。 不要试图靠正则补空格,唯一可信来源是第 1 条里的 TeX 源码。

结论:正确解法是放弃视觉层,只信 annotation。


3. 安装(在用户机器上部署)

scripts/ 里已经有全部代码(只有 .py,没有 .bat,原因见下), 复制到一个固定目录即可,例如 %USERPROFILE%\.workbuddy\tools\paste-cleaner\:

mkdir -p "$USERPROFILE/.workbuddy/tools/paste-cleaner"
cp "<本 skill>/scripts/"*.py "$USERPROFILE/.workbuddy/tools/paste-cleaner/"
cd "$USERPROFILE/.workbuddy/tools/paste-cleaner"
python ds_paste_cleaner.py --selftest         # 内置回归,必须全绿
python ds_paste_cleaner.py --make-shortcuts   # 生成双击启动器(.bat)
python ds_paste_cleaner.py --daemon           # 无窗口后台常驻
python ds_paste_cleaner.py --status           # 确认在跑

为什么包里不放 .bat,而要多一步 --make-shortcuts? SkillHub 上传时会拒绝可执行脚本,报 不允许的文件类型: scripts/_runner.bat。 所以发布包里只留 .py,启动器在本地由 ds_paste_cleaner.py 自己写出来 (源码里的 _RUNNER_BAT + SHORTCUTS 就是那 11 个文件的内容,实测与随包发布 逐字节一致)。副作用是好的:启动器从此能跟着版本一起更新,不会再出现 「脚本升级了、启动器还是旧的」。

日常用法只有一句:复制完直接粘。监听进程会在 Ctrl+C 之后约 0.25 秒内 悄悄把剪贴板换成干净 Markdown。

⚠️ 如果 agent 跑在沙箱/非交互环境里:--daemon 派生的脱离进程会被回收, 表现为「刚起来几秒就没了」。这不是 bug,必须让用户自己双击 启动监听.bat (没有的话先跑一次 --make-shortcuts)。 同理 cscript/wscript/.vbs 常被安全策略拦截(LOLBin),所以自启走 注册表 HKCU\...\Run,而不是启动文件夹里放 VBS。


4. 命令行参考

| 命令 | 作用 | |---|---| | --daemon | 无窗口后台常驻(推荐,用 pythonw.exe 派生)。已有实例在跑时会识别版本,版本不一致自动重启 | | --restart | 先停旧进程再按磁盘代码起。改完代码后必用 —— 否则旧进程占着单例锁,新代码上不了岗 | | --watch | 前台常驻,能看到日志(排障用) | | --stop | 停止后台监听(会写 control.txt=stop,此后自愈逻辑不会擅自拉起) | | --toggle-pause | 暂停 / 恢复净化(临时复制别的东西时用) | | --once | 净化一次并打印剪贴板现状(含 HTML 还在不在)。这是判断「故障在净化侧还是粘贴侧」的利器,见陷阱 p | | --once --dry | 同上,但只预览不写回 | | --force | 即使没检测到公式也强制转换 | | --file x.html / --text "..." | 离线转换,不碰剪贴板(最安全的调试入口) | | --selftest | 跑内置回归测试(含 dedup 兜底、视觉层反推、启动器规范、零误伤) | | --make-shortcuts | 在本地生成双击启动器(.bat)。发布包里不含 .bat,换机器后跑一次这个就恢复。加 --english-shortcuts 用英文文件名,加 --shortcut-dir DIR 指定输出目录 | | --status | 查看自启 / 进程 / 心跳 / 版本 / 暂停状态。发现没在跑会自动拉起(自愈) | | --diagnose | 一键体检:进程 / 心跳 / 剪贴板现状 / 三路径端到端活性自检(探针 A 文本路径 + 探针 B dedup 兜底路径 + 探针 C 视觉层反推)。排障首选,比 --status 信息全得多 | | --interval 0.15 | 轮询间隔秒(默认 0.15;越小竞态窗口越小,API 调用本身极轻) | | --heading-base 2 | 标题归一化到第几级(0 = 保持原样;默认 2,即 # → ##) |

--make-shortcuts 生成的 11 个文件与上表一一对应:

| 启动器 | 等价命令 | |---|---| | 启动监听.bat / start.bat | --restart 然后 --status(重启,保证按磁盘上的新代码跑) | | 停止监听.bat / stop.bat | --stop | | 暂停或恢复.bat / toggle-pause.bat | --toggle-pause | | 立即净化剪贴板.bat / clean-now.bat | --once(净化并显示剪贴板现状) | | 看剪贴板是什么.bat / peek.bat | --once --dry(只看不写,用来确认剪贴板干净了没) | | 查看状态.bat / status.bat | --status | | 运行自检.bat / selftest.bat | --selftest | | 一键体检.bat / diagnose.bat | --diagnose(最该先双击这个) | | 安装开机自启.bat / install-autostart.bat | --install-autostart | | 取消开机自启.bat / uninstall-autostart.bat | --uninstall-autostart |

外加一个内部用的 _runner.bat(解释器探测核心,上面 10 个都 call 它), 它不面向用户双击。

默认生成中文文件名(中文 Windows 用户体验更好);--english-shortcuts 生成 ASCII 名。两种都行,内容一致。想要别的名字,生成后自己改名即可。


5. 实现要点(要复刻或改的时候看这里)

  • 单文件、零依赖:html.parser.HTMLParser 自己搭 DOM 树(_TreeBuilder), ctypes 直调 user32.OpenClipboard / GetClipboardData / SetClipboardData。 不要去装 pywin32 / bs4 / lxml。
  • 两条输入路径:
    • 有 KaTeX 的 text/html → 走 HTML→Markdown(信息最全,优先)
    • 只有纯文本 → 走 clean_plain_text(),把 \[..\]→$$..$$、\(..\)→$..$, 并给裸的 \begin{align} 补 $$ 包裹
  • 判定是否该动手:html_has_math(html) or text_has_latex(text)。 不满足就原样返回,绝不改动。这是「不误伤普通文本」的唯一保证。
  • 框选复制时剪贴板里根本没有 annotation(1.4 新增):KaTeX 的 katex-mathml 层被 CSS clip: rect(1px,1px,1px,1px) 藏在公式角落,用户拖选公式时它不在选区里, 浏览器序列化出来的 HTML 只剩 katex-html 视觉层,连 class="katex" 外壳都没有。 这时只能按 KaTeX 的排版语义把视觉层「读」回 LaTeX(_KatexReverse):
    • mfrac:同一 vlist 里 top 更负(位置更高)的是分子;没有 frac-line 的是 \binom
    • msupsub:|top| > pstrut 高度 = 上标,否则 = 下标
    • sqrt:root 子树是指数,主 vlist 里非 hide-tail 的项是被开方内容
    • mop:∑ ∫ 取字形;lim / sin / log 还原成 \lim 等(\log_2 的 "log" 会被字距拆成 "lo"+"g",必须拿整棵子树的文本判断,只看直接子节点会丢字)
    • accent:accent-body 里是字符(^ ˙)→ \hat \dot;是 SVG 矢量(\vec 箭头)→ \vec
    • \not:视觉层 = 「符号 + 覆盖斜线(字体私用区字形 PUA)」,且覆盖层在 DOM 里 排在符号前面,合并时方向不能搞反(\x01= → \ne)
    • 定界符 + 表格 → cases / pmatrix / bmatrix / vmatrix; (...)^2 要补成 {(...)}^2,否则幂只绑到右括号上、渲染错位
    • 视觉层里的 { } _ % & # $ 是字形,回到 LaTeX 源码必须转义 —— 否则 \left\{ 的 { 会被当成分组符吃掉,公式直接坏掉
    • 判据全部来自几何量(style="top:-3.677em" 与 pstrut 高度),不依赖 具体行高/字号,所以放大缩小页面都不影响
  • 必须抹掉 HTML 格式:只写回 CF_UNICODETEXT,EmptyClipboard() 后 不复写 HTML。否则 Obsidian 又会去走 HTML 导入路径,前功尽弃。
  • 标题里的行内公式要拍平成纯文本:Obsidian 不在标题里渲染数学, 留着 $y$ 只会更丑。
  • $$ 块前后各要一个空行:这是 Obsidian 渲染独立公式块的硬要求, 少了就退化成行内。
  • 写回自己的内容要记住序列号(GetClipboardSequenceNumber), 否则监听进程会把「自己刚写的内容」当成新内容,无限循环。

6. 验证(改完必须跑,别只看代码)

cd "%USERPROFILE%\.workbuddy\tools\paste-cleaner"
python ds_paste_cleaner.py --selftest        # 10 组断言(含 dedup 兜底 / 视觉层反推 / 零误伤)
python test_clipboard_roundtrip.py           # 端到端:真写进 Windows 剪贴板再读回
python ds_paste_cleaner.py --diagnose        # 三探针活性自检(A 文本路径 + B 兜底路径 + C 视觉层反推)
python test_katex_visual.py                  # 可选:68 个真实 KaTeX 样本的反推回归(需 Node + katex)

test_katex_visual.py 的判定标准不是「字符串相等」,而是把反推结果再渲染成 MathML、与原文的 MathML 逐字节比对(剥掉 mrow/mstyle 包裹层)—— 这样才能区分 「写法不同但语义相同」(x^{2} vs x^2,正常)和「真的推错了」。当前 68 个样本 全部通过语义等价。

还有两件必须验的事,自动化测试覆盖不到:

① 必须从 cmd 里跑一次,模拟用户双击的环境(剥掉 PYTHONIOENCODING / PYTHONUTF8, 让 Python 回落到 cp936)。这一步才能抓出 emoji 编码这类只在控制台下暴露的崩溃:

env = {k: v for k, v in os.environ.items()
       if k not in ('PYTHONIOENCODING', 'PYTHONUTF8', 'PYTHONLEGACYWINDOWSSTDIO')}
r = subprocess.run(['cmd', '/c', '_runner.bat', '--selftest'],
                   capture_output=True, cwd=root, env=env, stdin=subprocess.DEVNULL)
assert r.returncode == 0, r.stdout.decode('gbk', 'replace')[-800:]

(stdin=DEVNULL 是为了让 bat 末尾的 pause 立刻返回,否则会挂住测试。 注意 _runner.bat 不在发布包里,跑这段之前先执行一次 --make-shortcuts。)

② 端到端活性自检——这是唯一能证明「它真的在干活」的证据:

python ds_paste_cleaner.py --diagnose

它往剪贴板写一个探针、等 1.2 秒、看是否被自动净化,然后恢复原内容。 不要用「进程状态显示正在运行」当作判据 —— 僵尸进程也会这么显示(见陷阱 k)。

再加两条手工实测(自动化测不出来的):

# 守护进程是否真的会自动改写
python -c "import sys,time; sys.path.insert(0,'.'); import ds_paste_cleaner as D, test_clipboard_roundtrip as T; T.put_dirty_clipboard(D.SELFTEST_HTML, T.DIRTY_PLAIN); time.sleep(1.5); print((D.read_clipboard()['text'] or '')[:46])"
# 期望开头: ## 快速解题步骤

# 普通文本不能被误伤
python -c "import sys,time; sys.path.insert(0,'.'); import ds_paste_cleaner as D; s='价格 \$100 和 \$200 两档。'; D.write_text(s); time.sleep(1.2); print((D.read_clipboard()['text'] or '')==s)"
# 期望: True

7. 已知陷阱

a. 发布包里绝对不能有 .bat(v1.4.1 的起因)。 SkillHub 上传时会逐个校验文件类型,遇到可执行脚本直接拒收:

提交失败
不允许的文件类型: scripts/_runner.bat

解法不是删掉启动器,而是把「打包」改成「生成」:_RUNNER_BAT + SHORTCUTS 两个常量存在 ds_paste_cleaner.py 里,--make-shortcuts 在本地把它们写出来。这样包内只剩 .md / .py,而用户双击的体验一点没少。

同类平台大多也拦 .cmd / .ps1 / .vbs / .exe。要做发布包时, 先只放 .md + .py,其余可执行的东西一律运行时生成。

b. 生成的 .bat 必须是 CRLF 行尾。 LF-only 的文件会让 cmd.exe 把多行括号块和 rem 注释解析得乱七八糟, 典型报错是 'te-cleaner' 不是内部或外部命令(路径被从中间截断)。 不要手工转换 —— _write_bat() 已经统一做归一化,源码里存 LF 即可:

def _write_bat(path, text):
    data = text.replace("\r\n", "\n").replace("\n", "\r\n").encode("ascii")
    open(path, "wb").write(data)

注意那个 .encode("ascii"):它让「不小心塞进中文」当场抛异常, 而不是悄悄生成一个乱码启动器。

c. .bat 里不要用管道回读解释器路径。 for /f ... in ('python -c "import sys;print(sys.executable)"') 看起来聪明, 但子进程按 locale 代码页输出、cmd 按 OEM 代码页解码,用户名含中文时回来是乱码 (实测 娓╄壇鎭淇璁‐),于是「系统找不到指定的路径」。 正确做法:静默探测 + 直接调用(py -3 -c "..." >nul 2>nul 只判断 errorlevel)。

d. .bat 内部保持全 ASCII。 包括注释。中文注释在 OEM 代码页下会变成乱码甚至破坏语法。 所有面向用户的中文文案放在 Python 里打印(Python 走 Windows 控制台 API,不受代码页影响)。 中文只允许出现在文件名里(explorer 走 Unicode,没问题)。

e. 上面 b/c/d 三条都要有守卫,否则早晚复发。 规则写在注释里等于没写。--selftest 会逐条检查:10 个启动器一个不少、 内容全 ASCII、行尾全 CRLF、_runner.bat 里存在真正的调用行 "%PC_DIR%ds_paste_cleaner.py" %*。 (最后那条特意匹配调用行本身而不是 ds_paste_cleaner.py 这个字符串 —— 后者在注释里也出现,用它会「看起来有检查、实际拦不住」。)

f. 不要用 os.remove() 做状态标志。 本机安全策略会拦截删除操作。改用单文件覆写状态机: 一个 control.txt 里写 run / pause / stop,监听进程轮询它的内容。 不删任何文件。

g. 中文用户名会打崩一些原生扩展。 本机用户名是 温良恭俭让。如果哪天给这个项目引入了 C 扩展(比如某些解析库), 导入期可能直接 access violation 段错误 —— 段错误无法被 try/except 捕获, 会带崩整个进程。这也是坚持「只用纯标准库」的原因之一。

h. stdout 是行缓冲/块缓冲的坑。 后台进程用 -u 或 flush=True,否则日志看起来「什么都没发生」。

i. 中文 Windows 控制台 + emoji = 进程猝死(本 skill 实测踩到)。 程序里用 ✅ ❌ ℹ️ ⏸️ 做状态标记。它们在 cp936 里没有对应编码, print 会抛 UnicodeEncodeError —— 而异常抛在 print 里会把整个进程带走 (退出码 1),现场只剩半截输出,stderr 还可能是空的。 必须在入口调用 make_stdout_safe():

sys.stdout.reconfigure(errors="replace")   # 编不出的字符退化成 '?',中文照常

实测对比(_runner.bat --selftest,剥掉 PYTHONIOENCODING/PYTHONUTF8 还原双击环境): 修复前 rc=1,输出停在 ✅ 自检全部通过 的前一行;修复后 rc=0,✅ 显示为 ?。 注意这条同时影响 --daemon 的成功提示(✅ 监听已在后台运行)—— 父进程会在打印那一刻崩掉,用户看到的就是「启动失败」。

j. 守护进程的 stdout/stderr 不要丢进 DEVNULL。 subprocess.DEVNULL 会把启动阶段的 traceback 一起吞掉,出问题时对着空日志无从下手。 改成追加写文件(paste-cleaner.out.log),代价是几 KB 磁盘,换来的是「崩了能查」。

k. 互斥体存在 ≠ 进程在干活。 被挂起/冻结的僵尸进程同样持有 mutex 句柄,于是判活函数返回「正在运行」, 而复制粘贴毫无反应 —— 这是最难查的一种假象(进程没死、句柄没放、CPU 不涨)。 解法:watcher 每秒写 heartbeat.txt,判活要求「互斥体存在 且 心跳新鲜(<15 秒)」。 实测有效:进程被冻结后,状态立刻从「正在运行」翻成「未运行」。

l. 主循环之外的裸调用要单独保护。 watch() 进入 while 之前有一行 clipboard_seq(),曾经没有任何 try 包裹。 它一旦抛异常,进程当场死掉(循环内的 try/except 根本轮不到)。 凡是「进主循环之前」的调用,都要自己兜住 —— 那是最容易被忽略的崩溃点。

m. 网页的「复制」是异步落板的 —— 净化结果会被覆盖(本 skill 最隐蔽的一条)。 浏览器在 copy 事件里同步写入富文本之后,往往还会走 navigator.clipboard.write 之类的异步路径再写一次。于是:

t=0.00  用户 Ctrl+C   → 剪贴板写入 KaTeX 双层富文本(脏)
t=0.25  净化器写回     → 干净的纯文本,日志记 "cleaned"   ← 看起来成功了
t=0.60  浏览器异步写入 → 剪贴板又被填回脏富文本           ← 被覆盖
t=2.0   用户粘贴       → 拿到脏数据,日志却写着「已净化」

症状极具迷惑性:日志、--status、端到端自检全部正常,唯独用户粘出来还是坏的。 解法是守护窗口:写回之后再盯 GUARD_SECONDS(5 秒)的序列号, 一旦发现被覆盖就再净化;若又被覆盖就继续后延(务必设硬上限,否则循环退不出去)。 本 skill 实测:连续两次覆盖全部被兜住。

n. 写回之后必须读回校验。 write_text(after) 的返回值如果没人检查,就等于闭着眼睛报成功。 剪贴板会被别的进程短暂占用(OpenClipboard 失败),也可能写完立刻被覆盖。 正确做法:写回后立刻读回,确认 HTML 真的被抹掉 且 纯文本与预期逐字一致, 不一致就退避重试几次。校验结果要进日志 —— 「verify=ok」和「verify=HTML 仍在」 是完全不同的两件事,日志里必须能区分。

o. 启动瞬间要补处理一次剪贴板。 主循环只认「序列号变化」,所以监听启动之前就已经躺在剪贴板里的内容 永远不会被碰。而用户的真实习惯恰恰是「先复制、再想起来去启动工具」。 在 watch() 里补一次 process_once()(只处理确实需要净化的,普通文本原样放过)即可。

p. 判断「剪贴板到底干净了没有」的最快办法。 别让人去开记事本做对照实验 —— 让工具自己说。--once 会打印剪贴板现状:

判定:已净化(来源 html)
剪贴板现状:纯文本 813 字符,HTML 无
---
## 快速解题步骤
设采购猕猴桃 $x$ 斤,苹果 $y$ 斤。

看到 $ / $$ 定界符 + HTML 无 → 剪贴板是干净的,故障在粘贴侧; 看到 xx、15x+8y=40000 出现两遍这种裸重复 → 剪贴板压根没被改写。 一刀切开「净化侧」和「粘贴侧」,不用再猜。

q. 判据漏掉「HTML 已丢的重复裸文本」→ 静默跳过(唯一不报错的失败路径)。 KaTeX 每个公式在 DOM 里有两份(katex-mathml 语义层 + katex-html 视觉层)。 浏览器复制时两份一起序列化;一旦 HTML 那一层掉了(某些浏览器/复制按钮只写 text/plain,或别的剪贴板工具重写后又覆盖回来),剪贴板里就只剩两份纯文本 首尾相连的产物:

15x+8y=4000015x+8y=40000      25-10=1525-10=15      x>500x>500
设采购猕猴桃 xx 斤,苹果 yy 斤。          ← 单字符公式同样成对出现

这种内容没有 $、也没有 \[ \(,而最初只写了两个判据:

def needs_cleaning(text, html):
    if html_has_math(html): return True
    return text_has_latex(text)      # 只认 \[ \( \begin{...

→ 判「不含 LaTeX」→ 静默跳过:日志、状态、自检全绿,用户看到的还是脏内容。 这是最坑的一条,因为它不报错、不写日志。 修法是加第三判据 looks_like_katex_duplication():正则 ([" + 数学字符集 + "]{3,80})\1 找 相邻重复,且要求重复段里至少有一个数字或运算符(避开 abcabc、 ShareXShareX 这类噪音);命中走 dedup 降级路径去重。 注意降级路径的能力边界:\frac{40000-15x}{8} 这类分数,mathml 与 html 的 文本形态不同(40000-15x8 vs 840000-15x),去重救不回来 —— 只能靠 HTML 层 里的 annotation 还原。所以日志里出现 cleaned via dedup 是个信号: 说明 HTML 层丢了,该去查是不是有剪贴板助手 / 浏览器扩展在中间插手。

r. 单例锁 + 旧进程 = 「改完代码不生效」;僵尸进程还占着锁不放。 这条坑出现过两次,症状都是「明明重启过了,行为没变」:

  • --daemon 见到已有实例就直接返回「已经在运行」,不比较版本。改完代码后 旧进程仍在值班 → 跑的还是老逻辑。
  • 更糟的是僵尸:进程被挂起/冻结时互斥体句柄不释放,新进程永远拿不到锁, 表现是「怎么点都起不来,且没有任何报错」。

解法三件套:

  1. 心跳文件里带上 时间戳|pid|版本,启动时比对版本,不一致自动重启;
  2. --restart:先 stop 再起,改完代码必用;
  3. kill_stale_watcher():按心跳里的 pid 用 OpenProcess + TerminateProcess 直接结束僵尸(不要去 taskkill 乱杀 python 进程,会误伤别的脚本)。

s. 让「工具没在跑」不再需要人来记。 守护进程类工具最真实的失败模式不是崩溃,而是根本没启动(用户忘了双击、 开机自启要下次登录才生效、测试时被顺手停掉)。给所有前台命令加自愈: --status / --once / --diagnose 一开始就检查 watcher_state(), 不在运行就 ensure_running() 拉起来(但 control.txt == "stop" 时尊重用户意图, 不擅自拉起)。这样用户只要动过任何一个脚本,保护就已经在岗了。


8. 文件清单

deepseek-paste-cleaner/
├── SKILL.md                        ← 你正在读的文件
├── references/
│   └── troubleshooting.md          ← 症状 → 根因 → 处置 速查表
└── scripts/
    ├── ds_paste_cleaner.py         ← 全部逻辑(单文件,含 --make-shortcuts 与内置自检)
    ├── test_katex_visual.py        ← 68 个真实 KaTeX 样本的「视觉层 → LaTeX」语义等价回归
    └── test_clipboard_roundtrip.py ← 端到端剪贴板往返测试

包里刻意没有 .bat —— SkillHub 拒收可执行脚本。用户跑一次 python ds_paste_cleaner.py --make-shortcuts,会在脚本同级目录生成 _runner.bat + 10 个双击启动器(详见第 4 节的对照表)。

运行时会在脚本同级目录生成(都可安全清空,不含配置): control.txt(状态机)、heartbeat.txt(心跳)、paste-cleaner.log(日志)、 paste-cleaner.out.log / paste-cleaner.crash.log(守护进程输出与崩溃)、 capture-last.txt / capture-prev.txt(最近两次处理的完整现场)。


作者:Primer 🧬