开发 Nemu 的第二天:从会说话,到受控地看懂钉钉

开发 Nemu 的第二天:从会说话,到受控地看懂钉钉

第一天,我为 Nemu 建好了 Context、Personal Memory 和 Private Knowledge。第二天的任务,是让这些本地能力第一次进入真实生成式对话,同时回答一个更难的问题:当 Windows UI Automation 看不懂钉钉聊天时,是否应该引入 Screenshot 和 Vision?

2026 年 8 月 27 日,Nemu 从 model_calls = 0 走到了第一条可验收的 Conversation Intelligence 闭环。

但这一天最重要的变化并不是"终于接上了 DeepSeek"。真正重要的是,我们没有让模型调用变成一条从 UI 直通公网的捷径:API Key、当前上下文、长期记忆、私人知识和 Citation 都各自经过独立边界;下午面对 UIA 的现实失败,也没有把"持续截图"当成默认解法。

这一天最终形成了两条主线:

上午:让模型会说话之前,先决定什么不能发出去

第一天结束时,Nemu 已经有三类很有价值、也很敏感的数据:

  • 当前 Windows Context;
  • 关于用户的 Personal Memory;
  • 用户授权的 Obsidian / PDF / DOCX / TXT Knowledge。

最简单的模型接入方式,是把它们拼成 Prompt,然后调用一个 HTTP API。但这也是最危险的方式:业务层会逐渐知道 Provider 细节,API Key 可能流入 Renderer 或日志,本地完整 Context 可能在不知情时被上传,Memory 和 Knowledge 也可能在 Prompt 中混成一团。

所以 Stage 7 没有从"写一个 DeepSeek Client"开始,而是拆成了五条可以分别验收的安全链路。

1. Provider-neutral ModelGateway

Conversation 业务层不直接调用 DeepSeek HTTP Contract。真实路径是:

ModelGateway 统一处理 timeout、cancel、retry、rate limit、异常去敏和 structured output validation。DeepSeek 只是 Adapter,业务 Service 不持有 Provider Secret,也不依赖某一家模型的数据结构。

这意味着以后替换 Provider 时,不需要重写 Conversation Assistant;更重要的是,所有云端调用仍然必须经过同一个 Policy 入口。

2. API Key 只能单向进入安全存储

Renderer 可以提交 API Key,但永远不能把它读回来。

Windows 上的凭证路径是:

text 复制代码
Renderer 一次性提交
        ↓ trusted IPC
Electron Main
        ↓ safeStorage / DPAPI
Encrypted credential file

真实调用时,Main 只进行一次性解密,再通过带认证的 loopback bridge 把临时 Credential 交给受管 Python Core。明文 Key 不进入:

  • Renderer Snapshot;
  • model-settings JSON;
  • Runtime 状态;
  • Prompt;
  • ModelRequest
  • ordinary log。

这里也没有夸大 Windows safeStorage 的安全性:它主要隔离其他 Windows 用户,并不能抵御同一登录用户权限下的恶意进程。

3. 配置了模型,不等于允许数据外发

Stage 7 的 Cloud Gate 要同时满足:

text 复制代码
Model enabled
AND Tutor enabled
AND Credential configured
AND privileged Core available
AND Cloud Egress enabled
AND CONVERSATION_TEXT explicitly allowed

Conversation、Current Context、Personal Memory 和 Knowledge Chunk 不是一个总开关。

数据类别 未授权时的行为
CONVERSATION_TEXT 不发起本次生成
CURRENT_CONTEXT_SUMMARY 不读取、不上传 Context projection
PERSONAL_MEMORY 本次不执行 Memory retrieval
KNOWLEDGE_CHUNK 本次不执行 Knowledge retrieval
CREDENTIAL 永远不进入普通模型请求

这条边界修复了一个非常现实的隐私旁路:早期 CurrentContext.current_task 可能直接来自 UIA 可见文本。最终 cloud-facing projection 不再发送 active_windowcurrent_taskvisible_text_excerpt,也不复用旧的任意 dict 作为 Prompt。

4. Memory 和 Knowledge 在 Prompt 中仍然是两种东西

