尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Operit ToolPkg Hook 超时通知可见性修复:从仅写日志到聊天/悬浮窗 Toast 的完整链路剖析

Operit ToolPkg Hook 超时通知可见性修复:从仅写日志到聊天/悬浮窗 Toast 的完整链路剖析 AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载本篇技术指南聚焦 OperitAndroid 上的 AI Agent 与 AI 聊天应用中 ToolPkg 同步 Hook 超时通知的可见性修复方案。原文档 1_toast_visibility.md 记录了这次超时提示从不可见到可见的追踪与修复过程本文将以该文档为主线结合仓库源码逐层还原修复前后行为、事件 ID 化与队列化设计、回调贯穿链路以及本地化文案帮助读者掌握 Operit 聊天消息发送链路中非致命错误事件 → Toast 主机的完整数据流以及如何在同步 Hook 桥中安全地复用该事件流上报用户可见通知。一、背景ToolPkg 的同步 Hook 体系与超时预算ToolPkg 是 Operit 中的插件/工具包运行体系。在消息发送的关键节点上ToolPkg 通过同步 Hook 参与干预主要包括两类ChatInput Hook聊天输入桥拦截聊天输入框的提交事件例如SUBMIT_REQUESTED可返回BLOCK阻止发送、CONSUME吞掉输入、REPLACE替换输入文本或ALLOW放行。Prompt Hook消息注入桥在消息真正注入模型之前对用户输入、历史、系统提示、工具提示、最终化等阶段进行改写。Operit 为每个阶段定义了独立的 Hook 族包括TOOLPKG_EVENT_PROMPT_INPUT、TOOLPKG_EVENT_PROMPT_HISTORY、TOOLPKG_EVENT_PROMPT_ESTIMATE_HISTORY、TOOLPKG_EVENT_SYSTEM_PROMPT_COMPOSE、TOOLPKG_EVENT_TOOL_PROMPT_COMPOSE、TOOLPKG_EVENT_PROMPT_FINALIZE、TOOLPKG_EVENT_PROMPT_ESTIMATE_FINALIZE见 ToolPkgPromptHookBridge.kt。由于 Hook 在消息发送的同步路径上执行如果每个 Hook 都独立拥有完整的超时时间串行调用多个 Hook 会让用户等待的时间成倍放大。为此 Operit 引入了共享执行预算shared Hook deadlineToolPkgHookExecutionBudget为一次同步 Hook 调度链创建单一截止时间链上的每个 Hook 只拿到剩余时间一旦预算耗尽立即跳过后续 Hook。// ToolPkgHookExecutionBudget.create() val timeoutSeconds DisplayPreferencesManager.getInstance(context).getToolPkgHookTimeoutSeconds() val startedAtNanos System.nanoTime() return ToolPkgHookExecutionBudget( startedAtNanos startedAtNanos, deadlineNanos startedAtNanos TimeUnit.SECONDS.toNanos(timeoutSeconds.toLong()) )超时秒数来自DisplayPreferencesManager.getToolPkgHookTimeoutSeconds()即用户在应用设置中配置的值。remainingMillis()在剩余时间为 0 时返回null调用方据此跳过后续 HooklogDeadlineReached与logTimeoutIfPresent负责把超时事件写入日志见 ToolPkgHookExecutionBudget.kt。二、修复前行为Previous behavior超时只写日志、用户不可见原文档对修复前的行为描述得非常精确核心结论有三点ChatInput 桥已有 Toast 路径ToolPkg 的 ChatInput 桥在submit_requested阶段检测到共享 Hook deadline 到期时会设置noticeMessage并把该消息放进ChatInputHookResult.noticeMessage返回。聊天输入底部栏ChatInputBottomBar继续使用ChatToastHost渲染这条通知。浮动 Toast 主机读取UiStateDelegate的 Toast 事件现有的浮动ChatToastHost从UiStateDelegate读取 Toast 事件流聊天页面与浮动聊天窗口共用同一套 Toast 渲染组件。AI 重试消息通过非致命错误事件流到达 UIAI 重试retry消息通过MessageProcessingDelegate.nonFatalErrorEvent进入该状态进而触发 Toast。Prompt Input Hook 超时此前止步于日志消息注入阶段的 Prompt Hook 超时此前在ToolPkgPromptHookBridge处中断并仅记录日志AppLogger没有任何面向用户的可视化提示。用户看到的是消息卡住但没有任何说明。这正是本次修复要解决的痛点同样都是超时ChatInput 路径可见Prompt Input 路径不可见。三、目标行为Intended behavior复用非致命错误事件流让超时可见且消息继续原文档明确了修复后的预期行为这也是本文的核心消息注入的 Prompt Input Hook 超时后桥接层通过与 AI 重试消息同一条非致命错误事件流上报现有聊天窗口与浮动窗口的ChatToastHost因此能展示本地化的超时通知在通知中标识出超时的 ToolPkg标识方式为「容器包名 Hook ID」通知展示期间消息继续发送超时 Hook 被跳过不回退、不阻止Prompt history/finalize 以及 Summary Hook 的超时保持仅日志不产生 Toast避免打扰用户。这一设计的精髓在于不新建第二套通知渠道同步 Hook 桥本可以从自身发出通知但那样会绕开既有的 Toast 生命周期与排队机制复用nonFatalErrorEvent则让超时通知天然获得与 AI 重试通知一致的展示行为聊天页 浮动窗双宿主、自动消失、可手动关闭。四、关键修复一UiStateDelegate的 Toast 事件 ID 化与排队为了让多个 Toast 事件不互相覆盖UiStateDelegate引入带自增 ID 的事件对象与队列见 UiStateDelegate.ktdata class ChatToastEvent( val id: Long, val message: String, ) private val toastLock Any() private val toastQueue ArrayDequeChatToastEvent() private var nextToastEventId 0L private val _toastEvent MutableStateFlowChatToastEvent?(null) val toastEvent: StateFlowChatToastEvent? _toastEvent.asStateFlow()事件发布与消费的规则是fun showToast(message: String) { synchronized(toastLock) { val event ChatToastEvent(id nextToastEventId, message message) // Preserve every user-visible notice; a single String StateFlow coalesces same-text events. if (_toastEvent.value null) { _toastEvent.value event } else { toastQueue.addLast(event) } } } fun clearToastEvent(eventId: Long) { synchronized(toastLock) { if (_toastEvent.value?.id ! eventId) { return } _toastEvent.value if (toastQueue.isEmpty()) null else toastQueue.removeFirst() } }设计要点每个 Toast 事件都有唯一 IDshowToast通过nextToastEventId分配事件不被同文案合并源码注释明确指出若用单一StringStateFlow相同文案的事件会被状态流合并丢失改为ChatToastEvent对象后每个事件都独立保留当前事件未消失时新事件入队_toastEvent.value ! null时新事件进入toastQueue前一个 Toast 关闭后自动弹出队首关闭必须指名道姓clearToastEvent(eventId)只有传入的 ID 与当前显示事件 ID 一致时才清除。这正是原文档验证清单中Require dismissal to name the displayed event, so an older Toast cannot clear a newer one的落地实现——旧 Toast 的关闭回调携带旧 ID永远无法误清新 Toast。五、关键修复二ChatToastHost按事件 ID 自动与手动关闭ChatToastHost见 ChatToastHost.kt以event: ChatToastEvent?与onDismiss: (Long) - Unit为参数LaunchedEffect(event?.id) { val activeEvent event ?: returnLaunchedEffect scrollState.scrollTo(0) kotlinx.coroutines.delay(estimateToastDurationMs(activeEvent.message)) onDismiss(activeEvent.id) }自动消失LaunchedEffect以event?.id为键事件变化时重新计时到达估算时长后调用onDismiss(activeEvent.id)携带当前事件 ID手动关闭右上角关闭按钮同样回调event?.let { onDismiss(it.id) }展示时长自适应estimateToastDurationMs按行数估算——2500ms 估算行数 × 850ms并约束在3500ms ~ 12000ms区间见同文件末尾。因此多行超时通知会展示更久短通知则快速消失双宿主复用同一个ChatToastHost既用于聊天页面AIChatScreen也用于浮动聊天窗口FloatingChatWindow两处都消费UiStateDelegate的 Toast 事件因此超时通知在两种界面下行为一致。六、关键修复三ToolPkgPromptHookBridge通过onHookTimeout上报容器与 Hook IDToolPkgPromptHookBridge的dispatchPromptHooks是消息注入阶段的核心调度函数见 ToolPkgPromptHookBridge.kt。修复后的关键片段val timeoutMillis budget.remainingMillis() if (timeoutMillis null) { budget.logDeadlineReached( tag TAG, stage current.stage, containerPackageName resolvedContainer, hookId resolvedHookId ) // Preserve the package and Hook ID so the existing floating Toast names the exact plugin. current.onHookTimeout?.invoke($resolvedContainer:$resolvedHookId) break } // ... if (budget.logTimeoutIfPresent(result, tag TAG, stage current.stage, containerPackageName resolvedContainer, hookId resolvedHookId)) { current.onHookTimeout?.invoke($resolvedContainer:$resolvedHookId) break }要点无论超时是预算预先耗尽remainingMillis() null还是本次执行超时执行结果报timed out都先记日志再回调onHookTimeout随后break跳出 Hook 链回调参数是${resolvedContainer}:${resolvedHookId}字符串——容器包名与 Hook ID 以冒号拼接被原文档与源码注释称为让现有浮动 Toast 精确点名超时插件的关键信息超时 Hook 被跳过、消息继续发送break后当前累积的 mutation 照常返回消息发送流程不中断。这与 ChatInput 桥的行为ALLOWnoticeMessage保持一致。该onHookTimeout回调来自PromptHookContext由上游消息构建方注入从而把何时触发、携带什么信息与如何展示解耦。七、关键修复四回调贯穿InputProcessor/AIMessageManager经nonFatalErrorEvent发布原文档验证清单中的关键步骤是Thread the Prompt Input Hook timeout callback throughInputProcessorandAIMessageManager——即超时回调必须从 Hook 桥一路贯通到消息构建入口最终汇入MessageProcessingDelegate.nonFatalErrorEvent。在MessageProcessingDelegate中见 MessageProcessingDelegate.ktprivate val _nonFatalErrorEvent MutableSharedFlowString(extraBufferCapacity 1) val nonFatalErrorEvent _nonFatalErrorEvent.asSharedFlow() fun reportNonFatalError(message: String) { if (message.isBlank()) { return } coroutineScope.launch { _nonFatalErrorEvent.emit(message) } }普通消息发送路径sendUserMessage在构建最终消息时注入超时回调同文件 L776-L798val finalMessageContent AIMessageManager.buildUserMessageContent( context context, messageText messageText, proxySenderName proxySenderNameOverride, attachments attachments, workspacePath workspacePath, workspaceEnv workspaceEnv, replyToMessage replyToMessage, // ... 其他参数 chatId chatId, roleCardId roleCardId, onHookTimeout { pluginIdentifier - reportNonFatalError( context.getString( R.string.toolpkg_hook_timeout_continue_sending_with_plugin, pluginIdentifier ) ) } )群组编排路径buildUserMessageContentForGroupOrchestration也以相同方式注入回调同文件 L327-L347。由此形成完整链路ToolPkgPromptHookBridge.dispatchPromptHooks │ 超时 → onHookTimeout?.invoke(container:hookId) ▼ AIMessageManager.buildUserMessageContent(onHookTimeout …) │ 回调触发消息构建同步路径内 ▼ MessageProcessingDelegate.reportNonFatalError(localizedText) │ _nonFatalErrorEvent.emit(...) ▼ UI 层收集 nonFatalErrorEvent → UiStateDelegate.showToast(...) ▼ ChatToastHost / FloatingChatWindow 的 ChatToastHost 展示由于该事件流此前就承载 AI 重试消息的通知UI 层聊天页与浮动窗无需任何改动即可渲染超时通知这正是复用同一条非致命错误流带来的收益。八、本地化通知文案与插件定位超时通知文案统一使用资源键toolpkg_hook_timeout_continue_sending_with_plugin参数为containerPackageName:hookId。仓库内已有多语言版本例如语言文案中文values/strings.xml前置插件「%1$s」响应超时已跳过并继续发送英文values-en/strings.xmlPre-send plugin %1$s timed out. It was skipped and the message will continue sending.韩语values-ko/strings.xml전송 전 플러그인 %1$s의 응답 시간이 초과되었습니다. 해당 플러그인을 건너뛰고 메시지를 계속 전송합니다.西班牙语 / 葡萄牙语 / 印尼语 / 马来语 / 罗马尼亚语均有对应翻译见 values-es、values-pt-rBR、values-id、values-ms、values-ro%1$s即${containerPackageName}:${hookId}。用户看到通知即可精确定位是哪个容器、哪个 Hook 拖慢了发送便于排查插件问题。九、验证清单与最终结果原文档的验证清单Verification逐条对应当前源码恢复 chat-input Hook 消息路径到ChatToastHostChatInputBottomBar继续使用ChatToastHostToolPkgChatInputHookBridge在SUBMIT_REQUESTED超时返回noticeMessage见 ToolPkgChatInputHookBridge.kt。给每个 Toast 事件分配 ID 并在UiStateDelegate排队见上文ChatToastEvent/toastQueue实现。关闭必须指名事件clearToastEvent(eventId)校验当前事件 ID 后才清除。回调贯穿InputProcessor与AIMessageManagerbuildUserMessageContent的onHookTimeout参数注入回调。经MessageProcessingDelegate.nonFatalErrorEvent发布reportNonFatalError实现。通知包含容器包名与 Hook ID$resolvedContainer:$resolvedHookId传入本地化文案。检查最终 diff 与调用点本文章节四至八即对调用点的逐步核对。最终结果Result [DONE]与原文档一致UiStateDelegate排队不同 Toast 事件、ChatToastHost按事件 ID 关闭、Prompt Input Hook 超时反馈改用nonFatalErrorEvent路径浮动窗口 Toast 主机可展示含容器包名与 Hook ID 的超时通知。十、设计边界哪些超时保持仅日志需要特别强调的是本次修复只让消息注入的 Prompt Input Hook 超时变得可见。以下超时仍然保持仅日志Prompt history / finalize Hook 超时历史注入与最终化阶段发生在消息继续发送的深层对用户而言不是前置阻塞过度提示会造成打扰Summary Hook 超时ToolPkgSummaryHookBridge参与总结类操作同样不进入 Toast 通道。这一取舍保证了通知的高信噪比只有那些真正影响消息发送是否继续的同步前置超时才值得占用 Toast 通道向用户解释。十一、源码速查清单需求与设计1_toast_visibility.md、index.mdToast 事件模型与排队UiStateDelegate.ktToast 渲染组件双宿主复用ChatToastHost.kt非致命错误流与回调注入MessageProcessingDelegate.ktPrompt Hook 桥与超时回调ToolPkgPromptHookBridge.kt共享超时预算ToolPkgHookExecutionBudget.ktChatInput 桥既有通知路径ToolPkgChatInputHookBridge.kt多语言文案toolpkg_hook_timeout_continue_sending_with_plugin见 values/strings.xml 及各语言 values-* 目录总而言之这次修复的本质是把一条已经存在、却被同步 Hook 桥绕开的通知通道重新接上通过事件 ID 化解决竞态通过共享预算保证时效通过container:hookId精确定位插件最终让用户在最需要解释的时刻消息发送被前置 Hook 拖慢获得清晰、本地化、可关闭的反馈而发送本身始终不被超时阻断。赞分享AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载相关推荐Operit 前置插件 Hook 超时 Toast 可见性修复从事件状态队列到非致命错误流的完整链路解析Operit 前置插件 Hook 超时 Toast 可见性修复从事件状态队列到非致命错误流的完整链路解析 导读 本文围绕 OperitAndroid 端 AAI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化Operit ToolPkg 前置 Hook 超时门禁聊天输入 / Prompt / 摘要 Hook 链的总预算中断机制Operit ToolPkg 前置 Hook 超时门禁聊天输入 / Prompt / 摘要 Hook 链的总预算中断机制 导读 本文围绕 Operit 中 TAI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化Operit 预发送链路超时治理天气注入 5 秒步进超时与 ToolPkg 共享 Hook 预算设计Operit 预发送链路超时治理天气注入 5 秒步进超时与 ToolPkg 共享 Hook 预算设计 本篇技术指南以 Operit 仓库中 weather_iAI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表