更新日志

每次版本都写清“改了什么、为什么改、怎么验证”;本页与 /api/changelog 同源,直接读仓库根 CHANGELOG.md,不手抄、不二次编辑。

当前版本 v0.4.5 · 发布于 2026-11-06 · JSON
v0.3.3 2026-10-24

一句话:修复 fd 耗尽——ThreadLocalConnection 每回收一个线程就永久泄漏 2 个 fd,服务跑约 2 小时把 1024 软限额吃满,此后所有需要打开文件的请求全部 500(经验库写入失败),而 /healthz 仍报 200。本版把连接按线程 ident 归档并在建连时惰性回收,同时抬高 LimitNOFILE。

修复:连接随线程回收(生产事故复盘)

  • 现象:/kb 页面 500、/api/v1/kb/documents 写入失败;/healthz 200、无 Key 的接口仍正常 401。进程 fd 实测 1023/1024,其中 data/danao.db 500 个 + danao.db-wal 500 个,基础 fd 仅 23;首次报错 OSError: [Errno 24] Too many open files: app/web/index.html(11:11 启动,13:07 耗尽)。
  • 根因:uvicorn 同步端点跑在 anyio 线程池,WorkerThread 空闲 MAX_IDLE_TIME=10s 即被回收;ThreadLocalConnection._all 用 list 强引用每条线程连接、线程消失后无人关闭 ⇒ 每回收一个线程泄漏2 个 fd(db + wal)。两条常规修法经实测均不可行:sqlite3 连接不支持 weakref,threading.local 子类的 __del__ 也不随线程结束触发(只随对象销毁)。
  • 隐藏放大项:线程 ident 会被 OS 复用(实测连续 12 个线程拿到同一 tid)。若只按 ident 归档而从不过期,旧连接既不在表内、又不属于存活线程,只能等 GC —— 这是第一版补丁仍残留 growth=44 的唯一原因。
  • 修复:_all 改为 dict[thread ident, conn];建连时在锁内回收不在 threading.enumerate() 的条目并 close(),同一 ident 覆盖前先关旧连接(alive 在锁外计算,避免业务锁与 threading 锁反序);新增只读属性 open_connections 便于排查 fd 水位。
  • 容量:danao-brain.service 增加 LimitNOFILE=65536(此前未声明,实际沿用默认 1024)。
  • 验证:判定「N 个建连线程后 fd 增量必须是常数」——旧实现 50 线程 growth=100、200 线程 growth=400;修复后 50 与 500 线程均为 growth=3。变异验证:换回旧 db.py 测试立刻红(assert 400 <= 20)。生产 60 次并发 /kb 请求后 fd 稳定 92、连接 35,40 秒后不变;/kb 由 500 恢复 200(48KB)。
  • 测试:新增 tests/test_threadlocal_fd_leak.py(2 用例)锁死该缺陷。
v0.3.2 2026-10-24

一句话:生产追平——把停在 0.2.1 的线上一次性同步到当前代码树(含 0.3.0 / 0.3.1 全部内容),并把 0.3.1 之后收口的站点外壳、探针、审计链与知识库存储层一起上线;版本三处一致、日志同源可查。

本次同步(0.2.1 → 0.3.2)

  • 生产此前停在 0.2.1(2026-10-21),本地已到 0.3.1:线上 /api/version 与 /api/changelog 只到 0.2.1(5 条),本次按 scripts/sync_repo.py 的安全流程(打包 → 校验上传 → 解包到 repo.new → 摘要比对 → 逐目录替换)整体替换;
  • 同步范围:app/ scripts/ tests/ docs/ tools/ deploy/ agent-skills/ + CHANGELOG.md README.md pyproject.toml;
  • 部署前先在服务器留全量备份,部署后校验 /livez、/healthz、/api/version、/api/changelog、/changelog 与冒烟脚本。

