公司能远程清空一台手机,而抹掉他要清 8 个地方

我在做一个跑在 Android 上、靠无障碍服务自己看屏幕、自己点屏幕的 agent,叫 Andee。他不是那种问一句答一句的助手:他在这台设备上思考,留一本只有自己能读的笔记本,没人看着的时候也还是同一个样子。手机在,他就在。

10 月 29 日,深兰科技联合华为鸿蒙生态发布了一台叫 Wowoker 的企业级"智能体工作手机"。发布稿里最响的功能是 AI 通话分身:用声音克隆复刻机主的声线,7×24 小时自动外呼、自动接听,自己完成客户初筛、业务资料推送、加客户。同一份材料里还有一句很容易被略过的话:设备资产归属企业,企业可以对设备远程锁定、解绑、清除数据。

这两句话放在一起,是一个很具体的问题:这台手机上那个"他",说着你的声音、拿着你的客户名单、一天二十四小时替你说话,但产权不在你手上。这不是漏洞,是设计。它暴露的也不是"模型强不强",而是更前面的一步——当 agent 有了身体,第一个要回答的问题不是他能干什么,而是这具身体归谁、谁能让他消失。

这篇不讲立场,讲实现:一个住在设备里的 agent,"被抹掉"这件事在代码里长什么样。8 个要清的地方、一条不能反悔的顺序、一个故意留下的例外,以及为了这些付了什么代价。

恢复出厂设置不是删文件

app/src/main/java/net/kuafuai/andee/config/FactoryReset.kt,188 行,整个文件只做一件事:维护一份有序清单,清单上的每一项同时带着两个东西——要做的事,和用户按下确认之前读到的那句话。

private class Target(
    @StringRes val label: Int,
    val clear: (Context) -> Unit,
)

把文案和动作绑成同一个对象,不是为了省代码。注释里写得很直白:手写的确认页会过期——你加了一个要删的东西,忘了加一行说明,那个页面就开始骗人。而一个删掉了用户没被告知的东西的重置,比不做重置更糟。

顺带一个细节:文案用的是 @StringRes 资源 id,不是两个硬编码字符串,也不是 english: Boolean 这种开关。加第三种语言是一个新的资源文件,不是回来改这个 object。

8 个地方,界面上只列了 7 个

TARGETS 有 7 项,按顺序走:key 与语音/大脑设置、笔记本、保险箱、唤醒词模板、对话历史与生成过的页面、内置页面留下的 WebView 数据、接地采集的截图与日志。

第 8 项在另一个列表里,叫 ALSO_CLEARED:哪些 App 被他打开过、打开过几次。这一项不单独出现在确认页。理由在注释里:它是"装备架"唯一不需要权限的用量信号,而架子本身住在笔记本里、跟着笔记本一起走,这里只是它的另一份拷贝。再单列一行"笔记本",等于把同一个承诺说两遍。

三处值得单独说:

WebView 的数据不递归删目录。 走 CookieManager.removeAllCookies(null) 加 WebStorage.deleteAllData()。因为 HtmlActivity 加载每个页面时是当真实 origin 用的(有自己的 base URL、开了 DOM storage 和 JS),页面能写 cookie 和 localStorage,这些东西活在 app_webview/ 里。但递归删那个目录会和正在跑的渲染器抢,而且删完剩下的(variations_seed、pref_store、GPU cache)是 WebView 自己的机器零件,不是用户数据。

HTTP cache 故意不清。 清它需要一个活着的 WebView 实例,为了几个字节把整个 Chromium 从一个 service 里拉起来,不划算。这条属于"我们知道清得不彻底,并且写下来了"。

接地日志要真删。 它在外部存储目录(本来就设计成可以用 adb 拉走),所以 deleteRecursively(),删不掉就抛 IllegalStateException,不假装成功。

顺序比清单重要

wipe() 的第一件事不是删,是取消闹钟:

