22 KiB
技术实现
模块按进程与功能域安装针对性的 Hook。入口只负责生命周期、进程筛选、配置注入与安装结果汇总;具体目标定位和拦截逻辑留在各自功能域中。
Hook 安装与诊断
- 每个功能域通过
HookRegistrar注册 Hook,并使用稳定 ID、PROTECTIVE异常模式和统一优先级策略。 - 安装结果区分
INSTALLED、MISSING、FAILED、SKIPPED,保留HookHandle,便于定位 ROM 或目标 App 升级后的签名漂移。 - 普通目标缺失与反射异常按功能域失败开放;
HookFailedError等框架级Error不会被普通异常隔离层吞掉。 ModuleMain会尽早过滤无关进程并调用detach(),避免在不需要的进程中继续保留生命周期回调。
日志与 Release 裁剪
Eta 使用同一组四级日志语义,并按运行环境选择后端:App 与 Agent Runtime 通过 AndroidAgentLogger 写入 logcat,Hook 进程通过 ModuleLogger 写入 Xposed 日志。业务代码不得直接调用 android.util.Log 或 XposedModule.log。
| 级别 | 使用范围 | Release |
|---|---|---|
DEBUG |
高频正常流程、目标匹配、重试细节、尺寸与计数 | 全部裁剪 |
INFO |
低频生命周期、Hook 安装汇总、特权动作的结构化摘要 | 保留 |
WARN |
可恢复降级、fallback、目标签名漂移、重试耗尽 | 保留;高频事件必须节流 |
ERROR |
当前请求或功能确定无法完成、关键不变量被破坏 | 保留 |
取消、功能关闭和可选目标缺失不记为 ERROR。debug 只接受惰性 supplier;supplier 必须是纯观察代码,不能执行 Hook、反射写入、状态变更或其他业务副作用,因为 Release 的 R8 会删除整次调用。
任何级别都禁止记录 Prompt、请求或响应正文、API Key、认证 Header、Cookie、工具参数与结果、原始命令、stdout/stderr、URI、文件路径、图片内容、应用清单和原始运行标识。异常默认只记录类型,不拼接 Throwable.message;外部或模型生成的名称必须先转成受长度和字符集约束的安全 token。只有确认不承载用户数据的框架或反射异常才允许附带完整堆栈。
Release 裁剪以 app/proguard-rules.pro 为唯一可执行事实来源,规则边界如下:
-maximumremovedandroidloglevel 3 class fuck.andes.** { *; }只删除 Eta 自有代码中的 AndroidVERBOSE/DEBUG,不影响依赖库。- 对
AgentLogger.debug(Function0)、AndroidAgentLogger.debug(Function0)和ModuleLogger.debug(Function0)使用精确的-assumenosideeffects,覆盖 R8 无法识别的 Xposed 日志后端。 - 不为
INFO/WARN/ERROR声明无副作用,不使用*Logger或全局android.util.Log通配裁剪规则。 - 每次修改规则后同时构建 Debug 与 Release,并检查 R8 configuration/usage、DEX 日志调用、代表性日志字符串和 Xposed 入口元数据。
上述策略依据 Android 官方的 R8 附加规则、日志信息泄露防护、AOSP 日志级别约定、OWASP 运行时日志测试 与 CWE-532。
Eta 原生数字助理
Manifest 注册 VoiceInteractionService、独立进程的 VoiceInteractionSessionService、全屏 TYPE_APPLICATION_OVERLAY 助理浮窗以及 Android 助理角色资格要求的 RecognitionService。设置页只负责打开系统数字助理选择界面;当前浮窗不请求麦克风权限。
VoiceInteractionSession 只承接系统入口并关闭自身 UI;EtaAssistantOverlayService 持有全屏窗口、彩色边缘动画和键盘输入。窗口通过 setFitInsetsTypes(0) 绘制到状态栏、导航栏与显示开孔后方,可交互内容再通过 WindowInsetsRulers.SafeDrawing 与 Ime 保持可触达,避免给根容器增加 Insets 后截断 edge-to-edge 背景。用户提交的文本交给 AgentRuntimeClient;请求、流式结果、前台工具收起、取消与归档沿用既有 Runtime 协议,当前不执行语音识别或语音朗读。
:voice、:voice_session 与 :recognition 进程只初始化本地偏好,不预热数据库、Skills 或 Xposed UI 服务。RecognitionService 仅保留 Android 数字助理角色资格所需声明,不由当前浮窗调用。
system_server
- 电源键接管:Hook
PhoneWindowManagerExtImpl$OplusSpeechHandler.handleMessage()处理系统分发给小布的唤醒消息(what == 0x3F3)。目标为小布时直接执行原方法;目标为 Eta 时才拦截并分发到对应入口。 - 兼容配置:三态目标写入字符串键;键不存在或值非法时读取旧
POWER_KEY_TAKEOVER布尔协议,true继续表示启用本项目助手(归 Eta),false表示小布。新安装默认保持小布,旧用户不会因新增 Eta 被改写目标。 - 数字助理配置修复:独立自动设置开关开启后,开机、解锁、切用户及启动失败恢复时,通过
AssistantManager异步校正当前 Eta 目标的android.app.role.ASSISTANT与 secure settings。小布模式和开关关闭时不写系统配置;校验缓存及异步回调同时核对用户与目标,旧目标任务不会继续覆盖新选择。 - 唤起逻辑优化:Eta 优先使用活动
voiceinteraction会话,再在已经配置为默认助理时尝试同包ACTION_ASSIST桥。所有路径失败后立即执行小布原逻辑,不阻塞系统回调。 - 无障碍保护:复用已验证的
SystemServer.startOtherServices(TimingsTraceAndSlog)生命周期点,在系统服务启动完成后接入事件驱动保护。后台工作复用 AndroidBackgroundThread,不开模块线程、不轮询;开关默认关闭,开启请求需同时通过 signature 权限、真实发送 UID、服务声明与 APK signer 钉扎校验。保护只维护 owner 用户中的 Eta 组件和总开关,保留其他服务;断连时通过仅允许systemUID 调用的健康 Provider 确认,并对 Eta 做带次数上限和冷却的定向重绑。
无障碍保护
「强制保持无障碍」默认关闭。开启后,注入 system_server 的保护后端会校验 Eta 的服务声明、调用 UID 与 APK 签名,并在无障碍服务列表、总开关、Eta 安装包或 owner 用户解锁状态变化时校正配置。它保留其他无障碍服务,不依赖 App 自启动,也不执行周期轮询。
如果服务仍在启用列表中但没有真实连接,保护后端只重启 Eta 自身,并最多逐步尝试三轮;持续失败后冷却一分钟。ColorOS 持续反删设置时,写回间隔会从 300 ms 退避到 30 秒,稳定一分钟后恢复。关闭开关只停止保护,不会替用户关闭当前服务。
GUI 工具执行前仍会确认真实服务连接。保护未开启、system 作用域未生效或重绑超时时,本次动作会明确失败,不会改用 Root 或 Shell 偷偷修改无障碍设置。
若 App 控制入口不可用,可用 ADB 停止保护后再从系统设置关闭服务:
adb shell settings put global eta_accessibility_protection_enabled 0
删除该设置会恢复默认关闭状态。开发阶段主动更换签名且确认 APK 来源可信时,还需清除旧 signer 钉扎值:
adb shell settings delete global eta_app_signer_sha256
小布记忆
ColorOS 系统记忆存在 com.oplus.aimemory 的 ai_memory 数据库中。Eta 只在小布记忆默认进程保留模块生命周期,Hook 其 DataShareProvider.call(String, String, Bundle) 安装内部查询桥。Runtime 通过 Root 以固定 method 调用该 Provider,Hook 在拥有数据库权限的目标进程内以只读模式执行固定查询。非 Eta method 会原样进入小布记忆自身逻辑;内部 method 只接受 UID 0,不向模型暴露任意 URI、表名或 SQL。
查询协议只允许系统记忆、个人订单和已保存地点三种操作,请求与结果都有 UTF-8 字节上限。数据库层只查询预定义的表和字段,SQLite 标识符统一引用,以兼容 shipments.order 等与 SQL 保留字重名的字段。结果继续按敏感工具处理,不写入持久会话。
进程内查询桥不可用时,Runtime 才回退到 Root 快照路径:将主数据库及存在的 WAL、SHM 或 journal 边车文件限大复制到 Eta 缓存,用同一查询引擎只读打开,并在查询结束后立即删除。
配置与实时生效
模块 UI 基于 Miuix 0.9.3。配置链路如下:
设置页与标准二级页面的顶栏使用 Miuix LayerBackdrop 捕获滚动内容并生成背景模糊;设备不支持 RuntimeShader 时回退为主题 surface 纯色。标准设置列表统一启用 Miuix 越界滚动和滚动末端触感反馈。
- Eta Runtime 配置:默认思考、网页浏览、设备直达、敏感信息读取、敏感设备操作和终端/文件工具保存在 App 私有配置中,不依赖 LSPosed。Runtime 在请求开始和每次工具执行前读取当前值;升级时会兼容迁移已有 RemotePreferences 值。
- Hook 配置:
FuckAndesApp在Application.onCreate注册XposedServiceHelper,框架通过XposedProvider推送 binder 后拿到XposedService。系统助手接管、厂商助手兼容等 Hook 开关通过XposedService.getRemotePreferences()写入 LSPosed 数据库;服务未连接时这些开关保持不可修改。 - Hook 进程:
ModuleMain.onModuleLoaded调用XposedInterface.getRemotePreferences()缓存只读SharedPreferences到Prefs。各 Hook 拦截回调入口直接读Prefs.isEnabled(key),关闭则走原逻辑;因此正常使用时,配置切换后的下一次相关触发表现为实时生效。这里的实时生效来自 Hook 入口读取当前配置,不是 libxposed API 102 的 hot reload 特性。 - 延迟任务复查:已排队的后台配置修复会在执行前再次检查对应开关,避免用户在任务排队期间关闭开关后被旧任务绕过。
个人数据直达
个人数据检索复用“敏感设备信息读取”开关;模型调用时可读取原始结果,但工具参数与结果不会持久化进会话记录。每个能力都固定到已验证的 Provider、投影字段和排序方式,只接受受长度限制的关键词与返回数量,不向模型暴露任意 URI、表名或 SQL。
Runtime 提示要求模型在用户目标会明显受益于本机上下文时主动调用已公开的只读工具。对于“了解我”、近期活动、习惯偏好和个性化建议等宽泛任务,模型应从多个相关来源按时间与代表性取样后再归纳;工具已公开即表示对应能力已由用户开启,不重复询问授权,也不因单个来源为空就直接停止。专用工具不存在、结果不足或数据源不可用时,如果 Root Shell、文件或终端工具已公开,模型会继续定位并只读检查相关应用私有文件与数据库,先识别格式和 schema,再执行有界查询,不修改源数据。
- 标准 Android Provider:相册图片、音频、共享文件、日历、通讯录、通话记录、短信和下载记录。
- ColorOS 数据源:通过固定 Provider 读取便签正文、待办、普通录音、通话录音与录音摘要;系统记忆优先由小布记忆进程内的只读 Hook 桥查询,再在桥不可用时回退到包含 SQLite 边车文件的 Root 临时快照。两条路径共用固定查询引擎,可检索记忆正文及其账单、日程、取件码、快递、地点和附件。
- 个人上下文:位置按需读取最近系统位置;应用活动与使用时长依赖用户授予的使用情况访问权;闹钟、计时器、输入法剪贴板历史和 Health Connect 聚合值通过固定数据库只读快照查询。健康工具只返回指定时间窗口的汇总,不返回原始测量序列。
- 通知历史:系统自身没有可用历史时不伪造旧记录。用户授予通知使用权后,Eta 从授权时点开始在独立本机数据库中保存标题、正文、来源包和时间,保留 7 天且最多 1000 条;查询结果仍按敏感工具规则从持久会话移除。
- 个人订单:优先检索系统记忆已经识别的外卖、购物、快递、票券和出行信息。第三方应用导出的进程通信 Provider 不等于订单查询合同,Eta 不依赖其易变私有订单库。
- QQ 与微信专用目录:仅扫描已验证的聊天图片缓存目录,按最近修改时间返回有界的文件元数据;不扫描视频、消息数据库、消息正文或任意其他应用私有目录。
- 设备上缺少相应应用或 Provider 合同变动时,工具返回结构化不可用错误;不会改用遍历其他应用私有目录的方式猜测数据。
文件视觉
read_image 属于通用文件视觉能力,随“终端/文件工具”开关公开,不依赖个人数据直达。它接受用户或其他工具已明确提供的任意本地绝对路径、file URI 或系统相册 URI;本机路径由 Root 读取。Root 将单张、大小受限且非符号链接的文件复制到 Eta 临时缓存;发送给模型前会仅为视觉请求缩放压缩,以避免多张原图撑大 OpenAI 兼容请求体,原始文件不会被修改。当前回合结束后立即删除临时文件。QQ/微信检索工具只负责提供可传入的图片路径。
运行时提示与工具描述共同要求模型每轮最多调用一次 read_image。需要查看多张图片时,模型必须先消费当前图片的视觉结果,再在下一轮读取下一张,避免同一请求携带多张工具图片导致部分 OpenAI 兼容服务长时间无响应。
内置浏览器
browser_use 是运行在 Eta 内的 Agent 浏览器,基于共享离屏 WebView,不是简单调用系统 ACTION_VIEW。它可以在不抢占前台的情况下加载 JavaScript 网页、提取保留标题/段落/列表/链接等结构的正文、查找并操作页面元素、提交表单、滚动和截图;用户想查看过程时,可在 App 中挂载同一个 WebView 直接接管。外部打开链接仍由独立的 open_uri 工具负责,两种能力不会混淆。
Eta 不对浏览器请求执行额外的 URL、DNS、IP、主机数量、请求方法、重定向或 Service Worker 拦截,页面直接交给系统 WebView 加载。浏览器允许本地内容、混合内容、第三方 Cookie、自动媒体播放和表单提交;系统 WebView 与 Android 平台自身的协议支持、TLS 校验和权限行为保持不变。网页工具可在设置中关闭。
终端与文件
在用户授权下执行 user 或 root shell 命令,读写文件、列目录、跑脚本、查日志、改配置。会话式 shell 保持 cwd 和环境变量,异步任务后台执行并分段读取输出。聊天中的终端工具卡片可展开查看并复制实际执行的完整命令;运行日志仍只记录长度等受控摘要,不记录命令正文。
终端按用途分为两个环境:
android是原生 Android Shell,负责系统、应用、日志、Magisk 和设备文件操作。Root 会话会自动发现 Magisk、KernelSU 或 APatch 提供的 BusyBox,并以 standaloneash补齐不在系统 PATH 中的 applet。linux是可选安装的 Alpine 工具环境,预装模型高频使用的rg、fd、Git/SSH、diff/patch、curl、rsync、jq、SQLite、常用压缩工具与 Python 工具链。Eta 下载固定版本的官方 minirootfs 并校验 SHA-256,在 App 私有目录中解压,通过独立 mount namespace + Root chroot 运行;Linux 默认在映射到 Eta Android 工作目录的/workspace中执行,共享存储位于/sdcard。它不是安全沙箱,也不会取代 Android 环境。
聊天输入栏可以引用内部存储与 /data/local/tmp 下的文件或文件夹,发送后以附件名称和原始请求分开展示。Eta 只把经过 Root 校验的规范绝对路径写入模型上下文,不上传、不复制或缓存原文件;模型再按任务调用文件或终端工具读取。系统文件选择器会解析内部存储文档,以及能转换为本地媒体库路径的“最近”文件;云盘和其他只有 content:// URI 的来源不会降级为上传。
长期记忆
跨对话长期记忆保存在 App 私有目录中的单一 MEMORY.md,不额外调用提取模型或运行后台整理任务。当前对话的主模型根据需要调用 memory_get 与 memory_write,负责去重、修正冲突和删除过期信息。
- 按需注入:每轮只自动提供预算内的
# 核心记忆、标题索引和文件 revision;详细章节由模型按任务检索,不把整个文件反复塞进上下文 - 动态预算:核心注入量根据当前模型上下文窗口计算;窗口未知时按 128K 处理,并始终为历史、工具、图片和回复预留空间
- 原子更新:文件上限为 1 MiB UTF-8 字节,模型使用 revision 进行局部更新,冲突时必须重新读取;完整文件通过
AtomicFile覆盖,失败保留旧内容 - 用户控制:设置页可查看用量、编辑完整 Markdown、清空或关闭记忆;关闭不会删除文件,但 Runtime 会立即停止注入并拒绝新的记忆工具调用
会话级 Thinking Effort
聊天会话保存独立的 ReasoningEffort,输入栏按当前 Provider、端点和模型能力显示 Thinking · Off / Default / Low / Medium / High / XHigh / Max 的实际子集。模型不支持推理时不显示入口;强制推理且没有可调档位的模型只显示不可点击的 Thinking · Default。模型切换或远端能力刷新后,已保存但不再合法的档位会向下裁剪到最近的有效档位,没有可比档位时回到 Default。
能力解析依次采用远端精确元数据、内置模型目录、Provider 与模型家族规则,最后安全降级。Default 保留供应商或高级自定义请求体的默认行为;显式档位在请求体合并完成后应用,因此会话选择是最终覆盖。Room、Runtime Bundle、RemotePreferences JSON 和外部归档同时保留旧 thinkingEnabled 布尔投影,旧 true/false 分别解释为 Default/Off;强制推理模型收到 Off 时直接报告配置错误。
聊天流式渲染
模型的 SSE 文本增量先在 App 状态层按 50 ms 合并,减少高频列表状态写入;思考、工具调用和块边界事件仍会立即刷新,事件顺序不变。聊天渲染使用增量 Markdown AST,但不会把尚未显示的大段网络 backlog 一次性交给布局:解析器每次最多追加 12 个 Unicode 字素,批与批之间让出一拍供重组排版,供给节奏与显现速度解耦;消息高度始终由显现进度驱动,解析领先不会提前撑高回答。输入器使用 state-based TextFieldState 在组件内持有编辑缓冲区,逐字编辑不会回写聊天页面状态,也不经过旧版 CoreTextField 管线。
逐字效果由单个回答级帧时钟驱动。段落、标题、代码块和表格单元格先完成一次真实排版,再由 DrawModifierNode 按字素边界裁剪 TextLayoutResult;帧间推进只使绘制失效,不重建 Markdown AST、AnnotatedString 或文本布局。显现速度随积压自适应(48–240 字素/秒),积压时允许单帧补多个字素,避免输出稳定滞后于模型。行内语法闭合(加粗、行内代码、链接折叠)导致渲染文本变短时,显现进度保持单调前进,不回退重打已显示的文字。前缀路径按字素增量累计,只有显现跨入新行时才触发一次测量并增加消息高度,因此隐藏文本不会提前把当前回答顶出视口。Emoji ZWJ、肤色修饰、组合音标、国旗和代理对均作为完整字素显示,不会从 UTF-16 中间断开。
底部跟随只在用户真实拖动时解除,并仅在消息实际增高、底部哨兵离开视口时滚动;纯网络状态更新不会反复触发滚动。网络结束、解析追平且显现队列排空后,回答切换到稳定 Markdown 状态,恢复完整链接解析与文本选择。token 用量事件只更新用量字段,不触碰消息的流式标记,避免流式/静态视图在轮次边界反复切换。
功耗与开销
追求极简,绝不给系统增加额外负担:
- 不轮询、不保活 Google 进程、不持续写日志
- 无障碍保护默认关闭;开启后只响应设置、包、用户生命周期与 Runtime 明确上报,设置争抢采用退避,断连重绑有次数和冷却上限
- 热路径只保留当前机型实际验证有效的
OplusSpeechHandlerhook - 默认助理配置检查带 15 秒冷却
- 高频成功路径使用
DEBUG;Debug 构建可诊断,Release 由 R8 确定性裁剪 - 电源键拦截路径不执行休眠、轮询或阻塞等待;本次触发只做快速启动尝试,失败即回退系统原逻辑
- 默认助理修复异步执行并按用户串行,完成时重新核验 role、目标与开关状态
预期行为
电源键目标为小布时,ColorOS 长按电源键保持厂商原始行为且不修改当前默认助理。目标为 Eta 且 Eta 已是默认数字助理时,长按会打开 edge-to-edge 全屏助理浮窗并自动聚焦键盘输入框;入口会在浮窗与 IME 出现前准备一张屏幕截图,只有用户选择后才作为下一条消息的图片上下文发送。用户提交文本后,工具执行、流式结果和归档仍由主进程中的 Agent Runtime 负责,当前流程不执行 ASR 或 TTS。
Eta 尚未成为默认助理且自动设置关闭时,按既定策略直接回到小布,不创建平行 Activity 会话。自动设置开启时,失败触发只在后台修复当前选择,当前长按仍立即回退;后续触发使用修复后的主路径。
配置界面按消费边界保存开关:Agent 与本地工具写入 App 私有配置,Hook 能力写入 LSPosed 侧 RemotePreferences。Hook 回调和延迟任务执行前都会读取对应开关,所以后续触发按当前配置执行。