Skip to content

Releases: veawho/via54Medit

v5.4.49: 飞书 TraeWork Bot 全局报错治理与生产级加固

Choose a tag to compare

@veawho veawho released this 18 Sep 20:17

核心变更

  1. 多维表格周报归档契约防御 (telemetry/bitable_sync.py & telemetry/daemon.py)

    • FeishuBitableManager 新增 finalize_weekly_drafts(report) 标准实现,统一返回 (bool, str) 二元组契约。
    • telemetry/daemon.py 调度调用处增加非迭代对象、Mock 对象与空值防御性解包保护,根治 TypeError: cannot unpack non-iterable Mock object。
  2. 自动同步与构建高可用升级 (scripts/auto_sync.py)

    • 增加编译回退机制:当环境缺少 GNU Make 或工作目录无 Makefile 时,自动回退到 go build 直接编译,彻底消除 make: *** No rule to make target 'build'. Stop. 报错。
    • 依赖静默自愈:冒烟测试前自动嗅探 pymupdf/fitz,缺失时后台静默自愈安装;净化测试错误日志输出,避免裸 Traceback 被日志监控器升级为假告警。
  3. 看门狗与生命周期稳定性提升 (telemetry/daemon.py)

    • 单次循环开始前优先刷新心跳,防止耗时文件扫描和复杂网络同步导致外部看门狗误判假死。
    • Windows 平台 stop_daemon_process 切换为 taskkill /PID {pid} /T /F 树状强杀,彻底消灭孤儿 pythonw.exe 幽灵实例堆积。
  4. 告警通道噪声白名单过滤 (telemetry/alerter.py)

    • 增加 IGNORED_ALERT_PATTERNS 过滤白名单,静默拦截 Chromium Crashpad 内部探测错误(如 directory_reader_win.cc:44 FindFirstFile: 0x3)等良性底座噪声,杜绝告警风暴。

v5.4.48 — 多平台部署与规范合规标准化

Choose a tag to compare

@veawho veawho released this 12 Sep 06:55