公开站:统一外壳(2026-10-23)

  • 新增 app/core/site_shell.py:7 个公开页共用一个导航/页脚真值源(NAV_ITEMS),不再每页手抄;
  • /kb:未连接时显示门禁而不是四个「加载中…」;支持一键导入 .danao_key.txt、清除凭据,401 时同步刷新所有面板;新增公开只读概览 GET /api/v1/kb/public/overview(只返回计数,不含标题/正文/URI),未连接也能看到库规模,点击检索页签给「导入凭据」按钮而不是红字;
  • /learning:改回 X-Admin-Token(原先提示 API Key,与接口实际鉴权不一致);
  • /changelog:支持 **加粗** 渲染(含加粗包住行内代码的写法);旧直链 /kb.html 等 301 到干净路由;新增服务端分页:?page=1&per_page=3,上一页/页码/下一页,越界页码夹到末页,页脚同样有分页器;
  • 公开脱敏模式(2026-10-24):新增 app/knowledge/coding/redact.py,公开检索/回答/来源统一出口脱敏(Key / 密码 / 私钥 / JWT / Cookie / 数据库连接串 / 内网 IP / 邮箱 / 手机号 / SSH 密钥路径);新增 /api/v1/kb/public/search|answer|sources|graph,匿名只读、按 IP 限流 60 次/分、不写 kb_queries;/kb 未连接时可脱敏检索,结果带「公开脱敏」标记,完整正文/写入仍需 Key;
  • 测试 tests/test_site_shell.py 锁住「所有公开页导航一致」。

探针分层:/livez 与 /readyz(2026-10-22)

  • /livez:只回答进程活着,不碰任何依赖,恒 200——编排器判「要不要重启」;
  • /readyz:查 brain 装配、独立短连接 DB 探针(1s 超时)、线程池水位(available=0 或排队 >2× 总量 → 503),探针自身不占线程 token;
  • 与 /healthz 的分工写进注释,避免「探针恒真」与「探针拖死自己」。

审计链:删改不再无痕(2026-10-22)

  • audit_log 懒迁移 prev_hash/entry_hash 两列,新写入逐条串链(覆盖 prev_hash/ts/actor/action/detail);
  • 新增 GET /console/audit/verify:ok=False 时给出 broken_at,支持 limit 快速巡检;只读、不写被校验表。

主动能力与随身渠道(0.2.1 之后)

  • 服务心跳 app/brain/proactive.py:定时主动 turn,NO_REPLY=无事,忙时 busy_deferral 不排队;
  • Dreaming app/brain/dreaming.py:Light/REM/Deep 三阶段,Deep 六权重 + 三门槛才晋升,技能一律走人审;
  • 用户模型蒸馏 app/brain/usermodel.py:对话中提炼 preference/fact/decision/boundary/habit/lesson,重复出现加权,密钥模式入库前拦截;
  • 对话侧技能自造 app/brain/chatteach.py:只产出 proposed,绝不自动批准、不改已批准技能;
  • 闭环健康看板 app/brain/loophealth.py:连续零产出即告警,解决「死锁 4 天零告警」;
  • Telegram app/brain/telegram.py + app/api/telegram_webhook.py:chat_id+user_id 双白名单、secret_token 校验、入站内容走 UNTRUSTED 围栏、凭据只走环境变量。

知识库存储层重写

  • app/knowledge/coding/store.py 重写为独立 kb.db:来源/文档/块/卡片/查询/运行记录六类表 + FTS5(unicode61,CJK 按字切分)+ 向量 BLOB;
  • FTS5 不可用时自动退回 LIKE 并在 kb_meta 标记,不静默假装有索引;
  • 知识图谱/簇映射设来源上限:只对 chunk 数前 400 个来源算语义边(生产 5880 来源时逐对余弦 >240s,/api/kb/search 直接超时;改后实测 graph 1.2s、cluster 1.1s、单次检索 4.2s,nodes_total/truncated 明示覆盖面);
  • 新增 tools/api_contract.py、tools/contract_attest.py、tools/memory_budget.py、tools/doc_ref_drift.py 等门禁/度量工具(不影响运行路径)。