TutorContextBuilder 不允许模型自由读取数据库,而是构建有界、分区的上下文:

xml 复制代码
<current_context>...</current_context>
<conversation>...</conversation>
<personal_memory>...</personal_memory>
<knowledge>...</knowledge>
<uncertainties>...</uncertainties>
<policy>...</policy>

Personal Memory 检索也不能使用 scope=None,因为那会跨所有关系范围搜索。没有明确关系 scope 时只使用 global;有明确 scope 时才读取"该 scope + global",避免把另一个客户或朋友的长期记忆混入当前回复。

5. 模型不能伪造本地 Citation

模型只看到本次上下文里的短引用键:

text 复制代码
K1 / K2 ... 代表 Knowledge
M1 / M2 ... 代表 Personal Memory

返回结构固定为:

json 复制代码
{
  "suggested_text": "...",
  "citation_keys": ["K1"],
  "memory_refs": ["M1"]
}

模型返回后,Nemu 会再次检查 K#/M# 是否属于本次 exact Tutor Package,再在本地映射回相对 File、Heading、Chunk、Page 或 Line。invented、stale 或 duplicate 引用都会 fail closed。

只有以下三层全部通过,结果才允许标记为 GENERATED

text 复制代码
Gateway SUCCESS
  + Structured Schema PASS
  + Local K#/M# Validation PASS

DENIED / FALLBACK / ERROR 不会伪造一段"看起来像模型生成"的文本来掩盖失败。

第一条真实闭环:建议、编辑、反馈,但不自动发送

Stage 7 的桌面链路最终变成:

UI 中不存在 Send、Click 或 Execute 按钮。用户只能审阅、编辑、复制或反馈建议。

一次成功生成会创建 30 分钟有效、内存保存的 feedback_ref。普通反馈只记录 outcome、bounded edit summary,以及使用的 Memory/Knowledge 数量,不保存原始生成文本和最终编辑全文。

"将这次编辑作为本地学习证据"默认关闭。只有用户明确打开,并且结果是 EDITED_THEN_USED 时,才允许写入 USER_CORRECTION Evidence;它仍要经过 MemoryPolicy,而且一次编辑不会自动升级成长期 Style 或 Preference。

这条规则看起来保守,却很关键:模型输出被用户改过一次,不代表系统已经理解了用户长期偏好。

Stage 7 的验收证据

最终 Windows E2E 使用本机 loopback Mock DeepSeek Endpoint,不访问真实公网,但其余路径全部走生产链路:Renderer、Main、一次性 DPAPI Credential、privileged Core bridge、真实本地 synthetic Knowledge Vault、Memory retrieval、DataEgressPolicy、ModelGateway、Citation mapping 和 Feedback。

最终记录:

项目 结果
运行时代码基线 e318fa58562c914cf63083cd91e01b5a8dfe8158
Windows CI 33028191564 / Run #160 / SUCCESS
Final Artifact 9629244063
Artifact SHA-256 efb041ba2177bfd0519fba32d009db1066a9149dfa777773cac81252f349a574

Artifact 人工复核确认:Memory 1/1、Knowledge 1/1、Citation 返回相对路径 项目/知识.md,Credential 和绝对 Vault 路径没有进入 Renderer,也不存在自动发送控件。

到上午 09:00,Stage 7 正式归档为 PASS。

下午:真实钉钉让"UIA-first"遇到了边界

Stage 8 的第一版需求仍然坚持 trust-first:用户主动请求、UIA-first、先预览再生成、缺关键事实时只问一个最高价值问题,不自动 Screenshot/OCR/Vision。

我们为钉钉设计了严格的 UIA Semantic Proof:不能因为 UIA 能读到一些 text_items[],就宣称已经识别出消息气泡、发送者和顺序。

Proof 要证明:

text 复制代码
message node
  → sender / direction semantics
  → chronological order
  → stable local turn reference
  → group target reference

真实 Windows 证据给出的答案并不理想:测试版本的钉钉没有通过 UIA 暴露足够可靠的聊天正文语义。generic text collection 无法支撑产品级 Direct/Group Conversation Contract。