核心变更

  1. 统一双平台一键安装入口 (对标 hermes-agent / openclaw)

    • 新增 install.sh / scripts/install.sh: 针对 POSIX (macOS / Linux),内置 unset PYTHONPATH/PYTHONHOME 环境变量隔离与 Python 3.10+ 自动多级嗅探。
    • 新增 scripts/install.ps1: 针对 Windows PowerShell,自动识别 python.exe / py -3 与标准路径,透传参数执行引导。
    • 强化 scripts/bootstrap_device.py:
      • 自动检测 Windows 追加 .exe 后缀,注入与 Makefile 对齐的 -ldflags 版本戳记,消除版本回退硬编码。
      • 无 pytest 时优雅回退至内置 python -m unittest discover -s tests,测试永不静默漏跑。
  2. 代码库硬编码清理与路径规范

    • 彻底修复 test_citation_sync.py、test_via54_rules.py、test_ppt_understand.py 及 strict_eval_ocr_locate.py 中的用户目录与 POSIX /tmp 硬编码,改用跨平台临时目录与通用路径。
    • 修复 test_hl_lib.py 孤立执行时的 sys.path 优先级,自动同步技能分发包(保持 test_repo_hygiene 全绿)。
    • 去隐私化:清理 .trae/rules/project_rules.md 中的个人绝对路径。
  3. GitHub 仓库与 Releases 交付规范

    • 规范 .gitattributes:强制 *.sh 使用 LF,*.ps1/*.bat 使用 CRLF。
    • 补齐社区治理与资助规范:.github/FUNDING.yml、CONTRIBUTING.md。
    • 建立 GitHub Actions 自动化发布流水线:.github/workflows/release.yml,打标自动触发 GoReleaser 多架构构建(Windows/macOS/Linux 各 amd64/arm64)。

v5.4.47 — 修红 CI(Windows 跨盘符 relpath)

Choose a tag to compare

@veawho veawho released this 12 Sep 04:29

v5.4.47 — 修红 CI

v5.4.46 的 CI 是 5/6:python (windows-latest) 在 "Deploy scanner tests" 挂了。

成因

File "scripts\deploy_scan.py", line 1253, in scan_platform_compat
    rel = os.path.relpath(path, REPO)
File "<frozen ntpath>", line 766, in relpath
ValueError: path is on mount 'C:', start on mount 'D:'

scan_platform_compat() 里 repopath(path, REPO) 在 Windows 上跨盘符会直接抛
ValueError,而不是返回相对路径。CI 上仓库在 D:,而新加的测试把 _compat_roots()
指到 tempfile.TemporaryDirectory()(落在 C:)—— 于是踩中。

这是真缺陷,不只是测试环境问题:_compat_roots() 本来就是可覆盖的(测试、以及任何把
扫描面指到别处的调用),而"报告里怎么显示路径"这件事不该让整段扫描崩掉。
macOS / Linux 只有一个根,所以本地和那两个跑者都看不出来 —— 典型的"只在第三个平台显形"。

修法

新增 _rel_display(path):优先 relpath,跨盘符(ValueError)时退回绝对路径。
只影响报告显示,不影响判定逻辑。

补一条跨平台的用例

test_rel_display_survives_cross_drive_roots —— 用 side_effect=ValueError 模拟跨盘符,
在所有平台都跑。否则那段 except ValueError 只在 Windows 上才被覆盖,
下次谁把它删掉,另外两个跑者会照样绿。

顺带检查了仓库里其余 14 处 relpath:都是"从某个根 walk 出来的路径再相对它自己算",
同盘符必然成立,只有这一处的根是可被覆盖的。

验证

  • 本机:扫描器用例 111 + 231 + 187,全绿。
  • 扫描结果未变:567 文件 / 40 处 posix_tmp,四类 macOS 硬编码为 0。

medit-telemetry 同步升至 1.5.47。

v5.4.46 — 清除 macOS 相关硬编码

Choose a tag to compare

@veawho veawho released this 12 Sep 04:20

v5.4.46 — 清除 macOS 相关硬编码

回应「检查并避免出现 macOS 相关的硬编码」。检查和避免分开做:扫描器原先对"写死本机账号"这一类完全无感,而它恰恰是仓库里最多、换机器/换用户后必炸的一类。先补规则,再清。

一、检查:兼容性扫描器补齐 5 类规则

scripts/deploy_scan.py 的 scan_platform_compat() 原来只有 2 条规则,现在:

类别 形态 什么时候不算问题
macos_user_path /Users/<账号>/… 占位符除外(/Users/x/… 这类合成样例)
macos_only_path /Applications/ /System/ /Library/ /opt/homebrew/ 有平台守卫,或有存在性探测
macos_only_cmd osascript sips pbcopy textutil hdiutil … 有平台守卫
posix_tmp /tmp/… ——
foreign_path G:\ C:\Users\via54 ——

扫描范围从 scripts/ 扩到 scripts + skills + internal + cmd + integrations + telemetry。

两条设计取舍:

  • 存在性探测算合法写法。候选表 + 逐个 os.path.exists / LookPath / shutil.which 正是跨平台该有的样子(见 internal/source/chrome_launcher.go 的 ChromeCandidates()),报它只会让人去改对的东西。
  • 刻意不把 open / defaults 列为 macOS 专属命令。它们作为字符串在 JSON 键、状态值里到处都是,列进去只会满屏误报 —— 而会误报的检查器等于没有检查器。

二、避免:604 处清零,且是零行为变化

全仓 grep /Users/ 命中 625 处,分类:macos_user_path 604 · posix_tmp 40 · macos_only_path 9 · foreign_path 3 · macos_only_cmd 1。

清除方式:~ 派生 / __file__ 派生 / $HOME 派生 / filepath.Join(home,…) + os.UserHomeDir(),不改变本机解析结果。规模 290 个文件 / 581 处替换 / 213 处补 import os。

顺手修掉原先就坏的逻辑:

  • internal/source/hlo_orchestrator.go — 首选候选写死账号路径;另有一条候选写作字面量 "$HOME/HLO_design/hlo_nlu_v2.py",而 fileExists() 不做 shell 展开,所以那条候选从来没命中过(死代码,已删);osHomeDir() 走 sh -c "echo $HOME" 在 Windows 上不可用 → os.UserHomeDir();HLOTruthQuery 写死 python3.11 → foundation.ResolvePython。
  • internal/integrations/feishu/feishu.go — 默认 lark-cli 写死账号目录 → defaultLarkCLI()。旧写法的报错发生在推送那一刻,与"配置缺失"很难区分。
  • scripts/tma_batch_highlight.py — 默认值写死某台 Windows 机器的盘符路径 → $HOME 派生。
  • skills/…/render_ppt_slides.py — 唯一一处无守卫的 osascript → 补 sys.platform 判断。

刻意没动 40 处 /tmp:那是 POSIX 假设而非 macOS 专属,是另一件事;扫描器仍照常报出,不存在"藏起来"的问题。

三、验证:逐字等价,而不是"看着对"

机械改了 290 个文件,review 不够用。四层验证:

  1. 等价性 — 逐个把新的 expanduser("~/…") 在本机解析,与它替换掉的旧绝对路径比对:577 处逐字相同。
  2. 静态 — AST 全仓扫描"用了 os.* 但文件顶层没有 import os"。
  3. 动态 — runpy 逐文件执行模块顶层代码(300 个),抓 NameError。
  4. 回归面 — Python 187 + 230 + 24 用例、Go 24 个包、go vet ./...、go build ./... 全绿。

第 2 层真的抓到一个回归:scripts/test_citation_sync.py 手工改 __file__ 派生时用了 os.path 却漏了 import os,已修。213 处自动插入的 import os 全部有效,但手工改的那几处同样需要同一把尺子。

四、守着它

新增 TestMacosHardcodingScan(7 个用例),其中 test_repo_has_no_macos_specific_hardcoding 是全仓不变量 —— 以后再写死账号路径,单测直接红。

已知仍未处理

  • 40 处 /tmp(POSIX 假设,非 macOS 专属),扫描器持续报告。
  • macOS 上 PowerPoint/Word AppleEvent 仍被系统阻止(-1712 / -1708),与本版无关。

medit-telemetry 同步升至 1.5.46。

v5.4.45 — 解决 PaddleOCR 的问题:解释器错配 + 脚本锚定 + 临时目录

Choose a tag to compare

@veawho veawho released this 12 Sep 04:01

v5.4.45 — 解决 PaddleOCR 的问题

回应:"解决 PaddleOCR 的问题"。

排查后发现 OCR 这条腿实际上是跑不起来的,而且有三个各自独立的原因叠在一起。之所以一直没被发现,是因为部署报告写着"✓ PaddleOCR 已就绪" —— 报告和命令用的不是同一个解释器。

一、实测到的三个缺陷

$ cd /tmp && medit anno2ppt ocr paper.pdf 1
/private/tmp/scripts/paddleocr_pdf_page.py: No such file or directory     # ← 脚本找不到
$ cd <repo> && medit anno2ppt ocr paper.pdf 1
ModuleNotFoundError: No module named 'paddleocr'                          # ← 解释器里没装
# 缺陷 成因
1 解释器错配(核心) ResolvePython 按名字挑解释器,候选链 python3.11 → python3 → python 先命中 ~/.local/bin/python3.11 —— 实测它有 pymupdf 却没有 paddleocr;而 python3 三个都有。名字对不等于包里装了东西
2 脚本路径依赖 cwd 首选项 ~/.hermes/skills/via54medit/via54medit-anno2ppt-phase7/scripts/… 根本不存在(多了一层 via54medit,且技能分发包里没有 scripts/);fallback 是相对当前工作目录的 scripts/…。两条都落空 → 只在"恰好 cd 到仓库根"时才工作
3 /tmp 硬编码 渲染出的 PNG 写到 /tmp/…。Windows 上 C:\tmp 通常不存在 → OCR 第一步就失败。部署扫描器的平台兼容段一直在报这一条,但没人把它和"OCR 不可用"联系起来

二、为什么它一直没被发现:报告与命令各说各话

部署扫描探测 OCR 用的是 sys.executable(仓库那个装了 paddle 的 3.10),而 medit anno2ppt ocr 用的是 ResolvePython 挑出来的 python3.11。两个不同的解释器,于是"✓ 已就绪"与 ModuleNotFoundError 可以同时为真 —— 与 v5.4.41 抓到的"记账调用点存在但不可达"是同一类问题:检测的对象不是真正会跑的那条路径。

三、修法

Go 侧(新增 internal/foundation/python_capability.go):

  • ResolvePythonFor(need, envOverride, cfg) —— 按"能不能 import 全部 need"挑解释器(用 importlib.util.find_spec,实测 0.00s;真 import paddle 是秒级开销)。显式指定(config.python_path / $VIA54_OCR_PYTHON)严格:不满足就报错,不静默改用别的 —— 悄悄换会把"我把解释器配错了"藏起来。
  • FindRepoRoot() / ResolveOCRScript() —— 脚本解析以仓库根为锚($VIA54_REPO > 可执行文件位置上溯 > cwd 上溯),彻底摆脱 cwd 依赖。$VIA54_OCR_SCRIPT 同样严格,找不到时会列出全部试过的路径。
  • medit anno2ppt ocr --smoke —— 不读 PDF,只做真识别自检,排障一条命令搞定。

Python 侧(scripts/deploy_scan.py):

  • ocr_python() / ocr_script_path() —— 与 Go 侧同一套规则、同一顺序(有测试钉住两边一致)。
  • _probe_ocr() 改为判跑 OCR 的那个解释器,并在结论里报出是哪一个;缺依赖时报错会点名"就是它缺" + 给出针对该解释器的 pip install。
  • pip_install(python=…) —— OCR 的依赖装进跑 OCR 的那个解释器(往错的那个装,探测会一直说"缺",而人去看的时候又"明明装过了")。
  • ocr_smoke_test() 改为执行 [ocr_python, ocr_script_path, "--smoke"] —— 用真正的解释器跑真正的脚本,一次把四件事验完:解释器选得对不对、脚本找不找得到、API 形状变没变、权重在不在。自检与业务共用 run_ocr_on_image()。

脚本侧(scripts/paddleocr_pdf_page.py):

  • 临时目录改用 tempfile(平台正确,macOS 上落在 $TMPDIR),顺手把 doc.close() 放进 try/finally。
  • 新增 --smoke 真识别自检。
  • 文档更正:删掉"表格识别 (PP-Structure)"这个实现里并不存在的说法,并把"必须用装了 paddleocr 的解释器"写进脚本头。

四、验证(前后对照)

场景 修复前 修复后
cd /tmp && medit anno2ppt ocr paper.pdf 1 No such file or directory 46 个文字块 / 45 行 / 7 行疑似表格
cd <repo> && medit anno2ppt ocr paper.pdf 1 ModuleNotFoundError: paddleocr 同上
medit anno2ppt ocr --smoke 没有这个开关 [L2][smoke] OK: 识别到 1 段文字
部署报告那一行 ✓ PaddleOCR 与 paddle 均可导入(判的是另一个解释器) ✓ PaddleOCR python3.10 (部署脚本自己的解释器) 三个依赖齐备 —— 报出判的是谁
显式指定缺包的解释器 仍报"已就绪" ✗ $VIA54_OCR_PYTHON 指定的解释器 … 缺 paddleocr, paddle(不静默换)
渲染临时文件 /tmp/paddleocr_p1_*.png(Windows 不可用) $TMPDIR/paddleocr_p1_*.png
平台兼容扫描 50 处 49 处(少的就是这处 /tmp)

五、顺带更正一处教错人的文档

skills/via54medit-architecture-honest-status/references/paddleocr-pdf-page-script.md 原先写着"用 python3.11 调用"、依赖在 "hermes-agent venv" —— 那正是复现本 bug 的配方。已改为首选 medit anno2ppt ocr(由它自己挑解释器),并补上"如何判断某个解释器行不行"的一条命令。

测试

项 结果
Go 新增 internal/foundation/python_capability_test.go:跳过不合格解释器、错误列全候选与各自缺什么、显式覆盖严格不回退、候选列表不随 cwd 变化(Chdir 实证)、找不到仓库根时优雅降级
Python scripts/test_deploy_scan.py 90 → 102,含核心回归:探测必须判"跑 OCR 的那个解释器"并点名它(旧逻辑在这里必然放行)、候选链与 Go 侧一致(解析 Go 源码比对)、安装目标解释器正确、OCR 脚本不得再出现 /tmp 硬编码、--smoke 与业务共用推理函数
全量 go vet + go test -race ./... 全绿;Python 187 条、scripts 222 条全绿

v5.4.44 — OCR 与 mmx-cli 部署集成:自动检测 + 未部署自动部署

Choose a tag to compare

@veawho veawho released this 12 Sep 03:35

v5.4.44 — OCR 与 mmx-cli 的部署集成

回应:"我需要将 OCR 的部署方式、mmx-cli 的部署方式集成到 via54Medit 中,并确保可以在部署阶段就能自动检测是否已部署,如果未部署则自动部署。"

两者此前已在能力矩阵里有探测与安装通道(v5.4.34/36),但 探测本身会给出假就绪。这一轮把"就绪"的定义落到 真实可用性 上。

一、OCR:"能 import" ≠ "能识别"

缺口 后果
探测只做 import paddleocr; import paddle 权重没下也算就绪。首次真实调用会联网下 ~170MB,离线/受限网络下必然失败 —— 而失败点在 L2 那一步,离"部署完成"已经很远
安装不带版本约束 某天会静默装上 2.x/4.x。管线调的是 PaddleOCR 3.x 的 API(use_textline_orientation + .predict() + result[0]['rec_texts'])

修法:

  • 新增能力 ocr_models:探测 = 权重是否已落地(便宜的文件系统检查,det/rec 两个家族都在才算齐);安装 = 预热,即真跑一次识别把权重下下来。权重目录认 PADDLE_PDX_CACHE_HOME/official_models —— 不是 ~/.paddleocr(那是 2.x 路径,3.x 上是空的,照它判断会误报)。
  • 新增 --verify-ocr:真跑一次识别并断言拿到非空 rec_texts。这是"OCR 到底能不能用"的唯一强证据,也是权重预热动作本身(单一实现 ocr_smoke_test())。断言只要求"识别到非空文本",不比对具体字符串 —— 实测 OCR 12345 会被认成 DCR 12345(置信度 0.996),拿精确比对当门槛是自找假红。样本文字可用 VIA54_OCR_SMOKE_TEXT 覆盖。
  • 安装带约束:paddleocr>=3.0,<4 / paddlepaddle>=3.0,<4(实测可用组合 3.7.0 + 3.3.1)。

二、mmx-cli:凭据探测接错了对象(更严重)

mmx-cli 用的是它自己的凭据(mmx auth login 写进 ~/.mmx/config.json,也支持全局 --api-key 覆盖),与 MINIMAX_API_KEY 是两条互不相通的路径。旧探测把结论建在环境变量上,于是同一台机器上同时出现方向相反的两个错误:

  • 假警报:本机 MINIMAX_API_KEY 未设置,而 mmx auth status 早已就绪、真能调通(后台用量都能读到),报告却写"缺 MINIMAX_API_KEY,调用会失败";
  • 假就绪(更危险):export 了 MINIMAX_API_KEY 但没跑过 mmx auth login 时,报告写"已配置"—— 而 mmx 实际调用会 401。这在"二进制已安装"的报告里完全看不出来。

修法:

  • _probe_mmx() 只判二进制可用,不再对凭据下任何结论(一件事一个人管)。
  • 新增 _probe_mmx_auth():判据是 ~/.mmx/config.json 的 api_key(快、不联网、api-key 模式下的权威),其次 mmx auth status --output json(覆盖 OAuth 模式;实测 0.09s)。
  • 新增能力 mmx_auth(替换 mmx_key):有 MINIMAX_API_KEY 时部署流程可代登(mmx auth login --api-key);没有则如实报"需人工",gate=False —— 凭据必须由人提供,把它算成部署失败会让部署永远无法成功。
  • 新增 --verify-mmx:二进制与认证分开报(修法完全不同),退出码只跟二进制走。
  • install_mmx.py 同步改掉那段"看环境变量下结论"的输出。

三、部署 / 更新阶段:自动检测 + 自动部署

位置 行为
deploy_scan.py(能力矩阵,唯一事实来源) 按平台探测 ocr / ocr_models / mmx_cli / mmx_auth,只装缺失的,可反复运行
bootstrap_device.py(部署后) 新增第 4 步跑 --verify-ocr + --verify-mmx;凭据缺失只记警告,不算失败
auto_sync.py(更新后) 拉取+重建之后新增视觉/OCR 复检,失败发告警
deps_auto.ensure_env()(管线第 [0] 步)/ medit doctor --fix 读同一份矩阵,自动继承新能力,无需改动

四、实测(不是"看起来对")

验证 结果
空 PADDLE_PDX_CACHE_HOME 跑 --only ocr_models 本次补齐 1 · 复验未过 0,该目录随即出现 5 个模型家族 / 177MB,退出码 0
同一空目录跑 --check ✗ PaddleOCR 权重 (真识别预热) 未发现本地权重
--verify-ocr 真识别通过: 识别到 1 段文字 (DCR 12345)
--verify-mmx mmx-cli : ✓ mmx 1.0.19 / 凭据 : ✓ 已认证 (api-key, 来源 ~/.mmx/config.json) —— 正是旧探测会误报的那台机器

测试

scripts/test_deploy_scan.py:78 → 90。新增 TestVisionToolchainDetection:权重齐/缺/半成品三态、mmx 凭据只认自己的状态(双向回归)、OAuth 模式回退 CLI、二进制探测不再判凭据、mmx_auth 替换 mmx_key 且不门禁、无凭据时如实失败且不调用 CLI、部署器与更新器都接入了复检;另有 OCR 版本约束守卫。

文档

docs/DEPLOY.md(新增「OCR 与 mmx-cli 的"部署"到底包含什么」一节 + 复检命令 + PADDLE_PDX_CACHE_HOME 说明)、README.md(OCR 版本约束)、CHANGELOG.md;telemetry 版本 1.5.43 → 1.5.44。

v5.4.43 — 飞书多维表格统计列对齐:补齐 8 列 + 定下对齐契约

Choose a tag to compare

@veawho veawho released this 11 Sep 21:27

v5.4.43 — 飞书多维表格统计列没有对齐最新统计项

回应:"飞书多维表格统计列没有对齐最新的统计数据项"。

一、实测确认:公司表少了 8 列

绑定的公司「监控数据周报明细」表当时只有 13 列,而代码里的统计项是 19 个字段。差的这 8 个不是"没算",是 算了但没地方放 —— COMPANY_FIELD_MAP 查不到目标列就静默跳过,写入不报任何错。

缺失的列 影响
检索节约工时(h) / 下载节约工时(h) / 高亮节约工时(h) 三个细分工时被丢,只剩一个总数,看不出结构
其他任务数 / 其他工作时长(h) / 其他Token消耗 v5.4.37 新增的「其他」类目整类只能挤进「备注说明」当纯文本:看得到,但筛选不了、分组不了、画不了图
其他未归属Token 与上面同因
API调用次数 同上

根因是结构性的:表是先建后用的,代码里新增一个统计项不会让已建好的表自动长出列来。

二、补齐 8 列(线上已生效)

medit-telemetry bitable --align-fields --dry-run   # 先看差哪些
medit-telemetry bitable --align-fields             # 少哪列补哪列

线上结果:公司表 13 → 21 列,8 列已建,数字列类型正确(工时 precision=2,计数 precision=0)。

一个插曲值得记下来:本仓库的应用身份对这张公司表只有读权限,建列返回 1254302 Permission denied —— 所以补列是用用户身份经 lark-cli base +field-create --as user 完成的;CLI 里的命令保留着,换到有写权限的应用或身份时可直接用。

三、写入侧:统计项一个都不丢

  • COMPANY_FIELD_MAP 补 8 个映射,build_company_payload 逐项成列;
  • 「备注说明」不再重复承载已有专列的数值(同一份账记两次会让人以为是两笔);
  • TABLE_SCHEMA_FIELDS 新增 其他未归属Token(标准表 18 → 19 字段),两类表口径齐平。

四、对齐契约:要么有列,要么有理由

状态 含义
目标表里有同名列 正常,COMPANY_FIELD_MAP 负责改名
显式写明为什么不该有列 见 INTENTIONALLY_NOT_A_COLUMN(当前只有 成员OpenID)

没有第三种状态。 unmapped_standard_fields() 必须为空,由 TestBitableColumnAlignment 守着 ——"静默丢弃"从此不是一种可选状态。

五、不再静默:缺列必须说出来

sync_weekly_report 写入前自检列对齐,缺列时把"缺 N 列 + 补齐命令"附在成功消息里(daemon 会写进日志/告警),--dry-run 的 JSON 带 missing_columns。但只报不改 —— 周期同步去改公司共享表的 schema 太越界,补列交给显式命令。状态查询新增一行 • 统计列: 🟢 已对齐 (schema=company, 21 列)。

六、两处连带的正确性修复

  1. schema 判定改为标记字段优先。补列后公司表 21 列里有 8 列与标准表同名,再比"谁命中多"会越来越脆弱 —— 改成认公司表独有的 记录标识/统计周次/提交成员/统计日期。
  2. 冻结历史 13 列列序(COMPANY_PAYLOAD_ORDER_LEGACY)。备份里那些"13 列、按公司列序写入"的历史行是靠列数认出来的;若列序跟着映射一起变长,它们就再也认不出来、会被当作脏行静默丢掉 —— 回退路径上少的就是它们。已用真实备份验证:表头迁移(15 → 19 列)前后都稳定回读出同样的 2 条记录,一行未丢。

七、顺带修正的既有缺陷

bind_existing_bitable() 原先无条件按标准表补列 —— 绑定公司表时会往里面塞 汇报周期/成员花名 这类用不上的同义列,而真正缺的统计列反而没补。现改为按探测出的 schema 补列并汇报结果。

测试

项 结果
Python 180 → 187(新增 TestBitableColumnAlignment 7 条,含"13 列历史行在映射扩展后仍可读")
更新的既有测试 4 条把旧行为当契约的(公司表列集、备注承载「其他」、dry-run"不发任何请求"→ 允许只读探测但禁止写请求)
实测链路 --align-fields --dry-run 精确报出 8 列 → 补齐 → 复查 21 列已对齐 → --sync --dry-run payload 21 列且新列有真实值(检索 4.99h / 下载 6.66h / 高亮 2.85h)→ --report 正常出图

文档

CHANGELOG.md、telemetry/README.md(新增「统计列必须与统计项对齐(对齐契约)」整节)、docs/TELEMETRY_GUIDE.md;telemetry/__init__.py / pyproject.toml / setup.py 版本 1.5.42 → 1.5.43。

已知遗留(不在本次范围)

公司表里 Peipei 的 2026-W37 存在两条记录(recvuWocx20BKt 与 recvuWPVcLKf8t)—— 是历史幂等失效留下的重复行,需要单独清一次;本次只对齐列结构,没有动已有数据行。

v5.4.42 — 修红 CI:子进程输出没指定编码(Windows 跑者)

Choose a tag to compare

@veawho veawho released this 11 Sep 21:13

v5.4.42 — 修红 CI:v5.4.41 在 Windows 跑者上红了(子进程输出没指定编码)

v5.4.41 的 CI 在 python (windows-latest) 上失败,另外 5 个 job 全绿。原因 全是我这一轮引入的,而且两条都只在 Windows 上现形。

一、真因:subprocess.run(..., text=True) 没给编码 → 把"成功"读成"失败"

我在 auto_sync.py 的"更新后强制校验 LLM 接入"里走了既有的 run_cmd(...),它是:

subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)

Windows 跑者的默认编码是 cp1252,而子进程打印的是 UTF-8 中文。结果是读线程里 UnicodeDecodeError(can't decode byte 0x8f),proc.stdout 变成 None —— 于是:

  • json.loads(proc.stdout) → TypeError: ... not NoneType;
  • 更糟的是 run_cmd 里那个宽泛的 except 会把它吞成 (False, "", 报错文本),也就是**校验通过也会被报成"LLM 接入校验未通过"**并发出告警。

一个纯粹的编码问题伪装成了功能缺陷,并且在 macOS/ubuntu 上完全看不见(它们默认就是 UTF-8)。

修法:捕获子进程输出时一律显式 encoding="utf-8", errors="replace"。范围限定在 判读"部署/更新是否就绪"的那条链路(deploy_scan.py / bootstrap_device.py / auto_sync.py / 全部 telemetry/)—— 这条链路的判读结果决定流程成败,不能依赖平台默认编码。telemetry/deploy.py 与 telemetry/daemon.py 里同类写法是本类问题的既有隐患,一并修掉。

二、新增不变量:往这条链路上加调用时,现场就红

tests/test_repo_hygiene.py 新增 test_captured_subprocess_output_has_explicit_encoding:凡是在该范围内捕获输出(capture_output=True 或 stdout=PIPE)的 subprocess.run/Popen,必须出现 encoding=;只写 stdin / 只看返回码的不算(不涉及解码)。

这类问题本地永远看不见,只有三平台 CI 才会现形 —— 与其等下次再踩,不如当场红。实测:把 auto_sync.run_cmd 的 encoding= 删掉,它会精确指出 scripts/auto_sync.py:153。

三、顺带修掉测试自身的 Windows 不适配

  • tests/test_llm_ledger.py 里两处 subprocess.run(..., text=True) 同样补上编码;
  • 读 Go 源码从 open(...).read() 改为 with open(...) —— Windows 上未关闭的句柄会拦住后续的替换/删除,也会刷一屏 ResourceWarning。

测试

项 结果
Python 179 → 180(+1 编码不变量)
test_deploy_scan 78 条全绿
tests.test_llm_ledger 34 条全绿
本地复检 --verify-llm 退出码 0、--dry-run 退出码 0、go build + go test 全绿

v5.4.41 已发布且标签不再移动,故用本版本承载修复。功能内容见 v5.4.41。

v5.4.41 — 强制验证接入了哪些 LLM + 确保 token 消耗真能读进库

Choose a tag to compare

@veawho veawho released this 11 Sep 21:04

v5.4.41 — 强制验证接入了哪些 LLM + 确保它们的 token 消耗真能读进库

回应:"部署和更新后还需要强制验证接入了哪些 LLM,并确保真能读取接入的所有 LLM 的 token 消耗"。

"接入了哪些 LLM"这个问题 光看文档或配置答不出来 —— 它由代码里的调用路径决定。所以这一轮先造了一把能回答它的尺子,再用尺子去量,量出 三处真实缺口。

一、三处真实缺口(都是"调用正常、报表少数字"的沉默型缺陷)

记账是 旁路:一条路径不再记录用量,调用本身不会报错、不会有异常、不会有非零退出码,报表只是安静地少一块数字。

  1. scripts/provider_llm.py 返回 usage 却从不落库 —— 它是 LLM_PROVIDER 的 默认值(deepseek)的实现,也就是说默认文本通道的 token 消耗 一直是 0,而卡片还写着"100% 控制台对齐"。
  2. scripts/sensenova_vision.py 把 usage 丢掉 —— 只返回 content,SenseNova 视觉调用的 token 全部无账。同类缺口,由源码扫描发现,不是靠人肉复盘。
  3. Go 侧的整个 LLM 调用面完全不计账 —— internal/foundation/llm.go / llm_glm.go 解析响应时只取 choices,usage 整个丢弃。受影响的是 medit ask / medplan / docproc / medit-mcp 四条真实链路,覆盖 deepseek / openai / hermes / glm 四个 provider。

二、新增 telemetry/llm_providers.py:一把能回答这个问题的尺子

  • 注册表是唯一事实来源:6 个 provider(deepseek / openai / minimax / zhipu / sensenova / hermes)的接入通道、凭据来源、单次用量来源、账户级用量怎么读。
  • 证据来自源码,不来自声明:注册表里 没有"记录者清单"字段 —— 谁在记账必须扫出来(Python record_llm_usage( + Go recordLLMUsage(),并做 AST 可达性判断。
  • 离线端到端摄入校验:各 provider 的 真实响应形状 → 落库 → 读回 → 报表层;只写临时库,不碰生产数据、不发网络请求。
  • Go 侧 spool 链路校验:spool → 摄入 → 读回 → 报表,额外验 别名归一(Go 叫 glm,落库必须是 zhipu)与 重放幂等。

三、新增 Go 侧用量落盘 + Python 幂等摄入

internal/foundation/llm_usage.go 把每次真实调用的 usage 追加到 ~/.medit/llm_usage_spool.jsonl(Go 没有 SQLite 驱动,引入 driver/cgo 会让构建显著变重),由 telemetry/llm_spool.py 幂等摄入 llm_token_logs:幂等靠 req_id,所以"写了一半崩了再重跑"不会重复计费。记账 永不返回错误 —— 它绝不能因为自己失败而让 LLM 调用失败。

顺手修正一处身份错位:openai / deepseek 复用了 HermesProvider 的实现,记账时若直接用 Name() 会把它们全记成 hermes。新增 usageName 字段,账各归各。

跨语言契约有测试守着:从 Go 源码里抠出 json.Marshal 那段 map 的键,与 Python 摄入端实际读的键做全等断言,字段一漂就红。

四、强制验证:部署与更新后都必须跑,失败非零退出

  • medit-telemetry llm(--live / --verbose / --json),有问题时 退出码 2;
  • scripts/deploy_scan.py --verify-llm(独立模式)与 --stage all 第 4 节;
  • scripts/bootstrap_device.py 第 4 步,失败 以非 0 退出;
  • scripts/auto_sync.py 拉取 + 重建 之后(更新路径)并接入告警;
  • medit-telemetry deploy 部署概览里跑同一份校验,不过就 不打印"部署成功"横幅且 sys.exit(2);
  • CI 新增两条:tests.test_llm_ledger 与 deploy_scan.py --verify-llm。

CLI 与部署校验 共用同一份渲染器 llm_providers.format_report(),免得两处口径分叉。

五、勘误:第一版检查器有假阳性,已修

第一版用正则数 record_llm_usage( 的出现次数判"有没有在记账"。把 provider_llm.py 里 所有对 _record_usage() 的调用删掉、只留函数定义,它依然判"记录中"。这比没有检查更危险(会给人"已验证"的错觉)。改为 AST 可达性判断:记账调用点所在的 私有 包装函数必须在同文件内被调用过;公开入口(如 vision_analyze,由 CLI 拉起)豁免,否则会产生大量假阳性。负向测试已固化。

实测:删掉那个调用点 → --verify-llm 报 6 条问题并以退出码 1 结束;恢复后回到 0。

六、诚实说明读不到的部分(不折算、不造数)

  • mmx / MiniMax VLM 的单次 token 读不到:CLI 只返回 content 不暴露 token;其账户级配额 mmx quota show 返回的是 调用次数 而不是 token,因此 不会 被折成 token 入库。
  • openai 的用量接口要 Admin key(sk-admin- + api.usage.read)。
  • deepseek 有余额接口没有逐条用量接口,逐条要走控制台导出 CSV。
  • 本机当前只有 SENSENOVA_API_KEY 一个凭据 —— 审计如实报出其余 provider "凭据 ✗"(不是缺陷,是这台机器的实际状态);凭据是否真能通过需要 --live 才会联网探。

测试

项 结果
Python 145 → 179(新增 tests/test_llm_ledger.py 34 条,含探测器自身的负向测试)
Go 新增 internal/foundation/llm_usage_test.go 6 条(含"provider 不得串账"与"没有 usage 就不落盘")
跨语言 真 Go 调用写出 spool → 真 Python 摄入 → 库与报表一致(111+22=133)
--verify-llm 6 个 provider 全部"记录点 ✓ / 单次用量可读 / 端到端往返一致",0 问题
负向验证 人为删掉记账调用点 → 退出码 1 且报出 6 条问题;恢复后退出码 0

文档

CHANGELOG.md、docs/TELEMETRY_DEPLOY.md(新增"部署/更新后为什么必须跑 llm"与校验矩阵)、telemetry/README.md(新增"LLM 接入与 Token 可读性"整节 + 目录结构补全)、telemetry/__init__.py / pyproject.toml / setup.py 版本 1.5.40 → 1.5.41。

v5.4.40 — 确保统计数据实时更新(修 3 倍虚高 + 条目不冻结 + 新鲜度可查)

Choose a tag to compare

@veawho veawho released this 11 Sep 20:37

内部版本 5.4.40 / medit-telemetry 1.5.40 · 完整历史见 CHANGELOG.md

查证"统计数据为什么不是最新的",找到三个具体来源,全部修掉并在本机真实库上验证。

一、备份目录被当成成果,报表虚高 3 倍

这是最严重的一条。RSV 项目下的 _bak_20260906_201108/高亮结果/ 与 _bak2_20260906_205621/高亮结果/ 被当作产出物扫进了库 —— 同一篇文献在库里出现 3 行:

修复前(错) 修复后(实)
Highlight 完成数 150 篇 50 篇
阅读页数 2917 页 977 页
累计节约总工时 20.19 h 14.49 h

而且时对时错:备份目录一删,数字又"自己变回去"。检索(44)与下载(215)无重复,未受影响。

  • 新增路径卫生 watcher.is_ignored_path():_bak* / .git / node_modules / __pycache__ / _trash 等目录里的副本一律不计入。
  • 判据是路径片段的前缀,不是整串子串 —— 否则会误伤名字里含 bak 的文献(如 Wang_bak_2020.pdf),也会牵连正常产物;这样 _2_pdfs/、_highlight_nested/ 这类合法的下划线开头目录天然不受影响,不需要例外清单。
  • 新增 refresh 的清理步骤修复已污染的历史数据。清理很保守:只在同一篇文献于非备份路径下也有行时才删 —— 即便判定有偏差也绝不会把某篇文献整删掉。本机执行结果:删掉 100 行,50 篇唯一文献一篇未丢。

二、已收录条目的可变字段被冻结

原先命中唯一性去重就跳过,于是标注数与文件大小停在首次登记时的值 —— 用户在已高亮的 PDF 上继续加标注、或重新下载了更大的 PDF,报表纹丝不动。

  • 新增 db.refresh_highlight_item() / db.refresh_download_item();watcher 遇到已收录条目改为刷新可变字段(页数、标注数、文件大小),并统计 refreshed 项数。
  • 刷新 ≠ 新增:篇数口径完全不变。
  • 没观测到(None)就不覆盖:拿默认值去覆盖已有数据比不刷新更糟。
  • 同时按 paper_id 兜一层去重:文件被移动/改名后,只按路径匹配会认不出来,结果是同一篇文献再加一行。

三、"是不是最新的"没有凭据

  • 心跳新增 freshness(最近入库时间 + 各表行数);status 末尾显示:
• 数据新鲜度: 最近入库 2026-09-10 18:19:52 (2056.3 分钟前) · 由守护进程每 30 秒巡检更新
  • 新增 medit-telemetry refresh:立即扫描配置里的全部监控目录(watcher.watch_dirs,不是写死路径)+ 刷新已收录条目 + 清理备份副本行,然后打印最新统计。--dir 可只刷指定目录。

顺带发现:跑着的守护进程用的是旧代码

本机守护进程 PID 1638 的心跳里没有 next_weekly 字段 —— 说明它加载的是改动之前的扫描器。配置每 5 秒重载,代码不会:于是它会把我刚清理掉的 100 行备份副本再插回去。

已重启(launchctl kickstart -k),之后验证:跑满一个 30 秒巡检周期,高亮仍为 50,没有再被污染。README 与指南里已写明这条运维注意事项。

关于已经推送出去的历史数据

需要说明:此前推送到公司公共统计表/多维表格的数字是虚高的(高亮 150 篇 / 2917 页 / 20.19h)。

  • 多维表格是按「汇报周期 + 成员」upsert 的,下次推送会覆盖当前周期的记录为正确值;
  • 公司公共统计表是追加式的,历史行不会被追溯修正 —— 如需纠正历史行,得手工处理。

数据库已备份(~/.medit/telemetry.db.bak-20260912-043439)。

测试

tests/test_telemetry.py 121 → 132 项(+11);全量 223 项通过;make test-py 全绿;go vet / go test ./... 干净。

新增覆盖:路径卫生的正反例(含"文献名里含 bak 不误伤"、"_2_pdfs 不被误排除")、备份副本不计数、已收录条目刷新而篇数不变、None 不覆盖、文件移动不重复计数、清理只在有真实副本时才删(且每篇文献都必须仍有行)、新鲜度助手、心跳带新鲜度凭据。