修复与运维

  • 修复 app/compliance/license_scanner.py 未闭合字符串:该文件此前无法导入(发布即炸),现已通过 compileall;
  • 修复 app/main.py readyz 线程池段 limiter 未定义(NameError 被 except 吞掉,threadpool 恒 readable:false;改 lim 后线上返回 total=40 / available=40);
  • SQLite 超时统一为 SQLITE_TIMEOUT_S=30 / SQLITE_TIMEOUT_RO_S=5(可环境变量覆盖),新代码禁止字面量;
  • evolution_runs.slot_key 幂等迁移(先 ALTER 再建唯一部分索引),避免旧库在 executescript 建索引时炸掉装配。

验证

  • python -m pytest -q tests/test_version_changelog.py:代码 / pyproject / MCP / 页面四处版本一致;
  • python -m compileall -q app:全部模块可编译;
  • 部署后 /api/version 返回 0.3.2,/api/changelog 条目数 8、首条 0.3.2;
  • scripts/smoke_deploy.sh 与服务状态通过。
v0.3.1 2026-10-23

一句话:电脑操控多出第三只手——nuphus-mcp 的语义闭环(observe → candidate → execute → verify)与 Chrome CDP(23 个 browser_* 工具),和 agent_s(视觉)、uia(Windows-MCP 控件树)并列。

新增执行器:nuphus(设备端)

  • 运行器:scripts/nuphus_runner.py,协议 2024-11-05 stdio;模型只看到不透明候选 id,坐标/句柄不出 MCP 边界;执行回执带 effect/verification,再用窗口标题二次复核(不靠工具自说自话)。
  • 安装:deploy/nuphus-mcp/install.ps1(钉 0.3.3 + SHA-256/SHA-1 校验 + 强制纯 ASCII 路径 + 先清空再落地)。
  • 配置:computer_use.executor: nuphus、nuphus_mcp、nuphus_confirm_write、nuphus_deny_tools;danao_runner register --nuphus-mcp 一并写进本机配置,老配置自动补默认值。
  • 白名单:默认挡 browser_import_cookies(读用户 Chrome 配置)、browser_evaluate(页内任意 JS)、browser_drag_files(本地文件拖进网页)。
  • 台账:每次 tools/call 落 ledger.jsonl(时间/工具/参数摘要/耗时/错误),结果回传tool_calls/denied/confirmed_actions/ledger。
  • 能力库:新增提供方 danao.device.nuphus(对比结论改为"控件确定性 vs 语义闭环 vs 视觉通用性")。
  • 验收:scripts/nuphus_accept.py 七项全绿——自检 45 工具、窗口枚举、语义候选、set_value 写入、标题复核、窗口截图、白名单拦截;--with-browser 另验 CDP 导航 + 快照(snapshot 2362 字符)。