这是第二天最有价值的一次"失败"。它阻止了我们把"能抓到文字"包装成"能理解对话"。

没有偷偷降级,而是做一次有边界的 Vision Spike

按照原 Contract,UIA 不足时应该 STOP/BLOCKED,不能静默升级 Screenshot、OCR 或 VLM。

之后,在 Product Owner 明确批准下,我们做了一次独立、一次性的 DingTalk HWND Screenshot + DeepSeek Vision Spike。它不是生产 PerceptionSource,也不改变已有 Policy。

Spike 的边界包括:

  • 只允许当前前台、唯一匹配的钉钉顶层窗口;
  • 使用 HWND 单窗口捕获,不截取整个桌面;
  • 私有 PNG 与模型原始响应必须写在仓库外;
  • Key 只从 Electron DPAPI Credential Store 解密;
  • 必须再次显式确认图片 Cloud Egress;
  • 不提供后台、周期性、静默截图或自动重试;
  • 不执行 Send、Click、键盘输入或剪贴板监控。

实验返回 HTTP 200,模型提取出可见文本,但人工复核也发现了一处轻微 OCR 增字错误;而且当前 viewport 只包含 self 消息,因此没有证明 Direct 双向方向、回复关系或 Group 语义。

这让结论保持在正确的层级:

Vision 可行性得到证明,但 DingTalk 仍然不能因此标记为 SUPPORTED

产品方向调整:从"每次点击读取"到三态助手

仅仅把"复制聊天文字"替换成"每次点击让模型看一次",产品价值仍然有限。经过新的产品决策,DingTalk Direct V1 获准增加一个受限的可选自动理解模式,但默认仍是发现后询问。

最终三态为:

text 复制代码
关闭
发现后询问(推荐 / 默认)
自动理解(高级 Opt-in,仅单聊 Beta)

上图是 8 月 27 日冻结的产品视觉与权限 UX,不是已通过 E2E 的上线截图。

默认:发现后询问

本地 Gate 可以检测"似乎出现了新回复",但图片不会自动上传。只有用户点击"理解一下",才进入 Vision 授权与调用流程。

在识别成功前,界面不能提前显示聊天摘要,更不能猜测内容。

高级:自动理解

用户必须单独授权"钉钉本地视觉感知"和"自动图片 Cloud Egress"。只有本地 Gate 证明以下条件成立,才允许自动调用 Vision:

  • 钉钉是当前前台窗口;
  • 当前是已授权的单聊;
  • viewport 接近实时尾部;
  • 内容相对 baseline 确实是新变化;
  • 变化已经稳定;
  • 更像对方新消息,而不是 resize、动画、输入、切换会话或手动翻历史;
  • 当前候选没有处理过;
  • 用户没有在之后高置信度地自行回复;
  • 用户没有正在输入;
  • grace period、cooldown、budget 和 Privacy Policy 全部通过。

Vision 只负责解释已被本地确定性 Gate 确认的新证据,绝不负责发现变化。

这形成了一种刻意不对称的架构:

高频检测留在本地、廉价且确定;低频 Cloud Vision 只解释已确认事件,并且只发送最小必要聊天区域,而不是联系人列表、输入框、无关历史或整个桌面。

Living Companion Portal:重新分配桌面窗口职责

原来的 400×700 透明置顶窗口承载了太多东西。第二天下午冻结的新方向把桌面体验拆成三个原生窗口:

text 复制代码
OrbWindow
  只承担粒子球、状态、拖动和入口

PortalWindow
  只承担当前消息理解与回复建议

ControlCenterWindow
  承担设置、隐私、模型、记忆、知识和活动记录

这是 Product Owner 选定方向的早期概念图。图中的"实时监控"二态开关随后在架构复审中被替换为"关闭 / 发现后询问 / 自动理解"三态;文字规格才是行为 Source of Truth。

