Repository navigation
Releases: veawho/via54Medit
Release list
v5.4.49: 飞书 TraeWork Bot 全局报错治理与生产级加固
核心变更
-
多维表格周报归档契约防御 (
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。
-
自动同步与构建高可用升级 (
scripts/auto_sync.py)- 增加编译回退机制:当环境缺少 GNU Make 或工作目录无
Makefile时,自动回退到go build直接编译,彻底消除make: *** No rule to make target 'build'. Stop.报错。 - 依赖静默自愈:冒烟测试前自动嗅探
pymupdf/fitz,缺失时后台静默自愈安装;净化测试错误日志输出,避免裸 Traceback 被日志监控器升级为假告警。
- 增加编译回退机制:当环境缺少 GNU Make 或工作目录无
-
看门狗与生命周期稳定性提升 (
telemetry/daemon.py)- 单次循环开始前优先刷新心跳,防止耗时文件扫描和复杂网络同步导致外部看门狗误判假死。
- Windows 平台
stop_daemon_process切换为taskkill /PID {pid} /T /F树状强杀,彻底消灭孤儿pythonw.exe幽灵实例堆积。
-
告警通道噪声白名单过滤 (
telemetry/alerter.py)- 增加
IGNORED_ALERT_PATTERNS过滤白名单,静默拦截 Chromium Crashpad 内部探测错误(如directory_reader_win.cc:44 FindFirstFile: 0x3)等良性底座噪声,杜绝告警风暴。
- 增加
v5.4.48 — 多平台部署与规范合规标准化
核心变更
-
统一双平台一键安装入口 (对标 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,测试永不静默漏跑。
- 自动检测 Windows 追加
- 新增
-
代码库硬编码清理与路径规范
- 彻底修复
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中的个人绝对路径。
- 彻底修复
-
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)
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 相关硬编码
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 不够用。四层验证:
- 等价性 — 逐个把新的
expanduser("~/…")在本机解析,与它替换掉的旧绝对路径比对:577 处逐字相同。 - 静态 — AST 全仓扫描"用了
os.*但文件顶层没有import os"。 - 动态 —
runpy逐文件执行模块顶层代码(300 个),抓NameError。 - 回归面 — 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 的问题:解释器错配 + 脚本锚定 + 临时目录
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 部署集成:自动检测 + 未部署自动部署
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 列 + 定下对齐契约
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 列)。
六、两处连带的正确性修复
- schema 判定改为标记字段优先。补列后公司表 21 列里有 8 列与标准表同名,再比"谁命中多"会越来越脆弱 —— 改成认公司表独有的
记录标识/统计周次/提交成员/统计日期。 - 冻结历史 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 跑者)
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 消耗真能读进库
v5.4.41 — 强制验证接入了哪些 LLM + 确保它们的 token 消耗真能读进库
回应:"部署和更新后还需要强制验证接入了哪些 LLM,并确保真能读取接入的所有 LLM 的 token 消耗"。
"接入了哪些 LLM"这个问题 光看文档或配置答不出来 —— 它由代码里的调用路径决定。所以这一轮先造了一把能回答它的尺子,再用尺子去量,量出 三处真实缺口。
一、三处真实缺口(都是"调用正常、报表少数字"的沉默型缺陷)
记账是 旁路:一条路径不再记录用量,调用本身不会报错、不会有异常、不会有非零退出码,报表只是安静地少一块数字。
scripts/provider_llm.py返回usage却从不落库 —— 它是LLM_PROVIDER的 默认值(deepseek)的实现,也就是说默认文本通道的 token 消耗 一直是 0,而卡片还写着"100% 控制台对齐"。scripts/sensenova_vision.py把usage丢掉 —— 只返回content,SenseNova 视觉调用的 token 全部无账。同类缺口,由源码扫描发现,不是靠人肉复盘。- 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(+ GorecordLLMUsage(),并做 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 倍虚高 + 条目不冻结 + 新鲜度可查)
内部版本 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 不覆盖、文件移动不重复计数、清理只在有真实副本时才删(且每篇文献都必须仍有行)、新鲜度助手、心跳带新鲜度凭据。