真模型实测(DeepSeek,2026-10-23 本机)

    设备链路实测(本机 server → 设备队列 → Runner → nuphus)

    • danao_runner 的 --windows-mcp 参数根本不存在(cmd_register 却在读 args.windows_mcp)→ 注册必崩,已补。
    • 心跳/领单只捕 SystemExit,一次 ConnectionAbortedError 就把常驻进程打死 → 改捕 Exception 退避重试。
    • /control/task 没按执行器选提供方,台账永远显示默认 uia → 按 executor 映射到 danao.device.nuphus(库里没有就回退默认),并同步 CATALOG_VERSION 到 2026-10-23.5。
    • Runner 必须用不受文件沙箱限制的解释器启动:用项目 venv 跑时,它派生的 nuphus-mcp 连桌面锁都建不起来(automation lock tmp create failed: os error 5);换系统 Python 后正常。另外两处收尾:
    • 追加不再覆盖原文:新进程第一次就"另起一行"时不知道原文,改成"定位文末 + 剪贴板粘贴"(实测保留住"第一行 A");
    • 凭据不落盘:nuphus_model_e2e.py 不再把 key 写进 spec.json,改走子进程 DANAO_MODEL_API_KEY。

    网页执行器增强(0.3.1 迭代,2026-10-24)

    • 抗 @N 漂移:页面插入节点会把 @N 顶走,而过期引用往往仍然存在(只是指向别的元素)。现在记住"模型看到的那份快照"的元素身份(role+name),引用对不上就按身份重定位,并在 history 里说明"@3 已失效,按 '确认提交' 重定位到 @8"。
    • 新增网页动作:btabs / bnewtab / bswitch / bback / bforward / bwait / bupload;wait_for 在有网页会话时也看页面快照(不再只盯桌面候选)。
    • 上传:路径白名单(只允许 data/、本次工作目录、.tmp/);实测 0.3.3 用 @N 上传会在页内抛 JS 异常,自动回退 input[type=file];上传后自动读页面文字作为确认,并挡住重复上传。
    • btype/bpress 后立即刷新 @N,避免"点了旧引用"。| 场景 | 结果 | 步数 | 耗时 | prompt / completion | |---|---|---|---|---| | 动态重排后点"确认提交"(旧 @N 失效) | succeeded | 5-6 | 17s | ~9k / ~330 | | 上传本地文件并读回文件名 | succeeded | 4 | 11s | 5026 / 284 | | 新标签页 + 切回第一个 | succeeded | 5 | 19s | 7178 / 1137 |

    生产侧预检(只读,2026-10-23)

    • 线上 danao-brain 0.2.1(2026-10-21),比本地 0.3.1 落后两个版本;
    • app/api/devices.py、app/api/control.py、app/capabilities/devices.py、computer_use.py、scripts/nuphus_runner.py 全部不存在,/api/v1/devices、/api/v1/control/* 访问返回 404;
    • computer_use 未配置;/data/danao/danao-brain-git 不是 git 仓库(无法 git pull);
    • 本地工作区有 1388 个未提交改动——直接打包会把别人的 WIP 带上线。因此没有动线上任何文件。启用方案、软链切换与回滚步骤写在[deploy/nuphus-mcp/PROD-ENABLE.md](deploy/nuphus-mcp/PROD-ENABLE.md),等真人批准后再执行。

    为了跑通真模型改的五处(都是实测暴露的)

    • 写入统一走 UIA SetValue:desktop_input 的 SendInput 在中文输入法下会丢字(实测把"第一行 A"写成" B第一行 A");SetValue 不动输入法,回执可验证。
    • 追加 = 已记录内容 + 新文字整段重写:执行器自己维护 doc_state,不再依赖光标位置。
    • 连续相同写入 = 收敛判定完成:模型写完看不见结果会反复重写(实测 8 步全在重写),同一写入 + 回执 OK 出现两次即判 succeeded。
    • 解析宽松化:模型偶发截断 JSON(如少了收尾引号),原来整步直接 fail;现在至少捞出 action 与字段继续执行。
    • open 走分页名单 + 等新窗口:应用名单每页 20 条,只读第一页会漏掉后半段;绑定后轮询新窗口并激活,避免模型以为没开成而反复 open。
    • 网页动作单独一套动词(0.3.1 追加):browse / bclick / btype / bpress / extract / bclose,网页元素用 @N 引用、桌面控件用编号;输入/回车后立即刷新 @N(页面重排会让旧引用失效)。有网页会话时桌面动作直接拒(实测模型会把 type 打到 Chrome 窗口上);extract 取不到正文时退回可交互元素快照。百度结果页仍会 timeout——动态重排导致 @N 漂移,属于站点限制,不是执行器缺陷。

    实测坑(已写进脚本注释与 deploy README)

    • 二进制放在非 ASCII 路径(如 D:\ai项目\...)时,所有 desktop_* 工具报automation lock tmp create failed(os error 3/5);装到 C:\nuphus-mcp 即正常。
    • 被文件沙箱限制的解释器(只允许写工作区)会复现同一个错误——runner 起跑前做桌面锁 预检并直接给出可操作的解释,而不是让模型十几步都在"点不动"。
    • 版本线要分清:桌面应用 1.0.0(2026-10-22)≠ nuphus-mcp 0.3.3(2026-10-13),两者独立发版;MCP 侧仍是 0.3.x,必须钉版本。

    版本号同时出现在 /healthz、/api/version 与控制台状态页;四处读的是同一个常量(app/core/version.py)。