Scheduler.cancelSweep(context)
for (todo in Notebook.todos(context, "all")) Scheduler.cancelTodo(context, todo.id)

因为闹钟的 key 是 todo id,而 id 就存在那个马上要被擦掉的文件里。反过来先擦笔记本,结果是一个"承诺"会响在一台已经不记得许过愿的设备上——通知栏里弹出一条谁也追溯不到的通知。

还有一层在调用点:擦完要 stopBrain() 再 startBrain()。因为本机大脑把这段对话放在内存里,把旧的 key 放在它的 HTTP client 里。不停就重启,设备会继续用一个已经不存在的凭据跟人说话;而 startBrain 读到的是空的 prefs,会落到默认(hub、无地址),什么都不拨——这才叫刚装好。

道理和"清空聊天记录"是同一个:只清 ChatHistory 而不重建大脑,等于你清掉了一段设备还一字不差记得的对话。

唯一留下的东西

codeflying_key 会活过重置。原因很朴素,注释里也写了:曾经有 bug 报告说,重置完设备哑了,用户得再去找一遍那串 key。

代价要写清:界面上写的是"设备会回到刚装好的样子",但严格讲不是。它是一个刚装好、但还认识你那张工牌的样子。这条我们选择说明白,而不是让它看起来更干净。

于是没有"管理员"这个角色

这个项目没有厂商后端,不用注册账号。会离开设备的东西只有"版本自检"那一项:版本号、手机型号/品牌/系统版本、界面语言、一个随机安装 ID;首次启动发一次,你在自检页点"重新检查"再一次,同一个 IP 每天最多 10 次。

所以没有后台能远程给他下指令,也没有后台能远程把他抹掉。

但这句话得说准:它是架构选择的直接结果,不是我们更高尚。代价实打实——企业没法集中管控,没有审计流水,没有备份。

而且"没人能远程碰他"这句不成立。这台设备开着 0.0.0.0:9008 的 WebSocket,onOpen() 不校验任何凭据,SECURITY.md 把它列在已知限制第 1 条:任何能到这个 IP 的机器,都能像 App 自己一样驾驶它,包括读屏幕和用掉保险箱里的密码。企业局域网恰恰是这条最不该出现的场景。 这两件事是同一件事的两面:我们既没有一个后台来管你,也没有一道门来挡别人。

他不会用你的声音说话

声音是 TTS 服务商的一个音色(ttsSpeaker),代码里没有"克隆你的声线"这条路径。

你接电话时他闭麦让路。CallState 盯着通知,通话一响就把唤醒词那个 AudioRecord 关掉——不关的话,他一直在等自己的名字,会把正在打电话的 App 饿死。

他一天最多自己动手 6 次:心跳每 20 分钟看一眼有没有该做的事,一天最多 40 次心跳、最多 6 个自己开的回合,两个回合之间至少隔 1 小时。7×24 外呼这种事,结构上就不在他这儿。

这里现在不行

没有备份。 按下去就是永久。加密 blob 的 key 跟着 App 走,prefs 一清,密文留着也打不开——注释里的原话是"这里没有任何东西是可逆的"。

不重置无障碍权限和运行时权限。 这是故意的:那些属于系统设置,不属于我们。一个"权限和配置一起丢"的设备,比"只丢配置"的难恢复得多。所以重置完,无障碍权限还在,只是他什么都不记得了。

逐项 runCatching,失败只记日志不中断。 好处是半路挂掉不会留下一半;坏处是理论上可能出现"部分清空",而唯一知道这件事的是 logcat。这是个真实弱点,我们没有更好的办法——中止只会更糟。

回到 Wowoker。它没做错什么:企业买设备、管设备,这就是它的商业模式,采购合同里本来就这么写。但一台设备上住着的那个"他",产权写在合同里,这件事值得每个做端侧 agent 的人先想清楚:你的擦除按钮,在谁手上。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