# Changelog 本文件记录 scout-agent 各版本的重要变更。日期为 UTC+8。 ## [Unreleased] ### Fixed #### 平台探测与实际执行不一致(`get_python_cmd` 从不验证可用性) **症状**:在只装了 Microsoft Store 占位符的 Windows 上(`python` 命令存在但执行即 失败,退出码 9009 / 弹商店),系统层面的探测认为 python 可用,而实际执行会失败。 **根因**:`platform.get_python_cmd()` 是纯字符串返回(`"python" if IS_WINDOWS else "python3"`),**从不验证可用性**。而兜底逻辑只存在于 shell 工具的**执行阶段** (`ShellTool._run_python_inprocess` 进程内改用自带解释器),形成"探测一套、 执行另一套"的分裂: - `self_heal.VerifyPipeline.run_pytest` 直接 spawn 该命令、**无兜底** → 直接崩 - `code_exec` 的 docker 分支同样直接 spawn - system prompt 告诉 agent「Python 用 `python`」,agent 据此生成的命令注定失败 **修复**: 1. `get_python_cmd()` 改为先探测(`python --version` 能否正常退出,覆盖 FileNotFound / 超时 / 非 0 退出三种失败形态),结果缓存避免重复 fork 进程。 探测失败且处于打包态时返回 `sys.executable`,与 shell 工具的兜底**同源**。 新增 `python_is_builtin()` 供调用方区分。 2. `get_platform_prompt()` 在探测失败时纠正 agent 的认知:直接写 `python xxx.py` 即可(会兜底),并明确**不要**去拼 exe 路径 —— `sys.executable` 是应用自身, spawn 它会启动整个 Scout 而不是执行代码。 3. `VerifyPipeline.run_pytest` 在打包态直接返回可行动的错误(打包版不含 pytest, 属 dev 依赖),而不是 spawn 应用 exe 后等一个永远不会回来的进程。 #### self_heal 用裸 `.decode()` 解子进程输出(中文 Windows 整体崩) **症状**:中文 Windows 上自修复功能抛 `UnicodeDecodeError` —— ruff/pytest 的输出 是 GBK,而 `stdout.decode()` 默认按 UTF-8 解。 **修复**:两处改用 `platform.decode_output()`(UTF-8 → GBK → latin-1 兜底, 全程不抛)。该函数本就是为这个场景造的,此前未被 self_heal 使用。 ### Added - `tests/unit/test_platform_python_probe.py`(18 条)—— 锁定探测逻辑 (商店占位符 / 超时 / 缓存 / 打包态降级 / 源码态保持)、prompt 与真实能力一致、 `decode_output` 鲁棒性、以及"不得把 exe 路径当 python 命令告诉 agent"这条不变量。 ## [1.0.0.6] — 2026-10-05 ### Fixed #### memory_save 写入前查重 + fact_key 暴露(修复"同义偏好堆成多条 active") **症状**:用户在 `D:\.scout\memory.db` 中观察到「用户要求禁止使用 file 工具和执行命令」 这一条语义相同的偏好,库中并存 **5 条 active** 记录,fact_key 各不相同 (`user:preference:excel_desktop_automation` / `user:preference:tool_usage` / `user:preference:desktop_tool` / `user:preference:tool_restriction` / `user:preference:desktop_automation`)。 **根因**:`MemorySaveTool.execute()` 此前**直通** `store.add_async()` 不做查重, 且参数 schema 不暴露 `fact_key` → `MemoryStore.add` 的"同 fact_key 不同 content 触发冲突失效"分支永远不触发;`SessionMemoryExtractor._dedup` 又只在会话结束 抽取时跑(对 LLM 对话中途调用 `memory_save` 完全无效)。两套本该互为保险的 去重机制在 `MemorySaveTool` 这条路径上**同时失效**。 **修复**: - `scout/tools/builtin/memory/__init__.py`:`MemorySaveTool.parameters` 新增 `fact_key` 字段并附使用指引;`execute()` 在写入前先调 `_find_dup()`: - 同 fact_key 命中(active) → `store.update()` 覆盖原条目,importance 取 max - 否则内容 Jaccard ≥ 0.85 → `store.update()` 覆盖 - 都不命中 → `store.add_async()` 新增(带归一后的 fact_key) - `scout/memory/store.py`:`add_async()` 此前接收 `fact_key/confidence/evidence` 但**没透传给 `add`**(dead 参数被默默丢弃)—— 这是个独立 bug,会话结束抽取 路径走的是同步 `add()` 不受影响,但任何走 `add_async` 的路径(含 `MemorySaveTool`)保存的 fact_key 都没落到 db。修复后三个字段都正确透传。 - 写入前 fact_key 归一逻辑复用 `SessionMemoryExtractor._normalize_fact_key`, 与抽取链口径一致——LLM 即使传入别名(`user:preference:tool_usage`)也会 被归一到规范键(`user:tool_restriction`)。 **测试**:`tests/unit/test_memory_save_dedup.py`(7 条新单测,全绿)。 #### 同义 fact_key 存量清理脚本 新增 `scripts/dedupe_memory_fact_keys.py`:把同义 fact_key 的多条 active 记忆 归并为一条(保留最新 + 重要度最高的那条,其余标记 `deprecated` 不物理删除)。 实测用户库干跑结果(未写库): - 可归并 2 簇 / 6 条重复:`user:tool_restriction` 簇 5 条、`project:repo_path` 簇 3 条 - 另报告 15 条**无 fact_key** 的疑似重复(语义同簇但字符 Jaccard 不足),按设计 仅提示不自动归并——避免误杀合法事实更新(详见脚本 docstring) 用法: ``` python scripts/dedupe_memory_fact_keys.py --db "D:/.scout/memory.db" # 干跑 python scripts/dedupe_memory_fact_keys.py --db "D:/.scout/memory.db" --apply # 写库(自动备份) python scripts/dedupe_memory_fact_keys.py --db "D:/.scout/memory.db" --rewrite-keys --apply ``` ### Changed #### 提示词清理:去除 prompt 中散落的版本日期标记 `scout/engine/agent.py` 中两处进入 LLM 可见的 react 提示词片段里的 `(2026-09-30)` 标记("重过程调查必委派"、"调查纪律")已移除。这些是开发者 维护用的版本标记,对 LLM 决策无价值,但容易与 `` 标签里的当前 日期声明混淆。代码注释里的日期标记(开发者看的,不进 prompt)保留不动。 ### Already Fixed (核验时确认仍有效) - `scout/context/fact_key_registry.py`:受控词表 + 别名归一 - `scout/context/memory_extract.py` `_normalize_fact_key`:写入前 fact_key 归一 (与 MemorySaveTool 共享同一归一逻辑) - `scout/engine/skills/retriever.py` `FALLBACK_MAX_SKILLS = 1`:技能冷启动 回退封顶为 1 条,避免零证据经验整批放行污染 prompt(`test_cold_start_fallback_is_capped` 覆盖) ### Fixed (打包链路 — v1.0.0.6 桌面版打包修复) #### exe 启动崩 _ssl ImportError(OpenSSL DLL 版本不匹配) **症状**:打包出的 `ScoutAgent.exe --no-gui` 启动即崩: ``` ImportError: DLL load failed while importing _ssl: 找不到指定的程序。 ``` **根因**:`desktop/scout_desktop.spec` 的 OpenSSL 强制替换逻辑(L299 原 `_ssl_dll_dir = os.path.join(sys.base_prefix, "DLLs")`)假设打包机的 `sys.base_prefix` 是标准 CPython 安装(DLL 在 `DLLs/` 下)。但本次打包机用的是 conda 派生 venv(`sys.base_prefix = D:\miniconda\envs\intent`),conda env 把 OpenSSL 放在 `Library\bin\` 而非 `DLLs\` → 强制替换的 `if not os.path.isfile(_src): continue` 静默跳过 → `_internal/` 里残留了 PyInstaller 从 PATH 上抓的**版本不对的** libcrypto/libssl → `_ssl.pyd` 加载时导出函数对不上。 **修复**: - `_find_conda_lib_bin()` 的 `cands` 列表新增 `sys.base_prefix/Library/bin` 候选——conda-venv 场景下 `sys.prefix` 是 venv 路径(无 Library/bin),但 `sys.base_prefix` 才是 conda env 根目录。 - OpenSSL 强制替换改为 `_ssl_dll_dir = _conda_lib_bin or os.path.join(sys.base_prefix, "DLLs")`,优先用探测到的 conda Library/bin(含配套 OpenSSL 3.5.7),fallback 到 DLLs。 **验证**:打包后 `_internal/libcrypto-3-x64.dll` = 7,343,944 字节,与 conda env `intent` 的 `Library/bin/libcrypto-3-x64.dll` 逐字节一致;exe 启动 `/chat` 返回 200。 #### pycryptodome 漏收回归(企业微信加解密) **症状**:1.0.0.6 打包后 `_internal/Crypto/` 目录消失(1.0.0.5 有 42 个 crypto 文件),`scout/adapters/platforms/wecom_crypto.py:98` 的 `from Crypto.Cipher import AES` 运行时会 ImportError。 **根因**:打包用的 venv 没装 pycryptodome,旧 spec 也没显式 `collect_submodules("Crypto")`——1.0.0.5 之所以有是借 conda base 环境的 pycryptodome 被 transitive 收集进来,换 venv 后这个隐式收集断了。 **修复**:`requirements-desktop.txt` 补 `pycryptodome`;spec `hiddenimports` 显式加 `*collect_submodules("Crypto")`。修复后 `_internal/Crypto/` 恢复 42 个文件,与 1.0.0.5 一致。 #### spec hiddenimports 清理 - 移除 `scout.memory.embedder`(模块不存在,过时声明;实际路径是 `scout.memory.vector.embeddings`)—— 消除 1 条 PyInstaller ERROR 噪音。 - playwright 加可用性守卫(缺包时 `_pw_hiddenimports = []`,不再报 4 条 ERROR)—— playwright 包 ~100MB,桌面打包默认不装,首次使用浏览器工具时由 runtime 自动安装到用户目录。 ### Build Verification 新增 `verify_build.py`(产物存在性 + 启动冒烟 + 版本号校验)和 `zip_and_verify.py`(打 zip + 解压到干净临时目录 + 实跑验证 + 体积对比)。 v1.0.0.6 最终产物验证全绿: - `dist/ScoutDesktop/ScoutAgent.exe` = 20MB,`_internal/` 1458 文件 - `ScoutAgent.exe --no-gui` 启动,`/chat` 返回 200,`/api/version/check` 报 `current_version = 1.0.0.6` - zip 解压到干净临时目录后实跑:顶层结构正确(`ScoutAgent.exe` + `_internal/`,不多套一层目录),`/chat` 200,版本号 1.0.0.6 - `dist/scout-agent-1.0.0.6-win-x64.zip` = 57.3MB(1501 文件),比 1.0.0.5 的 135MB 小 78MB——是 spec 有意排除 `onnxruntime`(−18MB)和 `pymupdf/fitz`(−38MB)的瘦身,非漏文件。