# 变更日志 > 按里程碑记录各阶段交付内容。每次分支合回 main 时追加条目。 --- ## v0.3.191:对话与实时连接稳定性修复(2026-07-30) - **修复保存配置时待聊按钮失效、热重载误回滚和 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 抖动误报成整个后端离线。 ## 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 请求,不改变初始化采集、偏好分析、画像生成或发现阶段各自的整阶段墙钟上限;存量配置中显式填写的合法超时继续保留。 - **修复 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 图标,不再单独绘制旧临时标记;社交分享图源同步切换,资产尺寸、桌面容器与各界面引用均有回归测试。 --- ## 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`),并对晚渲染 `