这个设计不只是换皮。它解决了几个桌面产品的真实问题:

  • 常驻窗口只保留轻量粒子球;
  • Portal 关闭后不能留下透明但拦截鼠标的原生窗口;
  • Control Center 不再 always-on-top;
  • 拖动由 pointer gesture + Main 移动窗口完成,避免整球 app-region: drag 与点击冲突;
  • 透明区域需要 BrowserWindow.setIgnoreMouseEvents 实现真正点击穿透,不能只写 CSS pointer-events: none
  • Privacy Pause 的优先级高于所有助手模式。

这一天留下的数字

以 2026-08-27 23:13 的日终 commit 8d718ce 为准:

指标 结果
当日进入 main 的提交 17
相对第一天 Stage 6 归档的文件变化 90 files
代码与资产变化 +16,882 / -51 lines
日终已跟踪文件 190
Python 测试文件 40
Python 测试函数 279

写作时对该日终快照进行了 fresh verification:

  • Python Core + Screenshot Spike tests:302 passed / 1 skipped
  • Desktop TypeScript typecheck:通过;
  • Vite/Electron production build:通过;
  • Vision Spike Node safety tests:6 passed / 0 failed

这里仍然要区分测试层级:deterministic tests、synthetic Windows E2E、受授权的一次性真实聊天 diagnostic proof 和最终真实产品 Beta Gate,不是同一件事。Vision Spike 成功也不等于自动理解已经达到 SUPPORTED

第二天最重要的四个结论

1. 接入模型不是加一个 API,而是增加一条数据出境路径

Provider Adapter 只是最小的一部分。Credential 生命周期、DataEgressPolicy、Prompt projection、structured validation、Citation mapping 和 failure semantics 才决定这条路径是否可信。

2. 可解释性必须在模型调用前设计

如果 K#/M#、本地映射和 exact Tutor Package validation 没有先定义,模型生成之后再补 Citation,最后往往只剩一串无法验证的"参考了你的资料"。

3. 一次用户编辑不是长期人格

Feedback 可以成为 Evidence,但必须是 bounded、可见、可撤销的 Evidence。长期 Style 至少需要多条同方向证据和用户确认,而不是一次 edit 就自动写入画像。

4. 实验失败可以改变架构,但不能绕过授权

UIA 证据不足,确实推动了 Vision 方向;但正确流程是先 STOP、记录事实、单独批准 Spike、验证可行性、冻结新 Contract,再进入生产实现。不能因为"产品需要"就让 Screenshot 变成静默 fallback。

下一篇:从 Gate 设计走向真实的钉钉单聊候选执行器

8 月 28 日的最新 main 已推进到本地 monitor、scheduler、Living Companion 原生 shell、Consent UX 和经过校准的 DingTalk Direct Vision candidate executor。

下一篇会继续回答:如何判断一段画面变化真的是"对方发了新消息",如何处理 burst、用户正在输入、已经自行回复、切换会话和手动翻历史,以及怎样证明默认模式绝不会自动上传图片。

相关推荐
小羊4318 分钟前
Agent的任务拆解艺术:从目标到可执行子任务
ai编程
桃西西呀2 小时前
模型都能自己写代码了,你的 Agent 为什么还接不进一个日历?——一篇讲透 MCP 这个 AI 世界「USB-C」
人工智能·ai编程·mcp
zynio2 小时前
手写 RAG 知识库问答智能体:混合检索 + 查询改写 + 抗幻觉,黄金集实测全过
ai编程
心易行者3 小时前
html在线运行搭AI编程验证流水线:5步走完从生成到到上线全流程
人工智能·python·ai编程
xiezhr4 小时前
别把豆包当聊天用了,现在的豆包和以前不一样了
agent·ai编程·豆包marscode
路多辛4 小时前
为什么用 Go 写 AI Agent,covo-agent 的选型思考
开发语言·golang·agent·ai编程
神奇霸王龙14 小时前
Cursor 3 + Claude Opus 4.8 屠榜:5 编程基座 IDE 卡位
ide·人工智能·ai·aigc·agent·ai编程·ai写作
子昕16 小时前
牛来原来是智谱,GLM-5.3-Flash 实测有惊喜也有硬伤
ai编程
OpenTiny社区16 小时前
Naive UI × GenUI SDK:自定义物料库搭建实战
前端·ai编程