# 变更日志 > 按里程碑记录各阶段交付内容。每次分支合回 main 时追加条目。 --- ## 未发布 - **项目首页与 README 重新对齐**:补齐一直遗漏的 YouTube / X 来源卡,使首页明确展示 B 站、小红书、抖音、YouTube、X、知乎、Reddit、Linux.do、Bangumi、V2EX、微博与开放 Web;首屏补回“本地运行、只为一个人构建、反馈可调教”的定位,产品入口从过时的“只有浏览器侧边栏”更新为浏览器插件、桌面 Web、移动 Web、Flutter 与 DSH 五端,并修正中文微博文案误用英文、Firefox、架构分层、聚合 Release 说明以及静态 HTML / 中文词典漂移。 - **README 新增 Linux.do 友情链接(折叠)**:主项目 README(中英)顶部原有的「LINUX DO Community」徽章移除,改在 README 底部新增可折叠的「友情链接」区块,内含指向 https://linux.do/ 的 LINUX DO 友情徽章;讨论帖徽章保留。DSH 插件仓库(dsh-openbiliclaw)README 底部同步新增同款折叠友情链接。 - 修复 `scripts/install.ps1` 在原生 Windows 上的一键安装解析失败(issue #157):双引号字符串内 `$InstallDir:` 会被解析为作用域限定变量引用,导致整个脚本在 PowerShell parse 阶段直接报错,改为 `${InstallDir}`;同时为脚本补充 UTF-8 BOM,确保 Windows PowerShell 5.1(脚本声明 `#requires -Version 5.1`)按 UTF-8 解码含中文注释与 here-string 的内容;`Invoke-Bootstrap` 内的 `$args` 改名 `$bootstrapArgs`,避免遮蔽自动变量(`PSAvoidAssignmentToAutomaticVariable`)。 ## v0.3.205:证据驱动时效推荐与可靠性升级(2026-08-14) - **发布与市场状态**:`backend-v0.3.205`、`extension-v0.3.205`、`desktop-v0.3.205` 与聚合 `openbiliclaw-v0.3.205` 均指向提交 `a49af312`;聚合 Latest Release 已附两份扩展 ZIP 与四份桌面安装器。Chrome Web Store 已上传 `0.3.205` 并进入 `PENDING_REVIEW`;Firefox AMO 已接受 listed `0.3.205`,文件状态为 `unreviewed`。AMO `eula_policy` API 仍返回 HTTP 406,manifest 数据类别、reviewer notes、商店描述和随包隐私政策已提交。 - **新增 DeepSeek Harness(DSH)客户端插件(独立仓库)**:OpenBiliClaw-dsh 插件把消费侧搬进 DSH Web GUI——DSH 界面常驻第四栏(aside 槽位),提供推荐 / 内容库 / 对话 / 画像 / 设置面板,并注册 22 个 `openbiliclaw_*` Agent Bridge 工具与 `openbiliclaw-adapter` skill,让 DSH 里的 Agent 也能读推荐、答探测、保存内容、参与学习闭环。插件只做消费侧,爬取 / 平台源管理 / 账号同步仍留在主项目。README(中英)顶部新增「重要更新」提示,核心入口由四个更新为五个(浏览器插件 / 桌面 Web / 移动 Web / Flutter 原生客户端 / DSH 插件)。 - **修复 embedding L2 缓存无界增长(issue #153)**:`data/embedding_cache.db` 持久化向量从 JSON 文本改为版本化 little-endian float32 BLOB(`OBLV` 头 + dtype/dimension 元数据),4096 维向量从约 90 KiB/行降到约 16 KiB/行;读取对 legacy JSON、BLOB 与降级回写产生的 mixed-format 数据按内容自适应解码,单行损坏只降级为 miss。旧库启动时自动执行幂等、小批量、可中断续跑的 JSON→BLOB 迁移(进度即 `encoding` 列),schema 升级补齐 `dimension` / `created_at` / `last_accessed_at` 元数据。新增可配置磁盘预算 `[llm.embedding].cache_max_bytes`(默认 0 = 不设上限)与高低水位,超高位后按「非 active namespace(含 legacy 行)→ active namespace 最旧/最近未访问」顺序批量淘汰;`last_accessed_at` 命中刷新限频,写路径按节奏抽样检查。新增 CLI `embedding-cache-stats`(行数 / 逻辑载荷 / 文件与 WAL 大小 / namespace 分布 / 水位 / 最近维护)与 `embedding-cache-clean`(默认 dry-run,`--apply` 执行迁移 + 回收失效 namespace + WAL checkpoint / `VACUUM INTO` / `integrity_check` / 原子替换做物理回收,`--keep-model` / `--keep-legacy` / `--no-compact` / `--batch-size`)。四表面契约:CLI 与后端程序化接口覆盖;桌面 Web / 插件 / 移动 Web 暂未提供该维护入口(属运维面,PR 显式声明排除)。 - **README Star 历史图表临时替换为 Star 徽章**:GitHub 自 2026-06-30 起将 stargazers API 限制为仅仓库管理员/协作者可读,star-history 实时图表对非协作者全部渲染为「GitHub restricted access to star data」错误占位图;README(中英)暂以 shields.io Star 徽章替代并保留说明,待上游恢复或配置新的加密 token 后换回实时图表。 - **新增「重新初始化 / 重建画像」入口(gui-init §4 收口)**:已初始化后,桌面 Web 设置页「通用 → 初始化与画像」与扩展 popup 通用 tab 提供「重新初始化 / 重建画像」按钮(`window.confirm` 二次确认后调 `POST /api/init {force:true}`,成功后回推荐 tab 展示四阶段进度);CLI `openbiliclaw init --force` 跳过已初始化二次确认并按重新初始化执行(交互终端默认检测到已初始化时先 y/N 确认,非交互保持直接重跑)。语义为 force 重建:重新拉取所选平台数据、重建完整画像并补足首轮发现池,现有事件 / 收藏 / 对话历史 / 手动编辑覆盖全部保留。**force 重初始化同时清空旧推荐池**(`pool_status='purged_by_reinit'`,按新画像重新发现并生成首轮推荐,避免旧画像推荐滞留并顶满 backfill 目标);可选 `reset_cognition`(CLI `--reset-cognition` / API body / 设置页复选框)清空旧 awareness / insight 认知层,适合换账号或大改兴趣。**重初始化前自动创建快照备份**(`data/backups/reinit-<时间戳>/`:SQLite 冷备 + `data/memory/` 全部画像/认知层,CLI 可 `--no-backup` 跳过),重建覆盖的画像与删除的认知层均可恢复。后端 `POST /api/init` 的 `force:true` 绕过 `already_initialized` 守卫,其余前置复验与写者门控不变;移动 Web 无设置页,重新初始化入口按四表面契约声明排除。 - **时效资格升级为证据驱动三态生命周期(temporal v2)**:Evaluation Agent 继续把相关性与时效解耦,并原子输出 `class/confidence/reason + validity_mode/valid_until/scope/evidence/state`;其中 evidence 必须是 Agent 实际可见 prompt projection 的逐字摘录,state 明确区分 `unknown/active/expired/superseded`,`evaluated_at/next_review_at/policy_version/evidence_complete` 则由代码生成并在 storage 重算。确定性 policy 返回 `eligible/review_due/expired`:只有置信度 `>=0.80`、证据组完整、`scope=core` 且 grounding 成功时,已过明确 deadline 或事件 `expired` / 版本 `superseded` 才 hard expire;deadline 证据必须明确给出日期、具体时刻和时区并与 `valid_until` 为同一瞬间,终态证据必须正向明示结束 / 替代,`active` 证据也必须正向明示仍在进行 / 仍受支持,条件句或泛化复核提示不能解除 hold。日期-only、反向证据、标题钩子、低置信、缺失、malformed、未 grounding 与不一致组合全部 fail-neutral;`freshness_only` 也必须逐字 grounding,虚构摘录不会生成复审时钟。`breaking/current/versioned` 的 1 / 14 / 120 天仅为复审节奏,旧 v1 `breaking/current` 的 3 / 60 天也只触发复审,不再按年龄判死。`review_due` 的 discovery 行回到 `pending_eval`,正式池行进入可逆 `pool_status='temporal_review_hold'`,不可展示、不计库存并复用现有评估链;两边失败复审均按逐行 1 / 2 / 4 / 8 / 16 / 24 小时领取有界租约,候选租约未到时不 claim、也不计 raw / projected / 来源容量;若模型执行超过租约,完成、失败与 orphan/release 出口会从落库时续租,避免立即热循环。只有高置信 grounded core 证据或高置信 `evergreen/historical + mode=none` 耐久结论能覆盖旧强分类并解除 hold;低置信、hook-only、中性或未 grounding 复审一律保留旧证据和租约,确定过期才进入 `rejected_temporal_stale` / `stale`。SQLite 在写锁内把整组证据与 lifecycle outcome 原子保存,raw 重抓和旧缓存也不能局部洗字段或复活 hold/stale;pipeline 在 admission 前重读 durable row,只有数据库最终状态仍为 `evaluated` 才可写入正式池。等待扫描、canonical 读取、copy/delight/通知、`PoolServeSnapshot`、最终 recommendation + shown 写事务和 API 1 秒 single-flight 快照使用同一 policy 复核,连续刷新与 snapshot 竞态不能绕过。publication bonus 与 ranking shadow 保持独立:前者只在合格内容之间排序,后者继续只保存聚合观测。 - **新增 Flutter 原生移动客户端(独立仓库)**:OpenBiliClaw-mobile 提供 Android / iOS / Web / Linux / macOS / Windows 全平台客户端,连接同一本地后端;推荐 / 对话 / 画像 / 收藏与 30 天历史 / 消息收件箱齐全,B 站封面 CDN 直连省两跳。首个安装包已随其 Latest Release 发布(Android 签名 APK / iOS 未签名 IPA)。README(中英)、项目主页与文档导航同步加入入口链接与下载入口。 ## v0.3.204:后台来源调度与搜索修复(2026-08-11) - **发布与市场状态**:`backend-v0.3.204`、`extension-v0.3.204`、`desktop-v0.3.204` 与聚合 `openbiliclaw-v0.3.204` 均指向提交 `72a92a96`;聚合 Latest Release 已附两份扩展 ZIP 与四份桌面安装器。Chrome Web Store 已上传 `0.3.204` 并进入 `PENDING_REVIEW`;Firefox AMO 已接受 listed `0.3.204`,文件状态为 `unreviewed`。AMO `eula_policy` API 仍返回 HTTP 406,manifest 数据类别、reviewer notes、商店描述和随包隐私政策已提交。 - **修复 V2EX 三种关键词模式的 Search 空召回**:正式 V2EX Search 不再被 inspiration 关键词开关误关;Exa / You 不可用时,匿名 latest/hot fallback 对 planner 多段长词采用整句精确优先、非通用核心词受限放宽,并继续交给共享 evaluator / admission。`keyword-inspiration-preview --platform v2ex` 现可用且在来源启用时注入只读 V2EX grounding client;同步修正 CLI 平台帮助与模块文档。 - **Linux.do 后台任务瞬时 listener 竞态修复**:同源 task tab 在 Discourse challenge / SPA 初始化期间发送消息失败时,扩展会在同一 task ID 上短间隔重试,并最多重载一次原 runner tab;只有有界恢复失败才回传 `sendMessage_failed`,避免闹钟调度把瞬时 content-script 缺失误判为任务失败后连续制造重复任务。Linux.do CLI 预览同时按 `linuxdo-*` strategy 过滤,避免显示共享候选管线中其它来源的旧条目。 - **所有扩展账号周期回拉改为默认关闭**:新增 `scheduler.source_incremental_enabled=false` 安全总开关;旧配置即使保留 `source_incremental_hours=24`,未显式 opt-in 也不会检查扩展 presence、创建账号 bootstrap 任务或打开平台标签页。scheduler-owned 任务带独立标记,升级前由调度状态记录的残留任务也会在领取前终止,避免知乎、Reddit、Linux.do 等来源在用户浏览其它页面时切换标签页并抢走焦点;手动增量任务不受误伤。手动初始化、手动 `fetch-*` 和正常 discovery 保持不变;设为 `true` 后才恢复原有全局 / 逐源周期。 ## v0.3.203:微博登录态初始化与插件可用性修复(2026-08-11) - **发布与市场状态**:`backend-v0.3.203`、`extension-v0.3.203`、`desktop-v0.3.203` 与聚合 `openbiliclaw-v0.3.203` 均指向提交 `cf5ac276`;聚合 Latest Release 已附两份扩展 ZIP 与四份桌面安装器。Chrome Web Store 已上传 `0.3.203` 并进入 `PENDING_REVIEW`;Firefox AMO 已接受 listed `0.3.203`,文件状态为 `unreviewed`,当前聚合包提供 Firefox 临时加载 ZIP。AMO `eula_policy` API 仍返回 HTTP 406,manifest 数据类别、reviewer notes、商店描述和随包隐私政策已提交。 - **微博补齐登录态初始化**:微博从 discovery-only 升级为 capability-specific full source;`init --yes-weibo` / `/api/init` 会在扩展确认浏览器登录和当前 uid 后,通过隔离同源任务只读导入收藏、关注和 mentions,后端只保存布尔 heartbeat、账号绑定与规范化事件,不接收 Cookie。个人 bootstrap 暂为 init-only,公开 discovery 仍匿名可用。 - **修复微博 H5 登录态 bootstrap 路由**:适配当前移动端接口 `/api/container/getIndex?containerid=230259`、`/api/friendships/friends`、`/message/mentionsAt` 与 `/message/mentionsCmt`,旧路径仅作兼容回退;HTTP 404 不再被当作真实空结果,扩展构建与任务回归测试同步更新。 - **修复桌面 Web 配置页空白**:合并 Linux.do / V2EX 来源卡片时出现的嵌套 HTML 让后续设置面板被错误收进来源卡片;现将两张来源卡恢复为同级节点,并加入 DOM 结构回归测试,确保模型、平台源、调度和高级设置面板都能正常渲染。 - **抖音账号周期回拉改为默认关闭**:`douyin_incremental_hours` 的缺省值从继承全局 24 小时改为 `0`,避免作品 / 收藏 / 点赞 / 关注的 `bootstrap_profile` 任务定期打开前台抖音页并抢走用户焦点;需要该能力时可显式设置 `1..168` 小时开启,手动初始化、`fetch-douyin` 和后台 feed / search / hot discovery 保持不变。 - **恢复插件底部「最近发生的事」动态栏**:side panel 的全局活动栏此前在切到「对话」Tab 时被 CSS 整块隐藏,看起来像功能消失;现改为四个一级 Tab 始终可见,聊天记录继续在剩余空间内独立滚动,输入框仍固定在聊天区底部,并加入回归测试防止再次按 Tab 隐藏。 - **收紧插件覆盖层与短侧栏可用性**:设置、消息和手机二维码覆盖层现在使用 modal 语义、背景 `inert`、Tab 焦点圈定、Esc 关闭与触发点焦点恢复,不再让键盘焦点落到被遮住的主页面或动态栏;动态历史高度随 `dvh` 收缩,避免矮侧栏展开后把底部挤出视口;停用来源的卡面也会退出键盘顺序并标记 `aria-disabled`,但启用开关仍可操作。 - **修复扩展配置页空白**:Linux.do 来源卡片缺少两个闭合标签,导致通用、调度、高级功能和日志面板被浏览器嵌入隐藏的平台源面板;现已恢复为同级面板,并加入 HTML 结构回归测试。 ## v0.3.202:Linux.do、V2EX 与微博来源扩展(2026-08-10) - **发布与市场状态**:`backend-v0.3.202`、`extension-v0.3.202`、`desktop-v0.3.202` 与聚合 `openbiliclaw-v0.3.202` 均指向同一绿提交;聚合 Latest Release 只包含两份扩展包和四份桌面安装器共六个 `0.3.202` 资产。Chrome Web Store 已上传并进入 `PENDING_REVIEW`,Firefox AMO 已接受 listed `0.3.202`,文件状态为 `unreviewed`。AMO 隐私字段 API 仍返回 406,manifest data-collection 声明、reviewer notes、商店描述和包内隐私政策已随提交提供。 - **新增 Linux.do 只读来源接入**:后端新增 `linuxdo_tasks` durable 队列、`LinuxdoDiscoveryProducer`、统一 topic/event 归一化、capability-specific source-auth 与周期增量同步;公开 discover 匿名可用,个人 profile/bootstrap/incremental 必须由同源会话正面确认。CLI 新增 `fetch-linuxdo` / `discover-linuxdo`,通用 `discover --source linuxdo` 也走同一 producer。五种 discovery 分支为 search / hot / feed / creator / related,三种个人初始化 scope 为 bookmarks / likes / read history。 - **扩展采用同源、最小回传边界**:Linux.do 请求只在真实 `linux.do` task tab 内以 `GET` + `credentials: include` 访问 JSON endpoint;Cookie `_t` 只转换成登录布尔心跳,Cookie 值、CSRF 字段和未裁剪原始响应都不上传。任务具备 tab/task 隔离、分页与条数上限、超时、2 MiB 响应上限及结构化错误。 - **真实安装版端到端验证完成(修复后仍保留重跑门禁)**:2026-08-09 在已登录 Linux.do 的 Chrome unpacked extension 上完成热更新与真实只读链路。bootstrap 两轮均返回 bookmarks=2、likes=5、read history=100(共 107 条),第二轮 durable ingress 零重复;search / hot / feed / creator / related 五分支均取得 canonical topic,五种正式 producer 均完成真实模型评估,组合运行继续遵守全局 limit 与候选幂等。测试还在真实 `in_progress` 任务中触发完整扩展重载,复现并修复 MV3 runner 丢失后以同一 task ID 安全恢复的问题。任务结果字段白名单、canonical ID/URL、first-final-wins 与无 Cookie/token/raw-response 均通过数据库断言。旧 `suggested_topics` 可能合法为空且语义是 new/unread/random 站点建议,不是严格相关;related 已改为 topic detail + 官方 `/topics/similar_to.json`,最终安装产物仍需补跑真实 Chrome/Firefox 门禁。 - **Linux.do 契约审计补漏**:新增 capability-specific auth matrix、账号 key fail-closed 分区、跨扩展实例 single-flight + claim token、backend-owned scope/cap/关键词 ID/交互 action 校验、严格 JSON Content-Type 与 true-empty 证据、partial/failed first-final staged replay、retained-only 日预算、持久分页 cursor、failed/degraded 不推进增量 cadence、guided-init success 时间种子、所有 claimed final 的 durable ACK/MV3 outbox、前台 tab 恢复和平台中性的缺作者文案。冻结 contract、acceptance ledger、自动审计器和 Chrome/Firefox build asset verifier 同步纳入发布门禁。 - **任务 tab 不再污染被动画像**:真实 E2E 发现 Discourse 会在 `document_idle` 前清掉 hash marker,自动任务页因而被误识别为普通浏览并写入 snapshot。任务入口现改用稳定 query marker,并继续兼容旧 hash 的恢复识别;后台任务页只运行只读 executor,不启动行为 collector。 - **长任务、重启恢复与部分成功语义**:Linux.do 默认端到端总等待为 32.5 分钟(pending 领取最多约 3 分钟、合法大任务按形状执行约 29 分钟、结果余量 30 秒),显式 CLI/env 等待值是总硬上限;claim lease 为约 35 分钟、共享 mutex stale 窗口为约 36 分钟。真实 `in_progress` 热重载复现了仅靠 `storage.session` 重绑会永久等待的缺口;runner 现把无凭据 task/tab/deadline 临时写入 `storage.local`,恢复后重发同一 task ID,仍存活的 content context 会合并重复执行,完整重载则安全重放只读 GET。bootstrap 部分 scope 或 discovery 分页 / 多输入中途失败以 `degraded` 保留有效 items,bootstrap `failed/degraded` 均不进入 6 小时近期任务复用;guided init 使用默认 Stage-1 预算时,Linux.do-only 至少给 32.5 分钟,多来源并选 Linux.do 时给 62.5 分钟,显式 override 不扩。 - **Linux.do MV3 恢复与 engagement 分支缺口修复**:runtime stream 现在先于可能等待 mutex 的恢复 barrier 建连;`storage.session` generation 区分普通 worker recycle 与完整 extension reload,完整 reload 会刷新 runner-owned 页面再重放同一只读 task ID。真实 Chrome 中两页 feed 在 `in_progress` 时热重载,约 25 秒后以同一任务行 terminal=ok,返回 37 个唯一 topic;修复全局 result cap 后的隔离复跑再次以同一任务行返回 36/36 唯一 topic。正式 search/related 同时增加 retained-only topic detail hydrate,真站各 2/2 候选均取得主题作者、浏览、总赞和回复,不再缺字段,也不使用匹配回复的作者/点赞冒充主题指标;双关键词 `max_items=1` 真站任务也严格只回传 1 条。两轮个人 bootstrap 各返回 `2 bookmark / 1 like / 20 read`,durable ingress 保持 23 条不重复;真实 404 被分类为 `failed/linuxdo_http_error` 而不是假空。扩展全量 1323/1323、Chrome/Firefox build 与 17/17 资产校验通过;冻结 Firefox 安装版真账号门禁仍单列未执行。 - **来源状态保留最近任务真值**:Linux.do 浏览器心跳优先;无心跳时,`/api/sources/status` 只读最近 `linuxdo_tasks` 作 `task_history` 间接证据。个人 bootstrap 成功可证明可选会话通路,公开 discovery 成功仍保持 `credential=none`;`login_required`、限流与运行中状态分别表达,不把匿名任务伪造成已登录。 - **新增微博匿名公开 discovery 来源**:后端以仅存内存的匿名 visitor 会话执行 search / hot / creator 只读发现,不读取用户 Cookie、不进入 guided init、不增加扩展 host permission,并复用统一候选评估、来源占比与三端文字卡。 - **重构新增平台来源 skill 为证据驱动的阶段门**:复盘本地 Codex session、Git/GitHub 首次接入与后续修复,把 `full / discovery-only / capability-increment / audit-only`、机器可读来源契约、逐能力 hybrid auth、fail-closed E2E 写动作边界、中央注册 audit、required/N/A 与 PASS/FAIL 分离、原子任务准入、MV3 恢复、真假空结果、增量同步、scope completeness、时间语义、双浏览器资产和安装包真机 provenance 固化为完成条件;新增历史失败索引与 skill 镜像/契约审计测试,只有全部 required gate 有证据通过才允许报告 complete,发布 mutation 仍需明确授权。独立盲测还复现并修复了 `browser_heartbeat` 对未知来源默认落到知乎的错源分派,现由 source→prefix 显式 registry 驱动,未知来源 fail closed,新增来源必须成组提供 DB getter、扩展 event handler 与往返测试。 - **V2EX 已安装扩展真实登录 E2E 通过**:在 `8420` 真实后端热更新开发扩展后,四个只读 scope 于 13 秒内返回发布 4、讨论 Topic 19、收藏主题 1、收藏 Node 0,并转换为 24 条 canonical 事件;登录态、observed identity、稳定 Topic ID / URL、事件 source 与 satisfaction 语义、四 scope 完整证据全部通过。`smoke_only` 未向真实库写入任何 V2EX event、Node Affinity 或收藏快照;隔离库重复写验证为首次 24/0、第二次 0/24。 - **V2EX 最终构建与 8420 热更新复验**:最终 Chrome 构建通过后端 runtime event 热重载并重新连回真实登录态,四 scope 再次返回 4 / 19 / 1 / 0、24 条 canonical 事件;真实库的 event、seen、Node Affinity 和收藏快照增量均为 0。五路公开读取为 Search / Tab / Hot / Latest 各 3 条、Node 5 条;隔离正式 Node producer 使用用户现有 LLM / Embedding 配置完成发现 3、入池 3、评估 3、准入 1、低分拒绝 2,只记录 1 条脱敏 usage,临时库自动删除。 - **修复 V2EX gzip 响应二次解码**:有界流读取拿到的是 httpx 已解码字节,旧实现却把上游 `Content-Encoding: gzip` 原样复制到新响应,真实 Node 请求会再次解码并报 `incorrect header check`;现在在保留解码后字节上限的同时剥离内容编码、内容长度和传输编码头,并新增 gzip JSON 回归测试。 - **V2EX 无封面卡收敛为紧凑文字卡**:桌面、移动 Web 和 popup 不再给 Topic 文字卡保留大块 16:9 空媒体区,改为有界正文预览、来源 / Node badge、作者 / 时间 / 回复数和原动作区;Chrome 商店三端截图已按最终构建重制。 - **修复真实 V2EX 末页越界重复翻页**:V2EX 对 `?p=3` 可能保持地址不变却渲染末页 `p=2`,executor 现读取 `.page_current` 与后续页链接作为权威耗尽证据;修复后同一真实任务从约 4 分半缩短到 13 秒,四个 scope 均 `complete=true`。 - **真实公开发现与正式评估链复验**:Search 真实返回 1 条,Node / Tab / Hot / Latest 各返回 3 条,分支 smoke 均为 0 本地写入 / 0 LLM;随后用用户现有 LLM / Embedding 配置在隔离库运行正式 producer → evaluator → admission,召回 4、入待评估池 4、准入缓存 3,主推荐池未被测试污染。 - **V2EX bootstrap 与画像链落地**:新增不污染画像的 `fetch-v2ex` smoke、四 scope staged task bridge、按 Topic 聚合的 `discussion_reply`、账号分区 Node Affinity、双完整快照 retraction/restore outbox,以及 PAT verified → browser observed → config/accepted 的后端身份阶梯;明确 PAT / 浏览器证据 freshness、身份冲突门禁和零站内写操作。 - **按真实 V2EX 页面和 API 收紧解析**:回复只绑定相邻 `.dock_area` / `.inner` metadata,API 2.0 按 `success/result` 解包,兼容旧 Topic 列表、Atom 嵌套作者/content/entry id 与 epoch rate-limit reset;producer 有界补 Topic 详情,并仅在 PAT 可用时读取 Reply 第一页生成讨论摘要。 - **V2EX full 来源硬化**:补齐逐能力鉴权 readiness、扩展 claim 前准入与 MV3 durable recovery、2xx result ACK、affirmative-empty / hidden / challenge 状态、配置驱动 scope cap、国内直连网络边界、最终保留候选预算和来源发布材料;桌面 / popup 身份冲突可交互选择,新账号证据先暂存、完整 Soul 构建提交后才激活,旧账号事件 / Affinity / 收藏快照保持隔离。被动点击不再从“回复 / 收藏”等按钮文字猜测站内成功动作;Topic 可见阅读满 30 秒后,Node / Topic / 域名 / active identity 校验通过才按 distinct Topic 幂等计入首版时间衰减 Node Affinity。V2EX 配置保存统一限制未知字段、数值与 slug,商店 / AMO 文案明确 A2 只检查存在性、零 Cookie 值与零站内写操作。 ## v0.3.201:探针聊天与 dislike 即时推荐(2026-08-08) - **修复“抓了一天但一条新可换库存也没补进”的双协调器死区**:8 月 9 日用户日志显示来源抓取实际完成 78 次 enqueue(输入 2,158、保留 740),候选评估完成 1,337 条,可换库存始终为 221–224;但 65–78 条 admitted 素材长期卡在待文案,`recommendation.write_expression` 自 8 月 7 日晚后为零。根因是 copy coordinator 在 unrestricted `copy_ready>=90` 时认为无需工作,而 candidate coordinator 又把全部同 topic 深层 `admitted_pending_copy` 算进 projected,`available + pending≈300` 后也停止评估。storage 现新增 canonical `admitted_pending_available`,只统计补齐文案后能进入 topic 三条展示窗口的 pending;公开加载与计数统一先剔除 durable seen / 不可链接行再套 topic cap,避免这些高分行占窗并让 eligible 产生假净增。评估的 projected/admission 改用该子集,表达需求改为 copy 水位缺口与 eligible 公开库存缺口的较大值并 eligible-first 领取,API/CLI/OpenClaw 三个组合根统一注入公开库存目标,`copy_ready_target_count=0` 仍保留 legacy drain-all。插件若首次 runtime HTTP 失败,后续 `pool_status` stream 会恢复 initialized 状态;三端同时把待整理素材与真实可换数分开显示,并把误导性的“上次成功补货/最近补进”改为“补货进展”。 - **V2EX 公开 discovery 首阶段接入**:新增只读 V2EX Client / Topic normalizer / producer,覆盖匿名 API、JSON/RSS Feed、可选 API 2.0 PAT 与 live probe,以及 `search / node / tab / hot / latest` 五个分支;候选通过共享关键词规划、inspiration grounding、评估和平台份额进入统一待评估池。配置、source-auth、CLI、runtime、桌面 Web、移动 Web / popup 文字卡和文档同步接入。 - **修复抖音 discovery 长时间空转与零结果误报**:本机任务历史复现到 `stale_pending` 分钟级循环、`tab_create_failed`、构建产物缺失,以及 feed 首屏响应早于任务 collector 后仍写 `ok + 0` 的竞态。Chrome / Firefox 构建现隔离清理并校验 manifest 资产;Douyin MAIN-world fetch / XHR tap 从 `document_start` 安装,页面替换网络原语后会幂等重包,isolated world 对早到的归一化 discovery 条目做 120 秒 / 256 条有界、按 scope 一次性回放,并单独记录 response observation。真实 `/jingxuan` 卡片不再只按 `a[href]` 解析,同时识别 `div[data-aweme-id]`、非 anchor `href` 与 `video_`,网络没有续请求时也能从已渲染卡片提取 ID / URL / 标题 / 作者;完全未观察到 feed 响应且 DOM 也无内容时才触发原后台 tab 一次 bypass-cache reload,仍失败才返回 `feed_no_observed_response`,不会冒充真实空或对限流 / 风控重试。search dispatcher 会在提交前监听结果路由;搜索触发真实整页导航时,新文档只恢复采集阶段,同文档 SPA 则由 execution key 去重,并移除首次 ready 后重复导航首页的竞态。dispatcher 在具备 tab 能力且拿到跨来源 mutex 后才 claim,alarm / WS poll 单飞,task-result 要求 2xx 并做有界幂等重试;daemon 在扩展离线时前置跳过,并对基础设施失败 / 预算耗尽分别使用 15 / 60 分钟退避。 - **抖音任务领取增加跨扩展实例单飞**:扩展内存 mutex 只能覆盖一个 service worker;后端 `DyTaskQueue.next_pending()` 现在会在原子领取事务内检查全表未过期 `in_progress` lease,存在时不发第二条任务,挡住两个 unpacked 扩展 ID 或 Chrome profile 并发 claim。15 分钟过期 lease 仍按原协议优先重领并修复 staged result。 - **候选评估新增语义时效分型与近期供给 shadow 闭环**:Evaluation Agent 在保持 `relevance_score` 只表达画像相关性的同时,输出 `breaking/current/versioned/evergreen/historical/unknown`、置信度与理由;结果贯穿待评估池和正式缓存。PoolCurator 不再把 `discovered_at` 当内容发布时间,而只对高置信、发布时间明确的前三类发放有界正向 bonus,缺时间和常青/历史内容保持中性。基于历史候选池/发现池回放,B 站主 API 与扩展 fallback 各自只保留一个 1×5 的 `pubdate` recent lane,仍走统一相关性与 admission;推荐侧同步记录含 bonus 与 no-bonus Top10/50/100 的 aggregate-only class/source/age shadow,30 天 / 5,000 行有界留存,不含候选身份且不改变 serving。 - **Agent Bridge 与当前内核能力对齐**:OpenClaw 历史接入升级为 `agent-bridge/v2` 协议中立适配层,Hermes / WorkBuddy 可复用同一套能力协商、24 个 skill descriptor、JSON CLI 和 Python alias;补齐多源推荐、换一批 / 追加、活动流与平台可用性、四态兴趣 / 避雷探针、惊喜反馈、durable chat history、画像编辑、本地保存与显式授权 native-save 同步。后续新增对外能力必须同时登记 operation DTO、skill descriptor、CLI(适用时)、capability manifest 与集成文档,并配套 idempotency / state-changing 边界测试。 - **全面真实 E2E 后收紧多源与控制面可靠性**:source-scoped discovery 的历史回填先按策略平台过滤,B 站空结果不再混入 Reddit 缓存;抖音插件 search/hot/feed 共用一次等待预算,超时/取消任务落失败终态,daemon 在扩展缺席时零入队并对空跑、错误和预算耗尽退避;抖音 live probe 的 legacy 与正交状态保持一致。配置页 LLM 探测扩到有限 120 秒后端 / 125 秒客户端窗口,覆盖 Ollama 冷启动;桌面 runtime 看板不再显示 `dy_task_available` 原始控制帧。 - **桌面配置页支持整套可移植数据导出 / 导入**:本机打开 `/web`,可把磁盘 `config.toml` / `config.local.toml` 合并、移除整段 `[api.auth]` 后的单份可移植配置,以及 SQLite、画像与记忆文件、平台 Cookie 文件、图片缓存和少量安全桌面 UI 偏好导出为 `.obcbackup`;导入前会再次明确提示目标机器的当前配置和用户数据将被替换。 - **迁移包是带校验清单的明文敏感 ZIP**:每个成员记录 SHA-256 与大小,导入会限制压缩包 / 解压大小、文件数量、单文件和路径,拒绝路径穿越、符号链接、加密条目、格式版本不匹配、校验和不一致、无效配置和损坏 SQLite。迁移包可能含模型 / 来源 API Key、平台 Cookie、画像与历史记录,不提供虚构的“已加密”保证;整段 API auth(密码 / hash、session secret、设备 key 等)、日志、历史备份、embedding 缓存、评测 / 临时缓存、证书、自启动文件、OpenBiliClaw Web / 扩展访问会话、外部 CLI 凭据和环境变量值不导出。 - **导入先暂存、重启后再替换**:`POST /api/migration/import` 在运行进程中只完成上传、完整校验和私有暂存;下一次受支持的后端启动取得 migration runtime lock 后,才以 journal + 同目录替换原子应用配置、SQLite 与画像,失败会恢复原状态,成功后原 `config.toml`、`config.local.toml` 和数据目录按存在情况保留为 `pre-import-*.bak` 回滚副本。桌面端也不在 202 暂存时提前切换偏好,只在后端报告 `applied` 后按 `migration_id` 为每个浏览器应用一次白名单 UI 设置;之后用户修改不会被同一旧 status 重写。 - **跨机器但不复制机器身份**:导入保留目标机器的数据路径、数据库路径、API host / port、日志路径、网络 / TLS / 自启动设置、浏览器 CDP 地址,以及 Bilibili 专用代理和本机浏览器可执行文件路径;目标机证书与自启动文件也继续保留。整段 `api.auth` 以目标机现值为基线,再轮换文件 session secret、把 prepared DB 的 `auth_epoch` 严格提升为 `max(来源 epoch, 目标当前 epoch) + 1`、清空并关闭扩展远程访问;因此保留目标机门禁 / 密码 / proxy / Origin 策略,但即使 session secret 由环境固定,来源 / 目标旧 Web 会话仍失效且扩展设备需重新配对。 - **来源遗漏与目标覆盖分别可见**:manifest / 导入响应用 `source_omitted_environment_variables` 提示源机依赖但没有写入包的变量名(`OPENBILICLAW_*`、Gemini 标准 Key、系统代理 / CA);暂存响应 / 状态另用 `target_active_environment_variables` 提示目标进程当前仍生效、可能覆盖导入文件的变量名,两个列表都不包含值。 - **迁移 API 坚持本机边界并支持对账 / 取消**:新增 `POST /api/migration/export`、`POST /api/migration/import`、`GET /api/migration/status`、`DELETE /api/migration/pending`。四者即使在 LLM 降级态也可用,但仍要求后端真实观察到 loopback transport、同源浏览器意图和显式 `X-OBC-Auth: 1`;浏览器扩展、局域网客户端和远程反向代理不能调用。导入另要求 `X-OBC-Migration-Confirm: replace-all`,接受 / 生成 UUID `request_id`;status 在上传 / 校验期返回匹配 ID 的 `processing` 与 `uploading|validating` phase。断连后桌面端最多强制查询 3 次,遇到 `idle/cancelled` 间隔 500ms 再确认,不能把一次瞬时 `idle` 当终局;每次打开配置「通用」也会强制对账。取消只删除 pending、不改 active 数据。guided init 期间导出 / 导入拒绝,status / cancel 仍可用;配置保存忙时导入 / 导出拒绝。 - **在线修改 `data_dir` 改为重启后切换**:`PUT /api/config` 仍持久化用户选择,但 canonical 路径与当前 active data dir 不同时返回 `restart_required=true`;本进程的 `RuntimeContext`、数据库、同次保存的外部 Cookie 凭据和迁移导出的数据快照继续使用已经取得 runtime lock 的旧目录,其它字段照常进入后台应用队列。只有完整重启并取得新目录锁后才启用新路径,`apply-status=applied` 不再被误解为数据目录已热重载。 - **修复启动冷备与 SQLite WAL 锁的顺序风险**:`openbiliclaw start` 现在在 guided init 或 runtime 建立持久 SQLite 连接之前完成健康检查和到期冷备;不再在已有连接后用普通文件复制打开 / 关闭主库 inode,避免 POSIX 进程锁被意外释放并导致后续 checkpoint 使用错误 WAL 世代。迁移导出的 online backup 也改用只读源连接;新增跨进程完整性探针后旧 / 新连接交替写入的回归,确保最终数据库仍通过完整性检查。 - **Issue #112 新增 30 天内容历史**:插件 side panel、桌面 Web 与移动 Web 都新增「历史记录」,按「主动点开过 / 出现过但没点开 / 最近移除」三组分页展示。后端复用 recommendation 点击事件与推荐记录,只为会随 membership 删除而丢失的本地收藏 / 稍后移除保存快照;重复内容按 canonical `item_key` 折叠,最近移除可一键恢复。三端封面统一按页、懒加载、低优先级走现有磁盘缓存代理,避免打开历史时并发请求整月图片。 - **Issue #112 内容库信息架构收敛**:插件、桌面 Web 与移动 Web 将原来的「稍后 / 收藏 / 历史」三个一级入口合并为单一「内容库」,内部保留三个语义子 tab;移动底栏和插件导航都缩为「推荐 / 内容库 / 画像 / 对话」四项。旧 hash、deep link 与 popup `?tab=` 参数会迁移到对应子项;键盘方向键、Home / End、焦点环、44px 触控目标与每个子项的滚动位置保持可用。 - **Issue #112 真实 E2E 加固**:在匿名 B 站实时数据、旧库迁移、三端真实浏览器、断服重启、并发读写与真实图片代理上补齐验证;legacy 推荐先确定 canonical key 再做等值关联,成熟库 `shown` 首屏由约 8 秒降至冷启动约 0.14 秒、热查询约 0.02 秒。历史 URL 只返回安全 HTTP(S);新界面首屏不发送 cursor,续页使用绑定分类、30 天窗口、全序位置与 source max-id anchors 的 opaque `next_cursor / has_more`,避免头部新增造成 OFFSET 漂移,但不把既有行更新/删除描述成跨请求快照。每个内容的 `contexts` 分别保留 favorite/watch_later/dismiss/dislike 最新事实,收藏与稍后再看独立恢复;坏封面显示可见 fallback,插件读取另有 12 秒截止时间。 - **Issue #112 保存卡坏封面回退**:真实图片代理返回 403 或浏览器已缓存失败结果时,移动 Web、桌面 Web 与插件的收藏 / 稍后再看卡片都会移除破图 ``,在原尺寸位置显示可见 SVG 占位;打开卡片的标题标签和布局保持不变。 - **探针聊天跨界面对齐**:从消息里的「多聊聊」提交的 `probe` / `avoidance_probe` durable turn 现在也会进入插件、桌面 Web 与移动 Web 的主对话历史;关闭消息面板或切换到「聊聊口味」后仍能找回这段对话,惊喜推荐 `delight` 继续保留在自己的内容卡片内聊中。 - **修正 dislike 的产品边界**:普通 dislike 不再被当成搜索词禁令,也不再让跨 digest 关键词整理撤销同词 pending;搜索与多源抓取可以继续宽搜,单卡反馈只同步隐藏该卡,主题证据确认后才约束相关推荐输出。平台库存饱和产生的 supply avoid 仍可独立淘汰冗余关键词。 - **关闭偏好写入到推荐展示的延迟窗口**:`get_profile()` 立即合并 flat preference 的最新 dislikes;推荐历史 snapshot 绑定 dislike digest,首屏、换批、追加、OpenClaw 与主动通知在最终输出边界复核,不再等待 1 秒缓存、Soul rebuild 或异步清池。 - **保留误杀保护与既有语义清池**:结构化 topic 精确命中始终排除,普通模糊命中在多卡全灭时只恢复 exact-safe 行,单条 push 不恢复;embedding + LLM 清池继续作为库存优化而非展示正确性边界。 ## v0.3.200:聊聊口味 Markdown 渲染(2026-08-07) - **Issue #147「聊聊口味」支持安全 Markdown 回复**:popup、桌面 Web、移动 Web 及惊喜推荐 / 探针内嵌聊天统一渲染 AI 回复中的加粗、斜体、列表、代码、引用和安全链接;原始 HTML 与不安全链接会被转义或拒绝,用户输入仍按纯文本显示。已通过真实后端请求与真实 `/web` 页面 DOM 验收。 ## v0.3.199:配置应用与保存列表体验修复(2026-08-06) - **配置保存与后台应用状态统一收口**:`PUT /api/config` 持久化成功后统一立即返回 `202` 与单调 `apply_revision`,由 app-owned latest-wins 队列在后台安全热重载;`GET /api/config/apply-status`、`config_reloaded` 与 `config_reload_failed` 成为桌面 Web、插件和 `/setup/` 的共同状态契约。向导会等待配置真正应用后再进入下一步,热重载失败同时恢复磁盘与内存 last-good runtime;桌面设置页忽略旧 revision 的迟到状态,并在保留新草稿时更新 Discard 使用的 canonical 回滚快照。 - **修复桌面 Web 保存列表徽标首屏缺失与乱序覆盖**:刷新 `/web` 时并行水合稍后再看 / 收藏列表,直接使用后端 `total` 显示侧栏徽标;零值和读取失败继续保持隐藏,不阻塞推荐首页。首屏请求与用户进入列表后的完整刷新通过 per-list generation fence 隔离,迟到旧响应不会把新数量覆盖回去。 ## v0.3.198:小红书 discover 后台化(2026-08-06) - **小红书搜索发现不再抢占当前页面**:search task 改为始终在 inactive tab 执行;MAIN-world bridge 只对小红书搜索接口响应提取与既有 DOM collector 等价的公开卡片字段,并通过页面内 replay 缓存交给 isolated task executor,解决隐藏页不挂载虚拟列表时的空结果。DOM 仍作为 schema 漂移兜底,creator 继续后台执行,只有需要点击个人入口和受控滚动的 `bootstrap_profile` 保持前台。真实登录态连续 3 次搜索均在约 4–5 秒返回 20 条笔记,原活动页面 35 次可见性采样全部保持 `visible`;后端、桌面、浏览器插件、Docker 与聚合 Release 统一使用 `0.3.198`。 - **发布门禁等待 detached saved-sync 完整收尾**:慢速 CI runner 上,测试在 durable `synced` 终态已落盘后继续等待 watchdog done-callback 清理完成,再断言内部 task 集合为空;生产 saved-sync 行为不变,消除数据库终态与事件循环回调之间的时序抖动。 ## v0.3.197:来源账号增量同步与登录态可靠性(2026-08-06) - **五个浏览器账号来源支持可靠的周期增量回拉**:画像就绪且插件在线时,runtime 默认每 24 小时按持久 round-robin 复用小红书、抖音、YouTube、知乎和 Reddit 的既有 bootstrap scope;五源全局串行,并受扩展在线、guided init、来源开关、热重载周期和跨进程 SQLite admission fence 共同约束。任务结果按 canonical result → durable event ingress → seen-key checkpoint → terminal flip 落盘,崩溃窗口可由租约重领修复,重复回拉不会重复学习同一事件。 - **Reddit 与小红书回传边界进一步收紧**:Reddit 补齐 first-final-wins staged ingestion、有界分型 identity 去重和 parent / short URL 防误认;小红书 bootstrap 的允许 scope 与 `max_items_per_scope` 由任务创建时的不可变 payload 决定,partial、final、直接完成和风控失败合并都累计裁剪。扩展重试、分批回传或未知 scope 不能再扩大画像事件预算,已接纳笔记仍可安全补发布时间与首个同 identity token。 - **真实页面登录态识别跟上当前小红书 DOM**:search / creator / bootstrap 除可见登录弹层外,也识别登录手机号输入框与侧栏本人登录按钮,并用完整祖先可见性排除隐藏控件和普通笔记文字。真实已登录浏览器验收确认 `/api/sources/status` 恢复为 `browser_heartbeat / verified`,旧 `web_session` 与新版 `/explore` 登录门不再造成误判。 - **PC Web 已登录后的黄色 Cookie 警示会自动消退**:收到 B 站、抖音、X 或 Reddit 凭据同步 runtime 事件后立即重读来源状态;页面可见时保留 30 秒离线轮询作为漏事件兜底,不再要求打开来源设置页或手动刷新首页。真实 Chrome 登录态 E2E 两次捕获 `bilibili_cookie_synced` 后 1ms 内紧随 `/api/sources/status`,独立只读探针为 `replayed=false / verified`。 - **发布门禁消除并发测试调度抖动**:image-fetch singleflight 回归先等待全部并发调用加入再断言计数,避免慢 CI runner 在任务尚未调度完时误报;生产协调器语义不变。后端、桌面、浏览器插件、Docker 与聚合 Release 统一使用 `0.3.197`。 ## v0.3.196:候选池即时补给与模型调用瘦身(2026-08-06) - **候选池从定时等待改为缺口驱动即时补给**:候选评估器缺少 raw work 时会按平台份额缺口并行唤醒全部已配置 producer;周期 tick 与即时 tick 共用 per-source lock。补池结果显式区分插入量、真实进展与有效产出,重复结果不再冒充成功,而会进入 30 / 60 / 120 / 300 / 600 秒无产出退避;任一候选真正入队即清零阶梯。真实端到端请求已验证抖音来源任务、模型评估、候选入池与推荐库存恢复闭环。 - **通过真实门的 sparse JSON 成为 batch evaluator 默认**:100 条候选 × 3 轮对照中,prompt-token 节省中位数为 `27.99%`,total-token 节省中位数为 `24.05%`,相对质量、分类、repair、route、embedding、recall、usage 与隐私门均通过。生产请求使用 request-local ID 和 canonical sparse envelope,未发送的 URL / 全局 ID 不再造成 cache false miss;完整正文、发布时间、多模态顺序和失败修复契约保持不变。 - **画像二级兴趣按推荐意图治理重复项**:候选门结合 embedding 与保守词面召回,连通分量替代首成员贪心聚类;父子兴趣、跨类通用后缀和用户显式 no-merge 仍受保护。任一模型批次失败都会留下 `retry_pending`,精确 raw Soul 快照支持回滚,apply 前的完整 revision 校验避免长模型调用覆盖并发写入的新兴趣。 - **三端待聊与平台状态恢复更可靠**:插件在后端恢复在线、runtime stream 重连和侧栏重新可见时刷新待聊角标;PC Web 将待聊计数提升为首屏高优先级请求,移动 Web 同步显示底部对话角标。PC 平台 Tab 的配置快照读取增加 1 / 2 / 4 / 8 秒有界重试,瞬时连接重置后仍会收敛到已启用平台集合。 - **发布类型检查在 Python 3.11 / 3.12 间保持一致**:`evaluation_wire` 用显式类型变量承接已验证 JSON 数字,消除 CI 的 `redundant-cast` 与本地的 `no-any-return` 分叉,row-wire 运行语义不变。 - **后端、桌面与浏览器插件统一发布为 `0.3.196`**:组件标签、GitHub 聚合 Release、Docker 镜像和桌面安装包使用同一版本;Chrome Web Store 以替换 pending submission 的方式提交审核,Firefox 继续走独立 AMO listed channel,并附可复现 reviewer source。 ## v0.3.194 / extension v0.3.195:小红书真实搜索修复与首启可靠性(2026-08-05) - **画像二级兴趣去重从“严格同义词”升级为“同一推荐意图”**:真实画像诊断显示,“社会时事 / 时事新闻”“生活日常 / 生活记录”等相近二级项在 bge-m3 下只到 0.77–0.82,旧固定 0.85 候选门根本不会送审;新流程用跨类 0.80、同 category 再放宽 0.04 的 embedding 门叠加保守词面召回,并以连通分量替换首成员贪心聚类,修复 A≈B、B≈C 但 A≉C 时 C 永久落单。跨 category 的通用后缀不连边;dislikes 仍保持 0.85。no-merge 现在只切断已判 pair、不再遮住新邻居,并同时进入 prompt 与代码校验;策略版本升级会重审旧严格口径下的模型 keep,但用户显式 revert 的 pair 独立保护,旧状态可从 run snapshot + changelog 恢复。likes judge 改按“是否重复占用同一推荐/搜索意图”裁决,但父子兴趣仍分开。另修复真实运行日志已复现的失败闭环:`soul.consolidation` 因 provider cooldown 失败后,旧实现仍写 clean digest;现在任一 batch 失败、缺失或校验拒绝都会标记 `retry_pending` 且不写 clean digest,下一 due tick 会重试同一输入。真实隔离 apply/revert 还暴露出旧 run record 只保存 flat preference、回滚时会重建而非恢复原始 Soul 树;新记录现在额外保留完整 raw `soul.json`,先恢复 overrides 再精确恢复 Soul 和有效画像镜像,旧记录继续走兼容重建路径。同期真实 daemon 多轮 preference 写入又验证了 40–60 秒裁决窗口内确有并发更新;apply 现在落盘前校验 active / archived / dislikes 的完整 revision,冲突时不写画像、不写 run/state、下一 tick 立即重试,避免旧合并快照吃掉刚落入的新兴趣。 - **CI 的 MyPy 1.19 / Python 3.11 收窄规则兼容**:`evaluation_wire` 的 JSON 数字解码不再依赖在不同 Python target 下会被判为冗余或必要的 `cast()`,改用显式类型变量承接校验后的原值;row-wire 运行语义不变,同时消除 CI 的 `redundant-cast` 与本地的 `no-any-return` 分叉。 - **修复候选池在重复供给中数小时只补进一条**:候选评估器缺少 raw work 时不再只触发 B 站 refresh,而会按平台份额缺口立即并行唤醒全部已配置 producer;周期 tick 与即时 tick 使用 per-source lock,避免同平台重复抓取。补池结果新增真实 `supply_inserted_count / supply_progress_count / supply_productive` 契约,单纯跑过策略、搜索结果全部命中 durable duplicate 时不再冒充成功,而是进入 30/60/120/300/600 秒无产出退避;任一候选真正入队会立即清零阶梯。统一 admission 阈值和来源配额不变。 - **推荐文案预生成改为有界 copy-ready 水位,且三组合根语义一致**:新增 canonical `copy_ready` / `admitted_pending_copy` readiness 计数,正数 `scheduler.copy_ready_target_count` 只补当前文案缺口,避免每轮排空全部 durable backlog;API RuntimeContext、普通 CLI 与 OpenClaw 都按 `min(max(configured, 0), max(pool_target_count, 0))` 注入同一有效水位,`0` 继续作为 legacy drain-all 的显式回滚值。serve、feedback 与维护提交只发非阻塞 refill 通知,锁内再次核对缺口,已有文案不会因降低目标而删除。 - **候选 evaluator prefilter 补齐隐私安全 shadow 证据与 enforce gate**:默认模式仍为 `shadow`,每个决策只以 candidate hash、平台/上下文类别、相似度/阈值和 embedding/profile digest 落入 30 天 / 20,000 行有界审计表,LLM 返回后再连接原始 score 与统一 admission 判定;标题、URL、正文、prompt、画像文本和 provider response 均不持久化。provider / parse 失败时产品路径的 synthetic 0 不会回填审计,行保持 incomplete 让 coverage fail closed。required-interest 与实际 embedding 输入统一按权重取 top-256;无 embedding service 改用固定域分隔 digest namespace,因此 missing-service 行也可持久化并连接;`profile_interests_missing` 正式纳入 degraded gate。embedding 缺失/异常、向量错维/非有限值及 telemetry 写失败全部 fail-open,显式 `enforce` 在本批决策证据无法完整落库时也不剔除任何候选。只读脚本按冻结 audit id 计算至少 100 条、全局/平台 recall、保护候选 false negative、100% coverage 与 degraded fail-open 证据,报告不会自动开启 `enforce`。 - **修复三端“待聊确认”数字角标偶发不出现**:插件 popup 在启动探测离线后恢复、runtime stream 重连及面板重新可见时都会刷新;PC Web 把待聊数量提升为首屏高优先级请求,避免被推荐卡 saved-status 请求扇出挤进浏览器连接队列,并随实时事件与重连去抖更新;移动 Web 新增底部对话 Tab 数字角标,并在首屏、重连与实时事件后同步。浏览器工具栏角标继续只表示后端健康,不混入待聊计数。 - **修复小红书真实搜索的隐藏页与失效登录双重误诊**:更新后的工作区插件第一次回执显示 `document.hidden=true`、46 个普通 anchor 但 0 个 note anchor;切到前台后 `hidden=false` 仍为空,随后在同一真实浏览器手动提交搜索,页面明确弹出“登录后查看搜索结果”,证明残留 `web_session` Cookie 把失效会话误报成已登录,而非搜索频率直接触发风控。dispatcher 现在先记录当前活动标签,search 短暂以前台标签渲染并在结束后恢复原标签;executor 看到可见登录弹层会立即返回 `xhs_login_required`,后端再把这份真实页面证据写回登录态为 false。creator 保持后台,默认 20 分钟目标间隔(稳定 ±25% 抖动)、每日 20 次预算、队列积压门控和 `1h → 24h` 指数退避全部保留。 - **搜索路由与空结果诊断继续收口**:task、bootstrap、被动采集共用 `/explore/{id}`、`/discovery/item/{id}`、`/search_result/{id}` 三路由 selector;搜索 SPA 最多等待 12 秒。真实空结果仍只回传 pathname、可见性、viewport 与 anchor 计数,不上传搜索词、页面正文、链接、Cookie 或 state 内容。 - **修复首启 bge-m3 下载进度不实时更新(Issue #142)**:安装包在启动拉取线程前发布进程全局 running 状态,setup、桌面 Web 与 popup 在初始化前即可接管并持续轮询下载进度,慢速 Windows 下载不再只显示静态等待。 - **候选评估使用真实发布时间**:单条、批量和补分类路径统一携带来源 `published_at` 与真实 UTC `evaluated_at`,缓存同时绑定发布时间摘要和评估小时桶;缺失或无效发布时间保持中性,不再让模型按知识截止时间猜测当前日期。 - **发现关键词轮换与积压保护增强**:planner 避免重复消费同一关键词,XHS producer 按默认 20 分钟节奏检查,并在 pending + in-progress 搜索达到 5 条时停止 claim 与 LLM 生成。 - **发布版本与 AMO channel 冲突处理**:后端、桌面和首轮 GitHub 插件包发布为 `0.3.194`;该扩展标签的既有自动签名流程先在 AMO 占用了 unlisted `0.3.194`,AMO 因此拒绝同版本转 listed。商店补丁版本提升为 `0.3.195`,Chrome 用它替换刚提交的 `0.3.194` 审核包,Firefox 以全新 listed 版本重提;仓库变量 `FIREFOX_SIGNING_ENABLED` 同步关闭,今后 GitHub 扩展发布不再抢占正式 AMO listed 版本号。 - **PC Web 平台 Tab 集合在配置快照瞬断后自动恢复**:水合时 `/api/config` 与库存快照并行读取,偶发的连接重置此前会被静默吞掉——已启用但零库存的平台(如 Reddit)因此永久缺席筛选行,直到整页刷新。配置快照现在与平台库存一致采用有界重试(1s / 2s / 4s / 8s),成功后 Tab 并集收敛;E2E 桩改用 HTTP/1.0 短连接消除并行水合时的 keep-alive 复位竞态,并新增「配置两次失败后 Reddit 仍以 0 计数出现」的回归用例。 ## extension v0.3.193:Firefox AMO 公开商店提审(2026-08-03) - **修复首启 bge-m3 下载进度不实时更新(Issue #142)**:桌面 `/setup/`、`/web` 与 popup 在初始化前就会接管后台 embedding 拉取并持续轮询;安装包启动拉取前先发布进程全局 running 状态,慢速 Windows 下载无需先点击「开始初始化」即可看到实时进度。 - **PC Web「聊聊口味」补上持续可见的模型等待态**:消息刚提交时立即显示「阿B 正在思考,等待模型回复…」与三点动效;后端创建 durable `pending/processing` turn、历史刷新接管后继续按真实状态显示,不再因临时提示被刷新覆盖而只剩用户消息。回复完成或失败时等待气泡由终态原位替换;状态使用 polite live region、`aria-busy` 并遵守 reduced-motion。 - **修复小红书搜索任务整页 0 条的路由漂移**:2026-08-04 真实插件请求复现 `Python` 搜索 0 条且无风控命中,随后确认 Chrome 商店 0.3.192 的通用适配器已经识别 `/search_result/{note_id}`,但 task executor、被动采集和共享 note selector 仍只认 `/explore/{id}` / `/discovery/item/{id}`。三条采集链路现统一使用同一选择器与 URL parser,后端 observed-urls、内容页分类和原生保存身份校验同步接受第三种 note 路由;搜索 SPA 等待上限由 5 秒调整为 12 秒(仍低于 30 秒 dispatcher timeout)。真实空结果会写 `xhs_empty_result`,并只回传 pathname、生命周期标志和各路由 anchor 数量,不包含搜索词、标题、正文、href、Cookie 或页面 state。真机对照确认在测试浏览器实际运行的是商店包 0.3.192;工作区 0.3.193 构建与回归已通过,修复版真实请求须先把该浏览器更新到新包,`chrome.runtime.reload()` 只会重启当前商店包、不会加载工作区 `/dist`。 - **Firefox 首次公开上架不再复用 unlisted 签名链路**:新增手动 `Submit Firefox AMO Listed Package` workflow,以全局唯一的扩展版本构建 `dist-firefox/`,携带双语名称、摘要、描述、MIT license、合法 Firefox / Android 分类和审核说明提交 `web-ext sign --channel=listed`;0.3.192 已作为 unlisted 版本存在,故公开提审使用独立扩展版本 0.3.193,后端与桌面版本仍保持 0.3.192。 - **审核所需源码与隐私资料和提交包同源**:workflow 从同一 Git commit 打包 `extension/`、共享 Web 模块、lockfile、构建说明和 `docs/privacy.md`,主动通过 `--upload-source-code` 附上可复现源码;提交后查询版本列表并要求 0.3.193 的 channel 真实为 `listed`,避免把上传成功误报成已提审。 - **AMO 隐私字段故障不再反向阻断版本提审**:连续真实请求证明 `eula_policy` PATCH 无论补齐与 `web-ext` 一致的 JSON headers、使用 Gecko GUID 还是 canonical 数字 add-on ID,当前 developer JWT 都只收到无正文 HTTP 406;该非必需字段因此移动到 listed 版本已受理并核验之后 best-effort 同步,失败会在 workflow 留下显式 warning 和 Developer Hub 手动回填指引。manifest 数据类别、reviewer notes、双语 listing 描述、随 reviewer source 附带的 `docs/privacy.md` 仍随提审送达,不会把隐私披露静默省略。 ## v0.3.192:多模态推荐、可靠反馈与连接增强(2026-08-03) ### 扩展在线账号信号刷新 - **五个浏览器账号来源支持周期增量回拉**:画像就绪且 installed extension 在线时,runtime 默认每 24 小时按持久 round-robin 复用小红书、抖音、YouTube、知乎和 Reddit 的既有 bootstrap scope;五源任务全局串行,扩展离线、guided init 活跃、来源/调度关闭或周期为 0 时零入队、零时间推进。全局与逐源 `0..168` 小时配置支持热重载和逐源 `null` 继承;成功 init 会种下最近 attempt,失败留给后续在线 tick 自愈。 - **五源 task-result 统一崩溃恢复和有界去重**:Reddit 加入 XHS / 抖音 / YouTube / 知乎的 first-final-wins staged 协议,按 canonical result → durable event ingress → 原子 seen-key checkpoint → terminal flip 执行;任一窗口失败都由租约重领从首写修复。五源 seen key 按真实响应顺序各保留最新 5,000 个,Reddit post/comment/subreddit/user 使用分型稳定身份,重复周期回拉不会重复写事件。 - **独立审查收紧并发与初始化边界**:runtime / CLI / guided init 共用 SQLite `BEGIN IMMEDIATE` admission transaction,五表 active scan 与 insert 在跨 facade / 进程场景也保持单飞,`force` 不绕过;崩溃窗口收养同步任务创建时间与 cursor,非法无时区 / 未来时间戳按到期自愈。`POST /api/init` 在来源 opt-in 热重载前先持久预定 run,新旧 controller 都看得到 init fence;Reddit 扩展与后端同时拒绝把 parent post ID、短 post URL 或仅标题社区行误作 comment/community identity。 ### 用户日志暴露的推荐空窗与后台振荡修复 - **候选评估使用真实发布时间**:单条、批量及推荐池补分类 evaluator 都携带来源已有的 `published_at`,并用精确 UTC `evaluated_at` 作为热点、时事和版本更新等时效性判断的权威基准;模型不再根据自身知识截止时间推测当前日期,缺失或无效时间保持中性。单条 / 批量评分缓存同时绑定发布时间摘要与独立评估小时桶,来源后补时间或 daemon 跨小时后不会继续复用旧分数。 - **避雷画像不再把推荐永久杀空**:正常情况下仍用 `disliked_topics` 对结构化 topic 与标题/简介/作者/标签/正文做即时出口过滤;只有模糊子串规则将整个 serve 窗口过滤为零时,才对该窗口降级为 `topic_key/topic_group/pool_topic_label` 精确硬禁用并记录诊断,显式类别避雷不恢复。 - **候选池维护不再恢复/裁剪振荡**:suppressed 恢复受 raw headroom 限制,raw 已满或超限时先裁剪;protected/token-owned excess 已无 victim 时返回 `has_more=False` 并把原 ERROR 风暴降为稳定 WARNING。用户日志中的 AB raw 状态不再每 tick 反复切换。 - **错误模型路由快速失败**:OpenAI-compatible 的 400/403/404/405/422 不再做三次无效 provider 重试;`404 model route not found` 保留完整原因交给 fallback/配置诊断,5xx、timeout 与传输错误继续按原策略重试。 - **稳定画像不再回放同一批搜索词**:关键词 generation cache key 纳入实时 `recent_keywords`,写入前再按大小写/空白/标点做近期词硬去重;候选预过滤若发现某个 `source_keyword_id` 的返回结果全部已存在,会立即把该词退役并保留冷却历史。`recycle_oldest_used` 只允许超过 `history_window_hours` 的历史有效词兜底,避免画像 digest 不变时在 `plan_ttl_hours` 内反复搜索相同内容。 - **灵感词按真实产出轮换并拦截换皮/错归因**:`materialize_platform_keywords()` 先覆盖每个选中兴趣再给同兴趣补第二轴,避免少量兴趣抢完平台配额;selection ledger 延后到装配与近期过滤之后,只记录实际留下关键词的兴趣。输出若冒用未选兴趣、或 core 明确命中另一个画像兴趣会被拒绝;只给旧 query 增加「复盘 / 解析 / 教程 / 盘点 / 测评」等尾缀也归入同一冷却 family。 ### 小红书访问节奏与风控背压 - **小红书主动搜索改用保守默认节奏,并补齐四层背压**:新配置或缺键配置的 `daily_search_budget / task_interval_seconds / min_interval_minutes` 从 `0 / 300 / 3` 调整为 `20 / 1200 / 20`;20 分钟是目标值,legacy search / creator 每次 claim 按任务 ID 施加稳定 ±25% 抖动(约 15–25 分钟)并把实际 `next_claim_at` 落 SQLite,避免后端、MV3 或多浏览器 profile 重启绕过,也避免固定周期访问。`XhsTaskProducer` 在 pending + in-progress search 达 5 条时直接返回 `backlog`,不 claim planner 词、不调用 LLM,只按空位补队列;默认每日 20 次与该积压门共同限制高缺口时的持续搜索。风险回调新增持久 `rate_limit_strikes`,独立风控轮次按 `1h → 2h → 4h → 8h → 16h → 24h` 指数退避并封顶;同一活动冷却内重复报告不加轮次,冷却后的正常 search / creator 成功才重置,晚到成功不能取消其它任务刚打开的冷却。状态 API 同步显示连续轮次与剩余时间。插件、桌面 Web、CLI、配置样例、模块文档和架构图已统一默认值;**这些数值是工程安全起点,不是小红书官方阈值,已有显式 `config.toml` 值不迁移、不覆盖**。 ### LLM token diet 与 sparse evaluator landing - **Cognition compact 按真实任务门分拆发布,只默认启用 Awareness-with-confusions**:2026-08-06 SenseTime A/A + A/B 实际覆盖 `build_awareness_with_confusions_prompt` / `soul.awareness_confusions`,不代表普通 `AwarenessAnalyzer.analyze()` 已通过。未发布的 `soul.cognition_prompt_view` 删除,改为 `preference_prompt_view / awareness_prompt_view / insight_prompt_view` 三个独立 `legacy|compact-v1` 字段,默认 `legacy / compact-v1 / legacy`;`awareness_prompt_view` 只进入 with-confusions seam,普通 `soul.awareness` 固定 `legacy`。Config load/save/校验、配置 API、RuntimeContext、CLI 与 OpenClaw 全链路逐项透传。Replay artifact 新增机器可读 `task_rollout`,任务键明确为 `preference / awareness_confusions / insight`,每项独立报告 route、token、quality、blocking reasons 与最终 selected view;`awareness_confusions` 可在全局 full-compact gate 仍失败时单独 enabled,Preference/Insight 保持 legacy,且 Insight 在没有预声明 token 阈值时 fail-closed。 - **新增 evaluator 候选字段瘦身与行协议的两阶段 replay-only 验证链**:`sparse-json` 先保持画像、负例、评分输出和 batch runtime 不变,只把每条候选改成 canonical sparse envelope,使用请求内 `0..N-1` ID,删除 URL、全局 ID、作者别名、逐条来源上下文、平台指标别名及所有空/零可选字段;`row-wire-v1` 再以完全相同的 sparse semantics 为 A,只把候选块改成带固定表头和严格 `\\ / \t / \r / \n` 转义的 22 列行协议。多成员结果禁止 positional fallback,未知/重复/缺失 local ID 进入既有 member repair;封面文字锚点与 image input 同步改用 local ID,图片 bytes/MIME/order 不变。回放 artifact 升为 schema v4,prompt/candidate/image/identity/path 摘要均使用运行级随机盐;provider cache 只作诊断,不成为跨模型正确性前提。相同 100 条静态候选块从 production pretty JSON `114861` 字符降至 sparse JSON `39836`(`-65.32%`)和 row wire `30280`(`-73.64%`,较 sparse 再降 `23.99%`);这些是上线前字符代理,随后继续由两个独立 100×3 真实 usage/质量门决定是否切换。 - **真实 100×3 门保留 sparse 证据但否决 row-wire 上线**:`sparse-json` 三轮相对 production 的 prompt-token 节省为 `17.12% / 29.19% / 27.99%`(中位 `27.99%`),total-token 节省为 `10.68% / 24.32% / 24.05%`(中位 `24.05%`),相对质量、分类、repair、route、embedding、recall、usage 与隐私门通过。原 auditor 曾把 A/A 控制响应偶发漏一个 `reason` 错归因为 sparse 漂移;`f644bbe9` 将契约归因限定到 changed-transport B,immutable calls 离线复算后无 blocker,artifact SHA-256 为 `f183fce2a98ac9e0edf188c8e741b60ec78652df42b2762735ac31b2507b23f7`。随后 `row-wire-v1` 相对 sparse 的 prompt 中位只省 `2.20%`(三轮 `-4.67% / 2.20% / 9.99%`),低于锁定 `5%`;total 中位虽省 `2.63%`,但 style/franchise agreement 同时低于 A/A floor,且一条 A-arm sparse root call 缺 billable usage,故严格失败。两个 artifacts 各自对 692 个源候选敏感值扫描均为零命中;该对比阶段不调阈值、不切 production、不改 eval-cache namespace 或 `CLAUDE.md`,row 失败 artifact SHA-256 为 `8fc7065df93e2d82e7cd3647b3e0245f9462971ca0e091c823babcfed8b573e0`。 - **通过门的 sparse-json 单独落地为生产 batch evaluator 默认**:后续 landing spec 不改变任何已观察门槛,只采用已通过的字段裁剪/local-ID 臂;`ContentDiscoveryEngine` 默认改为 canonical compact sparse JSON,eval cache namespace 升至 `content-eval-v3`,未发送给模型的 URL/BVID/content_id 不再造成 false miss,保留正文、权威 `published_at`、指标、mode、画像、负例、embedding 与 prefilter 失效边界;顶层精确 UTC `evaluated_at` 和缓存小时桶继续支持时效性判断与跨小时重评。多成员 response 继续严格绑定 request-local `id`,多模态 text/image anchor 同步本地化且图片 bytes/MIME/order 不变。显式 `evaluation_candidate_transport="production"` 保留 pretty-JSON/global-ID candidate rollback 并共享当前时间语义;`row-wire-v1` 因冻结协议不能承载 `published_at` 而明确拒绝,仍 replay-only。API/CLI/OpenClaw 与 8 个策略构造点均继承 sparse,无新增 CLI/config 或架构 wiring。 - **evaluator `json-minify` 单变量真实回放已否决上线**:replay B 仅把 batch evaluator 的 profile layers、negative examples 与 content batch 改为确定性 compact JSON,字段、值、system/output/runtime 均不变。首轮因发现 OpenAI-compatible `json_object` 空内容后的成功重试 usage 会被最终响应覆盖而主动中止,不作证据;harness 随后升级为逐 wire attempt 归因、累加并 fail closed。干净提交 `ad4ba670` 的正式 100 条 × 3 轮耗时 `6227.3s`,46 次格式 fallback 全部计费、3 次瞬时限流均恢复。B 的 prompt 字符/字节减少 `25.05% / 20.81%`;按实际 attempts 的配对中位 prompt/total token 节省 `13.57% / 11.29%`,三轮总计节省 `15.72% / 13.16%`,repair 与 topic/style/franchise 分类门通过。但 admission delta 为 `-6pp / -6pp / +4pp`,中位 `-6pp` 低于 A/A 推导的 `-4pp` floor;B 在两个干净重复中的 provider cache ratio 也分别下降 `32.20pp / 31.98pp`,另有一次 A 限流错误使完整 usage/cache 证据门失败。故不启用通用 `json-minify`:profile/negative 等仍保持原渲染,已通过独立质量门的 sparse candidate block 则单独使用 canonical compact JSON;隐私扫描确认 artifact 不含 100 条候选的标题、正文、原始 ID/URL,失败 artifact SHA-256 为 `873d90c9d46c6b45465201a883044d49d921d54eaa18878e012f377a87c2c8c9`。 - **新增 evaluator `reason-off` 独立真实回放臂,并由实测否决生产关闭**:在不改变生产默认 prompt 的前提下,`scripts/run_profile_diet_ab.py --arm-b reason-off` 以当前生产 reason-diet 为 A、完全省略成功评估结果中的 `reason` 字段为 B;两臂继续使用同一冻结画像、候选、recall、route 与重复 A/A 噪声包络。`c6327506` 上的真实 100 条 × 3 轮回放确认 B 的 reason 输出为 0,但相对 A 的 prompt / completion / total token 分别增加 `30.89% / 38.88% / 31.70%`;B 每轮还多触发 3 / 0 / 1 次成功 member-repair 调用,即使排除有 provider 错误的首轮,两个干净重复的 total token 仍增加 `7.18% / 25.97%`。排序相关性中位数 `0.709700`、admission flip 中位数 `5%` 和三个分类字段 gate 均通过,但准入差值中位数为 `-5pp`,低于 A/A 噪声导出的 `+2pp` floor,因此总质量门失败。结论是保留当前生产 reason diet,不把完全关闭 reason 上线;失败 artifact 仅留本地诊断,SHA-256 为 `f6b7276fc5024faa3a61c2524124bf43644044029fb6c93490a50ea30aaa81b2`。故障哨兵 `evaluation_response_missing` 不属于自然语言 reason,实验不会清除;下游 cap-drop 尚未在 replay 内复刻并会显式标为未测。 - **Phase 3 认知请求按真实调用形状减肥,不裁持久数据**:Preference 自动预算回退从“完整请求按事件比例估算”改为从每个剩余 offset 渲染独立 chunk prompt、二分寻找当前最大可装前缀;这既避免旧 preference 被重复计入,也避免后段局部超长事件把相邻小事件拆成额外调用,显式 chunk size 与合并语义不变。Insight 每轮只把最新 20 条 + 最新 20 条 judged/validated 假设送进模型,去重后最多 40 条,但继续与完整 `insight.json` 合并并保存。2026-08-06 生产只读冻结渲染中,3 条偏好事件由 2 calls/17133 chars 收口为 1 call/9537 chars(`44.34%`),441 条洞察历史由 119679 收口到 68885 chars(`42.44%`);这些是 tokenizer/provider 无关的输入字符结果,真实 billable usage 另由固定路由门验证。 - **画像 digest 抖动不再默认烧掉尚未使用的安全关键词**:新增 `[discovery].keyword_digest_grace_hours=24`(`0..168`,`0` 独立回滚)。planner 在 due 计算前按平台原子整理 regular pending:当前 digest 优先,旧 digest 只保留宽限内、未命中显式 dislike/平台 avoid、未重复且不超过动态高水位的词;保留行不改 digest 或 inspiration/axis/interest 溯源,claimed/executing/terminal/explore 不动。整理后用 all-digest pending 算水位,并把 retained pending 纳入 history 防同族再生成;能力缺失或事务失败自动硬过期回退。生产只读快照近 24 小时可回收候选为 240 条(B站 210、抖音 30),实际可保留量仍受避雷与 cap 决定。 - **Phase 3 固定日日新真实门最终通过**:`scripts/replay_token_diet_phase3.py` 支持 render-only 与固定 SenseTime 单实例 A/A+B,真实请求固定 temperature 0、单并发错峰、禁跨 provider fallback,并校验 route/usage/结构质量/JSON 修复率/重复假设/完整历史 merge;可选关键词 E2E 只把近期过期词复制到 disposable SQLite,贯通 planner→claim→真实 B站搜索→评估→cache→yield,不写生产库。artifact 禁止事件、画像、prompt、响应、URL、Cookie 和凭据。首次额度类 429 被如实保留为失败诊断;额度恢复后,同一冻结快照在 `openai_compatible/deepseek-v4-flash` 上完成正式重跑:Preference prompt `7211→3986`(`-44.72%`)、total `11103→6500`(`-41.46%`)、调用 `2→1`;Insight prompt `47321→25571`(`-45.96%`)、total `49680→29135`(`-41.35%`),虽 completion `2359→3564`,输入收益仍覆盖增长。兴趣 weighted overlap 为 `1.0`,creator 丢失/幻觉、JSON repair、洞察重复均为 `0`,完整 441 条历史 merge 后为 442 条。关键词 control 为 `1 call / 9098 tokens`,24h grace 为 `0 call / 0 tokens`(`-100%`),复用 30 条后真实 B站搜索得到 15 raw / 13 unique,12 条经模型评估、7 条入池,3 个 claimed keyword 全部 used 且 yield 正确归因。关键词评估出现两次同 provider 的 `response_format` 空正文 wire retry,但没有跨 provider fallback,且不参与 planner `9098→0` 的节省计算;最终总 gate PASS。 - **洞察 prompt 从固定尾窗升级为“保底 + 加权 + 同状态归并”**:持久 `insight.json` 仍完整保留,模型可见视图最多 40 条;latest-8、judged/validated-8 先保底,current awareness/profile 相关性最多 16 条,质量/裁决/重复支持/多样性再取 8 条并补位。NFKC + 英文词/CJK bigram 的纯本地打分不依赖模型或 tokenizer;同 confirmed/rejected/unjudged 状态的近重复只竞争一个 prompt 槽,不同状态分别保留,异常时退回 Phase 3 fixed-20+20。442 条只读生产快照选中 40 条,其中 24 条位于旧固定窗口外、最近 8 条全保留、同状态近重复为 0;render 字符相对完整历史少 `39.44%`,相对固定窗口多 `3.27%`。固定 SenseTime `deepseek-v4-flash` 的最终 A/A+B 门中,weighted prompt `27725` 相对 full `48523` 少 `42.86%`,相对 fixed `26724` 多 `3.75%`;B 为严格 schema、0 repair、9/9 结构有效、0 重复,confidence/evidence 漂移 `0.019/0.139` 均在 A/A 的 `0.112/1.5` 噪声内,完整 442 条 merge 后 444 条。此前一次 fixed A1 返回非数组、另一次 A/B evidence 漂移超门均作为失败诊断保留,没有放宽阈值;passing 隐私安全 artifact SHA-256 为 `932c5d955b7449b88065e8a5aec408966e40e0c02c2fd8ee506ff11b68e75932`。 - **真实 replay gate 升级为 fail-closed artifact v3**:`scripts/run_profile_diet_ab.py` 现在加载与生产一致的 effective profile(user overrides + active speculations)并镜像 archived-topic serialization;按生产 30 条 claim grouping、`source_context=mixed` 和 4096 output ceiling 跑 repeated A/A + A/B。每个 logical run 归因并校验实际 provider / instance / model;embedding exception、空 / NaN / Inf / 维度漂移、recall 不完整及 route / snapshot 漂移都会进入最终 blocking reasons 并非零退出。单个 chunk 遇到明确的瞬时 rate limit 时会按 65 / 130 / 260 / 520 秒指数冷却最多重试四次,重试前恢复候选评估字段;402/余额/计费错误与其它失败不重试,恢复的限流调用仍保留在 route artifact 中,完整 schedule 也写入 artifact。compact / reason-diet / reason-off / json-minify 保留臂都可生成不含正文、完整画像、原始内容/图片 ID 或 secret 的独立 JSON;temperature 固定 0 以隔离采样噪声,同时在 artifact 披露生产默认 0.7。2026-07-18 的旧 replay PASS 继续作废。 - **Replay 限流分类尊重 provider 规范化边界**:真实 compact 验收捕获到 transient HTTP 429 被误判为永久额度问题;根因是 replay 在看到 adapter 已归一化的 `LLMRateLimitError("rate limit exceeded")` 后仍继续扫描 SDK raw cause,而部分网关的原始 429 元数据含通用 `billing` 字段。分类现在止于第一个规范化限流异常:明确映射的 429 / cooldown 可按每 chunk 独立预算恢复,明确映射为 HTTP 402、余额不足或计费错误的异常仍立即失败。最终 clean-commit 回放又证明两次预算不足以覆盖网关“空响应 → 去格式约束 → 关闭 thinking”链中的持续节流,因此只扩展 replay 的有界指数冷却,不改变生产调用或质量门;测试覆盖误导性 raw cause、跨 chunk 独立预算、四次持续 429 后恢复,以及永久额度错误零重试。 - **真实质量门连续校准 compact 边界,48 / 16 进入收益与质量实测**:64 / 12 和画像增长后的 80 / 16 都被严格 100×3 replay 否决;96 / 16 虽完整保留当时 87 项兴趣,但 clean commit `f26c63e5` 的 replay 仍以 treatment flip-rate 中位数 `18% > 16%` control ceiling 失败(Spearman `0.616499` 与 admission `+10pp` 通过)。更关键的是 provider usage 显示 96 / 16 每个标准请求只少 174 个输入 token,100 条 / 4 batch 仅省 696 token(`0.67%`);实际含坏输出补救的 treatment 反而从 14 增到 17 次调用、total token 多 `14.75%`。因此不把 96 / 16 当 landing 收益,而按明确要求实验性改为 48 兴趣 / 每域 16 specifics、tail recall ranks 49..256。当前 93 项画像的分层 profile block 确定性缩短 `26.02%`,极限 fixture 缩短 `67.82%`;同一真实 100 条候选的完整 prompt 对拍在每条注入 3 个长尾标签后,字符减少 `10.14%`,`cl100k_base` 诊断 tokenizer 估算输入 token 减少 `8308 / 124141 = 6.69%`。不做 recall 的毛节省为 `9.33%`,即 recall 为质量换回约 `2.64pp`。这些只证明收益,最终是否保留仍由未放宽的真实 100×3 replay 决定。失败 artifacts `profile-diet-compact-failed-397fe03e.json` 与 `profile-diet-compact-failed-f26c63e5.json` 均只作诊断证据。 - **真实 Reddit 门否决正文 200+100 截断并完整回滚**:100 条长正文候选 × 3 组 repeated A/A + A/B 中,treatment flip-rate 中位数 `18%` 高于 control ceiling `8%`,Spearman 中位数 `0.192031` 低于 floor `0.632378`,admission delta 中位数 `-11pp` 低于 floor `-3pp`;42 条候选实际受截断影响,正文仅保留 `12.95%` 字符。该结果不是可调阈值的小幅噪声,因此 discovery single/batch eval、recommendation legacy/recovery 分类及 single/batch expression 全部恢复完整 `body_text`;replay 工具同步删除 `body-cap` 正式臂。失败 artifact `data/eval/profile-diet-body-cap-rejected-11f77a64.json` 只作不含正文的诊断证据,不作为 landing PASS。 - **Eval cache v3 形成可恢复的输入闭包**:single / batch key 覆盖实际 prompt-visible content/context(含 `published_at`)、compact profile + 精确 tail-recall pool、negative-examples 内容 digest、embedding namespace、prefilter mode、UTC 评估小时桶与 schema version;正文、指标、来源上下文、发布时间、跨小时、长尾权重或模型 namespace 变化都会 miss。临时 / 部分 embedding recall 失败不写 normal cache;混合平台、不稳定隐式 context 和 vision attempt 整批绕过 per-item normal cache。cache 保存 raw per-item result,franchise/style cap 在 caller grouping 上重放,包含 enforce-prefilter 边界的 cold/warm 结果一致;空 metadata 会明确清除对象旧值。 - **Eval reason 从 prompt 建议升级为 runtime 契约**:single、batch 和 cache hit 共用 `normalize_evaluation_reason()`;`score < 0.5` 与 diversity-cap drop 强制空 reason,`score >= 0.5` strip 后最多 30 个 Unicode code points,`None` 归一为空,其它非字符串 fail closed 并进入 bounded member retry。Prompt 同步标明 reason 仅供内部诊断,不是推荐文案。 - **Rebase 后画像序列化 owner 保持单一**:topic lifecycle archived filtering 与进程开关的 canonical owner 是 `soul/profile_views.py`,API / CLI / replay 共享;`discovery/strategies/_utils.py` 仅保留兼容 re-export,避免 current-main lifecycle 语义在 serializer 搬迁冲突中丢失。 ### 多模态推荐与高级配置 - **完整视觉 embedding pipeline**:视觉画像(P1)与关键帧(P3)使用带 cross-clean、contested 区和冷启动门控的 margin 几何评分;关键帧单独开启时也会构建质心,P1 cover bonus 仍由 `visual_profile_enabled` 控制。 - **失败可重试且缓存可追溯**:关键帧、弹幕和 embedding 的瞬时失败不再写永久完成戳;成功空结果与失败可区分,质心、关键帧、弹幕状态绑定 embedding fingerprint、维度和采样签名,模型切换会安全重建或重嵌入。 - **跨平台公平保持零点与符号**:正负 bonus 以 0 为固定点按平台分段对齐,多平台只对齐到当前观测到的全局侧最大值并受组合 cap 限制,单平台不放大绝对幅度,弱正向不会变成负惩罚;关键帧/弹幕预热范围、fetch limit 和完整摘要长度均遵循当前候选池与配置。 - **视觉结果不永久吞失败**:partial keyframe 结果携带 stable sampled-slot,成功槽位先入缓存但不落完成戳;只有 confirmed no-data 或完整采样且所有 embedding 成功才完成。Embedding provenance 同时隔离规范化 endpoint 的 fingerprint 与 L2 namespace;HTTP 200 的 HTML danmaku challenge 作为 transient failure 重试。 - **配置与离线一致性**:`keyframe_max_frames`、`keyframe_fetch_limit`、`danmaku_fetch_limit`、`danmaku_max_chars` 由文件和 `PUT /api/config` 共同校验并 round-trip;`scripts/ab_visual_bonus.py` 与生产评分共享 signed suppression 和零点保持归一化。 - **弹幕摘要严格遵守字符预算**:`condense_danmaku` 将 ` | ` 分隔符计入 `danmaku_max_chars`,保留完整弹幕,单条超限跳过,避免摘要实际长度超过配置预算。 - **桌面 Web 与插件设置页新增统一的「高级功能」Tab**:桌面端 7 个 Tab、插件端 6 个 Tab 都固定提供「推荐增强 / 多模态处理 / 搜索词生成」三个 section;P1/P2/P3 依赖关系、关闭无副作用、七个 discovery 字段的 round-trip 和搜索词三档 option 契约保持一致,调度 Tab 只保留真正的调度项。 - **设置页保存按钮只在有改动时启用**:桌面 Web 与插件 side panel 在配置无变化或保存请求进行中都会禁用保存,避免无操作的完整 `PUT /api/config` 和无意义热重载;输入、LLM 实例/调用链草稿及候选池建议比例等程序化修改都会进入脏状态。保存成功后按钮重新禁用,失败则保留改动并允许重试。 - **配置保存不再被长对话拖到 60 秒超时**:`PUT /api/config` 仍先校验、快照并落盘;检测到对话、结算或事件 owner 正忙时改为立即返回 `202 apply_state="queued"`,由 app-owned latest-wins 队列在后台安全 drain 后热重载。连续保存只保留最新待应用修订,旧修订失败不会覆盖新文件;最终成功/失败通过 `config_reloaded` / `config_reload_failed` 推送,并可由 `GET /api/config/apply-status` 回读。最新修订失败会恢复最后一次已生效配置,初始化在应用完成前返回 `409 config_applying`,插件与桌面 Web 分别展示“已排队”及最终失败回执。 - **插件配置保存栏固定在视口底部**:side panel 不再把位于长表单末尾的 sticky 保存栏留在页面底部;保存状态和按钮始终固定在扩展视口底边,滚动容器同步预留安全区与操作栏高度,最后一项配置仍可完整滚到按钮上方,不会被遮挡。 - **高级功能默认值统一**:新配置的搜索词生成默认使用“混合”(经典 merged planner + search-backed 灵感轴),桌面 Web、插件和 API 缺省回退同步为 `hybrid`;图像 Embedding、候选封面 LLM 评估、P1 视觉画像和 P3 关键帧继续全部默认关闭,已有显式配置不被覆盖。 - **补充 PR #135 贡献者致谢**:README 中英文与贡献指南正式记录 [@wuwafly3](https://github.com/wuwafly3) 对用户视觉画像(P1)、B 站弹幕语义(P2)、视频关键帧(P3)和跨平台视觉加权管线的贡献,并保留其此前在 [#100](https://github.com/whiteguo233/OpenBiliClaw/pull/100) 中完成的 DashScope 多模态 embedding 与封面 image-only 向量归属。 - **原生保存批任务顺序固定为快照顺序**:显式 `item_keys` 的 caller order 继续写入 `native_save_task_items.ordinal`,执行查询也改为按该 ordinal 读取;不再受秒级 `saved_memberships.added_at` 影响,避免同一批次在不同机器上随机先处理后加入的项目,并让 heartbeat 失败时的取消/释放行为保持确定。 ### 连接、部署与界面 - **README 与 GitHub Pages 首页恢复真实使用截图**:推荐、反馈、画像、对话、桌面端、移动端及首屏 Hero 全部换回真实运行记录,不再展示 Chrome Web Store 演示夹具生成的虚构内容;演示素材抓取脚本同时移除 `--refresh-docs` 入口,并拒绝把 demo capture 写入 `docs/images/`,避免再次覆盖真实截图。 - **对话 turn 绑定安全性收口**:三端把卡片/疑惑作为 durable turn,通过 `reply_to_turn_id` 只声明目标;服务端在 user INSERT 前冻结 canonical context、digest 和 ordinary/detached mode,统一贯穿 prompt、event、学习与结算,A→B replacement 只会安全 stale-drop。新增只读 context preview、opaque evidence 过滤、独立长列表滚动、reply quote 与失败草稿保留;恢复长历史时首次进入会落到最新消息,后续实时刷新仍保留阅读位置。真实三端 E2E 补齐结算态收尾:卡片已确认/修正/拒绝/稍后时会静默清除旧 context,已知服务端错误优先显示中文;移动端固定回顶按钮按 360px 窄屏重新留距,不再与发送按钮相交。 - **修复 GitHub Star 数量请求偶发 403**:桌面 Web 和扩展不再从浏览器匿名直连 GitHub REST API,统一改走公开同源 `GET /api/project-stats`;后端持久化 12 小时缓存、使用 ETag 条件请求,并在 403 / 429、断网或 GitHub 异常时按响应头退避、返回旧缓存或无数量的本地 200。GitHub Pages 静态官网没有可用的同源后端,因此保留 Star CTA、停止动态请求数量;所有浏览器入口都不再产生 GitHub 失败资源日志,点击仓库行为保持不变。 - **桌面惊喜推荐把“× / 看过了,不再推荐”移到卡片右上角**:关闭动作不再夹在喜欢、不感兴趣、稍后看和收藏之间,避免被误认成同级反馈或误触;按钮保留原有永久已读语义、可见键盘焦点与禁用态,窄屏触控区域不小于 44×44。 - **修复 X「测试连接」只读旧健康记录的问题**:设置页现在通过 `twitter-cli` 的只读账户状态请求即时验证 `auth_token` / `ct0`,401、403、429 和传输失败分别保持失败或待判定语义,不把网络故障误报成 Cookie 失效。 - **修复后端启动后 X Cookie 同步滞后**:启用 X 时,扩展每次新建 `/api/runtime-stream` 连接都会收到 `x_cookie_sync_requested`,立即把当前浏览器 Cookie 回传;原有启动、变更监听和小时 alarm 继续作为兜底。 - **移除扩展临时调试日志中继**:抖音任务仍通过正常的 `task-result` 回传结构化诊断,但不再向 `/api/sources/_debug/log` 额外发起请求;废弃 helper 和后端 relay 路由同步删除。 - **新增最简公网 HTTPS Compose overlay**:`docker-compose.https.yml` 可叠加到源码或预构建部署,使用固定版本 Caddy 自动申请 / 续期公网证书并转发 REST / WebSocket;Caddy 与后端共享 network namespace,宿主机 `8420` 收紧到 loopback,Uvicorn 仅信任 `127.0.0.1` 的 forwarded headers,证书数据使用 named volumes 持久化。Caddy 在 `/api/auth/status` 明确返回密码门禁已启用前不绑定公网端口,首次配置 fail closed;远程扩展仍使用独立设备密钥,现有 LAN/self-managed `tls` profile 保持独立、默认关闭。 - **修复桌面 Web 的 HTTPS 手机二维码降级**:从非 loopback 的 HTTPS 页面打开「手机版」时,二维码现在保留当前 scheme、host 和端口,不再被 `/api/qr-info` 的后端私网 IP 覆盖或硬编码成 HTTP;IPv4、裸 IPv6 与 URL 方括号 IPv6 loopback 都继续实时探测 LAN IP,插件和桌面局域网扫码行为保持一致。 ### 反馈、对话与事件可靠性 - **事件幂等 ID 采用严格字符串类型**:三个公开事件入口不会把数字、布尔或其它 JSON 类型自动 转成字符串;这类输入与缺失、空白、超长值一样在 route 前 422 且零写入。 - **修复事件消费与对话结算并发时 SQLite 抛出 `another row available`**:真实隔离运行中 chat reply 已完成,dialogue settlement sequence=1 却在锚释放更新 durable turn 时失败;根因是 `check_same_thread=False` 只取消线程归属检查,并不允许 status/event reader 与 settlement writer 同时 step 同一 connection。`Database` facade 现在保留初始化线程 primary,并为其它调用线程缓存独立 WAL connection;普通 facade 调用在主线程/worker 都保持既有 foreign-key PRAGMA,显式 `open_connection()` 的原子短事务继续单独开启约束。并发写由 SQLite `busy_timeout` 协调,不增加会阻塞 event loop 的全局 mutex;通用 write helper 在任何 `OperationalError` 后先 rollback,再 lock retry 或抛出。全仓验收随后暴露同一边界下的隐性事务泄漏:XHS token backfill 的两个 UPDATE 若都命中零行,SQLite 仍开启隐式写事务,旧代码却因 `updated==0` 跳过 commit,让已结束的 request-thread connection 永久占 writer lock;现在每条 best-effort direct DML 成功 execute 后无条件 commit、异常 rollback,self-info purge 异常路径也清理事务。回归覆盖 active reader + settlement writer、跨线程 recommendation 兼容语义,以及 zero-row request 返回后主线程立即写入;受 actual asyncio Task identity 保护的锚 mutation 仍只在唯一 settlement worker 内执行,不向线程 child 扩权。 - **聊天回复不再因请求超时永久失败或被并发历史串线**:`POST /api/chat/turns` 只落 pending + wake 后立即返回;app-owned durable reply scheduler 在启动时分页恢复全部 pending,并以单 worker 严格按 SQLite rowid 回复。provider、限流、配置、超时和 shutdown cancellation 保持 pending、原位有界退避,只有显式空/无效响应才 failed;completed/failed 均用 pending CAS,近崩溃模型调用可至少一次但可见终态只发布一次。惊喜、legacy chat、兴趣与避雷探针也进入同一个 app-stable max-active=1 dialogue lease,回复及必要副作用完成前不释放;热重载先停 admission、排空旧 owner,再发布新 runtime,等待请求恢复后动态解析新 dialogue/speculator,25 分钟超时则零 rebuild、恢复旧 lane。`/api/runtime-status` 新增真实 durable depth/active/error/processed,降级态仍可见且不泄漏消息内容。 - **修复连点点赞 / 点踩 / 聊一聊时 feedback 请求卡死或超时**:`/api/feedback` 过去在响应内同步等待统一兴趣线的偏好 LLM(真实一轮 30–76 秒),桌面端 30 秒先回滚后,后端仍可能成功,刷新又显示已写入;连续点击还会排出并发分析。现在成功边界明确为 event-first 的两次 commit:先按 `request_id` 提交 `events.feedback`,再用下一次独立 commit 更新 recommendation 展示投影;二者不是跨表原子,第二步失败会让请求失败,同 `request_id` 重试命中 duplicate event、校验 durable payload 后补做投影,冲突 payload 返回 409。HTTP 随后只 wake,不再获取 pipeline 锁;app-owned `EventProcessingScheduler`(旧名 `FeedbackBatchScheduler` 为兼容 alias)先由 generic cursor 领取普通事件/推荐点击,再由 content-feedback cursor 领取 `/api/feedback` 与 `/api/events` 的显式内容反馈(`like/dislike/comment/dismiss`,且不是 import snapshot)。两者都用 event row 稳定 signal ID,通过 `checkpointed_enqueue_batch()` 将 buffer+cursor 原子发布到同一份 `pipeline_state.json`,随后调用 `tick_if_buffered()`。hypothesis feedback 与 Bangumi 等导入反馈已有自己的学习 owner,只推进 cursor、不进强信号或 generic 增量路径;retraction 仍在 generic cursor 前完成折价。5 秒 safety scan 与启动 recovery 覆盖 commit→wake、scan→checkpoint 或 checkpoint→consume 任一窗口,恢复且不双计;`unified_interest_line=false` 的旧批线路径不变。升级边界分别用 owner version/cutover event ID 按 SQLite **最大 row id**(不按可乱序的 `created_at`)fence 旧 direct-ingest 行;`feedback_state.json` 只作迁移 provenance / 兼容镜像,owner 权威位于 pipeline checkpoint。 - **后端首次启动不再等待 event recovery 或画像 LLM**:lifespan 只同步完成 owner cutover fence、本地 durable 准备和 scheduler task admission,随后即暴露 listener/health;真正的 event scan、buffer checkpoint/consume 与 provider 调用由 app-owned background task 继续。provider 401、慢请求、pending buffer 或永不返回的 LLM 都不会卡住进程启动。shutdown 仍由 scheduler cancel+gather,不遗留 task;配置热重载继续同步 pause/drain/recover/rebind,确保旧 owner 清空后才发布新 runtime。实现没有给 `_process_once()` 套短 timeout 或粗暴截断,cursor、buffer、retraction 与崩溃恢复语义保持不变。 - **三个公开事件写入口拒绝空幂等键**:`/api/events` 每项 `event_id`、`/api/feedback` 与 `/api/recommendation-click` 的 `request_id` 都先 trim,再要求 1–400 字符;缺失、空串、纯空白或超长统一在 route 前返回 422,不产生 event、seen ledger 或 recommendation 投影。相同 ID 的不同 payload 继续保留 409。MV3 buffer、popup、移动 Web 与桌面 Web 都在动作首次创建时持久化 ID,失败/响应丢失重试复用、成功后才删除;CLI feedback 省略时生成并打印 ID,跨命令重试需回传 `--request-id`;OpenClaw CLI/skill 把 ID 设为 required。 - **画像流水线新增持久 enqueue 边界并串行 LLM 消费**:`ProfileUpdatePipeline.enqueue()` / `enqueue_batch()` 只执行撤销预处理、buffer append/evict、轻量 observe 与状态落盘,不触发分析;`ingest_batch()` 复用同一 locked helper 后再消费 ready layer。event owner 使用 `checkpointed_enqueue_batch()` 同次发布 buffer+cursor,随后以 `tick_if_buffered()` 恢复消费;空 owner pass 不跑 speculator/cognition。独立周期画像维护仍调用 `tick()`。`ingest_batch` / 两种 tick / `flush` 的 layer drain 继续由 `_ingest_lock` 串行,同一时刻最多一轮 layer LLM,且成功 drain 会在 maintenance 前立即保存,避免后续维护失败导致重启重放已应用信号。 - **移动端反馈提交补 30 秒超时,惊喜卡失败不再假装成功**:移动端 `submitFeedback`(点赞 / 点踩 / 聊一聊评论)此前没有超时,后端异常时会一直转圈;现在与桌面端一致设置 30 秒上限。惊喜卡「喜欢 / 不感兴趣」请求失败时,移动端不再本地移除卡片或标记已发,而是保留卡片、显示“操作失败,请重试”并恢复按钮;桌面端同样保留卡片并提示可重试。 - **工具栏角标只表达后端健康,不再把待聊数误当未读消息**:service worker 删除 `/chat/pending-confirmations?count_only=1` 轮询、数字状态、runtime 去抖刷新和 30 秒 alarm 附带刷新;健康且已初始化时 action badge 恒为空,后端不可达 / 未初始化仍按原优先级显示浅灰 / 橙色 `!`。popup 与桌面「对话 → 待聊确认」内部计数、列表和打开接口完整保留。 - **封面抓取不再被后台预取占满,也不会并发重复下载同一签名图**:新增 app-owned `ImageFetchCoordinator`,`/api/image-proxy` 前台 miss 与 refresh 预取共用总 active≤4 / background≤3 的 Condition priority gate,始终保留前台槽且 queued 前台优先;按剥离 XHS 轮换 token 后的 cache key singleflight,前台加入 queued background 同 key 会提升优先级并采用更新签名 URL。所有 waiter shield owned task,单请求取消不影响其他消费者;热重载新 controller 重绑同一实例,shutdown 先停 producer 再取消 active/queued work。cache glob/read/write 与 DB target scan 卸载到线程,命中不占网络槽;写盘改为同目录 tmp+flush+fsync+replace,失败只见 old-or-no file。proxy 保持 `X-Image-Cache`、类型、10MB/redirect/白名单与国内直连/境外代理语义;日志和 runtime counters 只含 host/cache hash/error kind 与整数,不泄漏 signed URL/token。 - **四个扩展来源任务改为崩溃可恢复的两阶段完成**:XHS / 抖音 / YouTube / 知乎最终 callback 先在 SQLite 写锁内冻结第一份 canonical result,再从持久结果投影 durable event 与 seen-key,最后才翻 `completed`;staged row 对并发 partial/final/fail/rate-limit 为逻辑终态,但保留普通 claim lease 的 stale reclaim,dispatcher 丢失非 2xx result 响应后也会自动重新领取并修复。三个 commit 间隙都可恢复,变化后的新 callback payload(含 XHS `self_info`)不会污染首写;seen-key 使用严格写后验证,event 已提交但 marker 失败时依靠跨 task 稳定 ingest key 去重。 - **三端交互 request identity 排除可变展示字段**:惊喜反馈不再把 title 纳入 durable pending ID;推荐点击在有 `content_id/bvid` 时不再纳入签名/轮换 `content_url`,响应丢失后即使重渲染标题或 XHS token 已变化也复用同一 request ID。API、CLI 与 OpenClaw 统一把 event 首写 + recommendation 投影作为反馈提交成功边界,画像 owner/认知 follow-up 暂时失败只报告 queued/warning,不把已提交操作误报失败。 ## v0.3.191:对话与实时连接稳定性修复(2026-07-30) - **修复桌面端夜间模式下账号同步提示过淡**:同步异常提示改用主题前景色和明确的状态底色,深浅主题下都保持可读。 - **修复桌面端惊喜推荐文字卡在夜间模式下难以辨认**:知乎、Reddit 等无封面内容不再把前景色当作渐变背景,改用主题表面色、轻平台色和明确的主题前景色;普通无封面文字卡也同步收敛到同一套高对比度样式。 - **补充社区贡献者致谢**:在贡献指南中记录 [@RayeLouis](https://github.com/RayeLouis) 对扩展服务端认证权威判定([#132](https://github.com/whiteguo233/OpenBiliClaw/pull/132))和可选 TLS 反代初版([#136](https://github.com/whiteguo233/OpenBiliClaw/pull/136))的贡献。 - **修复 Web 与插件聊天不同步,并补齐长页面回顶入口**:插件、移动 Web、桌面 Web 的主聊天统一使用 `session=popup&scope=chat`;聊天界面可见且在线时约每 2.5 秒增量读取共享 durable history,快照未变化不重绘,用户阅读旧消息时保留滚动位置。移动 Web 与桌面 Web 同时增加固定「顶部」按钮,页面滚动区和聊天内滚动区均可一键回到顶部。 - **修复桌面端夜间模式下账号同步提示过淡**:同步异常提示改用主题前景色和明确的状态底色,深浅主题下都保持可读。 - **修复桌面端惊喜推荐文字卡在夜间模式下难以辨认**:知乎、Reddit 等无封面内容不再把前景色当作渐变背景,改用主题表面色、轻平台色和明确的主题前景色;普通无封面文字卡也同步收敛到同一套高对比度样式。 - **修复桌面「聊聊口味」卡片一多就被压扁、无法继续下滑**:对话记录是一个固定高度的 CSS Grid,新增确认卡后隐式行会共同压缩;卡片自身又是 `overflow:hidden`,真实约 217px 的内容会被压成约 44px 并直接裁掉,滚动容器因此也算不出完整内容高度。现在对话记录和待聊列表都使用内容自然高度行,聊天区保留独立纵向滚动并恢复可见细滚动条;待聊列表限制在视口 32% / 300px 内独立滚动,不再挤掉聊天记录和底部输入框。刷新只在读者原本位于底部时跟随最新消息,向上阅读时保留位置,已展开的「依据」也不会因后台重绘自动合上;对话记录补键盘可聚焦的 region 语义。真实 Chromium 回归覆盖 9 张长卡片、10 条待聊项及 375 / 768 / 1024 / 1440px 四档视口。 - **「聊聊口味」三端补齐同一套真实卡片体验,并隐藏无意义的 ID 证据**:移动 Web 原先只读取 `session=popup&scope=chat`,会把同一 durable 历史里的 `hypothesis/confusion` 全部过滤掉,因此即使待聊 open 的真实 POST 成功也看不到卡片;现在移动端和插件一样按 session 读取完整对话流,补齐待聊列表、主动打开、四动作、`202` 按需轮询与跨端终态投影。桌面 Web、移动 Web、扩展 side panel 都使用内容自然高度的卡片和有界独立滚动,刷新保留读者位置与已展开依据,移动端同时保留草稿/焦点并采用两列 44px 动作。共享 renderer 会过滤纯数字、UUID、事件 / note 前缀、BVID、裸哈希等只有机器 ID 的依据,若没有可读说明则整块「依据」不显示,durable payload 和内部证据链保持不变。真实后端 + Chromium + 实际 MV3 扩展验收跑通三端 GET/open/action:桌面展开 10 条依据后的记录区 `494/838px` 且滚轮到达 `scrollTop=344`,移动端 3 条待聊独立滚动并打开真实卡片,扩展提交「稍后」后移动端刷新同步显示已结算;三端 composer 都保持在视口内。 - **修复保存配置时待聊按钮失效、热重载误回滚和 Web 实时流反复报断开**:生产日志显示长达 235 秒的对话学习占住唯一结算 worker,旧热重载会先停接新任务、只等 30 秒,于是连续丢弃 `confusion.open.sync`,再用空白 `TimeoutError` 回滚配置;现在队列保持接单直到真正排空,再原子暂停交接,安全等待窗口与 20 分钟 LLM 上限对齐到 25 分钟,桌面/插件在超过前端 60 秒预算时明确显示“仍在后台热重载”。待聊 open 会在 worker 忙时返回带 `Retry-After` 的 `dialogue_busy`,两端显示等待态并自动重试,required local job 不再留下“已 claim、未建 turn”的半截状态;若已有跨 session 的 `clarifying` 疑惑,列表只给尚未展示该 turn 的 session 暴露当前持有者,不再列出必然 409 的下一条。真实浏览器 E2E 进一步发现 pending-open 卡片仍是 `pending` 状态,点“稍后”虽改成 deferred 却不会走旧 `discussing` 解锚分支;现在会按 origin turn 精确释放同代锚,下一条待聊不再被不可见旧锚挡住。`runtime-stream` 每 20 秒发送空闲心跳,桌面端短暂关闭显示“重连中”、记录 close code/reason 并按原 3 秒节奏续连,不再把 WebSocket 抖动误报成整个后端离线。 - **修复扩展认证状态以服务端判决为唯一权威**:服务端 `/api/auth/status` 返回 `authenticated: false` 时(如 `auth_epoch` 升级或密钥撤销后),扩展设置页过去因 `readPopupSessionToken()` 仅校验本地过期时间而误显“设备已配对”并隐藏密钥输入栏,与推荐 tab 的 401 状态矛盾。现在 `checkAuthStatus` 以服务端 `authenticated` 为唯一权威,服务端未认证时直接展示输入栏。新增 3 个回归测试。 - **新增默认关闭的 LAN/self-managed TLS 反代,并补齐可持久化配置/CLI/Docker 入口**:`[tls_proxy]` 现可完整 load/save round-trip,环境变量 / `config.local.toml` 逐字段覆盖不会被无关保存烘焙进基础配置;`tls-proxy enable/disable/status` 会真实持久化。`serve-api --tls-port` 可临时覆盖 8443。源码 Compose 通过 `tls` profile opt-in,使用 `OPENBILICLAW_TLS_SAN_NAMES` 明确传入逗号分隔远程 SAN、`OPENBILICLAW_TLS_PORT` 同步端口映射;非默认 build-time `CERT_DIR` 会作为容器运行时证书根目录完整传入,未提供远程 SAN 的自动证书只承诺 localhost。插件手机版二维码和 `/api/qr-info` 探测现在保留已配置的 HTTPS scheme,不再把明文 HTTP 发往 TLS 端口;未知 scheme 安全回落 HTTP。转发主体使用 Python 标准库,自动证书生成来自可选 `[tls]` extra / 容器内 `cryptography`,不再误称“纯标准库”。 - **TLS 安全与启动可靠性加固**:HTTPS Web Origin 必须与请求 Host 的 host+port 精确同源(覆盖 IPv4/IPv6/默认端口),合法 Chrome/Firefox 扩展 Origin 继续放行;TLS 出口保留重复 `Set-Cookie` 并补 `Secure`,CA/服务器私钥路径硬拒绝,HTTP/1.1 HEAD/空响应/hop-by-hop header 与真实 WebSocket 双向 relay 有本地集成测试。已有证书缺新 SAN、cert/key 半残、SSL context 或端口 bind 失败都会在 uvicorn 启动前 fail loudly,绝不静默覆盖自有证书或假报 HTTPS 成功。详见 `docs/modules/tls-proxy.md` 与 `docs/https-deployment.md`。 ## v0.3.190:启动体验升级(2026-07-30) - **Windows 启动加载窗改成完整品牌启动卡**:原先 440×168 的纯色横条只有图标、应用名和一行等待文案,信息层级单薄。现在升级为 560×280 的深色渐变卡片,直接使用最新粉色猫爪品牌源图,加入柔和光晕、启动状态区和无虚假百分比的活动进度轨;右上角会在打包时读取 `pyproject.toml` 并展示当前 `vX.Y.Z`,字体缺少中文时仍保持英文降级。版本读取、传入版本渲染和品牌像素均有回归测试。 ## v0.3.189:图标与体验收尾(2026-07-30) - **桌面「聊聊口味」不再退化成裸文本和默认按钮**:共享确认 renderer 原本只有插件弹窗样式,桌面端的待确认条目、猜测卡片、依据和四个动作因此全部按浏览器默认样式平铺。现在桌面「待聊确认」直接对齐插件的紧凑品牌色折叠条、数字徽标、轻量箭头和单列小卡片,不再使用桌面仪表盘式重容器;猜测卡片补齐状态化边框和主次动作,对话气泡更紧凑,输入框也增加可访问名称和稳定 focus。430px 以下动作改两列,深浅主题与 reduced-motion 继续复用现有令牌。 - **初始化期间也能测试 LLM 与 Embedding 配置**:`/api/config/probe-service` 只在配置内存副本上构建临时服务并真实探测,不写盘也不热重载,过去却因使用 POST 被 guided init 的 deny-by-default 写端守卫误判为冲突,插件只能显示 `/config/probe-service request failed: 409`。现在该端点成为精确只读例外,实例、默认调用链、embedding 与网络测试在画像初始化期间保持可用;LLM 探测仍经过稳定 total gate,真正保存配置的 `PUT /api/config` 继续返回 `409 init_running`,不会替换本轮任务正在使用的组件。 - **彻底清理所有品牌图标入口的残余白边与旧图**:透明源图的半透明边缘过去仍携带旧白底 RGB 消光色,缩到 16–42px 会形成浅色晕边;根 `/favicon.ico`、PWA `maskable`、Apple 主屏幕图标、社交分享图、Chrome Web Store 素材和 README / 官网历史截图又各自保留了透明角或旧字母 `B`。现在扩展补齐 32px 精确尺寸,16 / 32 / 48 / 128px、根 favicon、PWA / Apple 全底色图标与页面头图统一用品牌粉承接透明角;普通 PWA 图标继续保留透明用途,专用 maskable 图标使用不透明底色,避免系统裁切露白。中英文社交分享图、商店三张成图与 14 张桌面 / 移动 / 插件文档截图均由本地确定性脚本重采集并重建;favicon URL 升版清缓存,像素测试锁定每种尺寸、透明策略和入口引用。 - **惊喜推荐“×”现在等于看过并永久去重**:现场复现显示同一 canonical 内容已经进入 `seen_items` 后,普通推荐会排除,`get_delight_candidates()` 却仍返回,导致看过、点赞或收藏的视频继续占据惊喜栏位,只有再次点开触发 `delight_notified` 才消失;移动 Web 原有“×”更只删内存,刷新必回。现在 delight 动态阈值、打分 backlog、候选计数和最终出队全部硬过滤 `seen_items`;三端叉号统一标为“看过了,不再推荐”,通过 `dismiss` 先写 canonical seen ledger、再消费惊喜状态,普通/惊喜两条推荐都不再出现。移动端与扩展叉号采用至少 44×44 触控目标、明确 aria-label、可见 focus 与提交中禁用,写入失败不再假装成功移除。 - **修复抖音 discovery 把任务故障当空结果、误消费关键词和 feed 长期饿死**:插件 search / hot / feed 现在把真实空结果、超时、失败与预算耗尽分开回传;producer 按每个关键词的真实终态分别 `used / failed / pending`,保留预算耗尽前已成功的候选,瞬时故障无损重排且不增加 attempts,统一关键词池暂空也不再阻断独立 hot / feed。大缺口改为 search + hot/feed 逐轮轮换,避免 feed 永远没有调度机会。`discover --source douyin` 已切到与 daemon 相同的正式 producer、统一关键词和待评估候选链;`search-douyin` 读取配置预算(`0` 为无限),并以非零退出码暴露 timeout / failed,真实空结果仍成功退出。 - **应用图标去除白色方边并进入启动页**:品牌源图从实色白底改为透明外缘,扩展 16 / 48 / 128px、PWA / favicon 192 / 512px、Windows `.ico` 与 macOS `.icns` 全部从同一透明源图重新派生,系统托盘、菜单栏、桌面和深色背景下不再露出白色方框;Windows PyInstaller 启动页同步加入同款粉色猫爪图标,不再只有应用名和启动文案。透明角与启动页品牌区域新增像素级回归测试。 - **PC Web 自动续页不再跳回平台 Tab**:用户点过平台 Tab 后再用滚轮浏览到底部,Tab 会继续持有键盘焦点;续页完成时重绘库存徽标,旧实现用普通 `focus()` 恢复焦点,Chromium 实测会把 `scrollY` 从 4849 拉回 85。现在重绘仍保留无障碍焦点,但通过 `preventScroll` 阻止浏览器改写视口;新增真实 Chromium 回归,锁住“焦点保留、滚动位置不变”两条约定。 - **小红书关闭后不再继续开搜索页,并增加安全验证熔断**:来源开关过去只停掉新关键词生产,已经写进 `xhs_tasks` 的旧 search / creator / bootstrap 仍可被 `/api/sources/xhs/next-task` 领取,扩展因此在用户关闭小红书后继续打开页面;现在 claim 端每次现读热重载后的 `sources.xiaohongshu.enabled` 与全局 scheduler,关闭时旧任务保持 pending、返回 204,重新开启后再恢复,用户显式 native-save 与 discovery 开关保持正交。真实扩展 E2E 还发现跨来源互斥顺序相反:抖音占锁时 XHS 会先 claim、再因锁忙返回,留下没有 tab 和回调的 `in_progress`;dispatcher 现改为先取得共享锁再请求 `/next-task`,锁忙时不会触碰后端队列。扩展新增可见安全验证 / 操作频繁 / 429 分类器,命中只回传结构化 `rate_limited`、不上传页面正文;后端用单行 SQLite 状态持久化 search / creator 任务间隔和 1 小时平台冷却,跨 FastAPI / MV3 重启及多浏览器 profile 生效,冷却期间 producer 和所有 XHS claim(含 native-save)暂停,关联 planner 关键词无损退回 pending 且不增加 attempts,来源状态显示剩余冷却时间。新增后端队列/API/native-save/producer/keyword 生命周期测试与扩展 executor/dispatcher 误报回归。 - **小红书自动任务默认间隔提高到 5 分钟**:`task_interval_seconds` 默认值从 45 秒改为 300 秒,并同步后端模型与异常兜底、桌面 Web、插件设置页、示例配置和架构文档;现有配置可通过设置页热更新,无需重载插件。 ## v0.3.188:降级配置原地恢复(2026-07-29) - **修好 LLM 配置后不再要求重启后端**:安装包覆盖安装本来已经退出旧 `OpenBiliClaw.exe` 并启动新版本,但新进程会先读取保留的旧 `config.toml`,若其中仍有启用但缺 Key 的实例就以 degraded 恢复态启动;用户随后在 `/setup/` 修好配置时,`PUT /api/config` 过去只写盘并返回 `restart_required=true`,导致刚覆盖安装完还要再手动重启一次。现在降级上下文复用启动时保留的数据库、MemoryManager、事件总线和稳定 LLM total gate,按正常热重载的原子构造路径一次性补齐 Registry、Soul、Discovery、Recommendation、来源客户端与 runtime controller;全部构造成功后同步解除 API 503 guard、刷新 `app.state` 镜像、重绑 feedback scheduler 并启动所需后台任务,返回 `reloaded=true / restart_required=false`,`/setup/` 当场进入下一步。核心构造失败时恢复 `config.toml.bak`、保持降级并返回可重试 503;核心已恢复而仅后台循环启动失败时不反向回滚有效配置。旧后端或无备份的异常 bootstrap 仍保留 `restart_required` 兼容兜底。 ## v0.3.187:初始化与模型配置自救修复(2026-07-29) - **修复首次初始化与插件模型配置互相锁死**:新装默认配置会带一个启用但尚未填写 Key 的 DeepSeek 占位实例,后端因此以 `llm_registry_unavailable` 降级启动来提供修复界面;此前降级保护层却把设置页自己的 `/api/config/probe-service` 与 `/api/config/discover-models` 也拦成 503,`/setup/` 切到 SenseNova 等新服务商时又把旧占位实例继续作为启用 fallback 提交,最终先出现“获取模型 503”,再以“DeepSeek 缺 API Key”返回 400,插件读取未写入的旧配置后继续显示同一 503。现在降级模式精确放行无写入的草稿探测、模型发现与来源比例建议接口,推荐/画像等业务 API 仍保持 503;首启向导只会停用诊断明确标为 blocking、且未被自定义模块链引用的旧占位实例,并把它从默认链移除(不删除正常实例或改写用户自定义链)。400 改为展示结构化 blocking issue,不再截断整段 JSON;降级保存返回 `restart_required=true` 时向导停在模型步骤,写入不含 Key 的 24 小时续接标记并轮询 liveness,重启成功后自动进入账号连接步骤,避免对旧降级进程直接启动初始化。新增真实降级 FastAPI 回归与 Chromium E2E,覆盖插件/桌面共用探测、SenseNova 替换占位、可诊断 400 和重启续接。 ## v0.3.186:移动端 IPv6 与生产稳定性修复(2026-07-29) - **手机端支持 IPv6 局域网访问([#130](https://github.com/whiteguo233/OpenBiliClaw/issues/130))**:默认 `host = "0.0.0.0"` 过去只让 uvicorn 创建 IPv4 socket,IPv6 地址即使存在也没有 listener;`/api/qr-info` 同时只枚举 IPv4,两个二维码生成器直接把含冒号的地址拼进 URL 还会得到非法 authority。现在源码 CLI、Docker 入口和 Windows/macOS 桌面包在默认 wildcard 配置下共用独立的 IPv4 `0.0.0.0` + IPv6 `[::]` listener,避免依赖各系统不一致的 IPv4-mapped IPv6 默认值;IPv6 不可用时 warning 后保留原 IPv4 行为。局域网地址探测继续优先 RFC1918 IPv4,在 IPv4 不可用时回退 ULA / global IPv6 并排除 loopback、link-local、multicast 与 IPv4-mapped 地址;插件和桌面 Web 的二维码统一生成 `http://[IPv6]:port/m/` 合法链接。 - **修复 `/api/events` 并发写偶发 500**:生产进程在 API、账号同步和后台任务同时写行为事件时复用同一个 `check_same_thread=False` SQLite connection;不同线程的隐式事务会互相提交或回滚,最终在 `insert_event()` 的 commit 处出现 `cannot commit - no transaction is active`,实测 13 小时内 24 次。单事件写现在和批量导入一致,使用独立短连接完成 event + `seen_items` + backfill cursor 的原子事务并在结束后关闭;`MemoryManager.propagate_event()` 通过 `asyncio.to_thread` 执行,使最长 30 秒的 SQLite busy wait 不阻塞 API event loop。新增 12 线程、120 次真实 SQLite 并发写回归,修前稳定产生数十到上百个 transaction/API misuse 错误,修后 120/120 成功且两张表数量一致。 - **OpenAI-compatible 明确关闭 reasoning 仍被网关忽略时自动自愈**:商汤 SenseNova 上的 `deepseek-v4-flash` 会在省略 `reasoning_effort` 时沿用模型默认 thinking;关键词 planner 虽显式传 `reasoning_effort=""`,仍可能把 4096 token 全耗在 `reasoning_content`,以 `finish_reason=length/content=""` 结束并退化为兴趣名。为保持 Groq、Together、vLLM 等通用协议兼容,首请求仍不向泛兼容端点强塞非标准字段;只有明确 no-reasoning、且实际观察到 reasoning-only 响应时,才追加一次 `thinking={"type":"disabled"}` 重试。JSON `response_format` 兼容重试顺序保持不变,仍无正文才交给实例链 fallback。 - **修复候选池维护在 299/300 附近无限恢复/裁剪同一批内容**:真实生产库在来源供给变密后稳定出现 `raw 594→638→594` 振荡——维护器为补 1 条 canonical available,先把 44 条 `suppressed` 改回 `fresh`,但这些行要么来自已占满 3 席的 topic、要么排名进不了 topic top-3,实际 available 仍是 299;下一批再压回 47 条,`has_more=True` 又触发同一循环。12.9 小时内因此执行 5624 批、约 27.97 万次无效状态写入并产生 3354 条 ERROR。恢复规划现在直接跳过已满 topic;批量试探后复用 canonical available 扫描做净增长校验,只有与净新增一一对应的可见恢复才保留,其余在同一事务内还原为 `suppressed`。生产库副本由修前四批持续 `594↔638 / has_more=True` 收敛为首批 `594→588`、第二批起 0 mutation / `has_more=False`;新增回归覆盖“满 topic 位移 + viewed 高排名占位 + 低排名恢复不可见”的组合。 ## v0.3.185:推荐列表稳定与 X / LLM 链路修复(2026-07-26) - **修复 Windows 后台不断弹出黑色控制台窗口(`E:\Python\Scripts\rdt.EXE` 等)**:用户群反馈用安装包版本后总有终端窗口自己弹出来,「尤其是设置保存的时候,弹好几次」。根因是 Windows 的进程创建语义:桌面 / 安装包形态的后端本身没有控制台,它每启动一个控制台程序,系统就会**新开一个控制台窗口**,除非显式传 `CREATE_NO_WINDOW`。而后端会自行调用一批外部命令,其中 Reddit 发现走 `rdt` 子进程——保存设置触发一轮补货时,先跑一次 `rdt status --json` 探测,再按关键词逐条跑发现命令,于是窗口一次弹好几个(窗口标题正是 `rdt.exe` 的完整路径)。现在新增 `openbiliclaw/proc.py::no_window_kwargs()`,后端主动发起的全部子进程统一 splat 该 kwarg:`rdt` / `opencli`、自动更新的 `git`、`agent-browser`、mcporter 灵感检索、SQLite 修复的 `sqlite3` / `lsof`、停止托管 ollama 的 `taskkill`。非 Windows 平台返回空 dict,行为逐字不变。用户自己在终端里敲的 `openbiliclaw` CLI 与 `codex login` 不受影响——它们本就该用当前控制台并需要交互提示。新增 AST 全量扫描测试守住该约定,之后任何新增的 spawn 点漏传即测试失败,不必依赖 review 时肉眼盯。**同时堵掉一条孙进程漏网**:`rdt-cli` 自己在凭据超过它的 7 天 TTL 时会 `uv run --with browser-cookie3 …` 拉浏览器 Cookie,这个孙进程由 rdt 而非我们启动,`CREATE_NO_WINDOW` 管不到——而且正因为我们把 rdt 起成了无控制台进程,Windows 反而会给它新开一个窗口,外加 30 秒下包等待。我们的凭据过期判定因此比 rdt 的阈值提前 6 小时(`RDT_CREDENTIAL_TTL_SECONDS`),过期时直接回落插件后端而不再调用 rdt;真机 E2E 顺带抓出 `source_auth/providers.py` 里一份**字面量复制**的同名 TTL(注释明写镜像该常量却不会跟着变),它让 `/api/sources/status` 在后端已放弃 rdt 之后仍绿着报「凭据就绪」6 小时,现已改为读取源头常量并加测试锁定;新增测试比对我们镜像的常量与 `rdt_cli.auth._CREDENTIAL_TTL_SECONDS`,依赖升级改了 TTL 会立刻失败。逐个核过其它来源:X 的 `twitter-cli`、YouTube 的 `yt-dlp` 都是**进程内 Python 库调用**,B 站 / 小红书 / 知乎 / 抖音走插件任务队列,Bangumi 是纯 HTTP,均不起子进程;`twitter_cli.auth` 里同样存在的 `[sys.executable, "-c", …]` Cookie 提取只在 `get_cookies()` / `extract_from_browser()` 路径上,而我们始终显式传入配置里的 cookie 构造 `TwitterClient`,不走那条路。 - **偏好分析无进展看门狗与 20 分钟单请求超时对齐**:阶段 2 的 idle deadline 从 10 分钟延长到 25 分钟,45 分钟绝对上限不变。进展仍只由完整分片结果刷新,心跳不续期;25 分钟覆盖 `[llm].timeout=1200` 的完整单请求窗口,并为两次 65 秒临时 429 cooldown 留出余量,避免 Provider 仍在等待时被外层看门狗提前取消。阶段 4 的 15 / 45 分钟双期限不变。 - **统一兴趣线新增落地主干 live E2E 验证器**:`scripts/verify_unified_line_live.py` 在隔离根上提交两条 dislike 与一条 like,联合核对 `pipeline_layer_update(source=feedback)` 台账增量、旧 `feedback_preference_overwrite` 零增长、一次性迁移 marker、`disliked_topics` 关键词落盘及可选 server log 错误,并以可读 PASS/FAIL 与 JSON 双格式输出;纯 helper 以离线样本单测锁住台账 delta 和优先选择未反馈卡片的规则。 - **单次 LLM 请求默认超时延长到 20 分钟**:全局 `[llm].timeout` 默认值由 300 秒调整为 1200 秒,可配置上限同步由 600 秒放宽到 1200 秒;OpenAI / OpenAI-compatible / DeepSeek / Claude / Gemini / Ollama / OpenRouter 的直接构造默认值、配置 API、桌面 Web、扩展设置页和示例配置保持一致。该值只约束单个 Provider 请求,不改变初始化采集、偏好分析、画像生成或发现阶段各自的整阶段墙钟上限;存量配置中显式填写的合法超时继续保留。 - **Token / reason diet 合并前硬化,撤销不成立的旧回放 PASS**:把 `perf/llm-token-diet` 前向合并到当前 main 后补齐四类安全修复。(1) `CandidateEvalCoordinator` 现在通过 `claim_ready_batch()` 真正执行 `[scheduler].eval_min_batch_size / eval_max_wait_seconds`,并按剩余 coalescing 时间唤醒;手动 CLI 因一次性进程无法保存内存等待,固定 `1 / 0` 立即 drain。(2) embedding 预筛的兴趣可见范围从 compact 可见块扩到与长尾召回一致的 top-256,余弦分数夹到 `0..1`;OpenClaw 和配置 GET/PUT 补齐 `eval_prefilter_mode`,调度 API 同步支持两个 coalescing 字段。(3) `admission_min_score` 支持范围收紧为 `[0.5,1]`,保证 evaluator `score < 0.5 → reason=""` 的候选无法通过任何配置准入;evaluator reason 明确仅为内部诊断,不再声称会经 delight 展示。(4) 重写 `scripts/run_profile_diet_ab.py` 的证据链:精确抽取最近 `evaluated/cached/rejected_low_score` 生产混合样本,不再人为均衡平台/策略;同一冻结快照中交替运行至少 3 组 A/A 与 A/B,两臂共用 source context 与生产 4096 output budget;生产 DB 只读、embedding cache 放临时目录;超时、短向量和解析缺失均直接使门无效;必须输出含 raw paired scores、快照 digest、路由和 usage 的 JSON artifact。此前记录的 21% A/A / 17% A/B PASS 使用了不同 source context、独立快照、回放专用 16K 上限且没有真正执行相对门,因此正式作废,合并前必须用修正脚本重跑。 - **修复 Windows 11 未勾选仍随登录启动、旧启动项无法从设置页识别([#128](https://github.com/whiteguo233/OpenBiliClaw/issues/128))**:冻结桌面包此前沿用了源码安装的 `pythonw + openbiliclaw-autostart.pyw` 注册格式,但 HKCU Run 实际先执行的是 `OpenBiliClaw.exe`,PyInstaller 入口又会忽略后面的 `.pyw` 参数;因此脚本丢失时 Windows 仍会启动应用,`is_registered()` 却返回 false,设置页只看 `config.enabled` 便显示未勾选,CLI 的残留清理也被这个假阴性绕过。现在冻结包直接注册 `OpenBiliClaw.exe`,仍兼容识别旧双路径格式;自启动 intent/OS 对账从 CLI 抽成 `runtime.autostart.reconcile()`,由 `openbiliclaw start` 与 `packaging/entry.py` 共用。对存量用户,`enabled=false` 会无条件幂等删除同名 Run 值(即使目标 exe / `.pyw` 已损坏、状态读端报未注册,也不会留下未来重装后复活的项),`enabled=true` 会把旧双路径项原地迁成当前 exe 的直启格式。桌面 Web 与插件 side panel 遇到 `enabled=false + registered=true` 会把开关显示为实际开启并明确提示“残留项”,用户直接关闭即可调用既有事务 API 清理;关闭仍不强杀当前进程,只影响后续登录。测试除 fake `winreg` 外新增永久 `windows-latest` job:构建前用真实 HKCU + 真实 PE 覆盖直注册、缺 `.pyw` 仍启动、损坏项清理和旧项迁移;先用 PyInstaller 输出、再把 Inno Setup 产物静默安装到临时目录并用已安装的真实 `OpenBiliClaw.exe` self-test 跑完“开启注册 → 旧项升级 → 关闭清理”生命周期。真机验收同时发现并修复安装器把带 Git SHA 的展示版本误写入数值型 `VersionInfoProductVersion`、导致 Inno 6.7 拒绝编译的既有问题;手动构建支持 `windows_only` 避开 Intel macOS runner 排队。 - **修复抖音初始化只导入首屏却显示完整成功([#129](https://github.com/whiteguo233/OpenBiliClaw/issues/129))**:`bootstrap_profile` 的 API 分页此前把页面偶然发出带 `sec_user_id` 的请求当成唯一身份来源;部分已登录会话始终停留在 `/user/self`,fetch tap 虽安装成功却拿不到 `sec_uid`,四个 scope 只能保留虚拟列表首屏 DOM,最终仍被 dispatcher 无条件提交为 `status=ok`。现在由 MAIN-world bridge 使用页面自身 cookie / 签名上下文请求已由登录探针验证的只读 `/aweme/v1/web/user/profile/self/`:只有 `status_code=0` 且 `user.sec_uid` 非空的正面结果才能成为最终身份并写入同 tab 缓存;URL 编码的 `#RENDER_DATA` 必须同时显式 `isLogin=true` 才能提供观察候选,但仍不能在 profile 探针失败时降级为分页身份,冲突时一律以 `profile/self` 为准。常驻 fetch / XHR tap 不再从被动请求 URL 提取或记录 `sec_user_id`,避免浏览他人主页时把他人公开 ID 送进本机诊断日志。MAIN / isolated 两侧消息 listener 增加同窗口、同源检查以降低跨 frame / 页面噪声误接收(同页脚本仍可发消息,因此这不是授权边界,sentinel / request ID / payload 校验照旧)。拿到权威身份后继续复用既有 cursor / max_time 分页;API 非零业务状态、`has_more` / cursor 类型异常、游标缺失、停滞、成环、触顶和中途 HTTP 失败都不再吞错。若身份仍不可得或分页报错,scope 和任务会以**终态 `degraded`** 完成:已抓到的有效条目继续去重、入 memory,但 `dy_tasks.result_json.status` 与聚合 scope 状态明确标为“不完整”,完成 / 失败终态也不会再被迟到 partial 或重试回调覆盖。CLI 阶段 1 会以 `warning / douyin_degraded` 结束,最终摘要与 API init 均显示“部分完成”(API 保存 `partial_success=true` / `reason=douyin_degraded`),仍让有效事件参与画像建模,不再把 8/8/8/1 这类首屏结果包装成完整成功;6 小时去重不会复用该降级 completed 结果,下一次会重新入队补齐分页。隐私边界同步写入隐私政策与商店说明:只有用户触发 bootstrap 后,页面消息桥才会传经 `profile/self` 确认的公开 `sec_uid`、请求关联字段和解析后的任务条目,不传 Cookie / CSRF token,也不转发未裁剪的原始响应对象。 - **`discover --source xiaohongshu` 的节流文案跟随配置,不再硬写「4 小时」**:把该命令的节流从写死 4 小时改成读 `[sources.xiaohongshu].min_interval_minutes` 时,只改了逻辑没改展示——命令仍打印「节流开关:4 小时节流」和「距离上次关键词生产不足 4 小时」,与实际行为差 80 倍。两处都改为读配置后实测:配置 11 分钟时提示「距离上次关键词生产不足 11 分钟」,`--force` 仍正常绕过。**同一轮真机采样还澄清了两件事**:小红书 producer 在预算放开后严格按 3 分钟出轮(16 分钟 6 轮、每轮 5 个任务、轮间隔 3.0/3.0/3.0/3.0/3.0 分钟),单位由小时改分钟的换算精确无误;而 `[scheduler].trending_refresh_minutes` / `explore_refresh_minutes` **只在候选池不缺货时才生效**——池子低于目标时 `_build_refresh_plan` 会直接返回 source-replenishment 计划,把 B 站四个策略整组下发且不查这两个间隔,实测缺货状态下两个时间戳每 ~1.1 分钟就推进一次。该作用范围已写进 [config 模块文档](modules/config.md)。 - **修复五个来源的节流地板「重启即失效、空跑却锁死」**:抖音 / YouTube / X / 知乎 / Reddit 的 `_is_due()` 依赖进程内的 `_last_run_at`,后端每次重启都会把它清成 `None`,下一个 tick 无视 `min_interval_minutes` 直接开跑。**在真实库里量到了实锤**:Reddit 25 天 517 条命令记录聚成 55 轮运行,其中 5 轮的轮间隔是 8.1 / 9.9 / 11.0 / 35.4 / 39.8 分钟,而当时配置的地板是 60 分钟——只可能来自重启。同一处代码还有个方向相反的毛病:时间戳写在「成功返回」路径上,跑完但零产出的轮次照样烧掉整个周期,而那恰恰是最该立刻重试的情况(刚换好过期 cookie 的来源要白等一整轮)。现在新增共享账本表 `source_producer_runs`(platform / discovered / created_at)与 `Database.record_source_producer_run()` / `source_producer_ran_within()`,八个来源统一以「上一次**真正产出候选**的轮次」为地板:落库因此重启不失效,只记录 `discovered > 0` 因此空跑不烧周期。新增 `runtime/producer_cadence.py` 收口三个helper,未接数据库构造的 producer(单测 / CLI 一次性调用)透明回落到原进程内时间戳,行为不变。真实验证:同一 SQLite 文件上「跑一轮 → 丢弃实例重建(等价重启)→ 再 tick」仍返回 `throttled`(修复前是 `ok`);零产出轮次后立即再 tick 返回 `ok`。三条回归测试锁住重启存活、空跑不记录、账本读写。B 站 / 小红书 / Bangumi 原本就是查库口径,本次只是并入同一张表的语义描述。**合入后补跑的真机 E2E 又抓出一处漏网**:`ZhihuDiscoveryProducer` 通过 `ZhihuTaskQueue(database)` 间接拿库,自身没有 `database` 字段,于是 `ledger_available()` 恒为假、静默回落到本次要替换掉的进程内时间戳——单测和类型检查都发现不了,只有在活后端里看到「知乎 throttled 但账本无它的行」才暴露。已补字段与接线,并新增 `tests/test_producer_cadence.py` 用参数化断言守住五个 producer 都能够到账本,防止下次再漏。 - **B 站主发现的刷新节奏也并入同一套语义:`trending` / `explore` 由小时改为分钟,同样默认 3 分钟**:上一条把八个来源 producer 的 `min_interval_minutes` 对齐后,B 站仍有一半路径不受它管——主发现(`search` / `related_chain` / `trending` / `explore`)走 `_build_refresh_plan` → `_run_refresh_plan`,由 `[scheduler].trending_refresh_hours = 3` 和 `explore_refresh_hours = 12` 门控,两套时钟单位不同、量级差两个数量级。现在 `[scheduler]` 新增 `trending_refresh_minutes` / `explore_refresh_minutes`(默认均为 `3`),`_is_due()` / `_is_due_soon()` 改为分钟制,桌面 Web 与插件的「调度」页标签由「热门/探索刷新小时」改为「分钟」。**旧配置安全**:`SchedulerConfig(**sched_raw)` 会把整张 scheduler 表 splat 进构造函数,直接改名会让每一份存量 `config.toml` 在加载时抛 TypeError 而无法启动;因此解析阶段先用 `_legacy_hours_to_minutes()` 读出旧键并按 ×60 换算(`3` 小时 → `180` 分钟、`12` 小时 → `720` 分钟),再把旧键从 splat 中剔除——**绝不把 `= 3`(小时)就地重新解读成 3 分钟**,那会让存量用户的 B 站请求量静默涨 60 倍。新键存在时优先于旧键,保存后只写新键、旧键自动消失。真实后端 E2E:用只写旧键的配置启动,API 如实返回 180 / 720,设置页回填 180 / 720,改成 3 / 3 保存后磁盘只剩新键;另加一条回归测试锁住「旧键换算、新键优先、缺省落 3」三种情形。 - **八个来源的 producer 最小间隔统一为 3 分钟,并补齐三个缺失的配置字段**:原先每个来源的补货节奏各自为政且大多不可配——YouTube / X / 知乎 / Reddit / Bangumi 是 `60` 分钟(可配),B 站写死 `30`、抖音写死 `15`、小红书用的是**单位为小时**的 `min_interval_hours = 1`,三者的 `[sources.*]` 段里根本没有对应键。现在 `BilibiliSourceConfig` / `XiaohongshuSourceConfig` / `DouyinSourceConfig` 各补一个 `min_interval_minutes`,小红书从「小时」换算为「分钟」(小时粒度无法表达 1 小时和关闭之间的任何值),八个来源的默认值统一为 `3`。接线一并补全:抖音工厂原本是字面量 `min_interval_minutes=15`、小红书构造时压根没传该参数、`discover-xhs --force` 之外的路径写死 4 小时,现在三处都读配置;config 解析器里另有一套硬编码的 `raw.get("min_interval_minutes", 60)` 兜底会**盖过 dataclass 默认值**,五处一并改为 `5`,并给三个新字段补上解析与序列化——否则写进 `config.toml` 会被静默忽略、保存后不落盘。桌面 Web 与插件设置页的 5 个占位符 `60` 同步改为 `5`。**行为影响**:默认节奏从 15~60 分钟收紧到 3 分钟,对后端直连型来源(抖音 / YouTube / X / Reddit / Bangumi)意味着单位时间请求量上升,插件任务型(B 站 / 小红书 / 知乎)只是入队更勤、后端本身不发请求;单轮规模不变,仍由 `[scheduler].discovery_limit` 与各分支每日预算封顶。**已显式写了 `min_interval_minutes` 的现有 `config.toml` 不受影响**——显式值优先,默认值变更只作用于未写该键的配置。补齐过程中另抓出两处会让新字段静默失效的缺口:`GET /api/config` 的响应构建对这三个来源不传 `min_interval_minutes`,Pydantic 默认值把磁盘真实值盖成 `5`;`PUT /api/config` 的 sources 合并对 B 站只接受 `enabled`、对小红书 / 抖音的数值白名单里没有该键,于是设置页改了也不落盘。两处都已补齐。桌面 Web 与插件 side panel 的「节流」分段同时补上这三个输入框(B 站原本没有节流段,本次新建并注明它只作用于风控兜底路径)。同时用真实 producer 类 + 真实时钟实测了闸门本身:`min_interval_minutes=3` 时连续 11 次每分钟 tick,只在 t+0/3/6/9 分钟开工,其余全部返回 `throttled`——**producer 路径的节流确实生效**。但同一轮排查也澄清了它的作用范围:B 站的主发现、手动「立即补货」和初始化回填三条路径都走 `_run_refresh_plan` / `discovery_engine.discover()`,完全不经过 producer 的 `_is_due()`,因此 `[sources.bilibili].min_interval_minutes` 实际只管「API 搜索被风控冷却时接管的扩展搜索兜底」这一条;非 B 站的 7 个来源则以 producer loop 为唯一稳态路径,配置真实生效。该作用范围已写进 [config 模块文档](modules/config.md)。真实后端 E2E:桌面 5/5、插件 4/4——磁盘 11/12/13 如实回填两端,改成 21/22/23 后正确落盘且未压扁其它来源,显式写了 `60` 的 YouTube 在改动前保持 `60`、显式改 `5` 后落盘 `5`;两端控制台 0 error / 0 warning。 - **平台源设置页改成一个来源一张卡(桌面 Web + 插件)**:原先桌面 Web 的「平台源」tab 把同样 8 个来源摊成 5 段——顶部一排启用下拉、「来源接入状态」8 行、「Cookie / 登录凭据状态」8 个折叠块、中段各平台参数、底部候选池占比——配一个来源要在页面 5 处上下跳,一屏 95 个字段且停用的来源照样全量展示;其中 B 站 / 小红书 / 抖音 / YouTube 四个平台连分区标题都没有,「抖音 Cookie 环境变量」甚至和小红书的三个预算并排在同一行里。现在每个来源收成一张卡:卡面是图标、名称、来源与接入徽章、候选池占比和启用开关,配置默认折叠;展开后 8 个来源共用同一套分段(接入方式 / 发现分支与每日预算 / 节流 / 平台专属 / 验证),差异只体现在段内容——Cookie 粘贴、个人令牌、插件登录态、公开接口四种接入形态共用同一个脱敏容器,分支开关与该分支的每日预算并排成表,没有 `source_modes` 的来源只渲染预算列而不伪造开关。停用的来源只留卡面、不可展开,但配置项仍在 DOM 中参与保存 payload。候选池占比另做一张带权重条形图的总览卡,与卡面数字双向同步。两端同时补吸底保存栏(「已修改 N 项,未保存」+ 就地保存,桌面端另有「放弃修改」按最近一次后端快照回滚)。**四端范围**:移动 Web 没有配置页,CLI 无此界面,故只动桌面 Web 与插件 side panel。所有输入框 id 与 `PUT /api/config` payload 逐字未变,本次只重排信息架构,不新增也不删除任何配置字段——B 站的分支预算、小红书 / 抖音的「最小调度间隔」在后端 `BilibiliSourceConfig` / `XiaohongshuSourceConfig` / `DouyinSourceConfig` 中尚不存在,页面据实说明而非放置空转开关。真实环境 E2E 共 111 条断言、0 个产品缺陷,全程无 mock:用真实旧格式配置(`default_provider` + `[llm.]` v1 布局)启动,只读不改盘;8 张卡的启用态、知乎分支勾选、Bangumi 条目类型均按 `config.toml` 正确回填,跨 5 种控件改 13 个字段后保存全部正确落盘,未触碰字段做全量扁平化 diff 后零漂移,B 站 Cookie 与 LLM API Key 留空保存均逐字保留;开关改动在保存前显示「保存后生效」pending 徽章,保存后 `/api/sources/status` 的运行时视图立即反映新启用态(无需重启)。真实凭据链路:注入真实商汤 `openai_compatible` 凭据,平台源页保存重写整份 `config.toml` 后再次真实调用 LLM 仍成功(1489ms),密钥逐字未损;注入真实 B 站 Cookie 后 `verify` 走 `live_probe` 真联网返回「已登录 B站」,卡面徽章转 `tone=ready` 并显示「◆ 联网验证」,页面 DOM 与 API 响应均不含 SESSDATA 原文。降级态(缺 API Key,`degraded=true`)下卡片、占比条形图与脏计数仍全部正常。插件保存不会压扁桌面端写入的 `subject_types` / `bilibili.cookie` / `reddit.backend`。浏览器覆盖:Chromium / Firefox / WebKit 三引擎跑桌面 Web,并在真实 Firefox 152 中以临时扩展加载 `dist-firefox` 真机验证 popup 的折叠、sticky 保存栏、脏计数与真实保存落盘;1440 / 1024 / 760px 与插件 420 / 360px 均无横向溢出,控制台 0 error / 0 warning。 - **桌面 Web 开启滚动自动加载后列表不再乱跳、不再重排**:用户群反馈「开启自动加载后总是乱跳,尤其是自动加载消耗库存、后台补货的时候,跳完还会重新排序」。查出两个独立成因,都在桌面 Web:(1) `refresh.pool_updated` 会经 `refreshInitStatus` 拽起一次 `renderAll()`,而 `renderVideos` 是整表 `grid.replaceChildren(...)`——一轮补货连发多次该事件,于是用户正在看的每张卡片被反复销毁重建,浏览器丢掉滚动锚点(跳动)、首屏之外的懒加载封面回落成占位、展开的推荐理由与收藏 / 稍后再看状态复位;同一份代码里 `refreshPlatformAvailability` 明确写着「库存事件不许碰已加载的卡片」,是 init-status 支路从旁边绕了过去。(2) 每次切走标签页都会置 `backendHydrationPending`,切回来必定再水合,而再水合是 `{ replace: true }`——`/api/recommendations` 只返回最新 top 窗口,于是滚动加载出来的卡片被整表覆盖并按后端最新排序重排(这正是「重新排序」)。现在 `renderVideos` 改由 `syncRecommendationCards` 按 recommendation key 增量对账:markup 未变就原样复用 DOM 节点,只增删差集并 `insertBefore` 调序,「列表没变」时一个节点都不动;`refreshInitStatus` 的重绘统一走 `initStatusRenderOptions()`,网格已装真实卡片时只刷新头部 / 库存 / 侧栏;`hydrateFromBackend({ replaceRecommendations })` 默认不替换列表,只有首屏引导和手动刷新才换。**四端范围**:移动 Web 的 pool 事件本来就只合并库存、不重绘卡片,且视图只在首次挂载时加载(切 Tab 保留卡片),扩展 popup 早在 fix 79042ce 已有同类守卫,CLI 无持久列表,故本次只动桌面 Web。真实 chromium E2E 回归:补货事件连发三次后卡片 DOM 节点身份、顺序、数量与滚动位置全部不变(修前卡片身份全丢),切走再切回后本地 34 张卡片仍在且不被后端反序窗口顶掉(修前掉回 24 张并重排)。 - **修复 X(推特)内容发现的搜索恒 404:自建 `x-client-transaction-id`**。本机真实网络排查发现 X discovery 的关键词搜索(`XSearchStrategy` → `XClient.search()` → `SearchTimeline`)在有效 cookie、代理正常、live queryId 也解析成功的情况下仍恒返回 `HTTP 404 + 空 body`,而同一 cookie 同一代理下账号同步的点赞 / 收藏(`likes` / `bookmarks`)全部正常。根因链已实机逐环验证:(1) X 的 `SearchTimeline` 端点**强制要求** `x-client-transaction-id` 请求头,缺失即裸 404,而 bookmarks / likes 端点不要求,所以只有发现搜索受影响;(2) 依赖库 `twitter-cli`(及其 `XClientTransaction`)用**匿名**请求 `https://x.com` 引导该头,但 X 已把 logged-out 主页换成新版 `x-web` 外壳、不再内联 `ondemand.s` 引用,库内 `get_ondemand_file_url` 的正则 `search(...).group(1)` 因此崩在 `'NoneType' object has no attribute 'group'`,transaction 生成器永远初始化不了;(3) `twitter-cli` 0.8.5 已是 PyPI 最新、无更高版可救。修复在 `XClient._client()` 这一 twitter_cli 唯一构建点自建生成器:改用**已登录 cookie** 拉 `https://x.com/home`(仍返回带 `ondemand.s` 的完整 `client-web` 包),构造 `ClientTransaction` 后注入到底层 client 并标记库自身那次注定失败的匿名引导为已尝试,从而跳过它。生成器按 `XClient` 实例缓存一次并复用(对齐 twitter-cli 自身的 1h TTL 模型),复用 twitter-cli 的共享 `curl_cffi` 会话以保持 TLS 指纹与代理一致;引导为 best-effort,任何失败只记 WARNING 并回退到修复前行为(搜索 404),绝不抛错,`likes` / `bookmarks` / `for_you` / `user_tweets` 通路不受影响。真机验证:修复后 `XClient.search("AI")` 返回真实推文,`likes` / `bookmarks` 回归正常;新增 5 个离线单测覆盖种入、缓存、失败缓存不重试与错误吞掉。(详见 [discovery 模块文档](modules/discovery.md)。) - **LLM 鉴权失败(401)不再重试、不再张冠李戴,并会指名道姓说是哪个 provider**:一位用户反馈初始化卡在「2/4 分析偏好」后报「AI 服务鉴权失败(HTTP 401),API key 可能填错或已失效」,但他的商汤日日新后台「token 却有记录」。排查出三个独立缺陷:(1) 401 此前被映射成通用 `LLMProviderError`,而 `_is_retryable()` 对通用错误返回 `True`,于是每个分片把注定失败的 401 重试满 `_MAX_RETRIES=3` 次——4 个并发分片就在 provider 后台留下 12 条被拒请求,正好制造出「有记录却报鉴权失败」的观感,同时把可操作的报错往后拖了好几轮退避;(2) 用户面文案只说「检查 LLM provider 的 API key」,不指明是哪个 provider / 哪个 endpoint,同时配了主用与备选(或额外 embedding 端点)的用户无从判断该改哪一个;(3) `describe_llm_failure()` / `classify_llm_failure_kind()` 的鉴权判定里含裸子串 `"401"`,而 auth 桶的优先级高于 rate-limited 桶,因此上游 body 里任何一个含 `401` 的 request id / trace id(`req-1401ab`)或 402 余额不足回包,都会被误报成「API key 填错」。现在新增 `LLMAuthError`(携带 `provider_name` / `endpoint`),OpenAI 系(openai / deepseek / ollama / openrouter / openai_compatible)、Claude 与 Gemini 的 `_map_error()` 统一在 401 时抛出它并记 WARNING(含 base_url 与上游 body 摘要),三家的 `_is_retryable()` 都把它视为终态、零重试;文案改为「{provider}({host})拒绝了当前 API key」,并显式提示「或是有有效期的临时 token(过期后需重新生成)」——AK/SK 换取的临时 token 中途过期正是「先成功留下用量记录、随后 401」的典型成因。endpoint 一律经 `urlsplit().hostname` 取主机名,base_url 里的内联凭据不会外泄到界面。裸 `"401"` 判定收紧为限定形式(`HTTP 401` / `Error code: 401` / `"code":401` / `status_code=401`,且排除 4010/4011),Gemini 另补 `UNAUTHENTICATED` 与 `API key not valid` 两种非 401 表述。 - **X / Reddit 命令来源统一接入海外网络模式**:X 的 `twitter-cli` 与 Reddit 的 `rdt-cli` / OpenCLI 不再只是碰巧继承宿主进程环境,而是完整遵循 `[network].mode`:默认 `system` 保留环境代理并把 macOS 等系统代理物化给第三方 CLI,`custom` 强制注入指定地址,`direct` 清除子进程代理变量;运行时切换还会重建 twitter-cli 缓存的 curl 会话,避免旧出口残留。浏览器扩展 fallback 的网络所有权不变,仍沿用浏览器设置。 --- ## v0.3.184:全端品牌图标统一(2026-07-23) - **全产品品牌图标统一为新的粉色猫爪标记**:感谢 [@xiongguixg](https://github.com/xiongguixg) 在 [issue #127](https://github.com/whiteguo233/OpenBiliClaw/issues/127) 中主动提供移动端图标方案;项目以选定的方形源图固化 `assets/brand/openbiliclaw-icon.png`,重新派生浏览器扩展 16 / 48 / 128px、PWA / favicon 192 / 512px 与官网图标。side panel、移动 Web、桌面 Web、首次设置页和 GitHub Pages 首页都从旧字母 `B` / CSS 圆环占位切到正式图标。桌面包同时补齐多尺寸 Windows `.ico` 与 macOS `.icns` 并接入 PyInstaller,系统托盘 / 菜单栏也直接加载同一随包 Web 图标,不再单独绘制旧临时标记;社交分享图源同步切换,资产尺寸、桌面容器与各界面引用均有回归测试。 - **弹幕文本加成(P2,补上"视频讲了什么"这一维)**:P1/P3 都在回答"视频**长什么样**",P2 补正交的另一半——**观众在讨论什么**。B 站候选喂给推荐链路的语义此前只有 `title` + `description`,而 description 常是"求三连"之类的无信息文本、`body_text` 在 B 站路径恒为空,弹幕这个 B 站独有信号只存了计数(`danmaku_count`)、文本被完全浪费。抓取走 `comment.bilibili.com/{cid}.xml`(**无需鉴权、纯 XML**,标准库解析;`cid` 直接从已有的 `/x/web-interface/view` 响应读,**零额外请求**——该响应早就返回了它,只是解析器没读),复用 `BilibiliAPIClient` 的 `trust_env=False` CN 直连与共享限速,**不引入 `bilibili-api-python`**(虽在 pyproject 声明但项目从未使用,引入会带来 Credential 体系 + 两套网络栈并存)。**清洗策略被实测推翻重写**:原计划"按频次聚合,高频弹幕 = 内容共识",但抓取 BV1LR336sEFX 的 3600 条真实弹幕后发现**恰恰相反**——高频全是社区梗(难说 613×、已取餐 350×、懂你意思 310×、666 9×)、语义价值为零,真正有信息量的是**只出现一次的长弹幕**("这就是本地AI的优势,除了延迟低,还有绝对的隐私性"、"苹果上市后系统优化导致零售机强于媒体机")。按频次取 top-N 会精准筛掉全部有用信息、只留噪声。改按长度后又发现刷屏会顶到最前(`保护`×30 = 76 字但只有一个词、重复句、长串标点),于是最终策略是**先压缩重复再按压缩后长度排序**;压缩规则还踩到一个坑:字符 run 压缩会把 "5000电池"/"10000mah" 压成 "500"/"100",因此数字必须豁免。摘要存 `content_cache.danmaku_text` 新列,**严格不复用 `body_text`**(它渲染到插件/桌面/移动三端卡片正文并进 5 处 LLM prompt,弹幕塞进去会把卡片正文变成一堆"已取餐");`danmaku_fetched_at` **空结果也打戳**(否则无弹幕的视频每轮重抓)。嵌入沿用摘要文本本身为键(与其它文本嵌入一致,**不碰 `_mmr_embedding_text`**——它在两处重复实现且要求逐字节一致,改动会让整个 MMR 缓存静默失效)。新 flag `[discovery].danmaku_enabled` / `danmaku_fetch_limit` / `danmaku_max_chars`(默认关;**纯文本信号,无需多模态嵌入模型**,与 P1/P3 不同),关闭时加成恒 0、排序逐字节一致。至此 `serve()` 上共四路独立信号并行叠加。 - **视频关键帧加成(P3,深度整合视觉信号第二步)**:封面是 UP 主手选的营销图、常常标题党,**不代表视频内容**——P1 用封面建的口味质心因此天然受限。P3 改用**真实视频画面**匹配同一套质心。原方案假设关键帧要"下载视频段 + ffmpeg 抽帧"(高成本,本排在最后),**实测推翻了这个假设**:B 站早已为每个视频预生成关键帧雪碧图(拖进度条时的预览缩略图),`GET https://api.bilibili.com/x/player/videoshot`(**无需 cookie / 无需 WBI 签名**)即可拿到——实测一张 61KB 雪碧图 = 100 帧,**不下载视频、不需要 ffmpeg**,成本与抓一张封面同级,比弹幕方案还便宜。30 个真实视频抽样(5 分区、时长 45s–5106s、含 2009 年老视频)**覆盖率 100%**,平均 277 帧/视频。两个只有实测才会发现的坑已处理:①长视频返回**多张**雪碧图(最多 11 张 = 1100 帧),采样必须跨全部雪碧图全局分布,只取 `image[0]` 会让长视频只覆盖开头;②单帧尺寸**不固定**(160×90 与 480×270 并存),必须从响应读 `img_x_size`/`img_y_size` 而非硬编码。帧向量 **max-pool** 后对 P1 质心算有界加成 − 惩罚,独立常量 `_KEYFRAME_*`(**未标定**,铁律 3),在 `serve()` 上与封面↔文本锚点、P1 视觉画像**三路并行叠加**。新 flag `[discovery].keyframe_enabled` / `keyframe_max_frames` / `keyframe_fetch_limit`(默认关,需叠加 `multimodal_enabled`),关闭时加成恒 0、排序逐字节一致。雪碧图下载复用 `runtime/image_cache`(`hdslb.com` 已在白名单且 CN 直连,铁律 1),预热挂 `prewarm_pool_mmr_embeddings` 并对**空结果也打时间戳**(否则无 videoshot 数据的视频每轮重抓)。 - **修复 P1 遗留的后台任务失控**:`_maybe_rebuild_visual_profile` 此前探测 `hasattr(registry, "create_task")`,但 `BackgroundTaskRegistry` 的方法叫 **`track`**(无 `create_task`),因此该分支恒为 False、静默回退到裸 `loop.create_task` —— 视觉画像重建任务不被注册表跟踪,热重载时 `RuntimeContext.cancel_all()` 无法取消它,旧运行时的任务会残留到新运行时。改用 `track` 并加回归测试(同时断言注册表确实没有 `create_task` 方法,防止再次猜错 API)。 - **用户视觉画像加成(P1,深度整合视觉信号第一步)**:在已有「封面↔文本兴趣锚点」跨模态加成之外,新增**独立并行**的视觉信号——把用户**点赞/踩过**的推荐封面聚成 k 个均值质心(`recommendation/visual_profile.py` 贪心凝聚,复用 `_normalize_topic_keys` 骨架但用均值质心表达多峰口味),候选封面↔质心同模态余弦映射为**有界加成(正向)− 有界惩罚(负向,即"标题党封面"降权,仅扣本信号、不跌破 0)**,独立常量 `_VISUAL_PROFILE_*`(floor/ceil 0.55/0.80,与跨模态 0.15/0.45 不同,**未标定**,换真实模型后按分布重标,铁律 3)。质心存新表 `user_visual_clusters`(主库,profile-scoped),由 `rebuild_visual_profile()` 在 `precompute_delight_scores` 同 tick **节流重建**(仅当 `recommendations.feedback_at` 比上次 `updated_at` 新才跑),`serve()` 热路径只读内存 + URL-keyed 封面缓存、零 API 零聚类;公平门同 `_VISUAL_COVER_MIN_COVERAGE`。新 flag `[discovery].visual_profile_enabled`(默认关,需叠加 `[llm.embedding].multimodal_enabled`),关闭/无反馈时加成恒 0、排序逐字节一致。**A/B 结论**:合成 fixture 下 +画像 vs 仅封面加成 nDCG 增量为 0(同向冗余,预期——合成数据里文本锚点与用户质心同向),真实增量需回放真实库验证(`scripts/ab_visual_bonus.py` 的 P1 variant + `data/ab_visual_bonus_report.json` 的 `visual_profile` 段)。 --- ## v0.3.183:多实例模型路由与真实模型发现(2026-07-23) - **模型配置从“Provider 名 + 一个迷惑的备选项”升级为可编排的端点实例路由**:新增 `[llm.instances.]`,每个实例独立保存 Provider 类型、Base URL、token、模型与协议选项,同类型渠道可同时存在;`default_chain` 支持任意长度、可拖拽排序的全局故障切换,Soul / Discovery / Recommendation / Evaluation 默认继承,也能各自配置严格不越界的实例链。Registry 改为实例 ID 注册与实例级 cooldown,响应和探针返回实际命中的 `instance_id`,初始化前置检查会沿完整链寻找可用端点。桌面设置页提供实例卡片、编辑器、链条排序与逐实例 / 整链真实测试;插件也可新建、编辑、删除和逐实例测试,使用窄屏友好的上移 / 下移维护全局默认链,并完整回传 PC Web 创建的模块链(模块链编辑仍留在 PC Web)。两端保存其他设置都不会再把新路由压回旧格式,密钥输入留空时保留已保存值。旧 `default_provider` / `fallback_provider`、Provider 分段和模块 model override 会无损投影,只有新版 UI 保存时才迁移;仅含样例默认模型、没有凭据且未被引用的远程模板分段不会误迁移成实例。安装器、CLI、setup、Docker 模板与配置 API 均保留全部实例和顺序。Embedding 本轮仍保持独立配置,避免 chat 切换时悄悄改变向量空间。 - **模型名可从当前渠道真实拉取,同时始终保留手填**:桌面 Web、插件实例编辑器和 `/setup/` 新增「获取模型」,把当前未保存的实例草稿提交到无写入的 `POST /api/config/discover-models`,后端使用该实例自己的 Base URL / token 调用 OpenAI 兼容 `GET /models`,排序去重后填入可编辑下拉框;失败不会清空用户已输入的模型名,加载期间按钮禁用并用 `aria-live` 就近反馈。OpenAI 协议没有“列出某模型支持哪些 reasoning effort”的标准接口,因此 Effort 下拉仅是按 Provider / 模型给出的本地建议,仍允许任意手填;泛 OpenAI-compatible 仅在新版实例中明确填写非空 Effort 时透传,旧格式升级继续保持“不发送”语义,避免安装新包后请求体静默变化。 - **模型路由迁移可以真实回退旧版本**:首次把已有旧 `config.toml` 写成 v2 前,中心保存层会创建逐字节、同权限且永不覆盖的 `config.toml.pre-llm-routing.bak`;只读、旧格式保存、新建 v2 和后续 v2 保存均不误建备份。新增 `openbiliclaw config-export-legacy [--output PATH] [--force]`,在不改当前配置的前提下生成 `0600` 的旧 schema 副本,并用回读校验后才原子替换目标;输出逐项披露旧格式无法表达的同类型端点折叠、全局长链截断、模块 fallback 截断和端点重绑定,Embedding 保持独立不变。自动测试冻结上一代解析契约;本次验收另用真实上一版源码解析导出文件,避免“当前版本自己能读”冒充降级兼容。 --- ## v0.3.182:账号同步、换批去重与桌面升级交接(2026-07-21) - **统一兴趣更新线默认开启(2026-07-28 门禁证据)**:真实 LLM A/B 三道门配套 `--aa-control`;相同输入、无 retraction 的 A/A 两轮出现 6 个无关新增兴趣(`Rust 编程`、`并发编程`、`异步编程`、`源码分析`、`科技/AI`、`科技(AI/数码)`),且 `raised_weights` 为空,证明旧门 3 的全局冻结在量 LLM run-to-run 噪声。门 3 因而改为撤回目标定向门:只拒绝匹配撤回 topic/title 的新增 dislike 与匹配兴趣涨权,无关兴趣漂移明确不归此门;默认 `scheduler.unified_interest_line=true`,`false` 仍是旧反馈批的逐字节回退。A/A 任一轮没有成功返回 `PreferenceAnalyzer` 结果仍显式失败。另补齐 `_render_config_toml` 遗漏的五个 `profile_consolidation_*` 字段及 round-trip 契约,并让 OpenClaw bootstrap 与 API runtime 一样向 `SoulEngine` 透传 canonical database 和 `satisfaction_filter_enabled`。 - **统一兴趣更新线 Wave B:反馈批退役成 shim + 旧游标一次性迁移(默认仍关)**:`process_feedback_batch_if_needed()` 变成一层薄 shim,方法名不变 → `FeedbackBatchScheduler`、CLI 反馈命令、OpenClaw 适配三个调用方零改动。`unified_interest_line=false` 逐字节走旧批线(回退路径,Wave A 的 `TestFeedbackBatchContract` 6 条契约继续钉着它);`true` 时先做一次幂等迁移——旧游标 `last_processed_feedback_event_id` 之后未消费的 feedback 事件逐条经 `signal_from_feedback` 还原成 `FEEDBACK` 信号入线(**不能用 `signals_from_events`**:它永远不产 FEEDBACK 类型,迁移行会静默丢掉优先级消费、dislike 归档、门控重建与 `source=feedback` 台账全部特权),再触发 `pipeline.tick()` 让已达阈值的 INTEREST 缓冲立即消费。迁移**跳过 retraction**(旧批线本就排除,且它们早已在写入当时抵消过对应正向行,此刻补折价只是对着没有那些正向行的偏好层重放噪声;「排除→折价」只对将来的实时信号生效)。**顺序是先落游标+标记、后入线**:两者同处 `feedback_state.json` 一次原子写入,结构上不存在「标记有、游标没推进」的半截态;剩余崩溃窗口最多丢掉未迁移的尾巴(有界,行仍在事件账本里),而反向顺序会在每次崩溃重启把真实反馈重新计入偏好层(无界重复)。幂等标记 `unified_interest_line_migrated_at` 是必需的——游标挡不住迁移后落账的实时行被二次入线。台账写点 `feedback_preference_overwrite` 在开关开时停写(只停写不删读,`openbiliclaw ledger` 仍能查历史行),接班的是 `pipeline_layer_update(source="feedback")`。同时新增 `scripts/run_unified_interest_ab.py`:同一组真实反馈跑旧批线与统一线各一次(各自 `copytree` 隔离项目根,源根只读),输出三道门(新增 dislike 超集 / top-10 兴趣名 Jaccard ≥ 0.8 / 注入合成 retraction 后无新增 dislike 且无权重上调)的观测值与机器可读 JSON;门不过不翻默认值。 - **修复 `scheduler.feedback_batch_threshold` 永远不落盘**:`_render_config_toml` 从来不 emit 这一行,插件与桌面设置页的「反馈分析积累阈值」输入框是只写不读——任何一次设置保存都会把用户调过的值静默复位成默认 3。统一兴趣更新线复用同一个键做优先级消费阈值,静默复位会连带静默改掉兴趣层节奏。顺带补齐三面构造点:`cli._build_soul_engine` 与 OpenClaw bootstrap 此前根本不传 `feedback_batch_threshold`(永远用硬编码默认 3),也没接 `unified_interest_line`(Wave A 只接了 `api/runtime_context.py`),现在 API / CLI / OpenClaw 三面读同一份 config。 - **统一兴趣更新线 Wave A:反馈接进认知流水线(默认关,暗发)**(用户决策 2026-07-27「兴趣更新合成一条路」):兴趣层长期有两条事件驱动写入路径——认知流水线快线,和 2026-03-09 遗留的反馈批(独立游标 + 阈值 3 + 另一次全量 `analyze_events`)。两套触发状态、两套 retraction 处理、两套台账写点,每次改兴趣语义都要问一遍「另一条线要不要同步」。Wave A 先把管道铺好:① 新增回退开关 `scheduler.unified_interest_line`(bool,**默认 false**);② 打开后 `/api/feedback` 把反馈作为 `FEEDBACK` 信号喂进 `ProfileUpdatePipeline`(`signal_from_feedback` 此前零调用者),且含反馈的 INTEREST 缓冲攒够 `feedback_batch_threshold`(默认 3,值不变、只是计数点从游标搬到缓冲)即**绕过 600s 最短间隔立即消费**;③ 消费侧 `_update_interest` 继承批线全部特权:新增 dislike 归档(归档非删除)、显著变化 → 接入点③门控重建(trigger 仍是 `feedback_batch`、写点仍是 `feedback_soul_rebuild`,旧批与新线共用同一实现)、批后 held-replay、台账 `pipeline_layer_update` 记 `source="feedback"`。**默认关是硬要求**:旧批线仍在跑,两条同时开会把同一条反馈算两次;关闭时 `/api/feedback` 与缓冲判定逐字节回到今天。旧批线现有语义(阈值触发、retraction 排除但推进游标、dislike 归档、门控重建 shadow/enforce 两态、held-replay、游标幂等)已由 `TestFeedbackBatchContract` 6 条特征测试钉死作为合并验收契约,每条突变各打红。retraction 从「整条排除」改为走 pipeline 既有折价是**有意变更**,需 Wave B 的真实 LLM A/B 门证明不放大 dislike 后才退役旧线。 - **对话直通深层:你亲口说的话当轮改画像**(用户决策 2026-07-27):对话线在兴趣层本来就是快速通道(≥0.8 当轮直写),但深层有两个窟窿——①过门的 goal/value/state 候选只喂一次偏好 prompt 就标 applied 消失,**不会成为重建输入**;②重建只在「偏好显著变化」时触发,「我最近其实处于转型期」这类不动兴趣权重的纯深层自述当轮进不了深层。现在:过门的深层自述落成 `validated=True / user_verdict="confirmed"` 的假设(**用户第一人称自述就是确认**,经 `merge_insights` 去重、重复陈述强化同一行而非复制),并强制当轮门控重建(`dialogue_soul_rebuild`),新写点 `dialogue_deep_selfstatement` 入台账与写点清单。interest/dislike 快线行为不变,态势门控两道(接入点①逐候选 + 接入点③重建)照过。 - **深层画像获得受控自主权:行为佐证足够久的假设可以不等用户点头**(用户决策 2026-07-27):此前深层唯一入口是「用户确认的假设 → 门控重建」,模型形成的判断哪怕被行为反复佐证也只能一直躺在待聊列表里。现在增加第二扇门——**行为挣来的自主资格**:假设满足置信度 ≥0.8(高于用户确认路径的 0.75;真实认知产出的新假设落在 0.5-0.75,只有跨周期反复佐证才到 0.8+)、创建 ≥7 天(一个下午的热情不能改深层)、证据 ≥3 条、且用户**从未裁决过**,即可进入与用户确认完全相同的状态机:同一台去抖(6h)、同一道态势门控、同一套台账与重试上界,台账 refs 以 `auto_hypothesis:` 前缀区分。首次入队会原子记录已消费 ref,避免同一假设在清标后每轮重复重建;门控上下文会收到自动假设正文而非只见哈希。带时区的 `created_at` 会与本地时钟对齐后计算资历。**用户拒绝过的假设永久丧失自主资格**(`user_verdict="rejected"` 一票否决,置信度 0.99 也不行)。另设**高置信快速档**(用户决策):置信度 ≥0.95 免掉 7 天资历等待——真实认知产出里 0.8+ 已需跨周期反复佐证,0.95 意味着模型在多轮佐证下几乎确定;证据下限、未被裁决要求、态势门控、拒绝否决全部照旧,快速档只买到速度。Codex review 补了三处:时区 `created_at` 与 naive `now()` 相减崩溃、同一自主假设清标后每 6h 重复重建(持久化一次性消费集)、门控此前看不到自主假设正文(`auto_validated_hypotheses` 入门控上下文)。 - **第一条更新线不再在真空里解读事件**:快线的兴趣更新此前只看「这批事件 + 现有偏好」——三条木工视频到底是新兴趣还是旧兴趣回潮,模型没有任何判断依据。现在 `_update_interest` 把近期认知尾巴(觉察/洞察各取 5 条,与 portrait 重生成同窗口)作为 `` / `` 段传入偏好分析 prompt(顺序稳定→易变,保 provider 缓存前缀);init 分片与反馈批**刻意不传**(init 尚无认知、反馈按其字面判断),不传时 prompt 与旧版**逐字节一致**(回放不变性有测试钉死)。真实 LLM 新旧对照:头部 8 个兴趣两版完全一致(无回退),差异仅在尾部——带语境版把 Rust 系列事件归并进既有兴趣而非另立门户,并多识别出几个小众兴趣。 - **重跑 init 不再把同一次行为记两遍**:`init` 结尾用 `propagate_events` 把这一轮抓到的账号快照全量写进事件账本,重跑时会把同一份快照再写一遍。真实实测(同一个库跑两次 init):**账本 56% 是重复行**,699 个键连观看时间戳都完全相同——那不是二次观看,是同一次行为被记了两次,凭事件条数算出来的权重全部虚高。现在导入前按 `(事件类型, 内容身份, 时间戳)` 与账本已有行比对跳过:键里带时间戳正是为了让真实的重看仍能落地(重看 `view_at` 不同、重新收藏 `fav_time` 不同);认不出身份的行一律保留——**认不出 ≠ 重复**,宁可留着也不能默默丢信号;读账本失败则什么都不丢,原样导入。跳过条数会打在 init 输出里。 - **看完了算证据,划走不算把柄**:`classify_event_satisfaction` 的规则表里从来没有 `view`——扩展看视频发的是带 dwell 的 `click`(会判定),B 站历史发的是 `view`(恒为 `unknown/fallback`)。后果落在相关链深挖上:种子优先级里「判为满意的观看」是 60、普通观看是 10,而历史全是未判定,于是**你真正看完的视频和随手点开三秒划走的排同一档**。现在 `view` 走一条**只判正向**的规则:完播 ≥80% 且观看 ≥15 秒 → `positive/finished_watch`,其余一律保持 `unknown`。低完播刻意不判负——自动播放、误点、看预告、重看时进度重置全长这样,把它们变成负面样本会污染 `negative_exemplars` 并直接影响内容评估。阈值有校准依据:真实 500 条历史(461 条带完播数据)完播率**中位数只有 0.071**、32% 不到 5 秒,≥0.8 命中 7%(30 条),≥0.7 是 11%、≥0.5 是 19% 都太松;0.8 同时与 `ProfileBuilder._history_weight` 既有的「看完」界线一致,全代码库一个定义。实测影响面:687 条事件里判为 positive 的从 187 增至 217,相关链种子分布从 `{100:187, 10:500}` 变为 `{100:187, 60:30, 10:470}`——只有那 30 条真看完的升档,其余一条不动。 - **一个收藏就是一个信号:收藏终于进得了初始画像**:收藏在事件账本、偏好分析(`analyze_events` 分片里各占一条)和增量管线(`favorite` 属 `ENGAGEMENT_EVENT`,强于 `BEHAVIOR_EVENT`)里一直是独立事件,唯独初始画像不是——`combined_history` 把 200 条收藏打包成一个 `[收藏夹汇总]` 行,而里面那份 `_favorites` 列表**写进去了却没有任何代码读它**:`_summarize_history` 只取 `_favorites_summary` 那一句「共 200 个收藏,涵盖: 默认收藏夹」。也就是说用户主动存下来的东西——最强的意图信号——是画像里唯一一个标题都看不见的部分。现在每条收藏与观看并排成行进入 `combined_history`,带 `event_type="favorite"`(这个字段承重:它同时决定强信号权重 3.0、采样里 40% 的预留份额、以及语境渲染成「收藏了」而非「看了」);`_history_timestamp` 补读 `fav_time`——收藏没有 `view_at`,读不到就等于没时间戳、会整体掉出时间分层。收藏夹名字仍保留一行汇总(「AI Agent」「学习」这类用户自建标签没有任何单条字段携带)。真实数据 A/B(500 观看 + 200 收藏):采样 100 条里收藏从 **0 条变成 60 条**;真实 init 对照下画像从泛泛的「想弄明白」收敛到「从论文到代码一路追下去」,洞察出现「强化学习在 LLM 中的应用」这类只存在于收藏里的判断,兴趣条目从 128 降到 87(更聚焦、不再重复),生活面(游戏 / 动漫 / 美食 / 健康养生)未丢失。 - **收藏拉回来的内容不再被扔掉**:收藏夹接口一次就返回 `intro` / `cover` / `cnt_info` / `pubtime` / `attr` 等完整条目,而 init 只留了标题、UP、收藏夹三个字段。实测 200 条真实收藏样本:① **13 条(6%)是失效视频**,标题字面就是「已失效视频」,原样进了画像 prompt 和事件账本——等于告诉分析器「这人对已失效视频感兴趣」;现在按 `attr` 判定(无 `attr` 时按标题兜底)在 init 与 account sync 两侧一并丢弃。② 播放量(中位 6.5 万,18% 不足 1 万)与发布时间写进事件 metadata——这是「爱挖冷门还是追热门」的唯一判据。③ 简介入库(截 200 字、跳过占位符 `-`)但**刻意不进画像 prompt**:154/200 有内容、中位 105 字,但相当一部分是充电 / 大会员这类恰饭文案,喂进去等于每次 init 多花约 2 万字符去稀释真实兴趣信号。注:收藏接口不返回分区 / tag,要补得逐条再请求一次视频详情(200 次请求),不划算——所以历史有分区、收藏没有。 - **老收藏自愈进去重账本**:只有「新增」收藏才会变成事件,所以装 OpenBiliClaw 之前收藏的、以及旧版本收藏事件没带身份的那批,靠事件永远补不回来——实测老库倒回重扫后 `seen_items` 一条没涨。现在 account sync 每 6 小时拿到完整收藏快照后,用新增的 `Database.mark_items_seen()` 直接把 bvid 写进去重账本:幂等、不产生事件(不会重复计入偏好信号)、冲突时保留既有真实事件的溯源。真实老库实测一轮同步 `seen_items` 500 → 575,收藏快照 53 条全部覆盖。同时把 account sync 每文件夹的收藏上限从 50 抬到与总预算一致的 500(与 init 同口径,`max_total_items` 已兜住请求量)——50 的时候一个 800 条的默认收藏夹有 750 条永远进不了去重。 - **收藏过的内容不再被推回来 + 初始化的觉察/洞察进长期库**:两处都是「init 产出没被下游认领」。① **去重**:stage 1 的历史一直是入库的(stage 1 结尾 `propagate_events`),但收藏事件既没有 `bvid` 也没有 url——没有身份就进不了 `seen_items`,用户明确收藏过的视频照样会被当新内容推荐;而且 `seen_items` 只从 `view` 事件派生,`favorite` / `like` / `coin` 一概不算。现在收藏事件带上 `bvid` / url / `fav_time`,硬去重来源同时纳入 `favorite` / `like` / `coin`(`follow` 不算——关注 UP 不等于看过这条内容),并给回填游标加了类型集版本号:老库游标停在最新事件上,不倒回就永远扫不到新纳入的类型,升级时会自动倒回重扫一次。历史事件也补上了 `content_id` / 完播秒数 / 时长 / 分区:`watch_seconds` / `video_duration_seconds` 在偏好分析的 compact 白名单里,模型据此区分满播与 3 秒划走;`ProfileBuilder` 的抽样权重也读它。(**注意**:这不改变 `inferred_satisfaction`——`view` 根本不在 `classify_event_satisfaction` 的规则表里,只有显式正向 / `feedback` / `click` / 被动浏览四类会被判定,扩展看视频发的是带 dwell 的 `click`,B 站历史发的是 `view`,后者恒为 `unknown/fallback`。)② **认知**:init 的觉察/洞察此前只活在内存的 `_init_cognition_context`,塑造完首份画像即丢弃,于是全新装机跑完 init 后「待聊确认」是空的——系统刚形成了具体猜测却一条都不问。现在它们经与常规认知同一条 merge 路径落库(去重、生命周期、用户判断语义一致),觉察引用本轮事件并**如实标注 `approximate`**(模型按轮归属而非按条),落库记 `init_cognition_persist` 台账。落库后 `build_initial_profile` 会同时看到持久化副本和内存副本,因此按归一化文本去重再喂画像构建——否则同一条觉察被当成两条独立观察加倍计权。 - **初始化的觉察/洞察草稿不再被最近的分片吃光**:偏好分析按 `events[i:i+200]` 分片,而拉取顺序是最新在前;`_merge_init_cognition_contexts` 旧实现按分片顺序遍历、到 cap(觉察 12 / 洞察 8)就 `break`,于是最近的一两个分片占满全部名额,更早时期的觉察和洞察一条都进不了画像。构造三个时期各 200 条事件实测:最近期贡献 6 条觉察、中期 6 条,**最早期 0 条**;洞察同理 4/4/**0**。现改为**从时间两端交替轮转**(最新片、最早片、次新片、次早片…),每轮各取一条、去重后进入下一轮,产出少的分片自然退出后续轮次而不浪费名额,cap 与去重语义均不变。两端交替是真机 A/B 逼出来的:纯轮转在单元测试(分片均衡)下通过,但真实历史的分片数量极不均衡——420 条刷屏占了总事件的 75%,也就占了约 75% 的分片,洞察只有 8 个名额,从最新分片顺序轮转前八片全是刷屏,实测**洞察里的木工/摄影反而从各 1 条掉到 0 条,比修复前更差**;改为两端交替后恢复为摄影 2 / 木工 1。同场景修复后为每个时期各 4 条觉察、洞察也覆盖三期。这与同批修复的 `ProfileBuilder` 历史抽样是同一类问题:按到达顺序取用,等于让最近的行为独占对「这个人是谁」的解释权。 - **初始化的历史抽样从「取前 N 条」改为「代表性 + 时间铺开」**:`ProfileBuilder._summarize_history` 此前按到达顺序切 `titles[:100]` / `contexts[:100]` / `recent|older[:50]`,而真实拉取顺序是最新在前——1000 条历史里模型只看得到最近约 100 条,更早的长期兴趣无论用户互动多强都进不了画像。实测生产数据:旧办法从 800 条里选出的 100 条只覆盖**最近 0.6 天**(全量跨度 5.2 天),且把全量中唯一一条收藏漏掉了。现在抽样与增量链路同源:权重复用满意度语义(明确互动 > 高完播 > 一般 > 划走,划走不归零),先留 40% 预算无条件收下收藏/点赞/投币这类明确互动——否则集中在某一时段的互动会被其他时间桶的配额挤掉,与「疑惑被高置信假设埋掉」是同一类问题——余额再按 6 个时间桶均摊,薄桶剩余回流给最有代表性的行为。同一份抽样同时驱动 titles / contexts / recent / older,不再是三个互不相干的前缀;`count` 仍报真实总量并附 `sampling_hint` 告知模型这是抽样。缺时间戳(过半)时退回到达顺序。同数据集实测:时间覆盖 0.6 天 → 4.8 天,事件类型从 view 97 + follow 3 变成 view 80 + click 12 + scroll 4 + follow 3 + favorite 1。**未改动**:偏好分片 `events[i:i+200]` 与 init 觉察/洞察本就全量覆盖,截断只存在于画像构建这一处;prompt token 预算维持不变(仍是约 100 行)。 - **用户对假设的否定不再被后续分析抹掉**:`InsightHypothesis` 新增 `user_verdict`(`""` / `confirmed` / `rejected`),与 `validated` 分工——后者是「当作真的用」,前者记录「用户是否表过态」。此前 `merge_insights` 一律取 `max(旧, 新)`,而 reject 只把置信度压到 ≤0.35、不留任何被否定过的痕迹;于是下一轮 12h 洞察提炼若再次产出同一条假设并打 0.8,`max(0.35, 0.8)` 直接把用户的「不准」抹平,该假设还会重新越过待聊阈值(0.60)去问用户一件已经否定过的事。同时置信度只增不减:一条假设一旦被打过高分就永久高位,无论后续行为怎么变。现在按 verdict 分流——`rejected` 取 `min`(能继续走低但不能被谈回去)、`confirmed` 取 `max`(一次弱分析不能打回确认下限)、未评价则双向跟随最新分析值。对话中由用户自己给出措辞的修正版假设同样记 `confirmed`。旧数据缺字段按「从未评价」加载,反序列化对取值做白名单。**深层准入未动**:仍是 `validated AND confidence >= 0.75` 的与门,事件给不了 `validated`,「事件自动下沉深层」依然不成立。 - **真机浏览器 E2E 与对话结算的实时刷新修复**:用 Playwright + 真实 `serve-api` + 真实 LLM 跑通桌面对话面三场景——待聊列表里疑惑确实占到保留席位(真实 UI 显示「有点疑惑 50%」,把 0.74 的假设挤掉)、修正式结算渲染成 `revised`「已按你的修正记下」而非「已标记不准」、以及对锚已失效的卡片点「准」时卡片保持 `pending` 且不弹「已确认这条猜测」。过程中挖出并修掉一个既有 UX 缺口:**对话内结算落库在回复完成之后**(worker 还要跑归属判断与队列 job),而桌面端只在回复完成那一刻刷新一次确认面,导致用户说完「我认可修正版」后卡片一直停在「正在聊这条」,必须手动刷新才更新。现在回复完成后按 1/2/5/5/5/5/5 秒继续重读,直到卡片终态或用完 ~30 秒预算(对齐卡片 action 的 `CARD_ACTION_POLL_DEADLINE_MS`),且只在屏幕上确有未结算卡片时才轮询——首版 8 秒预算在真机实测中被证明会漏。 - **认知链路遗留项收口(四修 + 三验)**:① **revise 不再谎称"已标记不准"**——修正式结算此前投影成 `rejected`,用户刚说"我认可修正版"却看到否定;新增 `revised` 终态(文案「已按你的修正记下」)贯穿 `card_settlements` 投影、锚释放白名单、状态机允许转换与两端 toast。② **疑惑在待聊列表拿到保留席位**——假设的 confidence 是"我多有把握这是真的"(越高越该问),疑惑的 interpretation_confidence 是"我对猜测多有把握"且低置信才配当疑惑,两者语义相反却混在同一降序里排,叠加 top-3 截断与真实画像里 334 条 ≥0.60 假设(截断线 0.76),疑惑永远出不来;现固定预留 1 席,空席回落给假设。③ **觉察证据链升级到事件粒度**——prompt 要求每条 note 给出 `source_event_ids`,解析侧校验其必须是本批事件 id 的子集,越界即整条降级回整批并 WARNING;真实 LLM 实测 4/4 精确归属、零编造,`approximate` 标记从"恒为真"变成"仅在无有效归属时为真"。④ **防双计确认已是硬不变量**(非本次新增):candidates 侧 Jaccard 过滤 + settles 侧"有活跃锚即整批不处理",两道都有测试且突变有效;同时实测记录其边界——它是词面防线(同义改写 0.06 分漏过),bge-m3 语义兜底经校准后**主动放弃**(重复类 0.599–0.796 vs 旁支类 0.446–0.569,分隔带仅 0.03,误伤代价高于漏放)。验证侧补齐:认知循环的真实调度链路(refresh `_loop_soul_pipeline` → `pipeline.tick()` → `cognition_cycle.run_if_due()`,开 scheduler 真机跑通、觉察自动产出且证据链精确)、移动 Web 只读契约(此前**无任何测试守护**,现补四条守卫,防止第二个结算入口回潮)、Docker 形态下的配置回滚容错(独立 runtime root + 模板 + ollama 播种链路实测 4/4)。 - **事件→画像链路两处修复(真机端到端验收发现)**:① **增量 pipeline 的新兴趣跳过 trial 试用期**——`apply_evidence` 只接在 `analyze_events` / `learn_from_dialogue` / 反馈批三处,而日常浏览走的 `ProfileUpdatePipeline → layer_updaters._update_interest` 漏接,产出的 interest 没有 `state`;`get_state()` 把无 state 读成 `active`,于是随手看几个视频产生的新话题直接以活跃身份参与推荐权重,12h `scan_lifecycle` 也不补(无 state 的 interest 在它看来已是 active)。现在增量路径同样计一次证据并落 `topic_lifecycle` 台账(`source="pipeline"`)。② **空池永久饿死画像更新**——画像流水线归 `soul.*` = MAINTENANCE,`RefillAdmissionSemaphore` 在库存 EMPTY 时无条件 park 它给补货让路;当补货永远不来(source 全关、凭据失效、网络不通)就是无限期停摆,全程只有启动时一行 `Background LLM work gate blocked`,实测 `POST /api/events` 10 分钟不返回而 LLM 直连 1.1 秒正常。新增 5 分钟 `MAINTENANCE_STARVATION_GRACE_SECONDS` 饥饿上限:到点放行并 WARNING 指出补货可能失败,库存恢复后豁免立即撤销并重新武装;补货正常到来时的预留语义完全不变。两处均补回归测试并实测突变有效,真机复验 7/7(空池 `POST /api/events` 324 秒返回 200,兴趣落 `trial`/`evidence_count=1`)。 - **对话确认入口三处诚实化修复(真机验收发现)**:① 锚被另一张卡片占用时,卡片 action 后端本就诚实返回 `stale_anchor`/`anchor_dependency_failed` 且零写入,但共享前端 `responseCardState()` 因 `"stale"` 不在 `CARD_STATES` 里而回落到**乐观终态**,卡片渲染成「已确认」、桌面/popup 还各弹一条成功提示,用户这次确认静默丢失、刷新后打回待确认;现改为归入既有 `retryable_error` 路径回滚乐观态并给诚实文案,两端 toast 同步。② deprecated `POST /api/insights/feedback` 把同一种锚拒绝包装成 `200 {"ok":true,"matched":false}`,老客户端无从得知失败也不会重试;现返回 `409` + 可诊断 detail,`Deprecation`/`Link` 头与 `202 processing`、正常 `200` 路径均不变。③ `config.toml` 出现未知 key 会让 `SchedulerConfig(**sched_raw)` 抛裸 `TypeError` 直接拖垮 `load_config()`(升级写入新字段后回滚旧版本即复现,本仓一个残留 worktree 配置就曾连带 74 个用例变红);新增 `_filter_dataclass_kwargs` 覆盖 scheduler / storage / logging 与全部 8 个 provider section,未知 key 过滤后按 `[section].key` 逐条 WARNING,已知字段值不受影响。三处均补真实回归测试并实测突变有效(删除修复后对应用例必红)。 - **对话结算单队列 Wave 0 护栏与 RED 契约**:冻结 spec §2.2 的 12 项入口、10 类 protected mutator 与 pending-open 三类 raw sink,确定性复现文件 fence、future-anchor、建锚 reservation/failed-head/同 ref owner/commit-point 及当前入口旁路;新增实际 worker Task + lifecycle nonce 双校验 guard primitive,child task 不能继承写权限,旧 worker 迟到 cleanup 不能撤销新 permit。此 Wave 不接线现有 runtime façade,也不改 force_tick、exploration、pipeline、OpenClaw 或 CLI writer。 - **对话结算单队列 Wave 1 Task 1.1(typed queue + admission registry)**:`dialogue_learn_queue.py` 原地泛化为 11-kind typed envelope、单 consumer 与 completion Future;admission 同步完成深拷贝、sequence、exhaustive anchor transition、owner reservation、snapshot 和入队。`AnchorAdmissionRegistry` 支持 persisted/reserved/failed/absent/not-applicable、同 ref 多 reservation/最新 head、owner-only 单次 resolve、failed head 原子前移与旧引用 GC;六类 terminal 在 mutator 返回后的下一次 await/effect 前转正。queue worker 已绑定实际 Task permit,reentry 与 child-task mutation fail closed,reload 可先 exact revoke old 再注册 new,旧 finally 只能 compare-and-clear。runtime 统一 dispatcher、显式 legacy-direct 与 LLM 在线内串行留在 Task 1.2–1.3,本项未做 Wave 2 receipt/schema/锁栈改造或 Wave 3 endpoint cutover。 - **对话结算单队列 Wave 1 Task 1.2(runtime dispatcher + 显式学习模式)**:API runtime 的旧 learn-only adapter/attribute 已收敛为唯一 `DialogueSettlementQueue` 与 typed dispatcher,`SocraticDialogue(mode=queued)` 同步提交 `learn`,缺 queue 在 LLM 前显式失败,测试可选 `reply_only_test` 零学习;CLI/OpenClaw 仅在两个 allow-listed 构造点显式 pin `legacy_direct`,仍各 direct learn 一次且 queue submit=0。热重载按 pause/drain old → exact revoke old → start/register new → publish new 交接,失败 rollback 用 fresh nonce 恢复 old,旧迟到 `finally` 不能清掉 new permit;其余 typed kind 在 Wave 3 cutover 前 fail closed,未改命令/adapter response、force_tick、exploration 或 pipeline。 - **对话结算单队列 Wave 1 Task 1.3(LLM 在线内串行)**:runtime dispatcher 的 callable 边界改为 `DialogueDispatcher`/`DialogueJob` 强类型;`learn` 在 background-admission bypass 内由唯一 worker 直接 await `learn_from_dialogue`,不 detached、不进 task registry、不包 whole-job timeout,provider 自身有限 timeout 保持不变。确定性双 job 用例固定 LLM `max_active=1`,首项阻塞 0.5 秒时 heartbeat ≥10,force_tick/exploration/OpenClaw 调用均为 0,并核对每项 `queue_wait_ms/run_ms`。生产仅记录 `202 ratio >1%` / p95 `>5s` 的后续拆分观察阈值,不抽 DTO、不加 digest/CAS、不预埋第二队列。 - **对话结算单队列 Wave 2 Task 2.1(轻量 winner receipt)**:`card_settlements` table rebuild 收敛为 `ref/verdict/turn_id/payload/applied/result/event_id/created_at/updated_at`,最早表与旧 claim/segment 表升级均保留 winner,旧 event/applied 进度映射到稳定 event identity;删除数据库级进程锁、文件锁、5 分钟 lease、claim token 与三段 CAS DAO。event INSERT 与 receipt 标记同事务且可重放,completion 无 token;50 路同 ref 仍只有一个 immutable winner。本项不做 Wave 3 endpoint cutover。 - **对话结算单队列 Wave 2 Task 2.2(worker-only apply + future-anchor 删除)**:`SoulEngine` 的 direct `settle_*` executor 拆成公开 `submit_*` admission façade 与实际 worker Task 才能进入的 `_apply_*`;锚 relation 和普通 chat settles 在当前 learn worker 内直接 apply,不递归提交自己。每个 job 只消费受理时冻结的 persisted/absent/failed snapshot,彻底删除 generation 0 在执行期读取 current anchor 的补抓;非预约未来锚与旧 generation 均在 receipt 前零副作用退出。旧文件 fence XFAIL 改成 worker await 期间 heartbeat 继续的无锁 GREEN,用真实 queue 顺序证明 nested settle 不增加 queue depth。生产 card/legacy/anchor-builder/confusion endpoint cutover 仍严格留在 Wave 3。 - **对话结算单队列 Wave 2 Task 2.3(七段 crash gap + stable effect)**:worker 固定执行 event → object → derived → rebuild marker → durable `applied=1` → projection → exact-generation anchor release,并在七个边界提供精确故障注入;confirm/revise/confusion 共 12 个参数化代表 case 加 runtime 重建用例证明显式同 ref retry 始终采用原 winner,event/object/derived/marker/解锚语义各至多一次。`applied=1` 现在先于 frozen-anchor 校验进入 publication-only 分支,只补 audit observer、跨 session projection 与精确解锚,不重做对象语义。`profile_update_ledger` 新增可选固定 hash `effect_key` 与 partial unique index;结算主台账和 revise-derived 台账首次写失败不阻断 applied,恢复后显式 retry 可补齐且 `INSERT OR IGNORE` 不重复。锚 admission snapshot 补齐 kind 字段,confusion 不再被默认解释成 hypothesis。单 turn GET 已按 F7 只 submit `card.reconcile`:首次可返回 pending,worker 补齐跨 session publication 后再次 GET 见终态,request direct write=0、object/derived/rebuild 计数仍为 1。除此之外未接线 card action/legacy/锚 builder/疑惑等 Wave 3 production 入口,也未增加自动 scanner。 - **对话结算单队列 Wave 3 Task 3.1(cards/open/reconcile/legacy cutover)**:卡片 confirm/reject/discuss/defer、待聊 open 的 confusion schedule/retarget/rollback 与 anchor establish、列表/单 turn GET reconcile、deprecated legacy feedback 全部改为 typed submit;endpoint task 不再直接写对象、卡片、疑惑或锚。discuss admission 在入队前建立 owner reservation,worker 内完成 `pending→discussing→anchor`,失败补偿回 pending;同 ref 重复 builder 各自 resolve。card/legacy 只 shield 等待 1 秒,空队列保持原 200,队头阻塞才返回 202 processing,已入队 Future 不被 HTTP timeout 取消。 - **对话结算单队列 Wave 3 Task 3.2(chat/anchor/probe/replay cutover)**:普通 chat 的 speculation/insight/confusion settles 与锚 relation 保持当前 learn worker 内 apply;probe/confusion durable reply 只提交 typed side-effect job。弱正向 probe classifier 在 worker 恰执行一次,返回 immutable exploration intent 后由原 producer task 在 permit 外交给既有 exploration 路径。12h cognition hook 只枚举并提交专属 `confusion.attribution.replay`,不借 `learn`/`confusion.reply.apply`,重复 identity 复用现有 receipt;`force_tick`、pipeline、direct probe button、avoidance 与 exploration writer 边界未扩大。 - **对话结算单队列 Wave 3 Task 3.3(按需 202 与客户端轮询)**:popup 与桌面 Web 的共享卡片 helper 仅在 action 返回 `202 processing` 时按 1/2/5 秒(随后 5 秒)读取 durable turn,总截止 30 秒;终态立即停止,deadline/读取失败/页面 abort 转本地 `retryable_error`,不改 durable pending。同步 200 与 opposite `already_settled` 仍直接采用权威结果。进程重启可丢失内存 job,用户重试同 action 后按 immutable winner/receipt 三分支恢复;没有新增 durable job 表。移动 Web 无卡片 action UI,CLI/OpenClaw 显式 `legacy_direct`,均未加轮询。 - **对话结算单队列 Wave 3 Task 3.4(护栏、旧栈删除与交付)**:Wave 0 的 10 组 production wiring strict XFAIL 全部改为 worker-only GREEN,并增加 production AST/raw-sink、inherited child、热重载 permit 与 100 次 declared-entry 交错守卫;仓库不再保留 strict XFAIL。删除 card settlement claim/lease/文件锁/三段 CAS/恢复 scanner,以及 discussion `attempt_token/discussing_at` CAS/scanner;fresh runtime schema 只保留轻量 winner receipt,旧列仅迁移读取。旧 takeover/fencing、stale lease 与旧 schema 测试按原业务意图改写为 serialized winner、stable-effect retry、orphan discussion reconcile 与 migration-only 证明;CLI/OpenClaw compatibility allowlist 仍精确两处。 - **对话结算单队列第二轮验收修复 F1/new-3**:除 owner resolve 外,non-builder release/relation completion 也不能把全局 `_latest_head_key` 从更晚受理的跨 ref reservation 拉回旧 ref;真实 worker barrier 固定“旧 A 已释放、新 B reservation 已受理、A completion 刷新”后 latest 仍是 B,并保持 failed-head 前移语义。 - **对话结算单队列第二轮验收重构 F2/new-1/new-2**:删除 worker-lineage inline dispatcher 与 delegated-task 临时授权;actual worker 内的普通 chat/锚嵌套结算只沿当前 task 调用栈直调 `_apply_*`。`submit()`/`submit_and_wait()` 对 actual worker、任意层 active child 和跨 job detached stale child 都立即报 reentry;guard 始终只认 actual worker task + lifecycle nonce,旧 child 在下一 job 运行时仍无写权。 - **对话结算单队列第三轮验收修复 F2**:dispatch 完成刷新不再只依赖 payload target,而会从 effective frozen snapshot 或 builder transition 推导实际受影响 ref;因此 targetless `learn` 内的 support/contradict/revise/confusion answer 直调,以及 confusion replay builder follow-up 内的解锚,都会在 completion 前把 registry 刷成 durable absent/新 generation。刷新继续携带原 job sequence,同 ref 与跨 ref 的更晚 reservation 都不会被旧 completion 覆盖。 - **对话结算单队列独立验收修复 F3**:confirmed/rejected 卡片收到迟到 defer 时返回 `already_settled` 与真实终态,不再伪报 deferred,也不创建或延长该 ref 的确认 cooldown。 - **对话结算单队列第二轮验收修复 F4/M3**:orphan recovery 增加 30 秒 claim-age fence,并继续在同一 UPDATE 内校验 ask-turn identity 与 `NOT EXISTS(chat_turns)`;双连接固定 recovery 插入 claim→create 窗口时返回 `released=False`、creator 随后成功建 live turn,真正超龄无 turn 的 crash claim 仍可恢复。官方测试分别以零年龄强制命中 live-turn fence,以及用不存在的旧 ask-turn 尝试回收当前 claim;删除 `NOT EXISTS` 或破坏 `ask_turn_id = expected_ask_turn_id` 任一条件都必红。 - **对话结算单队列第二轮验收加强 M7**:官方动态 expiry probe 让同一 detached child 在父 job 内先触发 reentry、跨到下一 job 再尝试 protected mutation 与 submit,旧 child 两条路径均被拒;静态契约同时要求 inline/delegated 授权符号为零,临时授权 reset mutation 无藏身路径。 - **对话结算单队列独立验收修复 F5**:删除“单一 settlement + fake dispatcher”的 100 路自证,改由安装在真实 `create_app` runtime 的 dispatcher 跑完 11 个 typed kind,并在同一总括用例固定跨 ref head 与 worker-child reentry 两个 blocker 交错;十类 protected category 逐类调用各自生产 mutator/handler,核对 SQLite、锚、卡片、疑惑与 cooldown 均零旁路写。 - **对话结算单队列独立验收修复 F6**:`anchor.establish` admission 仅接受三类已声明 producer source,任意其他非空来源也 fail closed;计划内三处 legacy-column 静态门改为递归路径 glob,实际排除 migration-only `storage/database.py` 后命中数从 6 归零。 - **移动端惊喜卡恢复整卡点击打开(issue #126)**:用户反馈手机上「惊喜推荐一定要点『看看』才能跳转,下面的内容却是整卡点一下就进去」,问这是防误触还是别的设计原因。查下来两者都不是——这是实现不一致而非有意为之:移动 Web 的普通卡片在 `renderCard()` 里绑了整卡 click(动作行 `stopPropagation` 排除按钮),而惊喜卡的 `.delight-tray` 上只有左右滑动切卡的 pointer handler,位移不足 50px 时松手什么也不做,于是卡体在视觉上像可点、实际是死区。现在死区内松手(位移 <10px,`DELIGHT_DRAG_DEAD_ZONE`,与桌面端 `_DELIGHT_DRAG_DEAD_ZONE` 同值)等同点击「看看」:走同一个 `handleDelightAction(d, "view")`,因此已读标记、`POST /api/delight/respond` 与打开链接的语义和按钮完全一致,不是另一条旁路。10px–50px 之间仍然刻意不触发任何动作,手指轻微拖动不会误开内容;≥50px 继续是切卡。反馈按钮、聊天输入框等交互元素本就在 `pointerdown` 阶段 `stopPropagation`,天然不会被整卡点击吸收;已反馈完成(`show_actions=false`)、聊天 composer 展开中、或拿不到内容 URL 时不接管点击,避免抢走「点空白处收起输入框」的预期。**四端范围**:桌面 Web 的普通推荐卡本来就只有动作按钮、没有整卡点击,惊喜卡另有封面点击区 + 「去看看」按钮,自身已经自洽,本次不动以免改变桌面既有手感;插件 popup / side panel 没有惊喜卡(delight 只走 background 通知),CLI 无此交互。新增三个 Playwright 真机级 E2E:点击卡体打开内容并只上报一次 `view`、拖动 30px 不打开也不上报、点「喜欢」不会连带打开内容。 - **认知画像流水线(四 Wave)+ 深层线归一合入**(2026-07-22,分支 `feat/cognitive-profile-pipeline`,codex 共十轮对抗 review、59 findings 闭环;规格见 `docs/plans/2026-07-17-cognitive-profile-pipeline-{spec,plan}.md` 与 `docs/plans/2026-07-22-deep-line-consolidation-spec.md`;真实环境 E2E 7/7。三线更新、觉察/疑惑/假设生命周期、态势门控(默认 shadow)、统一台账 `openbiliclaw ledger`、topic 生命周期状态机、深层影响收敛为「假设确认→门控下 soul 重建」。细分条目如下)。 - **第二轮独立验收:结算原子边界与入口收口**:claim takeover 与对象/派生/marker/台账现共用跨连接临界区,每个非 SQLite 副作用前锁内重读 token 并把 segment CAS 留在同一边界,真实 lease 接管只执行一次对象;LLM 返回后的首个 anchor 副作用改为锁内 generation CAS 并强制消费,失配整批 WARNING+台账,赢家 payload 固化代号且旧收据不能释放同 ref 新锚;无锚 chat 的 speculation/insight/confusion settles 删除直写旁路,与卡片、legacy、锚统一进入 ref 仲裁并返回 `already_settled`。新增三组真实线程/异步屏障测试及对应 fencing/CAS/旁路 mutation 复验。 - **对话确认入口 Wave A(Task 0–3)**:durable turn 新增兼容 `payload`、稳定 `(created_at,rowid)` 顺序与创建时固定本地时间;单学习队列捕获持久化锚的 `ref/generation` 快照,既有 insight extraction 追加 kind×relation 白名单矩阵,无锚 prompt 保持逐字节不变。confusion 结算所有权从 API completion 迁到串行锚处理器:classifier 输出先入 FIFO `replay_queue`(≤5、精确队头 fencing、四类解锚清队列并记 dropped),副作用失败留队且后续轮不能越过;12h cognition cycle 重放已存输出并幂等补扫 completed crash gap。卡片 action/API 属后续 Wave,本轮只落 `card_settlements` 仲裁基础。 - **对话确认入口 Wave B Task 4(durable 卡片 action)**:`scope="hypothesis"` 结构卡片创建即 completed,worker 不触发 LLM;`POST /api/chat/cards/{turn_id}/action` 支持 confirm/reject/discuss/defer。confirm/reject 用 ref 主键仲裁、5 分钟可接管 claim token、三段 flags 与 fencing;event 段在同一 SQLite 事务完成占位+INSERT,投影只认 `applied=1` 并批量刷新跨 session 卡片。discuss 以 `discussing_at+attempt_token` CAS 后建锚,失败回滚、超时读取清 token 拒旧请求。单一 history 回灌所有 session 的 completed chat/hypothesis/confusion;UI 列表仍按 session 过滤。legacy insight feedback 保留响应契约、标记 deprecated 并以 `source=legacy_endpoint` 转发同一路径。 - **对话锚结算统一仲裁(验收修复)**:`support/contradict/revise/answer` 不再从 `SoulEngine` 直写对象或直接 patch origin 卡片;卡片 action、legacy endpoint 与锚处理器统一进入 ref 级 `INSERT OR IGNORE → claim token → event/object/marker fencing → applied → 全 session 投影`。仲裁行新增赢家 payload,接管者只续做原赢家的 revise/answer 语义;冲突时画像、收据与各端卡片只呈现同一 applied verdict。 - **锚 generation 二次校验(验收修复)**:串行学习任务在 LLM 返回并完成本地解析后、任何对象/`replay_queue`/候选/画像副作用前,再从持久化单锚原子读取并同时比较 ref+generation;期间被同 ref 新 generation 顶替的迟到输出整批丢弃,WARNING 并追加 `anchor_stale_generation_drop` 台账。入 LLM 前已 stale 的排队快照也直接丢弃,不再退化成无锚提取。 - **门控解析错误分流(验收修复)**:`PostureGate` 的非法 JSON、非 object JSON、缺失/越界 verdict 与无 registry 统一按 provider 异常处理;enforce 仍保守 downgrade,但明确 `is_error=True` 让 `rebuild_pending` 保留重试,shadow 记 `shadow_error`。只有模型明确返回白名单内 accept/downgrade/reject 才是 `is_error=False` 的真实判定。 - **疑惑 resolve/pop 崩溃恢复(验收修复)**:12h 兜底扫描先选取**任意状态**下非空的 `confusions.replay_queue`,再补扫 clarifying completed-turn gap;因此 resolve 已提交、FIFO 队头尚未 pop 即崩溃时,重启仍会幂等重放 terminal 对象并精确出队。共同对象结算存在未 applied 收据时同步续做其 claim/fencing;defer 的 open 状态恢复不再重复累加 `defer_count`。 - **结算台账严格 best-effort(验收修复)**:`card_settlements.seg_marker` 先在独立事务按 claim token 提交,`settle_insight/settle_confusion` 台账随后用另一事务尝试追加;ledger INSERT 失败只 WARNING,不能回滚 marker、阻断 `applied=1` 或改变 API/锚结算结果。 - **rebuild marker 写盘失败显式化(验收修复)**:marker 采用同目录临时文件写入、`flush+fsync` 后原子替换,任何序列化或文件系统失败都会 WARNING 并向结算调用方抛出,且在 `finally` 清理 `.tmp`。失败收据停在 `seg_marker=0/applied=0`,不发布卡片投影;后续 claim 接管可从该段安全续做,避免出现“结算已发布但永无 rebuild_pending”。 - **rebuild marker 触发幂等(验收修复)**:已有 pending 再收到相同 trigger ref 时不再重写文件或重置 `set_at/retry_count`,因此重复结算不能延长 6h debounce、也不能清零错误重试次数;只有首次出现的新 trigger ref 才合并来源并按「新证据重开」重新置时、重置 retry。 - **学习队列 drain 超时阻断热重载(验收修复)**:`DialogueLearnQueue._join()` 超时在 WARNING 后重新抛出;`RuntimeContext` 在旧队列 drain 失败时恢复其接单并让配置事务感知失败,不执行 `cancel_all`、不构造/安装新 generation。只有 drain 成功才 swap,彻底关闭新旧学习 worker 并发窗口;shutdown 超时仍在 `finally` 强制取消 worker。 - **关键并发/代次测试去假绿(验收修复)**:锚 stale 用例保持 ref 不变、只推进 generation,删除 generation 比较即失败;`rebuild_pending` 错误分支改由真实非法 JSON 穿过 `PostureGate` 解析器,不再直接伪造 `is_error=True`。confusion resolve→pop 故障后关闭并重开数据库验证恢复;卡片旧执行者用线程屏障暂停,在新 token 接管但尚未写段时恢复,实测 event/object/marker/applied 四次写全部被 fencing。 - **对话确认入口 Wave B Task 5(待聊与双轨冷却)**:新增 `GET /api/chat/pending-confirmations`(高优先级最多 3 条,支持 `count_only=1`)与 `POST .../{ref}/open`。用户主动 open 不受时间冷却,`(ref,session)` 在 `BEGIN IMMEDIATE` 内查重,同端复用、跨端各自产卡/提问并以 `pending_open` 建锚;疑惑仍受 `clarifying <= 1` 硬约束。系统抛出同时满足全局 12h 与同对象 72h,状态持久化;只在非空、非幂等重放的 durable 用户消息处理内先 INSERT 确认 turn 再 INSERT 用户 turn,`attached_to_turn_id` + `(created_at,rowid)` 保证故障恢复不重附且顺序确定。此 Task 仅交付后端 badge/count 数据,SW/桌面消费属于 Task 6,未提前实现。 - **对话确认入口 Wave C Task 6(卡片 / 待聊 / 角标前端)**:popup 与桌面 Web 共用 `web/shared/dialogue-confirmation.js`,在 durable 对话流中渲染假设卡片四动作、依据展开、已结算态、疑惑纯提问和无 payload 文字降级;confirm/reject/discuss/defer 先乐观更新,失败回滚,跨窗口 `already_settled` 以服务端 verdict 校正。两端都提供待聊列表与主动 open,分别固定 `session="popup"` / `"webui"`,桌面侧栏同步显示待聊计数。service worker 复用既有 30 秒 alarm 拉 `?count_only=1`,runtime-stream 刷新去抖,离线 / 未初始化抑制数字,健康 / 错误角标继续优先。移动 Web 本 Wave 仅把 active insights 改为只读展示并引导到插件或桌面端对话确认;移动端卡片仍按规格留待跟进版,Wave D 的 CLI / 其余旧确认入口迁移未提前实施。 - **对话确认入口 Wave D Task 7(只读收尾)**:新增 `openbiliclaw questions`,按配置端口只读调用 `GET /api/chat/pending-confirmations`,直接复用服务端过滤/排序/上限且不提供结算动作。popup 与桌面 Web 的画像/认知更新区移除旧「准 / 不准」按钮、handler 和 `submitInsightFeedback` 客户端写入口;连同移动 Web,active insights 三处均只读,主动假设确认只留在 durable 对话卡片。deprecated `POST /api/insights/feedback` 后端转发兼容仍保留。模块/API/CLI/扩展文档与 `docs/architecture.md`、`docs/spec.md`、README 中英文顶部架构图同步增加「确认入口」节点。 - **GateDecision 错误分类(F7)**:`soul/posture_gate.py` 的 `GateDecision` 新增 `is_error` 字段。enforce 下 LLM/解析**异常**仍保守 downgrade,但 `is_error=True`,让重建调用方对「真实 downgrade 判定」(清标)与「瞬时错误」(保留 pending 重试)分流;与 shadow 侧 `shadow_error`/`shadow_downgrade` 语义对齐;接入点①调用方不消费该字段,行为不变。 - **P1 退役 + 一次性迁移**:pipeline 不再消费 VALUES/CORE——`_BUFFERED_LAYERS` 摘除、`FEEDBACK` 只路由 interest+surface、对话 `value/state` kind 在 pipeline 内失活、`update_layer(VALUES|CORE)` 封死 no-op + WARNING(接入点②随之退役)。新增 `migrate_pipeline_deep_buffers`:构造时幂等地把持久化 buffer 里残留的 VALUES/CORE 信号确定性转成 awareness note(前缀 `[migration:pipeline-deep]`,内容 hash 去重,marker + 台账行 `pipeline_deep_migration`,清空旧键)。 - **接入点③快照泛化 + P2 补门控**:`_gate_soul_rebuild` 泛化承载三触发源(dialogue / feedback_batch / confirmed_hypotheses),快照带 `trigger`/`write_point`/旧 soul 摘要/触发上下文,返回 `GateDecision`。反馈批显著变化的整份重建此前绕过所有门控,现接入接入点③(`feedback_soul_rebuild` 写点,enforce 可拦)。 - **重建输入过滤 + rebuild_pending 状态机**:所有 soul 重建只纳入 `validated && confidence>=0.75` 的假设(`_rebuild_active_insights`),rejected/未验证假设不可见。`update_from_feedback` 单点:confirm/reject 均置持久化 `rebuild_pending {set_at, trigger_refs, retry_count}`;去抖 6h 后由 12h 认知循环 / 下次对话学习 / 反馈批触发门控重建(`hypotheses_soul_rebuild` 写点)。清标语义:accept 清 / 真实 refusal 清+记 `last_gate_refusal` / error 保留有界重试(`is_error` 区分,retry<2);`set_at` compare-and-swap 对账并发 re-mark;重启自动恢复。 - **文档**:`docs/modules/soul.md` 新增「深层影响唯一模式」小节 + 台账写点表与态势门控/更新器行更新;架构图无跨模块布线变化(深层节点注记)。 - **topic 生命周期状态机(Task 8)**:新增 `soul/topic_lifecycle.py`,给 interest(flat `InterestTag` 与 Onion `InterestDomain` 两层)叠加 `state ∈ {trial|active|decaying|archived}` + `evidence_count` + `last_evidence_at` + `parent_topic`。序列化点(`soul/profile.py`)**只在非默认时 emit 这些键**——默认 `active` 的 topic 与旧数据序列化**逐字节不变**,旧数据缺字段兼容默认 `active`。跃迁(常量带首轮校准注释):新 topic 首见→`trial`;证据 ≥5 或持续 ≥7 天→`active`(`apply_evidence` 接在 `analyze_events`/`learn_from_dialogue`/反馈批偏好写 chokepoint);`last_evidence_at` 静默 ≥30 天→`decaying`(权重×0.5),再 ≥30 天→`archived`(不删);`archived`/`decaying` 遇新证据→直接复燃 `active`。衰减扫描 `scan_lifecycle` 并入 12h `ProfileConsolidator`(缺 `last_evidence_at` 的旧 topic 永不被扫,启用不团灭)。**dislike 改「归档+避雷」**(`archive_topics` 置 `archived` 保留台账不删);**细分提议**(子类占父域权重 ≥60%)只经 `topic_subdivision_proposal` 记台账不执行(shadow)。所有跃迁进 `profile_update_ledger`(`topic_lifecycle` 写点)。 - **最小消费 + 开关(Task 8)**:新增 `[soul].topic_lifecycle_serialization`(`off`|`on`,默认 `off`)。`off` 时 `build_profile_summary` 与未接状态机前**字节不变**(回放门);`on` 时把 `archived` topic 排出 LLM 可见画像(domain/tag 两级)。进程启动由 `create_app`/CLI 读入设 `discovery.strategies._utils.set_topic_lifecycle_serialization`;trial 小流量推荐消费仍 out of scope。 - **觉察提炼节奏(Task 9)**:`cognition_cycle` 除 12h 兜底外新增**提前触发**——未提炼事件 ≥30 条 或 强信号事件(`comment/danmaku/reply` 带文本 / `feedback` / `inferred_satisfaction∈{positive,negative}`)→ 本 tick 立即跑觉察。新增**单飞锁**(`run_if_due` 见锁被持有即跳过,due-check + watermark 消费全在锁内,重叠 tick / 提前触发抢跑恰一执行);state JSON 写入 **tmp+fsync+`os.replace`** 原子化;异常/取消时 watermark 不前进(下轮重做)。同批事件 awareness prompt 输出路径不变(回放不变性)。 - **架构图**:Wave D 只在 `soul/` 内部加状态字段与触发节奏,无新模块 / 跨模块布线 / 数据流变化,架构图(Wave C 已含态势门控节点)无需改动。 - **态势门控 builder + 执行体(Task 6)**:新增 `llm/prompts.py::build_posture_gate_prompt`(静态 system——三判定 accept/downgrade/reject + 「冲突不是错误是新假设」+ `sort_keys`,入 invariance 清单)与 `soul/posture_gate.py::PostureGate`。三模式:`off`=完全旁路(门控 LLM 零调用、逐字节等价);`shadow`(默认)=commit boundary 捕获不可变快照 `{before,after,source_refs,gate_id}`,异步旁路任务只消费快照(判定前活状态再写入不污染,带断言)、**零延迟不阻塞原写入**、判定落台账 `shadow_accept/downgrade/reject`、LLM 异常落 `shadow_error`;`enforce`=同步判定,异常/解析失败/非白名单 verdict 保守 downgrade。新 caller `soul.posture_gate` 注册 usage recorder。 - **配置 + save-time 校验(Task 6)**:`[soul].posture_gate_mode`(默认 `shadow`)与 `posture_gate_force_enforce`(默认 `false`)。切到 `enforce` 需 save-time 三条件——最早有效 shadow 判定距今 ≥14 天 **且** 近 14 天有效判定 ≥10 条 **且** 近 7 天 ≥1 条,否则 blocking 拒绝(`Database.posture_gate_shadow_stats` 供数,config PUT 处理器接线);`posture_gate_force_enforce=true` 无条件放行(有风险,文档注明)。 - **三接入点接线(Task 7)**:①对话候选按 kind 分流——interest/dislike 现路径,goal/value/state 过门控(reject 丢弃、downgrade 置信×0.6 转 insight);②管线 VALUES/CORE 层 updater 写入前过门控(downgrade 固定置信 0.5 转 insight、层写入回滚;ROLE 不过门控);③soul 整份重建 diff 过门控(downgrade/reject→放弃本次 rebuild + 台账)。`off` 模式 `learn_from_dialogue` + pipeline 与现状逐字节一致(回放门)。 - **Wave B 遗留接线**:**held 重放消费端**——`ConfusionManager.pending_replays` + `SoulEngine.replay_held_updates`:resolved 真实兴趣型的 replaying held 项作为证据并入下一次偏好分析(rebase 语义,不直接写权重),成功后回调 `mark_replay_applied`(幂等);引擎构造时 `recover_replaying` 处理上一次会话崩溃残留(置 `applied_unverified` 不重复提交),反馈批后运行消费端。**代理行为折价**——疑惑 `proxy_behavior` 出口对 `evidence_refs` 关联事件调 `Database.discount_events_by_confusion`(盖 `discounted_by_confusion` + 强度折至 0.2,`real_interest` 不折)。 - **疑惑对象 + 两产生源(Task 4)**:新增 `confusions` 表(partial unique index `WHERE status='clarifying'` 跨连接原子保证 clarifying ≤1,即打扰预算)与 `soul/confusion.py`(`ConfusionManager` 状态机 `open→clarifying→resolved|dismissed|expired`)。疑惑不写画像,只驱动下游澄清与冻结。产生源一=觉察:新增独立 builder `build_awareness_with_confusions_prompt`(静态 system,入 invariance 清单)+ `analyze_with_confusions()`;既有 `analyze()`/`build_awareness_prompt` 一字不动,`cognition_cycle` 切新 API 属**有意行为变更**(新旧输出 A/B 语义等价,过质量铁律)。产生源二=推测僵局:`SpeculatorTickResult.stalemate`=expire 时 `0/verify`,验的是平台 cookie 与 LLM 无关)、embedding 修复(`/api/embedding/repair`,只碰 config + 托管 Ollama)全部 503 连坐——设置页平台源 tab 报误导性的「请确认后端服务可用」、测试连接透出原始 503、「一键启用本地 Ollama」报假的「检查后端连接」。三个端点都只依赖 config/database(降级上下文齐备),现加入白名单:降级期间用户可以边修 LLM 配置边把平台登录态配好验好。同时插件「语义去重未启用」横幅补上时机门控:仅在**后端健康且已初始化**时显示——降级时唯一正事是修配置,未初始化时 init 清单里的「向量模型可用(推荐,非必须)」行才是 embedding 信息的归宿,而「可能刷到重复视频」在没有推荐流之前毫无意义。 - **用户可见文案不再说「降级」**(用户反馈:「降级」是工程内部概念,对用户而言 LLM 坏了产品就是坏了,不存在"降级运行"):插件底部提示与初始化引导、设置浮层横幅、桌面 Web 恢复摘要、移动端状态角标、`POST /api/init` 拒绝与 init-status detail、降级下保存配置的返回消息、CLI 状态面板标题,统一改为直白的「AI 服务配置有误,修复并重启后端」措辞;代码、日志与 API 字段里的 degraded 术语保留(面向开发者)。 --- ## v0.3.178:降级新装机直达初始化引导(2026-07-20) - **插件在「降级且从未初始化」时优先走初始化引导,而非只给修复入口**(用户实测反馈):v0.3.177 的降级态把未初始化的新装机也导向「去设置修复」,但完整初始化旅程的第一步本来就是配置 LLM——现在推荐页借 `/api/runtime-status`(降级白名单内)区分「从未初始化」与「初始化过后来降级」:前者照常渲染「开始初始化」面板,前置检查清单如实显示降级阻塞原因(来自 init-status 的降级 detail),并保留「去设置修复 →」快捷按钮;后者维持纯修复态。无 runtime 快照时保守落回修复态。 --- ## v0.3.177:初始化与安装全流程加固——降级不再是死胡同(2026-07-19) - **修复降级模式下 setup 向导整页瘫痪(Base URL 填不了、配置修不了)**:来源登录态契约把平台源清单抽进 `/shared/source-status.js` 供 setup / 桌面 Web / 插件三端共用,但 degraded-mode guard 的静态恢复面白名单没跟上——降级时该脚本被 503 拦截,向导顶层 `SourceStatus.SOURCE_KEYS` 直接抛 TypeError,整页事件全灭:切换服务商无反应、OpenAI 兼容接口的 Base URL 字段永不出现,用户被锁死在无法修复配置的死循环里(真实 Windows 用户复现:deepseek 缺 api_key → 后端降级 → 向导瘫痪 → 无法改配其他 provider)。现将 `/shared/` 加入恢复面放行,并在降级测试中锁住该脚本必须可加载。 - **setup 向导不再提供「本地 Ollama」当聊天 provider**:随装的 Ollama 定位是 embedding(bge-m3),聊天模型需要用户自行 `ollama pull` 且小模型跑内容管线质量不达标,首次配置把它摆在选项里等于引导新用户走进坏体验。向导下拉移除该项(API Key 相应变为全 provider 必填);桌面设置页、config.toml、后端 provider 注册表不受影响,进阶用户仍可在设置页选 ollama。E2E 与向导测试同步迁移到 deepseek + 显式填 key 的路径,并断言下拉中不再出现 ollama。 - **本地 Ollama「不当聊天默认」的口径在所有 onboarding 面统一**:继 setup 向导下拉移除该项之后,把剩余入口对齐同一口径——`openbiliclaw init` 交互菜单、`scripts/agent_bootstrap.py` 人类安装菜单都不再把「本地 Ollama」列为聊天 provider(菜单序号相应收缩,`ollama` 仍作为来自既有配置 / 显式 flag 的合法值被解析,只是不再交互式提供);`install.sh` 的 provider 清单与「Ollama 无需 Key」提示同步剔除,并把它标注为 embedding-only;`config.example.toml` 的 `default_provider` / `fallback_provider` 注释重写为「embedding 定位、聊天属高级用法」。此外向导对**已保存但已失效的默认 provider**不再静默丢弃:当 `/api/config` 回一个下拉里没有的 `default_provider`(如 `ollama`)时,`#msg0` 给出一条 info 提示「本地 Ollama 仅用于向量检索……请重新选择一个聊天服务商」,新增 Playwright 用例锁住该提示。`install.sh` 的安装后「下一步」还补上了图形化引导页 `http://127.0.0.1:/setup/` 的指路(与 `docs/docker-deployment.md` 的宣传一致)。后端 provider 注册表、桌面设置页、`[llm.ollama]` 聊天支持一律不受影响。 - **初始化界面四项一致性修复**:(1) 初始化来源清单收口——桌面 Web 与插件侧栏此前各自硬编码一份平台列表,现统一从 `/shared/source-status.js` 的 `SOURCE_KEYS` 派生(标签优先取共享模块、本地映射兜底),并加漂移锁测试锁住三端与共享清单一致;(2) setup 向导 B 站轮询生命周期修复——离开步骤 1 时清掉 3 秒轮询并置空以便重进重启,且连续几次未检测到登录后把转圈行替换为中性可跳过提示(仍在步骤 1 继续轮询);(3) 桌面 Web 在推荐失败点识别降级 503 信封(`details.status==="degraded"`)→ 走既有模型设置恢复流程而非通用重试 UI;(4) 跨端初始化文案对齐——向导「已初始化」采用桌面口径带设置页指引,插件未初始化提示改为按钮驱动文案。移动端按四端契约有意排除。 - **初始化状态的四类失败路径不再吞掉真实原因(铁律 7)**:(1) 后端重启打断初始化后,`reconcile_init_runs_on_boot` 只把行状态翻成 `failed`,却不降级 `stages_json` 里的 running/pending 阶段、也不写 `error_detail`,于是 `/api/init-status` 报 `interrupted` 却 detail 为空、还留着一个幽灵「running」阶段;现按 `InitCoordinator.reconcile_orphaned_run` 的语义把这些阶段降级为 `failed`/`interrupted` 并写入同款中文 `error_detail`。(2) 降级模式(LLM registry 构建失败)下 `POST /api/init` 原本回裸 `{"error":"llm_not_ready"}`、`/api/init-status` 也无降级分支;现两者都识别 `llm_registry_unavailable` 降级态,回可操作文案指向设置页修 LLM 配置后重启。(3) `llm_not_ready` 从不具体——chat 探针本知道失败原因(无效 API Key / 服务不可达 / 模型不存在),现经 `describe_llm_failure` 把分类原因透传进 `POST /api/init` 的 409 detail 与 init-status reason detail,探针严格度不变。(4) `_maybe_autostart_embedding_pull` 原只处理 MODEL_MISSING/MODEL_BROKEN,托管 Ollama 未启动(DIAG_NOT_RUNNING)时静默无操作;现复用手动修复端点同款守卫先启动托管 Ollama 再重新诊断,仍保持 best-effort、非阻塞。 - **插件推荐页在后端降级时给出修复入口,不再只说「接口没回」**:降级后端对业务路由统一回 503 信封(`status:"degraded"` + 具体 issue),`requestJson` 早已把它挂在 `error.details` 上,但推荐页把一切错误揉成通用文案「后端连上了,但推荐接口这会儿没回」,可操作的事实(LLM 配置坏了、设置页能修)被完全隐藏——降级时初始化引导也不会出现,用户无路可走(真实 Windows 复现)。现在识别降级信封为独立状态:标题「AI 服务配置需要修复」+ 后端返回的具体 issue 文案 + 「去设置修复 →」按钮直达后端设置浮层(复用既有降级横幅与「保存并提示重启」模式)。桌面 Web 已有降级恢复流程(v0.3.175),移动端维持既有排除(修配置属桌面/插件场景),CLI 本就透传真实错误。 --- ## v0.3.176:来源登录态契约统一——让绿灯诚实(2026-07-19) - **八个平台的「凭据已就绪」不再一视同仁**:此前设置页把强度天差地别的判断渲染成同一个绿灯——B 站只是数出 cookie 串里有三个字段名(全程不联网)、小红书与知乎靠浏览器 72h 心跳、Reddit 只看本地文件未超 7 天(注释明说绝不联网)、X 是上次请求没报 401、YouTube 干脆是硬编码常量;抖音则即使 cookie 完全有效也永远显示「状态待验证」。用户无从分辨「真能用」与「只是填了值」。现在每个来源如实标注结论来自哪种证据(`live_probe` / `passive_health` / `browser_heartbeat` / `local_file` / `none`),并配「测试连接」按钮当场发起验证。Bangumi 作为第 8 平台一并纳入契约:它打破了「不需要登录 ⟹ 无从验证」的隐含假设——公开收藏匿名可读,但配了个人令牌就能经 `/v0/me` 真验,因此 `verify_method=live_probe` 名副其实。 - **抖音「已验证」不再一分钟就过期**:`PROBE_OK_TTL_SECONDS=60` 一个常量身兼两职——既是写入门的探针复用窗口(防 msToken 轮换招风控,60 秒正确),又被拿去驱动 UI 的「已验证」新鲜度(60 秒太短,点完测试连接一分钟后又变回待验证)。拆出独立的 6 小时新鲜窗口并按 CLAUDE.md 铁律 3 记录标定来源,加测试锁住两者不许再合并。 - **移动端有意排除,并写进规格**:手机是躺着刷推荐的场景,填 cookie、点测试连接属于持有登录态浏览器会话的桌面与插件。移动端仍经 saved-sync 透出各平台的登录需求,不会对「这个来源需要登录」失明。按 CLAUDE.md 铁律 5 的四端契约要求,该排除在指标脚本注释与 spec 里显式声明,而不是默默少做一端。 --- ## v0.3.175:海外出网默认跟随系统代理、Bangumi 身份诚实化与小红书封面修复(2026-07-19) - **修复「小红书内容都没头图」:封面 URL 与字节都在扩展抓取时采集**:用户日志复盘定位到两个叠加缺陷。(1) **后台标签页懒加载图片永不升级**——搜索/创作者任务在后台标签页刮取,卡片 `` 永远停在内联 `data:` 占位符,DOM 提取拿不到真实封面 URL(铁证:受影响后端 7-12~16 对 hdslb / ytimg / douyinpic 抓取全部正常,却从未尝试过一次 xhscdn 抓取——它手里从没有过可抓的 URL);(2) 即使有 URL,服务端预取也是与轮换 token 过期、与本机 CDN 出网的赛跑。修复三件套:扩展 `cover-harvest.ts` 从 `__INITIAL_STATE__` 形状无关深扫按 note_id 回填真实封面 URL(循环安全、深度/节点有界;DOM 两路提取一律拒收 `data:`/`blob:` 占位符);随后在页面上下文抓封面字节(token 最新鲜、走用户浏览器会话),转 base64 挂 `cover_data`/`cover_content_type` 随既有通道上报(每批 ≤12 张、单张 ≤1MB、4s 超时、best-effort);后端把非 http(s) 的 cover_url 归一为空,`save_extension_cover` 校验(白名单 / `image/*` / 大小 / base64)后写入既有 `data/image-cache/`——缓存 key 本就剥离轮换 token,serve 零改动全走缓存命中,已知候选去重的笔记也先存封面以就地治愈存量。已在隔离 serve-api 用真实小红书页面采集的真封面端到端验证(入库→落盘→原 URL 与换 token URL 均缓存命中 200)。同期补齐链路可观测性(`image_cache` 模块此前零日志,本次定位只能靠「慢取日志里 xhscdn 完全缺席、其它 CN CDN 都在」反推):`fetch_cover_bytes` 失败按 host 限频 WARNING 且 detail 携带真实上游状态码,image-proxy 失败补 DEBUG 关联行,xhs ingest 每批 INFO 汇报缓存封面数。 - **账号同步的 LLM 故障不再谎称「稍后会自动重试」**:画像分析因 LLM 不可用失败时,`last_sync_error_kind` 此前不写入,桌面 Web 只能落到通用文案「账号同步出错,稍后会自动重试」——但当真实原因是模型未配置(API Key/Base URL 被清空)或模型名不存在时,重试永远不会成功,用户被误导干等(真实用户日志复盘:provider 配置被手改清空 → 后端 degraded → 横幅仍承诺自动重试)。现在 `_persist_profile_analysis_error` 把 `classify_llm_unavailability` 的分类(`no_provider` / `model_not_found` / `rate_limited`)随错误一起持久化,`_user_facing_sync_message` 为前两类渲染指向设置页的可操作文案(「修复后会自动恢复」),`rate_limited` 如实保留自动重试承诺且 severity 降为 warning。文案仍由后端统一计算;**本次仅桌面 Web 消费该横幅**,扩展 popup / 移动 Web / CLI 不展示此状态。 - **未初始化时根路径直接引导进 setup 向导**:`GET /` 原本无条件 302 到 `/web`,只有打包版启动器(packaging/entry.py)会在首启时判断初始化状态并打开 `/setup/`——git / Docker 安装没有启动器,手动访问端口的用户在降级或未初始化时落到桌面 SPA 而非配置向导。现在服务端复刻 `_decide_landing_path` 的规则:后端处于 degraded 模式、或画像未初始化(`is_profile_ready()` 明确为 False)时 `/` 302 到 `/setup/`;就绪或探测结果未知时保持 `/web`(SPA 的引导初始化卡片仍是安全网)。仅当 setup 静态目录存在时才改道。 - **修复 LLM 配置失效时访问 Web 只看到原始 503 JSON**:degraded-mode guard 原本只放行移动端 `/m`,却在静态路由执行前拦住 `/`、桌面 `/web` 及首次配置 `/setup`,所以浏览器直接显示 `llm_registry_unavailable` 响应,用户反而进不了可修复配置的页面。现精确放行四个静态恢复 surface 及其资源,业务 API 仍保持 503;provider-free 的 `GET /api/ping` 仅在降级时附带 reason / issues,桌面 Web 先用它识别恢复态,跳过推荐、画像、平台源等必然失败的 hydration 请求,再读取配置并自动打开「模型」设置,以中文说明补齐 Provider 的 API Key / 模型 / Base URL,保存后提示重启后端。正常模式的 ping 响应保持兼容。 - **修复惊喜推荐文案渲染成结构化数据**:模型偶尔把整批结果塞进单个字段(如 `{"expression": [{...}, {...}]}`),而各处消费点都用 `str()` 无条件转换,于是 Python repr 通过了非空校验并作为推荐文案落库,用户在惊喜卡正文看到 `[{'expression': ..., 'topic_label': ...}]`。全程无任何日志,因为解析"成功"了——只是值不对。现对五个会持久化 LLM 文本的源头统一加类型守卫:推荐文案单条/批量、推荐分类(`reason` / `topic_group`)、发现分类单条/批量(`reason` / `topic_group` / `franchise_key`,经 `relevance_reason` 流向惊喜文案)。发现侧暴露面更大——`_clamp_score()` 遇到坏 score 返回 `0.0` 而非抛错,没有任何其他机制能拦住 repr 化的 reason。`update_pool_copy()` 另加一道基于解析的兜底(而非前缀匹配,后者既会漏 JSON/空白变体又会误伤含代码的正常文案)。**行为变更**:非字符串 `reason` 现在判为分类失败,不再持久化 repr。存量清洗脚本见 `scripts/clean_serialized_pool_copy.py`(默认 dry-run;已推荐的行写确定性 fallback 文案而非置空,因为待补文案查询会跳过已有 recommendation 的 bvid,置空只会让卡片变成空白)。 - **修复 B 站 Cookie 过期只显示英文报错**:桌面端本就有中文「请重新登录」分支,但 `last_sync_error_kind` 从引入起就被 `memory/manager.py` 的两处 key 白名单和默认状态静默丢弃,读盘后恒为空,UI 永远落到兜底分支直出 458 字符的英文原文。同一原因还被重复拼接三遍——history 阶段打 `/x/web-interface/history/cursor`,favorites 与 following 各自调一次 `get_nav_info()` 取 mid,同一个 -101 抛了三次,现已去重。Cookie 过期是预期的生命周期事件而非故障,因此改由后端渲染用户文案(原始英文保留在诊断字段),桌面按 warning 而非红色 danger 呈现,时间戳复用本地化格式化器(`formatUpdateCheckTime` → `formatLocalTime`)。**本次仅覆盖桌面 Web 与 API**,扩展 popup、移动 Web、CLI、OpenClaw 适配器暂不展示该状态。 - **修复账号同步并发覆写**:`sync_now()` 无互斥,而后台 `run_forever()` 循环与 OpenClaw 手动触发可同时进入,两者加载同一份状态、后写者覆盖对方的游标、错误串与 error kind。单纯的 `asyncio.Lock` 不够——API daemon 与独立 OpenClaw adapter 各自基于同一数据目录构造服务实例,实例锁互斥不到;且普通锁只是让第二个调用排队后再跑一遍冗余同步。现改为进程内共享 in-flight task + 状态文件旁的非阻塞 OS 文件锁,抢锁失败方返回 `already_running`,持锁进程崩溃由内核释放;`sync_if_due()` 在加入 in-flight 后重查游标,关闭 TOCTOU。 - **Bangumi 作者字段落地(原「作者字段恒空」限制解除)**:`bangumi_subject_to_content` 改为从 subject 行内 `infobox` 解析 `author_name`。两个 discovery 端点(`POST /v0/search/subjects`、`GET /v0/subjects`)本就内联返回完整 Subject,2026-07-18 实测 250/250 行都带 `infobox`,且 `GET /v0/subjects/{id}` 没有多出任何字段——因此制作方署名是**零额外请求**拿到的。Bangumi 没有统一作者字段(书籍叫「作者」、动画叫「导演」、游戏叫「开发」),故按 SubjectType 走优先级阶梯(书籍 作者→原作→作画→出版社,动画 导演→原作→动画制作→製作,音乐 艺术家→作曲→厂牌,游戏 开发→发行→游戏开发商,三次元 导演→编剧→主演),阶梯顺序由同一次 250 行实测的各 key 填充率标定并连同复标定要求写进代码注释。兼容官方三种 `value` 形态(裸字符串 / `[{v}]` / `[{k,v}]`,后两者只读 `v`,`k` 是「总导演/副导演」这类子标签而非人名),同 key 重复取首个非空值,多名字最多留 3 个、渲染长度硬限 80 字符且切在名字分隔符上(不会断在半个名字或未闭合括号里);`infobox` 缺失、类型漂移、条目非映射、阶梯 key 全缺一律解析为 `""`,杜绝历史上 `COALESCE` 无法自愈的字面量 `"None"` 脏行。**遗留限制**:用户收藏 `GET /v0/users/{username}/collections` 内嵌的是 SlimSubject,不带 `infobox`,经该路径进来的条目 `author_name` 仍恒为 `""`(已记入 mapper docstring 与 `docs/modules/bangumi.md`「已知限制」)。 - **修复 Bangumi 扩展身份被前端护栏堵死(GUI 三面里没有一面能用)**:packaged setup、桌面 Web 与扩展 popup 各带一份逐字相同的前端前置判断——"仅选 Bangumi 且没填用户名/令牌就直接拒绝、不发请求"。这份拷贝早于三级账号阶梯的第三级(浏览器扩展在已登录 bgm.tv 页面上报的身份),看不见它:真机同后端进程、同 payload 实测,GUI 操作 `/api/init` 实际请求数为 **0** 并报"请填写个人令牌或公开用户名",而同一 payload 直打后端返回 **202** + `warnings:["Bangumi 使用浏览器扩展识别到的账号 sai。"]`——正是铁律 5 的四面契约漂移。修法是把准入判定交回后端:setup 与桌面 Web 两处判断删除(后端在三级全空时才 `409 no_profile_signal_sources`,实测响应可读且已被两面如实渲染),三端 reason 文案同步补上"或先在浏览器登录 bgm.tv 让扩展自动识别账号"(原文案只提令牌和用户名,删掉判断后仍会误导)。Playwright 真页面回归(setup + 桌面参数化)钉住"仅选 Bangumi、不填任何凭据时 `/api/init` 必须真的发出",回插旧判断后 4 条全部超时失败。**遗留**:`extension/popup/popup.js` 的同款判断(含内联拒绝文案)本轮未动,`tests/test_bangumi_web_surfaces.py` 有一条 `strict=True` xfail 钉住,修好后连 xfail 一并移除。 - **Bangumi 身份校验 fail-open 改为诚实 fail-open**:`_verify_bangumi_identity` 此前在任何异常下原样持久化未经校验的 DOM 上报用户名,且只打 **DEBUG**;而这条护栏当初正是为了防"时间线陌生人被抓成自己"。该结论标定于本节「海外出网默认值 `direct` → `system`」那条之前:当时默认 `[network] mode = direct` 连不上 bgm.tv(海外 CF),因此它对"零配置、不用令牌"这批目标用户**永远是空的**(默认改为 `system` 后,有可用系统代理的机器会真的跑到校验,没有代理的仍然走 fail-open)——隔离后端实测:`POST /api/sources/bangumi/identity {"uid":123456,"username":"sai"}` 这组胡诌配对返回 `{"ok":true,...}` 并落库,全程零 WARNING。fail-open 本身保留(fail-closed 会让国内零配置用户直接不可用),改的是诚实性:(1) 校验失败 DEBUG→**WARNING**,带真实原因(`bangumi identity: could not verify uid=… username=… against bgm.tv (timeout: …); storing the extension report UNVERIFIED`,铁律 7);(2) 记录改写为 `{uid, username, verified}`,`verified` 仅在 bgm.tv 给出确定答复(2xx / 404)时为 true,接口响应同步回传;(3) guided init 与 CLI init 用到未校验身份时文案说实话——"(未经 bgm.tv 校验,可能不准)"并给出改用用户名/令牌的出路,已校验的保持原文案。**向后兼容**:旧记录无该键,一律按未校验读取(无法证明校验跑过),下次访问 bgm.tv 页面重报即就地升级;CLI `_load_extension_bangumi_username` 随之改名为 `_load_extension_bangumi_identity` 并返回 `(username, verified)`。 - **`verified` 标记改为对同一身份单调(sticky-true)**:对抗式 review 抓出上一条自己会擦掉证据——`_persist_bangumi_identity` 无条件用本轮结果覆盖,于是「第一次校验成功存 `true` → 第二次上报赶上 bgm.tv 超时」会把已拿到的校验证据改写成 `false`,guided init/CLI 随即对用户的真实账号谎报「未经 bgm.tv 校验,可能不准」,网络抖动还会让标记来回翻、每翻一次写一次盘。成功的交叉校验是关于某个 uid↔username **配对**的证据,不因我们后来连不上而失效,所以标记只允许往上抬:本轮成功→`true`;本轮失败但已有记录 uid 与 username 与本轮**完全相同**且已是 `true`→保持 `true`;uid 或 username 任一不同→是另一个从未验证过的主张,按本轮结果写。合并跑在 `update_discovery_runtime_state` 回调**内部**(`update_json_state` 先取进程锁+文件锁再从磁盘重读,回调看到权威最新值),并发上报不会互相覆盖;回调外那次读取只用于「确无新信息就跳过加锁与写盘」的幂等快路径。`POST /api/sources/bangumi/identity` 改为回传**落库后**的 `verified`,响应不再与读回的记录自相矛盾。 - **infobox 解析兑现「绝不落库字面量 None」的承诺**:同一轮 review 指出 `5a117d96` 的说明确有夸大——`_infobox_value_text` 当时把裸字符串**完全透传**(源数据里的 `{"key":"作者","value":"None"}` 就真的写成 `author_name="None"`,正是我们声称杜绝的那类脏值),且对列表元素的 `v` 无条件 `str()`(schema 漂移的 `{"v":["押井守"]}` 被制造成字面量 `"['押井守']"`)。现在 `v` 非字符串一律跳过、不做 `str()` 兜底;新增 `_is_placeholder_credit` 把「只是拼写出缺席」的值归一为 `""`:空串、语言级 null 字面量(`none`/`null`/`nil`/`nan`/`undefined`)、无歧义的 `n/a` 形式、以及纯标点(`-`/`——`/`/`/`?`/`()`),裸字符串与两种 list 形态都过这道。边界刻意收窄,**不**过滤裸 `na`(可能是罗马音姓氏)、`无`/`未知`/`暂无`/`不明`(普通汉字,可能出现在真名里,属编辑散文而非序列化产物)、单字母与数字(可能是艺名)——误杀会静默删真实数据,理由连同名单写进代码注释并有测试钉住。顺带修掉截断在未闭合括号处收尾的问题(`…(総監督` → 回退到括号前),并把「整条 credit 就是一个长括号块时保留硬切、不抹掉真实署名」这一取舍显式写成测试。 - **收紧 `verified` 的定义:答复 ≠ 确认**:`verified: true` 现在**仅**表示 bgm.tv 正面确认了这个 uid↔username 配对(`get_user` 成功、`id` 与上报 uid 一致、用户名非空)。此前 404 与 uid 不匹配也记 `true`——上报 `{uid: 999999, username: "does-not-exist"}` 会落库 `{"username":"","verified":true}`,可我们从未确认过这个 uid 属于谁,bgm.tv 只是否定了用户名;叠加 sticky-true 后,同 uid 的后续上报遇到网络失败会把这个从未确认过的身份永久钉成 `true`。现在 404(无论有无上报用户名)、uid 不匹配、以及匹配成功但用户名不可用,一律 `false`;404 清空用户名的行为不变。逐路径取值表已写进 `docs/modules/bangumi.md`。 - **删除锁外快路径:响应不再可能落后于磁盘**:`_persist_bangumi_identity` 原先在锁外读一次状态,若看着"没变化"就直接用那份快照的 flag 提前返回。并发请求在这中间完成校验并在锁内写成 `true` 时,前一个请求会用过期快照回 `false`——磁盘 `true`、响应 `false`。`load_discovery_runtime_state` 每次从 JSON 重建独立对象,与回调内那份不是同一引用,所以两者确实会分叉。现在**总是进原子段**,合并与取值只在回调内发生。代价是重复上报同一身份多一次幂等重写(`update_json_state` 无条件写盘):接受,因为不加锁判断"没变化"只能依赖一份随时可能被作废的快照。原并发测试用空字典强制进锁,恰好绕过了这条路径,已改为让锁外视图与锁内真值**故意不一致**来钉死它。 - **落库失败不再回传幻影状态**:上一轮"响应回传落库值"的说法在异常路径不成立——磁盘写失败、或运行时没有 memory manager 时,仍会回一个 `{"ok":true,...}` 携带没能存下的 flag,下一次读取直接打脸。现在响应体**整体读自实际写入的记录**;写不进去(异常或无 memory manager)则返回 **500** 并带可诊断文案(铁律 7),扩展本就把上报当 best-effort,下次 bgm.tv 页面访问会重报。三种情形各有测试:写异常 + 本轮成功、写异常 + 磁盘已有 `true`、无 memory manager。 - **占位符与括号回退两处解析修正**:(1) 从占位符表里去掉 `nil`——它是真实存在的日本摇滚乐队名,且 Python/JS/JSON 都产不出这个拼写(那是 Ruby/Lisp),本就不属于"我们会造的脏值";音乐条目 `{"key":"艺术家","value":"nil"}` 此前会被抹成空。(2) 括号回退改为**同族配对**:`(credit]` 里的 `]` 不再无条件弹出 `(`,否则截断结果仍带未闭合的 `(`——上一条 commit 说明里"回退所有非零位置未闭合括号"因此并不成立。 - **旧版 `verified` 坏记录不再被继承,并在读取侧自愈**:上一版的 404 路径写下过 `{"uid":…,"username":"","verified":true}`。升级后再次上报同一个 404 用户,新校验正确得出 `false`,但 sticky 继承比较 uid 与 username 时两边的 username 都是 `""`、判定为「同一身份」,于是继承了旧的 `true`——这条记录违反刚立的「`verified` ⟹ username 非空」。现在继承额外要求旧记录在现行规则下合法(username 非空);同时 `_load_bangumi_identity` 与 CLI 的 `_load_extension_bangumi_identity` 在**读取**时把「空用户名 + `verified:true`」归一为未确认,因此不依赖用户再次访问 bgm.tv 触发覆盖写。 - **纯标点/符号的作者名不再被当作占位符丢弃**:`・・・・・・・・・`(全部 U+30FB)是真实偶像组合名,`!!!` 是真实乐队(chk chk chk),「真实名字至少含一个字母或数字」这条判据把两者都清空了——而更粗糙的旧手写表反而保住了它们。这是过滤范围第三次放大、第三次删掉真实数据(前两次:手写表漏 `…`;为多抓 null 加入 `nil` 而误删同名日本摇滚乐队)。代价不对等:纯标点作者名只是卡片观感怪,删错真实艺名是数据丢失。因此过滤范围收回到**本栈真能产出的 null 字面量**(`none`/`null`/`undefined`/`nan`,对应 `str(None)`、JSON/JS `null` 与 `undefined`、`float('nan')`),标点与符号一律不再判断;`n/a`/`(none)`/`` 同步移出(编辑散文,与已保留的 `无`/`未知` 同类)。三次误杀的经过写进代码注释,作为「不要再加判据」的负面知识。 - **令牌入口补上「为什么填」和「不填会怎样」**:五处会问 Bangumi 令牌的界面(setup 引导页、桌面 Web 设置页与初始化面板、扩展 popup 设置页与初始化面板)此前只说「约 1 年有效,视同密码保管」,看不出这东西值不值得去弄,也没说清令牌 / 公开用户名 / 已登录 bgm.tv 让扩展识别是**三选一**——本轮修的准入 bug 正源于这层关系没讲明白。现在五处都加了同一句取舍说明:个人令牌最完整(自动识别当前登录账号,可读私密收藏);公开用户名次之(只读公开收藏);两者都留空时,只要浏览器已登录 bgm.tv,扩展会自动识别账号(只拿到账号名,可能未经校验)。popup 沿用其对话式语气,桌面 / setup 保持说明式。新增 `test_every_token_field_explains_the_three_ways_to_supply_an_account`:五处逐一断言点到三条腿及各自取舍(私密收藏 / 公开收藏 / 未经校验),任一处漏写即失败(铁律 5 的漂移防线)。移动 Web 没有平台源设置页,不在范围内。 - **「已存凭据但来源未启用」不再静默**:真机实测发现的空档——用户在设置页粘贴个人令牌,后端经 `/v0/me` 校验通过并把用户名回填成 `215952`(一路都像"配好了"),但没勾启用开关;此时 `/api/sources/status` 只回 `state=disabled` + `detail="Bangumi 来源未启用。"`,`token_state` 还因为 `bangumi_source_status` 在 `if not enabled` 处**早于** token 计算就 return 而恒为 `""`,于是三端都拿不到任何线索,用户以为只差等待。现在 disabled 分支同样计算并下发 `token_state`,`detail` 点名已存的是哪种凭据、它当前不会被使用、以及只差哪一步:有令牌 → 「已保存个人令牌,但它现在不会被使用;把 Bangumi 来源开关切到「启用」并保存后才会生效。」,只填公开用户名 → 同句式(`token_state` 仍缺省,用户名不是令牌),令牌已被拒 → 指向重新生成并同时提醒启用,两者皆无 → 保持原文案不变。**`state` 语义不动**(仍是 `disabled`,它跟踪的是发现运行状况),所以桌面 Web 与扩展 popup 依旧按中性灰渲染"来源未启用",只有 `token_state=rejected` 才走既有红色警示——待启用不是出错。**零前端改动**:两处渲染器本就逐字显示后端 `detail`,把话说清楚的责任留在后端,不给三端新增 per-platform 分支(铁律 5)。文案刻意不含"未启用"三字——两个渲染器都会自己前置状态标签(popup 还额外加 `(未启用)` 前缀),重复写会让同一行出现三次"未启用",该约束由测试钉住。 - **新增取令牌分步文档,UI 只链过去**:`docs/modules/bangumi.md` 新增「获取 Bangumi 个人令牌」一节——先登录 bgm.tv、在 demo 页生成、令牌只完整显示一次、约 1 年有效、视同密码不要外传,以及过期后的实际表现(保存时经 `/v0/me` 当场拒绝;已保存令牌被拒则降级匿名并置 `token_state=rejected`,桌面与 popup 状态区显示「令牌已失效」)。**刻意不把外部页面的操作步骤写进 UI**:那是 bgm.tv 自己的页面,改版后写死在界面里的说明会过时且误导,因此文档里注明「以 bgm.tv 实际页面为准」,UI 只放一个「取令牌步骤」链接。文档标题上方留了注释说明五处 UI 链接该锚点,测试同时校验锚点存在与该节覆盖前置条件 / 有效期 / 失效表现。 - **海外出网默认值 `direct` → `system`**:`[network].mode` 的**内置默认值**改为 `system`。本节列的全是纯海外服务(LLM SDK、YouTube、Bangumi、GitHub 更新器、Codex OAuth),`direct` 下国内网络必然超时,而这是开箱默认值——新用户配好令牌、启用来源,然后撞上一句没头没尾的网络错误,机器上明明已有可用代理。`system` 即 `trust_env=True`,读环境变量 `HTTP(S)_PROXY`,macOS 上还读系统偏好设置里的代理;**没配代理时 `system` 与直连完全等价**,所以海外用户零影响。**只有「从没写过」才吃新默认值**:`_build_network_config` 按 `mode` **键是否存在**判定,不看解析后的值——显式 `mode = "direct"`(含 `OPENBILICLAW_NETWORK_MODE=direct`,env override 注入的是同一张表)照旧直连,因此凡是通过设置页保存过配置的用户磁盘上已有显式 `mode`,升级后行为一律不变;受益的是全新安装与从未配置过 `[network]` 的老配置。非法值(未知模式、`custom` 但 `proxy` 为空)仍回退 `direct` 而非新默认值——用户确实写了东西,不该因为写错就悄悄开始继承环境代理。**国内直连隔离不受影响**:B站 / 抖音 / 小红书 / 知乎 / Ollama / localhost / 国内 CDN 图片全部硬编码 `trust_env=False`,从不读取该策略;国内大模型网关豁免同样按 endpoint 生效。`tests/test_network_proxy_isolation.py` 新增 `system` 模式下的隔离守卫——此前的守卫只钉 `custom`(泄漏表现为多出一个代理 URL),而 `system` 的泄漏更安静:不出现任何 URL,客户端只是翻成 `trust_env=True` 开始听环境变量,既然这已是全新安装拿到的模式,就直接钉死它。`config.example.toml` 同步改为 `mode = "system"` 并写明理由(例子文件此前显式写 `direct`,照抄即绕过新默认值)。**「缺 mode 即 system」三条支路一并对齐**:`PUT /api/config` 与 `POST /api/config/probe-service` 收到只带 `proxy` 的 payload(旧版 UI、第三方客户端)时不再兜底 `direct`,与磁盘缺键走同一判定——非空 `proxy` 仍是 `custom`,清空则落 `system`;探测按真实策略跑,不再报告一个运行时根本不会用的 `direct`。`create_app` 镜像配置到进程级策略时的 `getattr` 兜底同样改为 `system`:残缺 config 对象没有可尊重的用户值,属「从没配过」而非「写错了」,且 `direct` 并非中性选项(它主动 `trust_env=False` 覆盖用户环境,`system` 才是顺从),配置坏到说不出偏好时顺从优于覆盖,也避免已经降级的启动再多长一个查不出来的超时。`network.py` 模块级 `_outbound_mode = "direct"` **刻意不动**并加注释说明:那是 import 到首次 `set_outbound_proxy` 之间的预初始化哨兵(两个入口都在 `load_config()` 后、任何消费者构造前就镜像,窗口内无人发请求),不是配置默认值,且与 `set_outbound_proxy` 的 URL-only 契约(空 URL ⇒ direct)一致。 - **推荐卡片不再把非 B 站的内容 ID 叫「BV号」**:`bvid` 是通用标识列而非 B 站专属列——`bangumi_subject_to_content` 把 subject id 存进 `bvid`,于是 `openbiliclaw recommend` 对 Bangumi 条目打印 `BV号 8`(8 是 bgm subject id)。改为与作者行同款处理(`_content_id_row`):bilibili 出「BV号」,其它来源出「内容 ID」,缺省 / 未知 `source_platform` 回落 bilibili 以保住存量数据的原标签。 - **需要海外出网的来源,在配置页直接说出来**:改默认值救不了显式配了 `mode = "direct"` 的用户,他们照旧配好令牌、启用来源、撞上一句不提代理的网络错误。现在 `GET /api/sources/status` 的每个来源多带 `requires_overseas_network` 与 `network_hint`,后者是**后端写好的成品文案**,只在 `mode = direct` 时非空;桌面 Web 设置页与扩展 popup 设置页各自只做一件事——非空且该来源已启用就原样渲染成一条警示,**两端都不认识任何平台名,也不自己读 `[network].mode`**,加一个海外平台是改后端一行。海外桶经逐平台核实为 bangumi / youtube / twitter / reddit,但只有 bangumi / youtube 的出网真由 `[network].mode` 掌管(X 走 `twitter_cli`+curl_cffi、Reddit 走 `rdt`/OpenCLI 子进程或浏览器插件,而 `direct` 从不清 `HTTP(S)_PROXY`),所以后两者拿到的是「改这个设置修不好它,请确认系统代理本身可达」的措辞,而不是一句修不好问题的建议(铁律 7)。CLI 侧不另写一份判断:提示直接挂在 `BangumiClient` 的失败点上,`discover-bangumi{,-ranked,-latest}` 三条 smoke 与 API / discovery 链路一起受益。设置页里「仅作用于海外 AI 服务 / YouTube / 更新检查」那句同时改掉——它漏了 Bangumi,正是「读完整个网络设置仍不知道自己的来源连不上」的来源。新增 `tests/test_source_network_hints.py` 防漂移:清单只有后端一处、两端不含平台名分支、文案零副本。 - **Bangumi 正式接入平台来源契约体系(第 8 个 provider)**:合入时它走的是过渡态——`_bangumi_status_item` 以 `auth=None` 进状态端点,所以设置页没有「测试连接」按钮、没有证据徽章,`POST /api/sources/bangumi/verify` 还 404。现在它有了真契约。**它打破了 `auth_required` 布尔的隐含假设**:前七个平台要么必须登录、要么(YouTube)无凭据可验,而 Bangumi 是第三种——公开收藏 / 排行**匿名即可发现**,但配了个人令牌就能验证令牌。解法:`auth_required` **恒为 `False`**(你从不「需要」登录 Bangumi,无令牌时就是 YouTube 的形状、零告警,满足「匿名可读是正常状态」);配了令牌时 `credential=present` + `verify_method=live_probe` + `can_verify_now=true`,`verification` 读共享探针缓存的 `GET /v0/me` 结论——从未验→`unverified`,令牌被拒(`unauthorized`)→`failed`,通过→`verified`,网络/超时/限流→`unverified`(indeterminate,绝不 `failed`)。出网走 `outbound_httpx_kwargs()` 代理策略,本机自定义代理到不了 api.bgm.tv 时如实报 indeterminate 而非误判令牌失效。控制实验(§0.1 / I3):2026-07-19 实测 `/v0/me` 真令牌→`username='215952'`、伪造/无令牌→`unauthorized`,判据是两组之间有差异,故 `live_probe` 名副其实。`legacy.py` 一致性检查放宽一处——原「`auth_required=false` 不得带 live 方法」收紧成「且 `credential='none'` 才禁止」,可选凭据验证是诚实而非过度声称,YouTube 的过度声称仍被拦。`CredentialSpec` 新增 `opaque_credential`,把整串令牌(而非 cookie 字段名)作为探针缓存指纹。令牌写入仍走 config / init 表单(`kinds=()` + `form_kind='none'`,表单只给「测试连接」和「去获取令牌」链接),不经统一 `/credential` 端点。**如实记录的契约缺口**:因 `auth_required=false`,前端把 Bangumi 渲染成「无需登录」并抑制证据徽章,所以令牌的 `verified`/`failed` 虽如实写进契约字段,却不会以常驻 ◆ 联网验证 徽章出现——令牌结论目前经「测试连接」消息与 `token_state` 徽章暴露;要把它做成常驻徽章需给契约加一档「可选凭据」并改前端,本次零前端改动故未做。**零前端改动**:共享模块早已认 Bangumi,补上契约后徽章与按钮自动出现。新增 4 个冻结 case(无令牌 / 有令牌未验证 / 令牌已验证 / 令牌+停用)与四条 verify 复现(有效→verified、伪造→failed、无令牌→indeterminate、network_error→indeterminate),冻结断言新增 `token_state` 轴。 --- ## v0.3.174:Bangumi 平台来源(2026-07-18) - **新增第八个正式内容来源 Bangumi**:使用官方 `v0` API 匿名只读直连,`BangumiDiscoveryProducer` 以 `search / ranked / latest` 三分支、逐日预算、cursor、最小间隔和 `Retry-After` 冷却接入统一关键词与 `discovery_candidates → shared LLM eval/admission → content_cache` 主链;`sort=date` 在三端明确显示为“按日期浏览(可能含未播条目)”。用户显式提供公开用户名后,可把公开收藏转换为统一事件参与 guided init;Bangumi-only 初始化缺用户名会在预留 run 前拒绝,混合来源则仅跳过该画像分支并返回 warning。 - **目录评分不再冒充社交互动**:新增通用 `rating_score / rating_count / source_rank`,与 Bangumi 真实收藏人数一起贯穿 `DiscoveredContent`、待评估池、正式缓存、推荐/惊喜 API、桌面/移动/扩展卡片;排名按原始序号 `#N` 展示,不套互动人数的万/亿缩写。canonical identity、点击 URL 与保存 identity 均识别 `bangumi/bgm`、`bgm.tv/subject/` 与 `bangumi.tv/subject/`,封面代理白名单加入 `lain.bgm.tv`。 - **三端配置、状态和只读诊断齐备**:`[sources.bangumi]` 支持开关、公开用户名、条目类型、分支预算、节流和 bootstrap 上限,候选池 share 默认 `1`;桌面 Web、扩展设置/初始化与 packaged setup 同步接入。新增 `fetch-bangumi`、`discover-bangumi`、`discover-bangumi-ranked`、`discover-bangumi-latest` 只读 smoke,以及正式 `discover --source bangumi [--force]`;状态页纯本地读取,不因打开设置访问上游。匿名基础路径不收 Cookie、不调用 Bangumi 站内写接口(可选令牌与扩展身份识别见下方条目)。设计与验收见 `docs/plans/2026-07-17-bangumi-source-{spec,plan}.md`,模块说明见 `docs/modules/bangumi.md`。 - **Fable review 收口运行边界**:显式 `discover --source bangumi` 不再被后台 scheduler 总开关误判为 disabled,并补全 disabled/no-profile 指引;每日预算改为跨分支去重与最终 limit 后按实际保留候选扣账,空白 keyword claim 直接标 failed,429 则 rollback;本地状态读取不再为 cooldown 临时构造 producer,匿名来源的 legacy `logged_in` 改为表达本地 ready 状态而非简单复制 enabled。复核进一步补上 browse total 缩小时超界 cursor 的 400 自愈、搜索第二词限流时保留首词候选、small-limit 下 subject type 持久化轮转、guided init 显式空用户名覆盖旧配置且三端草稿保留、设置页完整五类型,以及目录评分字段仅在非零时进入 evaluator prompt,避免改变存量平台模型输入。 - **新增 Bangumi 个人令牌(Personal Access Token)认证通道**:`[sources.bangumi].access_token`(生成地址 https://next.bgm.tv/demo/access-token ,约 1 年有效,视同密码保管,日志只记存在与否/长度)。提供令牌后,guided init / `fetch-bangumi` 经 `GET /v0/me` 自动识别"你是谁"并以 `Authorization: Bearer` 读取本人收藏(含私密);显式用户名与 `/v0/me` 不一致时以 `/v0/me` 为准并告警。令牌经 `/v0/me` 校验通过后才写入 config(坏/过期令牌当场拒绝并回传真因),无令牌时匿名公开用户名老路径完全不变。CLI 新增 `init --bangumi-token`、`fetch-bangumi --token`;guided init `source_options.bangumi.access_token` 白名单打通扩展 popup、桌面 Web、packaged setup 三个 GUI surface(含错误文案映射),令牌通道满足完整 four-surface 契约。后台发现链路的 token 在遇 401/403 时降级为匿名公开发现并给出清晰诊断,绝不静默吞掉。`trust_env=False` 与只读边界保持不变。 - **新增 Bangumi 扩展自动识别通道(零配置主推路径)**:浏览器扩展新增 `*://*.bgm.tv/*`、`*://*.bangumi.tv/*` host permission 与两段内容脚本(**Chrome 商店权限披露需同步更新**)——MAIN-world 桥读取页面公开 `CHOBITS_UID`(>0 即已登录;登出绝不上报),isolated 脚本从导航栏 `/user/` 链接解析用户名,经 `POST /api/sources/bangumi/identity` 持久化到 `discovery_runtime_state["bangumi_self_info"]`(非正整数 uid 422 拒绝、非法用户名当缺失)。guided init 与 CLI init 的账号解析统一为三级优先:令牌 `/v0/me` > 显式/已配置用户名 > 扩展上报用户名 > 报错,命中扩展身份时明示来源。真机 E2E 抓出并修复一处 plausible-but-wrong:泛化 avatar 兜底选择器会在匿名首页把时间线路人用户名当成本人——client 侧删除泛化兜底只留本人专属导航区,后端持久化前经匿名 `GET /v0/users/{username}` 权威比对 `id == uid`(不一致/不存在 → 只存 uid 丢弃用户名并 WARNING;网络失败 → best-effort 接受待下次复校;实测未设自定义 slug 的用户 `username == str(uid)`,uid-only 上报亦可解析)。Bangumi 内容脚本只做身份识别,不采集浏览行为、不碰 Cookie;uid/用户名均为公开资料。 - **个人令牌可在设置页配置(补齐初始化外的入口)**:桌面 Web 与扩展 popup 设置页新增 Bangumi「个人令牌」password 输入框 + `生成个人令牌` 链接,此前只有初始化页能填令牌,用户初始化后想开启私密收藏只能改 `config.toml`。GET `/api/config` 新增 `access_token_set` 布尔(只报是否已配置,绝不回传明文);设置页据此显示「已配置(留空保持不变)」占位,仅当用户实际输入新令牌时才发送 `access_token`,留空保存不会误清空已存令牌。凭据状态卡文案同步更正(不再宣称"不保存 token")。四端令牌入口(init popup/setup/桌面 + 设置页桌面/popup)齐备。 - **Fable review 二轮收口**:`_subject_tags` 对非列表 `meta_tags`(schema 漂移的裸字符串/字典/标量)不再逐字符拆分,仅遍历真正的数组;`BangumiDiscoveryProducer` 在所有启用分支都因当日预算耗尽而完全未发起请求时返回顶层 `reason=budget_exhausted`(区别于真正跑通却为空的 `empty`),`mode_results` 逐分支如实记账,`discover-bangumi`/正式 discover 文案随之改为"今日预算已用完"。扩展 popup、桌面 Web 与打包 setup 三端 guided-init 统一 omit-vs-clear:仅当用户手动编辑、或在成功 `/api/config` prefill 后显式清空用户名时才发送 `username`(清空即发 `""` 覆盖配置),prefill 失败/未完成/字段从未触碰则省略该字段,避免用空值误删已配置的 Bangumi 用户名;三端同时读取并按现有状态/提示样式安全渲染 `/api/init` 202 的 `warnings`(如未填用户名的 discovery-only 提示),不再静默丢弃。`fetch_bangumi_public_collection_events` 对正常 bootstrap 按 50 行请求、较小全局 `limit` 不超过目标量,并按 lane 缓存富余行复用;在保持 per-scope 公平份额、去重、限速、终止与不过量导入的前提下,用较大的缓冲分页替代默认 `per_pair=20` 造成的大量小页请求。 - **令牌拒绝状态持久化 + 设置页保存 /v0/me 校验 + 清除入口(令牌登录态可见性三缺口)**:(A) 发现链路 401/403 降级时把拒绝标记持久化到 `bangumi_discovery_state`(`token_rejected` 行,`note` 存令牌 SHA-256 前 12 位**指纹**+ISO 时间戳,绝不存明文),重启后同一令牌(指纹未变)直接走匿名不再重复吃 401,换新令牌先试用、成功即清标记;`/api/sources/status` 新增 `token_state`(`ok`/`rejected`/未配置缺省),`rejected` 时 detail 明写"个人令牌已被拒绝(可能过期)…请重新生成",桌面 Web 与扩展 popup 状态区渲染红点/"令牌已失效",凭据卡追加失效提示。(B) `PUT /api/config` 收到新的非 masked 非空 `access_token` 时镜像 init 语义经 `/v0/me` 校验——401→400 `invalid_bangumi_access_token`、网络/上游失败→502 `bangumi_token_check_failed`,绝不静默接受坏令牌;成功才写入令牌+`/v0/me` 用户名并清除拒绝标记;masked echo/省略 key/其它配置保存零网络。(C) 桌面设置页与扩展 popup 设置页各加「清除已保存的令牌」勾选控件,勾选后本次保存显式发送 `access_token:""` 清空令牌并清除拒绝标记,不破坏"留空=保持不变"语义。指纹只在本地 SQLite,不涉及明文,隐私边界与商店披露不变。 - **审计缺口收尾——文档披露、init 放行与封面路由**:主页 `docs/index.html`(CN/EN i18n 三处)、Chrome 商店 listing 与隐私政策同步 bgm.tv/bangumi.tv host permission 真相——扩展仅做账号身份识别(读公开 uid + 用户名),不读 Cookie、不采集浏览行为、不传令牌(既有"不新增 host permission / 不保存 token"文案已纠正);init 写保护 allowlist 精确放行 `POST /api/sources/bangumi/identity`,让 guided init 当轮就能拿到扩展刚上报的身份(三级账号解析最需要它的时刻);实测(2026-07-18 curl)确认 `lain.bgm.tv` 为 Cloudflare 海外 CDN,直连超时而系统代理 200,属 ytimg 走代理模式而非 CN 风控模式,故封面保持 `trust_env`(不加入 `_DIRECT_FETCH_HOST_SUFFIXES`),结论记入代码注释与测试。 - **端到端盘点补口**:`fetch-bangumi --rebuild-profile` 不再走 B 站认证门(画像重建只吃 Bangumi 事件,非交互终端不再被"请先 auth login"卡死——`_prepare_init_runtime(require_bili_auth=False)`);收藏事件语义补全——metadata 新增可读 `subject_type_label`(动画/书籍/游戏/音乐/三次元)并把 subject 的 `meta_tags`(TV/剧场版等,防 schema drift)透传;`docs/modules/config.md` 与 `config.example.toml` 的 `[network]` 作用范围补记 Bangumi 海外服务需代理,`docs/modules/bangumi.md` 显著位置加网络要求并记录作者字段恒空、delight 惊喜信号不适配 bangumi 两条已知限制;补齐 `/api/feedback`·`/api/saved`·聊一聊 bangumi 端到端断言与 PreferenceAnalyzer 满意度过滤真实测试。 - **删除死代码 `DelightScorer`(推荐输出零变化)**:`recommendation/delight.py` 里的 embedding 多信号打分器(`deep_need_alignment` / `insight_resonance` / `likes_alignment` / `novelty_factor` / `quality_indicator` / `exploration_match` / `dislike_penalty` 加权)自被 Evo 复用方案取代后就再无生产调用点——全仓 `DelightScorer(` 实例化 0 处(仅单测),`score()` / `_build_reason_stub()` / `_quality_indicator()` 等在模块外 0 引用,生产代码从该模块只 import `effective_delight_threshold` 与 `DEFAULT_DELIGHT_THRESHOLD`。真实评分路径是 `precompute_delight_scores()` 复用 Evo 写入的 `relevance_score`(有意省掉一次 LLM 调用),目录评分 `rating_score / rating_count / source_rank` 则经 `_prompt_visible_content_fields` 进入共享 evaluator prompt 由 LLM 在语境中权衡。本次一并删除只服务于它的 `DelightSignals` / `DelightWeights` / `SupportsDelightCandidate` / `SupportsRecommendationSignalStore` 与失效的 `_DEFAULT_WEIGHTS`,并修正 `llm/embedding.py` 中指向该类的空缓存守卫注释。**保留**阈值口径 `DEFAULT_DELIGHT_THRESHOLD` / `CONSERVATIVE_DELIGHT_THRESHOLD` / `effective_delight_threshold()` 及其标定注释,所有生产 import 与阈值取值逐字节不变——**本次改动不改变任何推荐输出、API 响应或 CLI 输出**。`tests/test_delight_scorer.py` 中打分器用例删除,阈值用例改为直接测模块级 `effective_delight_threshold()`,其余 delight 存储层用例原样保留,并新增防复活断言锁定「delight 模块只暴露阈值口径」。 ## v0.3.173 / extension v0.3.173 / desktop v0.3.173:自动更新链路全面加固(2026-07-16) 后端源码走 `backend-v0.3.173`,浏览器插件走 `extension-v0.3.173`,桌面安装包走 `desktop-v0.3.173`。三渠道版本重新对齐(0.3.172 因未发 extension tag 导致聚合校验失败、Latest 页面短暂空资产,已降级为 prerelease;本版为所有渠道的推荐升级目标)。 - **抖音 discovery 出货量修复(claim/search 对齐、热词轮换、search API 快速失败)**:四处协同解决抖音 80 分钟仅约 3 条 vs B站 149 条的低出货。(1) **claim/search 对齐**——producer 每轮 `claim(n=keywords_per_run=3)`,与策略实际搜索的关键词数一致,不再 claim 5 个只搜 1 个、把 4 个未搜索的词误 `mark_used` 烧掉。(2) **节流减半**——`min_interval_minutes` 30→15,80 分钟内的 search 轮次翻倍。(3) **热词轮换**——`DouyinPluginSearchClient` 用进程内 `sentence_id → 单调时钟` 表(TTL 6 小时,基于 2574280 连播 3 轮只出重复的日志证据校准)过滤近期已用热词后再截断到种子数,全部近期时回退陈旧词(宁可陈旧不要空手),避免连续 hot 轮反复挑同一个 top 热词只出被 dedupe 吸收的重复。(4) **扩展 search API 快速失败**——`harvestSearchViaApi` 的每次 `w.fetch` 加 `AbortController` + 15s 超时(`search/single` 缺 a_bogus 时风控会永久挂住连接),让卡死的路径快速失败切换到下一路径,而非每关键词任务空耗约 50s 撞上 45s 内容脚本桥接超时(`harvestHotRelatedViaApi` 不动,该端点正常工作)。a_bogus 签名重写不在本次范围内。 - **抖音 search 升级为被动优先分页采集**:不重写 a_bogus——扩展触发真实 UI 搜索后,页面自身会发带完整签名的 `search/single` 请求,被动 fetch-tap 直接收割。`runSearch` 改为滚动**真实结果容器**(`pickSearchScrollTarget` 从 `/video/` 锚点向上找最近的可滚动祖先,找不到才回退 window 滚动)驱动页面自发翻页;固定 4 轮 × 1s 睡眠替换为自适应循环(上限 10 轮、按去重后计数连续 2 轮无增长即停、每轮 250ms 轮询增长最多 3s、增长即中断等待)。被动采集从副产品变为一等信道并进入遥测:debug 新增 `passive_items_harvested` 与 `scroll_rounds`,消除此前 `videos=15, dom=0, api=0` 的诊断困惑。API bridge 兜底保持原样(15s 快速失败)。 - **修复 B 站取消点赞被误记为第二次正向点赞**:B 站按钮用 class `on` 表示选中态而没有 `aria-pressed`,DOM kernel 无法识别撤销;既有 MAIN-world `bili-interact-tap` 现按网络写入权威采集点赞(`like=1/2`)、收藏增删与投币,仅 HTTP 2xx 且业务 `code===0` 才发事件,取消赞 / 取消收藏归一为 `feedback` retraction(`signal_strength=0.2`)。B 站 adapter 同步将 `{like,favorite,coin,retraction}` 纳入 tap 权威集合,DOM 对这些动作零发射,避免双计;端点 fixture 依据 bilibili-API-collect 公开记录构造,仍待真机验证。 - **源码自动更新链路按 effective URL、强 TLS 与进程唯一写入全面加固**:remote 守卫同时校验 `ls-remote --get-url` 和全部 `get-url --all` 值,放行 GitHub 官方 SSH-over-443、拒绝 `insteadOf` 镜像改写与凭据地址且绝不改写用户 git 配置;API 传输失败会尝试 Atom,空异常、畸形 JSON、git 缺失/超时、lockfile checkout 与 `index.lock` merge 都保留稳定 reason 和真实 `last_error`。更新器删除 `verify=False` 降级,staged 改动算脏但未跟踪文件仍豁免,tag 通道在 git 变更前封闭,apply 锁跨配置热重载保持进程唯一;custom 代理显式贯通 git/uv/pip,direct/system 继承行为不变。检查间隔加载时保证至少 1 小时、保存时拒绝非法值;桌面 error 卡优先显示明细,扩展 popup 对 frozen/docker/unsupported 禁用自动应用,含空格仓库路径的修复命令统一加引号。prerelease 候选排序补齐 SemVer §11 语义(同号 stable > 任意 prerelease、`rc.10 > rc.9 > rc1`),UI 展示保留 prerelease 后缀。移动 Web 更新面板与 CLI update 命令明确维持现状,未在本次范围内。规格与实施计划见 `docs/plans/2026-07-16-auto-update-hardening-{spec,plan}.md`(基于 Codex 全链路审计 13 项发现的裁决)。 - **平台来源接入契约统一:一个绿灯只有一种含义(Wave A + Wave B Task 8/9)**:`/api/sources/status` 此前用一个 `state` 字段承载四个正交维度,同一个 `logged_in=true` 背后是六种强度迥异的证据——B 站只是数出 cookie 串里有三个字段名(不联网)、小红书/知乎是浏览器 72h 心跳、Reddit 是本地文件未超 7 天(注释明说绝不联网)、X 是上次请求没报 401、YouTube 是硬编码常量,而抖音**即使 cookie 完全有效也永远显示「状态待验证」**。四个平台在设置页并排显示同一个「凭据已就绪」,用户无从分辨"真能用"与"只是填了值"。(1) **契约正交化**——新增 `SourceAuthContract`(`auth_required` / `credential` / `credential_origin` / `verification` / `verify_method` / `verify_ttl_seconds` / `verified_at` / `can_verify_now`),其中 `verify_method`(`live_probe` > `passive_health` > `browser_heartbeat` > `local_file` > `task_history` > `none`)如实标注结论的证据强度;`sources_status()` 从 **424 行**巨型 if/elif 拆成 `api/source_auth/providers.py` 的 7 个纯函数聚合器(**38 行**),`SourceAuthContext` 刻意只持有 config 与 database、拿不到 HTTP client,使"状态端点绝不出网"由作用域强制而非 review 自律。旧 `state`/`logged_in` **逐字节零变化**(原样承袭而非推导——bilibili 与 douyin 正交字段完全相同却对应 `ready/True` 与 `unverified/False`,证明旧值不可推导,这一不可能性本身即诊断 D1 的最强证据),改由 `check_legacy_consistency()` 断言两套视图互不矛盾。(2) **抖音状态误报修复**——`api/app.py` 原 docstring 断言抖音"没有稳定 nav 端点能区分未登录与软风控",经剥离对照实验(实验组=完整 cookie,对照组=剥掉 12 个登录 cookie 的游客态,同签名器/UA/时刻)推翻:`/aweme/v1/web/user/profile/self/` 已登录返回 `status_code=0`+非空 uid、未登录返回 `status_code=8` "用户未登录",实测延迟均值 329ms。注意 `/aweme/v1/web/query/user/` 两组返回**相同**的设备级 uid,只看"有没有 uid"会误判,探针与测试均对此设防。(3) **`POST /api/sources/{slug}/verify`**——7/7 平台一键验证,按固定动作表分派(**不按 `verify_method`**:后者随状态变化,知乎无心跳时回落 `task_history`,照它分派会让知乎在最需要验证时反而无可执行动作);`outcome`(这次点击验证到了什么)与 `auth.verification`(我们现在相信什么)严格分离,避免渲染出绿色「已验证」配「插件未连接」;三态而非两态,探测超时/插件未回/平台限流/YouTube 无需登录一律 `indeterminate`,绝不显示成「凭据失效」诱使用户删掉好 cookie;每平台 10s 去抖 + in-flight 标记,防止连点变成自造风控。B 站原有的两条各自缓存的活体验证路径(`init_prereqs` 喂 `/api/init-status`、状态端点各一份)合并为进程级单一 store,根除两界面对同一 cookie 给出相反结论的可能。(4) **修 B 站凭据双读取路径**——`runtime/init_prereqs.py` 此前只读 config.toml,而 CLI `auth login` 只写 `data/bilibili_cookie.json`,导致同一份凭据下引导页报「未登录」、设置页报「已就绪」;现统一走 `resolve_runtime_cookie()`。(5) **凭据写入归一**——新增 `POST /api/sources/{slug}/credential`(结构校验 → 活体校验 → 落盘 → 广播 → 返回重算后的契约),7 条老端点保留为响应结构逐字段冻结的 `deprecated=True` 转发;**`PUT /api/config` 一并纳入**(它路径叶子是 `config`、按词根扫描看不见,却一条路由写四个平台凭据,且设置页手工粘贴走的正是这条路),四处凭据写入改为委托同一校验门,消除「同一份无效 cookie 经 POST 被拒、经 PUT 静默落盘」的矛盾;无效凭据经磁盘/DB 断言确认从不落盘;传输失败**拒绝**而非存入未校验 cookie。**架构上无法校验的绝不伪造**——xhs/zhihu 只存一个 bool、后端零字节 cookie,其写入显式返回 `checked="none"` + `unverified_reason`。(6) **三端「测试连接」按钮**(桌面 Web + 插件 popup,复用既有 `renderProbePending`/`renderProbeResult` DOM 约定并扩展为三态 tone,`neutral` 特意不用灰色以免"判定不了"被读成"仍在探测")。新增 `scripts/source_contract_metrics.py` 作为 CI 量化门(聚合器行数 424→38、有 verify 动作的平台 0→7、凭据写入端点命名形态 4→1)。规格与实施计划见 `docs/plans/2026-07-18-source-auth-contract-{spec,plan}.md`;新平台接入的强制契约写入 `docs/platform-source-integration.md` §0.1–§0.4,其中明确规定**声称某平台"无法验证"之前必须先做剥离对照实验**,拿不出对照数据不许写进 docstring。 - **平台源设置改为后端描述符驱动,三端共享同一份渲染(Wave B Task 10/11)**:接着上一条,把最后一份手抄副本也收掉。(1) **表单描述符下发**——`GET /api/sources/credentials` 每项新增 `form`(`kind` / `label` / `placeholder` / `env_var` / `required_keys` / `required_keys_mode` / `actions` / `help_text`)与 `summary`,全部由 `CREDENTIAL_SPECS` **派生**而非另写一份,表单声称的必填 cookie 名与写入校验门永远同源。`kind` 是能力声明:xhs/zhihu 为 `extension_only`,后端一个字节的 cookie 都不存,因此三端**不得渲染可粘贴输入框**(给能填的框是让用户往虚空里打字),但保留 `verify` 与 `open_login_window`——「去浏览器登录」才是这两个平台唯一有效的修法;youtube 为 `none`。`required_keys_mode` 是 spec 字段表之外补的:抖音三个 session cookie 是**任选其一**,平铺成 `required_keys` 会让 UI 声称校验器要求它其实不要的东西。spec 举例的 `clear` **故意未实现**——全 API 没有端点能抹掉已存凭据(`PUT /api/config` 空字段意为「本次没编辑」,恰恰相反),先挂按钮后补端点就是 UI 开始说谎。(2) **三端共享渲染模块**——新增 `src/openbiliclaw/web/shared/source-status.js`(独立 `/shared` mount,因为 `/web` 挂的是 `web/desktop/`,`web/shared/` 从 `/web/shared/` 根本取不到),桌面 Web、插件 side panel、setup 引导页全部加载它;插件走 build 期复制(MV3 CSP `script-src 'self'` 禁止从后端拉脚本,产物已 gitignore,提交它就变第四份副本)。删除桌面 `SOURCE_ACCESS_STATE`、插件 `SOURCE_STATUS_DOT`/`SOURCE_STATUS_LABEL`,以及两端各一份的 `VERIFY_OUTCOME_TONE` / `renderVerifyResult` / `startSourceVerifyCooldown` 与三份 7 平台名单。**修掉两个用户可见的漂移**:插件此前把 `no_auth` 与 `unverified` 都画成同一个灰点(`#9aa0a6`),于是「这个源不需要登录」和「这个源状态不明」长得一模一样——两个状态后端都真的会发(YouTube 的 `no_auth`、xhs/douyin/reddit 的 `unverified`);现在 `unverified` 走 `pending` 蓝(`#3898ec`,取自桌面 `--source-pending`),真机截图确认两端色调与文案逐字一致。未知状态兜底也从「桌面显示『状态未知』、插件显示**空字符串**」(一个没有任何说明的灰点)统一为「状态未知」。引导页的 `checkBili` 从直接 string-test `config.bilibili.cookie`(看不见 data file 与环境变量里的 cookie)改为读同一份契约。**状态表刻意留在 JS 而不上收进契约**:后端发 `access_label`/`access_tone` 看着更「契约驱动」,但会留下 Python 一份 + JS 兜底一份、两种语言两份同一知识,而指标脚本只扫得到其中一份——那正是 spec I7 说的语法代理陷阱;文案实质(`detail` / `message` / `summary`)本来就全由后端下发。指标脚本第 2 项 1→0、第 3 项 2→1。**saved-sync 那一族的 6 份映射不合并**:判据是「这张表的键是不是 `/api/sources/*` 发出来的字段值」,它与本枚举共用 `login_required` / `rate_limited` 两个**拼写**,但回答的是「这一条收藏同步成功没有」而非「这个源接不接得上」,属另一个枚举(引导页 `INIT_REASON_TEXT` 同理)。新平台约定见 `docs/platform-source-integration.md` §0.5–§0.6。 - **接入状态改由正交契约驱动,证据强度终于可见(Wave B Task 12)**:上一条把契约铺到了后端与共享模块,但 `describeAccess()` 仍**只读 legacy 的 `item.state`**,全文件唯一消费 `auth` 的地方是 `hasCredential` 读 `auth.credential`——于是整个契约重构对用户不可见:抖音后端早已是 `verification=verified`,界面照旧显示「状态待验证」;B 站(`live_probe` 但尚未探测)与 Reddit(`local_file` 且已验证)依然并排显示同一个「凭据已就绪」,而「同一个绿灯背后强度不同」正是整个 spec 要解决的问题。现在标签与色调全部由 `auth` 派生,判定顺序 `auth_required=false` → `credential` → `verification`;凭据维度先于结论维度,是因为两者正交、可能互相矛盾,此时宁可少报也不点亮一盏兜不住的绿灯。**`verify_method` 升为独立的第二维度**,用三种方式同时编码且没有一种是颜色(色觉障碍用户读不到颜色):字形(◆ 联网证据 / ◇ 本地或间接证据 / — 无验证能力)、中文方式名(联网验证 / 请求反馈 / 插件心跳 / 本地文件 / 历史任务)、边框(实线 / 虚线 / 点线)。本版本不认识的 `verify_method` 刻意渲染成**弱**而非强——猜一个没见过的方式「很硬」正是这套契约要消除的过度声称。文案命名的是**能力**而非结果(B 站在首次探针跑之前 `verify_method` 就已是 `live_probe`),结论跑没跑由后缀承担:`verified_at` 有值渲染「3 分钟前」,为空渲染「尚未验证」,TTL 概念因此对用户有了意义。`auth_required=false` 单独一档「无需登录」且不显示证据徽章——不需要凭据的源没有证据可评级,它既非已验证也非待验证。**修掉一个跨时区的时间 bug**:`verified_at` 有两种线格式,多数 provider 发带 `+00:00` 的 isoformat,而三处从 SQLite 读回时间戳的(X 的 `x_source_health`、知乎与 Reddit 的 `task_history`)发的是 `CURRENT_TIMESTAMP`——是 UTC 但不带时区标记,`Date.parse` 会当本地时间,UTC+8 用户看到的新鲜结论会凭空老 8 小时(方向还正好错:让真证据显得陈旧);`normalizeTimestamp()` 仅在字符串确实不带时区时补 `Z`。`item.auth` 缺失或畸形时**优雅回退**到既有 `state` 表,装在插件商店里的 side panel 对着更老的自建后端仍渲染出真实 chip 而非空白。三端出口不同、数据同源:桌面页给证据独立徽章 `.source-evidence-badge[data-rank]`,侧边栏每源只有一行故用 `access.line` 内联括号(`已验证(◆ 联网验证 · 3 分钟前):…`),setup 引导页的 B 站步骤从写死的「已检测到 B站 登录」改为打印同一份标签与证据。真机验证(隔离 `OPENBILICLAW_PROJECT_ROOT` + `HOME` 的一次性 8435 后端)构造出 B 站 / 小红书 / Reddit **legacy `state` 同为 `ready`** 的一帧,三者标签同为「已验证」、tone 同为绿色,但徽章分别是 `◆ 联网验证 · 刚刚`、`◇ 插件心跳 · 5 小时前`、`◇ 本地文件 · 2 天前`;抖音同帧显示 `已验证 ◆ 联网验证`,即 D11 那个「可修的误报」在 UI 上翻正。指标脚本第 2 项保持 0、第 3 项保持 1。 - **接入契约外部 review 修复:12 项后端缺陷,每项先有复现测试(Codex gpt-5.6 / reasoning=max)**:上面三条交付后做了一轮对抗式外部 review,12 项全部成立,共性是"看起来测过了但没测到点子上"——所以每项都先写一个复现触发场景的失败测试,红了再修。(1) **活体缓存击穿「无效凭据绝不落盘」(BLOCKER)**——写入门按 60 秒窗口复用正面结论时**只认平台不认凭据**,于是旧 cookie 验证成功后 60 秒内提交另一份结构完整却已失效的 cookie,会命中缓存直接放行并落盘,一个网络请求都不发。复用本身的理由成立(抖音 `msToken` 频繁轮换,插件每次启动重发整个 jar,每次都探测就是自造风控),故保留优化并加凭据同一性校验:`ProbeVerdict.credential_fingerprint` 存该平台**登录态字段**的 SHA-256(字段名直接取自 `CREDENTIAL_SPECS` 的校验门,所以"什么算同一份凭据"与"校验门要求什么"不可能漂移),`msToken` 不在其中因此轮换仍命中;写入门用严格的 `peek_matching()`(指纹不符或缺失一律重探——猜错的代价是死凭据落盘),状态端点用宽松的 `contradicts()`(只有明确不符才丢弃,缺失指纹仍显示,其暴露面被 60s TTL 兜住且自愈)。命中缓存的结论**不再重新记录**,否则每次插件重发都顺延自己的有效期,一份凭据可以永远"刚刚验证过"而实际从未复验。(2) **X 是伪验证(违反 I3)**——`x_source_health` 的行以 `state='ok'` 为**默认值**建出,"从未发过请求"与"上次请求成功"在 `state` 上完全同形,于是全新数据库首次写入一份从未用过、甚至早已过期的 X cookie,`/api/sources/status` 立刻宣称 `verification=verified` + `verify_method=passive_health`——而 `passive_health` 恰是唯一无法主动重跑自证的方式。新增 `last_success_at` 列(只由 `record_success` 写),无真实流量报 `unverified`;`clear_relogin_block()` 会**清空**它(凭新 cookie 给的乐观解封不是用新 cookie 拿到的结果);迁移**不回填**,老行里没有任何信号能区分两种情况,猜一个就是把同一个伪造推迟一次迁移。(3) **`PUT /api/config` 事务顺序(BLOCKER)**——抖音/X/Reddit 的凭据在字段解析处就写盘,而 `[network]` 校验与保存锁在几百行之后,于是同时提交有效抖音 cookie 与非法 `network.mode=custom, proxy=""` 会返回 400「未写入」而 cookie 早已被覆盖,且这三个 store 既无快照也无回滚。四个平台的写入改为延迟闭包,在 `_CONFIG_SAVE_LOCK` 内、`save_config()` 成功之后执行;并发 PUT 也不再可能拼出"config 来自甲请求、凭据来自乙请求"。**改的是顺序不是位置**——持久化仍留在 handler,因为它正处在 config.toml 事务中。(4) **去抖窗口重放过期结论**——10 秒去抖按**平台**存结果,恰好撞上修复路径:验死 cookie(窗口以 `failed` 武装)→ 保存能用的 → 10 秒内再点验证 → 原样回放旧失败,用户读到"修了也没用",下一步多半是删掉那份真能用的 cookie。凭据落盘即 `note_credential_changed()` 清条目。(5) **PUT 路径丢弃成功探针的 verdict(违反 I5)**——它确实出网探测、确实拒绝了该拒绝的,然后把结论扔了,状态仍是 `unverified`;同一份 cookie 走 POST 却是 `verified`。两条写入路径改为共用 `_credential_landed()`,校验**强度**相等之外**结果**也相等。(6) **旧 `/api/bilibili/cookie` 响应字段退化**——缓存命中分支不带 `username`/`user_id`,装机扩展拿到 `authenticated=true` 配 `username="", user_id=0`;verdict 现随缓存条目一起存取。(7) **`except Exception` 漏掉 `CancelledError`**(3.8 起继承 `BaseException`)——前端 fetch 取消或上层超时会让 in-flight 标记残留至 60 秒上限,期间每次点击只回"正在进行中"而那次验证早已停止。(8) **legacy 一致性表键集错配**——表里收录了后端根本发不出的 `expired`(读起来像"过期这条覆盖了",而真正会发的 `missing_cookie`/`expired_cookie` 一个约束都没有),同时 `rate_limited` 被钉死要求"有凭据",于是"限流后用户删掉 cookie"这个完全合法的状态被误报为契约违规。表按**后端实际发得出的 13 个状态**重建(**注意 spec D6 说的 11 个漏了 Reddit 的 `login_required`/`error`,两者均有冻结用例证明可达**),不约束的状态显式写成全集加注释而非留空——运行时二者等价,对读者不等价;并加测试把键集钉死在冻结用例证明可达的状态集上,死键与漏键都不可能再悄悄出现。(9) **冻结测试名不副实**——`test_sources_status_state_and_logged_in_are_frozen` 只断言 `(state, logged_in)`,而对外声称的是**五个 legacy 字段**逐字节冻结,`detail`/`enabled`/`feed_paused` 无任何保护;旧端点测试也只比对键集、不比对值(一个键还在、值被掏空的响应对它是隐形的)。五个字段全部纳入冻结,旧端点改为逐字段比对值。**变异验证**:悄悄改掉小红书一句 `detail`,新断言 2 个用例失败,旧的两字段断言 37 passed 全绿。(10) **`validate_live` 削弱承诺——判定为删**:全仓零调用方(扩展、三端前端、CLI 都不发),等于给任何能连上 localhost 的东西一个关掉端点核心承诺的官方途径,不换来任何好处;老端点 `validate_with_bilibili` 保留(装机扩展一直在发且恒为 `true`),但即便传 `false` 结构校验门仍跑,故现在可达的最弱校验也比过去强。(11) **抖音 `detail` 与 `verification` 自相矛盾**(前端真机截图暴露)——`detail` 是 D11 之前"抖音无法验证"年代写死的常量,探针接上后没人回头改,于是三端切到正交契约后同一张卡片同时写着「接入:已验证」「◆ 联网验证 · 刚刚」和「需在实际任务中验证」。所有测试都没抓到,因为 `detail` 只在"尚无结论"这一个状态下被冻结,而那恰好是老文案仍然正确的状态。B 站与抖音的 `detail` 现按 `verification` 查表,`unverified` 一档保留原字符串故冻结用例逐字节不变。(12) **`verified_at` 两种时间格式**——三处从 SQLite 读回的(X 的 `x_source_health`、知乎与 Reddit 的 `task_history`)发 `CURRENT_TIMESTAMP`,是 UTC 却不带时区标记,`Date.parse` 当本地时间,UTC+8 用户看到的**新鲜**结论显示成 8 小时前,方向还正好反了:让最硬的证据显得最陈旧。改为在 `SourceAuthContract` 的 field validator 统一补齐时区——**放在契约边界而非各 provider**,新 provider 无从绕过,移动 Web 与 CLI 也不必各自再防一次;无法解析的字符串原样透传而非清空(`""` 会被读成"从未验证")。另外顺带收紧结构校验门:必填 cookie 名现要求**存在且非空**(`SESSDATA=` 登录不了任何人),这同时消除了它与 rdt-cli 自身解析器(丢弃空值)的分歧——一份 Reddit cookie 曾可能通过校验后被它刚刚为之校验的 store 拒收。 - **外部复审第二轮:两条「只堵住了举例的那条路径」+ 一次自查连带修复**:Codex 复验上一条的 12 项修复时发现两条**只覆盖了被举例的路径**,病根相同——验证了「这个**平台**成功过 / 这**次调用**要不要校验」,而契约需要的是「这**份凭据**成功过 / 这**条路径**都要校验」。(1) **X 换 cookie 继承旧凭据的 `verified`**——`last_success_at` 只记「成功过」不记「谁成功的」,于是写入一份从未发起过任何请求的新 cookie,直接继承上一份的结论**连时间戳都一字未变**;`clear_relogin_block()` 救不了(健康行本就是 `ok`,无 block 可清,返回 `False`)。这与上一轮修掉的「活体缓存按平台取值」是同一个错误换了个 store,因此用同一种解法:新增 `last_success_credential` 记录产生该成功的凭据指纹,由 producer 在解析 cookie、构造 `XClient` 的同一处绑定("记录成功的那份凭据"即"发出请求的那份凭据"),读取时与当前 cookie 比对,不符即非证据。**刻意不挂在写入路径钩子上**——cookie 也可能经环境变量或直接改 data file 变更,那些路径一个钩子都不经过(复审给出的复现正是直接写 store)。build 期绑定亦是安全方向:cookie 变了而 producer 未重建时,指纹仍跟着真正在发请求的那份凭据,新 cookie 显示 `unverified` 而非冒领。(2) **废弃路由仍能一键关掉活体校验**——上一轮以"扩展总是发 true"为由保留了 `POST /api/bilibili/cookie` 的 `validate_with_bilibili`,实测传 `false` 会让结构完整但已失效的 cookie 在**探针零调用**下落盘。"扩展总发 true"是兼容性论证而非安全性论证:请求不只来自扩展。字段**仍接受但不再生效**(装机扩展每次同步都发它,拒绝该键会 422 掉它们的 cookie 同步——"接受这个字段"与"这个字段能降低校验"是两件事),`validate_credential()` 的 `live` 参数一并删除,写入面从此没有任何"少查一点"的入参。(3) **自查连带修复**——按同一标准复查上一轮其余 10 条,确认 `probes.record()` 全部带指纹、8 处凭据写入(7 条废弃转发 + 统一端点)全部经 `_credential_landed`、无其他请求侧校验开关;查出两处新问题并修掉:**其一**,第 (2) 条修复本身在 X 上制造了一个全新的 #11 式自相矛盾——`verification` 变诚实后,`_X_STATE_DETAIL['ok']`(「X 来源正常,cookie 有效。」)仍挂在读作「待验证」的 chip 下方,这是本轮唯一一处**有意移动**的 legacy `detail`(冻结用例同步更新;真正确认过的那条路径逐字节保持原文案);**其二**,`_verify_twitter` 把 SQLite 的裸时间戳直接拼进用户可见文案,是上一轮时区修复漏掉的另一扇门(时间戳归一化因此从 field validator 提取为模块级 `normalize_timestamp()` 复用)。另外把 `PUT /api/config` 延迟写入的**部分失败**从裸 500 改为具名错误:四个 store 之间没有事务,I/O 失败必然留下部分已写,但报错须说清哪个平台失败、哪些已经落盘、重试是幂等的(pitfall #7)。 - **「稍后再看 / 我的收藏」列表卡片补齐并优化反馈操作(issue #111,三个图形界面)**:两个 saved 列表的卡片此前只有 同步 / 移除,现补上一行低调的反馈操作——喜欢 / 不感兴趣 / 聊一聊(纯图标幽灵按钮)+ 一个「跨列表」保存 toggle(稍后再看卡=收藏、收藏卡=稍后再看),靠留白分组、封面保持干净。经 ui-ux-pro-max 评审去冗降噪:删掉与「移除」重复的「所属列表 toggle」与桌面的 dismiss,所属列表成员仍由「移除」管理。喜欢 / 不感兴趣 / 聊一聊因 saved 项不带 `recommendation_id`(推荐维度 `/api/feedback` 无之即 404),改走内容维度信号 `/api/events`(与扩展原生 like/dislike 同通道,`type=feedback` + `metadata.feedback_type`,画像消费与推荐反馈一致,仅不触发推荐流的下次刷新隐藏);跨列表 toggle 复用各端 saved 注册表。**桌面 Web / 移动 Web / 插件 popup 三端统一实现并真机 E2E 验证**(桌面新增 `savedCardFeedbackBarHtml` + 复用 `handleSavedCardFeedback`,推荐卡 `cardFeedbackBarHtml` 字节不变;移动 `web/js/saved.js` 自包含 + 新增 api `sendBehaviorEvents`;popup 复用 `bindWatchLaterToggle/bindFavoriteToggle` + 新增 `sendBehaviorEvents`),CLI 无卡片 UI 排除;后端零改动(复用既有 `/api/events`)。新增 `tests/test_desktop_web_saved_card_actions.py`、`tests/test_mobile_web_saved_card_actions.py`、`extension/tests/popup-saved-card-actions.test.ts`。 - **「稍后再看 / 我的收藏」卡片封面/标题/作者空白回填(issue #111 顺带修)**:部分内容(尤其 B 站 / Reddit)被保存进 saved 列表时 `cover_url`(及偶发的标题/作者)没写进 `saved_items`,导致卡片缩略图空白,但 `content_cache` 里其实有封面。`Database.list_saved_memberships` 现按 `content_id`/`bvid` 从 `content_cache` 兜底回填空的 `cover_url` / `title` / `author_name`(相关子查询,只读侧增强、不改存量数据、`NULLIF` 只填空值不覆盖已有);移动端保存 payload `normalizeSavedItemInput` 也补齐 `cover ?? pic ?? thumbnail ?? image_url` 兜底字段(与桌面对齐),防止后续再丢。存量空封面 saved 卡随之显示真实缩略图。新增 `tests/test_saved_list_cover_backfill.py`。 ## v0.3.172:候选补货空转冷却——源枯竭不再烧 CPU(2026-07-16) 后端源码走 `backend-v0.3.172`(backend tag 与 Docker 镜像完整可用);desktop/聚合发布因桌面上传卡死且缺 extension tag 未完整发布,已降级 prerelease,桌面与插件用户请直接升级 v0.3.173。 - **候选源枯竭不再让补货协调器以 2–3 Hz 热轮询(用户报告:更新后笔记本风扇狂转、发热,退出桌面端恢复)**:连续无产出的 supply 结果改按 30/60/120/300/600 秒阶梯冷却,模拟小时从约 9000 次空转降到不超过 10 次;任意 runtime 通知仍可立即穿透当前窗口一次,手动/配置/启动通知与真实产出会复位阶梯,旧式非 mapping 结果保持不节流。空 refresh plan 的全量 INFO 与三组数据库诊断按指纹和 300 秒窗口节流,重复调用保留带累计数的 DEBUG;阶梯首次登顶每个枯竭事件只发一条 `candidate supply starved` WARNING,runtime status 新增 supply streak 与 cooldown deadline 字段。规格与实施计划见 `docs/plans/2026-07-16-supply-spin-cooldown-{spec,plan}.md`。 ## v0.3.171 / extension v0.3.171 / desktop v0.3.171:旧版补池兼容、长期避雷与源码更新修复(2026-07-16) 后端源码走 `backend-v0.3.171`,浏览器插件走 `extension-v0.3.171`,桌面安装包走 `desktop-v0.3.171`。 - **撤销的赞不再以满强度留在画像证据里(retraction 确定性折价,事件采集补全 Wave A)**:用户明确「反悔」的正向行为(取消赞 / 取消收藏 / 取消关注 / 取消转发)此前只被记为 neutral 的 retraction 事件,被撤销的那次正向证据仍以 0.85/1.0 满强度继续参与后续画像消费。现在双面折价:**内存面**——`ProfileUpdatePipeline.ingest_batch()` 开头新增原子折价预处理,早于任何阈值消费,把同批 / 缓冲中同 identity key(tweet_id / bvid / mid / xhs note_id,统一到共享 `sources/identity_keys.py`)、事件类型 == `retracted_action`、且事件时间早于 retraction 的正向信号折到 `retracted=true` + `signal_strength≤0.2`;乱序到达用内存 tombstone(`(key,action)→retraction 时间`,TTL 24h / cap 500)处理,`like→retract→like` 的重新点赞不折,事件时间缺失保守不折。**离线重读面**——`Database.mark_positive_events_retracted()`(`/api/events` 钩子 + `openbiliclaw init` 全量重建 / 12h 认知整理)按 identity key 全局标注,迟到正向事件(account_sync 回填旧 like)在落库路径对账已存 retraction 行。偏好与 12h 认知觉察两个重读 LLM 消费面共用 `render_retraction_marked_events()` 给渲染 context 追加「(已撤销)」,偏好 system prompt 增一条静态撤销语义规则;无 retraction 的事件集渲染字节不变(回放不变性有测试兜底)。`retracted_action` 白名单 `{like,favorite,share,follow}` 越界跳过 + WARNING。规格与实施计划见 `docs/plans/2026-07-16-event-capture-completion-{spec,plan}.md`(经 Codex 对抗 review 六轮)。 - **用户亲手写的评论 / 弹幕正文首次进入画像证据链(事件采集补全 Wave B)**:此前除 X 经 GraphQL tap 发出的 comment 事件(正文被刻意丢弃)外,各平台评论只记「点了评论按钮」,B 站弹幕零采集。现在评论 / 弹幕正文**均在提交成功后经网络层采集**:X tap 从 reply `CreateTweet` 提取 `variables.tweet_text`,仅当响应无 GraphQL `errors`(业务码校验)才附带正文(既有 comment 事件发射时序不变);新增 MAIN-world `src/main/bili-interact-tap.ts` 观察 B 站 `POST …/x/v2/reply/add`(评论)与 `POST …/x/v2/dm/post`(弹幕),仅当响应 `code===0` 才发,桥接 `content/bilibili.ts` 构造 `comment` 事件(评论 `comment_kind="comment"`、弹幕 `comment_kind="danmaku"`+`signal_strength=0.6`,校准注释:低于书面评论 0.75、持平 follow 0.6)。正文经**双端净化**(扩展 `shared/text-sanitize.ts` 与后端 `sources/event_format.py::sanitize_comment_text` 各自截断 200 字符 + 剥离 Unicode category-C,`comment_kind` 白名单 `{"",comment,danmaku}` 越界清空 + WARNING),偏好分析 preserved keys 保留正文进 LLM,渲染 context 追加「评论:『…』」。kernel 新增 `tapAuthoritativeActions` 按动作粒度 DOM 抑制契约(取代旧 `strongSignalSource`):X 声明 `{like,favorite,share,comment,retraction}`、B 站声明 `{comment}`,消除「网络提交 + DOM 点击」双计与「仅打开评论区即记事件」假动作。隐私政策与商店 listing 同步扩大「个人通讯」采集范围。规格与实施计划见 `docs/plans/2026-07-16-event-capture-completion-{spec,plan}.md`。 - **小红书赞 / 收藏强信号从 DOM 裸奔升级为网络层认定(事件采集补全 Wave C)**:此前 xhs like/favorite 靠按钮文案匹配,图标按钮直接漏采、`aria-pressed` 撤销「名义覆盖实际未验证」。现在新增 MAIN-world `src/main/xhs-action-tap.ts` 观察写端点 `POST …/v1/note/like`→like、`/note/dislike`→retraction(取消赞)、`/note/collect`→favorite、`/note/uncollect`→retraction(取消收藏),仅当响应业务成功(`success===true` 或 `code===0`,不变量 7b)才发,`postMessage`(`obc-xhs-action`,与 token sniffer 的 `obc-xhs-sniffer` 隔离)桥接 `content/xhs/action-event.ts` 构造 like/favorite/retraction 事件,事件 URL 由 note_id 拼 `…/explore/`,与后端 `sources/identity_keys.py` note 键型互通(Wave A 的赞→撤销折价能对上同一批事件);xhs adapter 声明 `tapAuthoritativeActions:{like,favorite,retraction}`,kernel 抑制这三类 DOM 发射(comment/share 仍走 DOM)。补齐 xhs adapter 单测(note-id 24-hex 边界 / page-type / action 推断 / 图标按钮无文案→null 的降级契约)——xhs 此前是仅有的两个缺 adapter 单测的平台之一。note-card DOM selector 从 passive.ts / bootstrap.ts 双处重复统一到 `content/xhs/selectors.ts`(纯移动零行为变化),被动降级契约(空 title→null、部分字段缺失→部分数据不抛)固化为回归测试。后端 `POST /api/sources/xhs/observed-urls` 的裸链 `urls` 分支从只收 `/explore/` 扩为兼收 `/discovery/item/`(与 `notes` 分支对齐,不再静默丢弃)。xhs 端点形状按公开社区文档构造,待真实端到端验证。规格与实施计划见 `docs/plans/2026-07-16-event-capture-completion-{spec,plan}.md`。 - **事件获取层可靠性修复(2026-07-05 事件获取体检的落地)**:五项联动修复原始行为数据的丢失 / 双计 / 静默失效。(1) 插件事件缓冲从纯内存改为 awaited 写穿持久化到 `chrome.storage.local`——MV3 service worker 空闲 ~30s 即被回收、恰与 30s flush 周期同量级,此前被杀即整批丢事件;强信号在网络 flush 前已落盘,SW 冷启动经 `bufferReady()` init gate 恢复。后端 `not_initialized` 时事件改进停车场(500 条 / 48h TTL)而非消费即弃,初始化完成后自动补发。(2) `AccountSyncService` 新增 48h 跨源去重:扩展已实时上报的同一行为(view/favorite 按 bvid、follow 按 mid、X 按 tweet ID)不再被账号拉取二次写入双倍加权;查询排除自身来源(`exclude_source="account_sync"`),仅历史 API 观察到的重看不受误伤。(3) 账号拉取的画像更新不再绕过洋葱层管线直接整层重算 preference——画像就绪后与实时事件同走 `ProfileUpdatePipeline` 增量路径(四行回退矩阵,管线不可用时 WARN 回退旧路径,cookie-only 安装的首次 bootstrap 不变)。(4) 同步失败不再静默:各阶段异常 WARN 落日志并分类为 `auth_expired` / `error`(新 runtime-status 字段 `last_account_sync_error_kind`,**需重启后端生效**),桌面 Web 在有错时显示状态 chip,B 站 Cookie 失效终于用户可感知。(5) X 获得服务端 6h 定时增量(复用 init 的 `XClient` + `resolve_x_cookie`):likes/bookmarks 各拉 200 映射为 `like`/`favorite` 事件(`source_platform="twitter"`),tweet ID 集合去重(上限 2000),首轮从 events 表已持久化 X 事件播种——不开浏览器 X 数据也不断更。规格与实施计划见 `docs/plans/2026-07-05-event-acquisition-fixes-{spec,plan}.md`(经 Codex 对抗 review 两轮)。 - **事件采集精度与覆盖改进(2026-07-05 事件获取体检第二批)**:六项修复让原始信号更准、更全。(1) X 取消操作(unlike/unbookmark/unretweet)此前被 GraphQL tap 直接丢弃——现映射为 `feedback` retraction 事件(满意度恒 neutral、`signal_strength=0.2`、不进反馈批学习、不走强信号旁路:撤销是"中和"非负偏好)。(2) DOM 强信号读 `aria-pressed`:点已激活的赞/收藏/关注按钮识别为撤销而非再记一次正向(X 由 tap 权威、DOM 只压制不重复发)。(3) `watch_seconds` 从墙钟改为**真实播放时长**(分段累计,暂停/闲置不计;额外报 `page_dwell_seconds`),并对晚渲染 `