ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Mika 的 request_confirmation:让「花钱的那一步」必过用户同意

Mika 的 request_confirmation:让「花钱的那一步」必过用户同意

【免费下载链接】Mika

Mika — 你手机上的私人 AI 助理,替你操作 App、上网、把事办成。An on-device AI agent for Android that operates apps and the web to get things done for you.

项目地址:https://gitcode.com/gh_mirrors/mika2/Mika
点击查看免费下载

本篇以 request_confirmation.md 这份工具说明为主线,讲清 Mika 这个 Android 端 AI Agent 如何处理「要花钱的那一步」:模型在什么时机申报、申报什么字段、用户同意之后同意凭证如何与真实点击逐一核对、对不上时任务会看到什么错误码。读完你可以准确写出可被放行的申报,也能从confirmation_required/confirmation_mismatch的返回里定位到具体失败环节,并理解为什么「付款」必须走hand_to_user而不是这个工具。

一、工具定位:只有花钱的那一步才需要同意

原文档开篇就把边界说死了:

Ask the user to approve a step that spends their money, before you do it: placing an order or booking, or another final step on a page that shows the amount to pay (submit, confirm). This is the only kind of step that needs approval: sending, posting, submitting forms, deleting, greeting or applying, changing settings and everything else that spends no money is done directly, without asking.

这句话对应仓库里一条硬规则:审批判定是纯函数,只有payment为真时才可能返回Ask。见 Approval.kt:

object ApprovalPolicy { fun decide(risk: Risk, run: RunProvenance = RunProvenance()): Approval = when { !risk.payment -> Approval.Allow run.attendance == Attendance.Unattended || run.origin != RunOrigin.Ui -> Approval.Deny else -> Approval.Ask } }

也就是说:

动作是否询问用户依据
发消息、发帖、提交表单否,直接做!risk.payment -> Allow
删除、改设置、打招呼、投简历否,直接做同上
下单、预约、屏上有应付金额的「提交 / 确认」是risk.payment == true且有人在手机旁、来源是本机 UI

这条规则在测试里被逐项钉住:PaymentOnlyAskTest.kt 的sendsWithoutAskingAndAudits与deletingUninstallingTypingPasswordsAndPlainTapsAllRunWithoutAsking证明「发送、删除、卸载、往密码框打字、普通点击」都会照常执行,不会弹确认卡片。

因此request_confirmation不是一个「万能批准按钮」,而是花钱动作的专用申报通道。误用它(例如为发消息申请同意)不但没有意义,还会被参数校验直接打回——原文档中「the only kind of step that needs approval」这句,落地就是代码里的amount必须含数字:

// 只有花钱要问:写不出金额的操作不用申请,直接做 if (a.str("amount")?.any(Char::isDigit) != true) throw ArgError("amount", "required: the total the user will pay, as shown on screen. An action that spends no money needs no approval: do it directly")

二、工具签名与参数:五个必填字段各自绑定什么

工具定义在 RequestConfirmation.kt,与提示词快照 prompt-surface/tools.txt 一致:

参数必填含义代码中的约束
action是花钱动作的种类枚举send、delete、order、submit、other(pay被刻意排除)
target是钱付给谁 / 在哪(店、卖家、服务方),as shown on screen上限CONFIRM_FIELD_MAX = 200字符,超长截断加…
content是买了什么:商品、规格、数量同上,200 字符
amount是用户将支付的金额与币种,屏上显示的必须包含至少一个数字字符,否则ArgError
app是动作发生在哪块屏 / 哪个 App手机端必须是 Android 包名;网页端必须是站点主机名

三处容易被忽略的细节:

1)action的pay是「拒绝入口」。IrrevAction里确实有pay这个枚举值(risk/Irreversible.kt),但可用值列表把它过滤掉了(tool/Tools.kt):

const val CONFIRM_FIELD_MAX = 200 val CONFIRM_ACTIONS = IrrevAction.entries.filter { it != IrrevAction.pay }.map { it.name }

即使模型绕过枚举、把"pay"塞进字符串,执行分支也会把它转成付款接管请求并返回失败:

if (kind == IrrevAction.pay) { ctx.refusedPay = ConfirmationRequest(kind, ...) return fail("payment_requires_takeover", HINT_PAYMENT_TAKEOVER) }

2)app的写法按屏不同。工具描述不是写死的常量,而是运行时按当前有哪几块屏拼出来(RequestConfirmation.def(ctx)):只有一块屏时写那一块的写法,有多块屏时逐块说明。手机端校验 Android 包名(PhoneCapability.kt),网页端要求是currentSite里的主机名(WebCapability.kt 与declaredHost),写错会抛ArgError而不是静默接受。

3)框架会自动附加surface与reason。在多屏场景下,工具定义里还能看到surface(phone/web)和可选reason;reason明确写着「not used for execution」,只作说明。surface由通用包装层注入,用来指定这次申报针对哪块屏,参数校验的第一步就是checkSurface(a, ctx)。

字段长度限制的影响不止于显示。capField会把超长文本截成 200 字符并补一个…(tool/ToolContext.kt):

internal fun capField(s: String): String = s.trim().let { if (it.length <= CONFIRM_FIELD_MAX) it else it.take(CONFIRM_FIELD_MAX - 1) + "…" }

而末尾的…会被下游识别为「申报内容被截短」,从而把核对方式从「完全相等」放宽为「前缀相等」——这是第四节的伏笔。

三、调用时序:三步之内,且必须是同一个屏上的下一个动作

原文档给出的操作纪律非常具体:

Fill everything in first, then call it, then do the step as your very next action on that screen: tapping elsewhere, swiping, going back or opening another app or page in between, or waiting more than 3 steps, cancels the approval. One approval is used up by the next matching step even if that step fails; ask again for the next one.

这条纪律在 ConfirmationGate.kt 里有三处独立实现:

(1)步数窗口MAX_STEPS = 3。同意时记下步数,核对时比较:

const val MAX_STEPS = 3 ... step - g.step > MAX_STEPS -> "the approval expired ($MAX_STEPS steps)"

ConfirmationGateTest.kt 把边界测成:第 1 步同意,第 4 步仍可放行,第 5 步报Mismatch。

(2)中间做别的写操作即作废。noteWrite(verb)只在「打字」和「清空输入框」两种动作上保留同意:

private val KEEPS_GRANT = setOf(Verb.TypeText, Verb.ClearText) ... @Synchronized fun noteWrite(verb: Verb?) { if (verb !in KEEPS_GRANT) grant = null }

测试把phone_tap、phone_swipe、phone_scroll、phone_back、phone_launch_app、phone_press_key逐个跑了一遍,全部使同意失效——这正好对应原文档里的「tapping elsewhere, swiping, going back or opening another app or page in between」。换句话说,允许的只有「继续往框里填字」,不允许任何改变屏幕状态的探索动作。

(3)一次性消耗。permits()第一件事就是把grant置空:

val g = grant ?: return Permit.NoGrant grant = null

无论核对结果是Allowed还是Mismatch,这次同意都不会留下。这就是「One approval is used up by the next matching step even if that step fails」的实现方式,也让「先点一下试试,失败再点一次」这种重试路径必然要重新申报。

回到主循环,模型调用的结果被包成Outcome.Confirm(ToolExecution(okObj { put("asked", true) }, outcome = Outcome.Confirm(req, fg))),由 AgentLoop.kt 转交给UserExchange.confirm:

(e.outcome as? Outcome.Confirm)?.let { e = user.confirm(it, step, running) }

所以模型在request_confirmation这一轮拿到的工具结果,已经是「用户回答之后」的结论,而不是「已提交申请」。

同一轮申报还会记录「当时的前台包名」:ctx.session.peek()?.packageName ?: ctx.lastSnapshot?.packageName。这个值之后要用于第一道一致性检查。

四、同意如何与真实点击对上:五重绑定

这是整套机制里最容易被低估的部分。用户的「同意」不是一句口头授权,而是一份带字段的凭证;真正执行时,ActionExecutor 会先让ConfirmDecision.judge判定这一步到底是什么动作,再拿凭证去比对:

val d = decision.judge(call.name, args, origin) when (d) { is Decision.Required -> return blocked(call, args, origin, before, d.hit, null, followUp) is Decision.Mismatch -> return blocked(call, args, origin, before, d.hit, d.reason, followUp) Decision.Denied -> return refuse("approval_unavailable", origin, before) ... }

ConfirmationGate.permits()里的顺序判定,就是「五重绑定」:

顺序检查不通过时的reason
1付款永远不放行payment is never approved here
2步数窗口 ≤ 3the approval expired (3 steps)
3动作种类相同approved order but this step is send
4前台包名与申报一致approved in com.x but the screen shows com.y
5对象在屏上;发送时额外校验正文;金额在屏上the target "…" is not shown on the screen等

4.1 对象必须真的出现在屏上

private fun targetShown(r: ConfirmationRequest, s: ScreenSnapshot): Boolean { val t = norm(r.target).take(MATCH_CHARS) if (t.length < 2) return false return s.elements.any { e -> !e.editable && t in norm(e.name) && (r.action != IrrevAction.send || (e.bounds.size == 4 && e.bounds[1] <= TITLE_BAND)) } }

三个约束值得记住:

  • 至少两个字(t.length < 2直接 false),单字对象无法通过;
  • 只比不可编辑文本:候选对象只写在输入框里不算,因为输入框里的字是模型自己打的;
  • 发送类必须出现在顶部标题栏(TITLE_BAND = 150,0–999 坐标系里约屏幕上方 15%),聊天对象的名字通常在这里。测试targetBindingIsStrict专门构造了「在和李四的聊天里打『张三说明天见』」以及「张三只出现在聊天正文里」两种情形,都判Mismatch。

比较时只取申报对象的前 20 个字符(MATCH_CHARS = 20),并做去空白 + 小写归一化。

4.2 发送类要比对正文,且是「完全相等」

private fun contentMatches(content: String, s: ScreenSnapshot, typing: String?): Boolean { val capped = content.endsWith("…") val c = norm(content.removeSuffix("…")) fun same(x: String) = norm(x).let { if (capped) it.startsWith(c) else it == c } if (typing != null) return same(typing) return s.elements.any { it.editable && !it.hint && same(it.name) } }

这里有两条实操结论:

  1. 申报的content必须与输入框里的字逐字相同。测试里「同意『明天见』,却打进去『明天见 + 链接』」判Mismatch;聊天正文里恰好有一条「明天见」,而输入框是别的字,同样判Mismatch——因为只认输入框。
  2. 若content因 200 字符上限被截短(以…结尾),核对降级为前缀匹配。这也解释了为什么字段上限不是纯粹的显示问题。

type_text直接带回车时,typing参数会替代读屏结果参与比较,避免「字还没渲染出来就核对」的假失败。

4.3 金额核对:只认屏上的钱,不认「已售 25 件」

amountShown()是整个闸门里规则最密的一段,也是错误率最高的地方。它的策略是:

private val CURRENCY = Regex("[¥¥]\\s*(\\d+(?:\\.\\d+)?)|(\\d+(?:\\.\\d+)?)\\s*元") private val TOTAL_LABELS = IrreversibleDetector.TOTAL_LABELS // 合计 / 实付 / 应付 / 总计 private val DISCOUNTS = listOf("优惠", "减", "抵扣", "运费", "券", "红包", "已省")

按优先级排列:

  • 只要带币种的金额(¥/¥前缀,或元后缀)。因此「已售 25 件」「满 25 减 3」不会被当成价格;
  • 输入框与提示语里的金额不算(filter { !it.editable && !it.hint });
  • 紧邻优惠、减、抵扣、运费、券、红包、已省(或前面带负号)的金额不算,窗口是金额前 4 个字符;
  • 有合计类标签时:可滚动区之外的每个合计都必须等于申报金额;若都在滚动区里,取离被点按钮最近的那个;
  • 没有合计类标签时:取可滚动区之外最大的那个金额;
  • 有合计字样但读不出金额(例如「合计:」和数字分属不同元素、拼行也拼不出来)→ 直接不放行;
  • 千分位逗号会被移除(THOUSANDS正则),¥2,599与2599等价。

totalOf()还处理了一个真实电商界面常见的坑:标签和数字被拆成多个元素。它会把同一行右侧的几段按横向顺序拼起来(IrreversibleDetector.rowText),所以「实付 | ¥ | 39.90」这种四段结构可以正确解析。测试splitTotalIsJoined用合计:、¥、1299、.00四个元素验证了这一点。

反过来,typedAndPerShopTotalsIgnored证明「备注框里打的『合计¥1』」「列表里每个店铺的小计」都不参与核对。

4.4 读屏读不出字时的兜底

自绘界面(画布、游戏化 UI)可能整屏没有任何带文字的节点。此时targetShown天然失败,闸门改用「看图找到并复看确认过的所见文字」(seen列表)来核对象是否出现,但要发的字读不到就无法核对正文,代码里用!(textless(screen) && seenShows(r, seen))的组合条件表达了这个取舍。

五、被拦下时模型看到什么

这是排查任务失败时最实用的一节。所有拒绝文案集中在 ActionOutcome.kt 的Refusals里:

error触发条件返回的message要点recoverable
confirmation_required命中花钱动作但没有任何可用同意「先调用 request_confirmation 说明在哪、买什么、多少钱」+ 一次性说明true
confirmation_mismatch有同意但对不上(见第四节)附reason,明确「那次同意已经作废」true
approval_unavailable本次运行无法取得用户同意直接拒绝,不执行—
confirmation_declined用户拒绝、超时、无人值守、非本机来源「不要再次尝试这个操作」false
payment_requires_takeoveraction = "pay"指向hand_to_usertrue

confirmation_required的完整结构如下(节选):

put("ok", false); put("error", "confirmation_required") put("action", hit.action.name); put("matched", hit.matched) put("message", if (hit.action == IrrevAction.pay) "……" else "这一步要花钱(${ACTION_LABELS[hit.action]}),没有执行。先调用 request_confirmation 说明在哪、买什么、多少钱,用户同意后再做。$SINGLE_USE") put("recoverable", true); put("counts_as_failure", false)

三个字段读法:

  • action:闸门认为这一步属于哪种不可逆动作(send/delete/order/submit/pay)。它来自词表判定,不一定等于模型申报的种类;
  • matched:命中的那一段文字——按钮名、资源 ID、模型写的target、输入框标签,或滑动的起点。它是「为什么被拦」的直接证据;
  • counts_as_failure = false:被拦不计入连续失败熔断。否则模型反复申报失败会把任务提前熔断掉。

confirmation_mismatch额外带reason,值就是第四节表格右列的英文句子。实践中最常见的三条:

  • the approved amount ¥25 is not on the screen—— 金额申报错,或屏上的合计已经变了;
  • the text to send is not exactly what was approved—— 申报正文和输入框不一致(增删了链接、标点);
  • approved in com.taobao.taobao but the screen shows com.example—— 中途切了 App 或有效屏换了。

用户拒绝后,同一任务内同类动作 + 同 App 不会再问第二次。declare()用一个集合记住被拒组合:

if (req.action to req.app in declined) return DeclareResult.Declined

这条规则对应原文档最后一句「If the user declines, do not try the step again; report the current state and end」——它不只是提示词纪律,而是运行时强制的。

六、付款为什么不在这里:hand_to_user的分工

原文档有一句独立的否定式要求:

Payment itself is not confirmed here: use hand_to_user with reason payment.

代码里三处呼应:

  1. 申报阶段拦截pay(payment_requires_takeover),并把这次申报暂存到ctx.refusedPay;
  2. 闸门declare()对pay直接返回Unsupported;
  3. 即便存在别的同意,permits()第一条也判payment is never approved here。

暂存的refusedPay随后会被hand_to_user复用,让模型不必重复填字段(OutcomeTools.kt):

val declared = ctx.refusedPay.takeIf { r == "payment" } // 只给紧接着的那次付款接管用,之后无关的付款不串用 if (declared != null) ctx.refusedPay = null val item = a.str("item", max = 10000)?.let(::capField) ?: declared?.content val amount = a.str("amount", max = 10000)?.let(::capField) ?: declared?.amount

于是模型可以只写:

{"reason":"payment","message":"请你在付款页完成支付并告诉我结果"}

商品与金额会自动从刚才那次被拒的付款申报里补上。这解释了为什么失败提示里会同时出现「付款由用户亲手完成」和「item 写商品、amount 写屏上应付金额、address 写收货地址」——直接调用hand_to_user时这些字段需要自己填,走pay申报失败这条路则可以省略。

reason的合法值集合见 tool/Tools.kt:

val TAKEOVER_REASONS = listOf("captcha", "login", "access_blocked", "payment", "decision", "other")

hand_to_user属于「用户亲手操作屏幕」,因此要过锁屏关卡(withScreens = true),而request_confirmation的risk是Risk.Read——它只问话,不动屏幕。这个区别也体现在时间统计里:docs/architecture.md明确「等待、问用户、请确认、交给用户这类控制调用不算读也不算操作」,所以确认卡片上的等待时间不会污染任务的「读屏 / 操作」耗时。

七、问不到人的七种情形

同一份提示词假设「有人在手机旁」。真实运行时并不总是这样,UserExchange.confirm 依次处理:

when (g.declare(r, o.foregroundApp)) { DeclareResult.Mismatch -> ... // app 字段与前台不符 DeclareResult.Declined -> ... // 本任务内已被拒过同类操作 DeclareResult.Unsupported -> ... // action = pay DeclareResult.Pending -> Unit } if (provenance.attendance == Attendance.Unattended || provenance.origin == RunOrigin.Child) { g.decide(ConfirmChoice.Deny, step) return error("confirmation_declined", CONFIRMATION_DECLINED, recoverable = false) } // 付款只在手机上同意:渠道、定时、MCP 发起的任务不问 if (provenance.origin != RunOrigin.Ui) { g.decide(ConfirmChoice.Deny, step) return error("confirmation_declined", CONFIRMATION_DECLINED, recoverable = false) }

归纳如下:

情形行为说明
前台包名 ≠ 申报app(或读不到前台)confirmation_mismatch要求先 observe 再用屏上包名重新申报
无人值守(Attendance.Unattended)视作拒绝定时任务不挂起等人
子任务(RunOrigin.Child)视作拒绝子任务本身只读
非本机 UI 来源(渠道、定时、MCP)视作拒绝付款只能在本机确认
超时无回答视作拒绝,记为timed_out默认 15 分钟(TaskService.CONFIRM_TIMEOUT_MS、UserExchange中的15 * 60_000L)
用户点「不同意」confirmation_declined同 App 同类动作不再追问
本次任务开工即自主模式没有确认关卡,工具不可见见下节

注意「默认不同意」这个方向:val choice = answer ?: ConfirmChoice.Deny。原文档没写超时,但代码选择的是安全侧——绝不自动同意。

八、自主模式下的两个「看不见」

询问 / 自主是一个全局开关(设置 › 开发者选项里的「自主模式」),存在ApprovalStore,由 ApprovalControl.kt 在任务开工时发一份RunApproval:

/** 这个任务(和它的子任务)用的付款确认关卡:开工时是询问才有,每次一份新的。 */ fun gate(): ConfirmationGate? = if (startedAutonomous) null else ConfirmationGate()

两个副作用值得写进排查清单:

  1. 工具本身不可见。AgentLoop.kt 只在有确认关卡时提供这个工具:
    return (confirmations != null || verb != Verb.RequestConfirmation) && ...

    所以自主模式下调它只会得到unknown_tool。而UserExchange.confirm在confirmations == null时的返回也是同类错误(put("tool", RequestConfirmation.name))。

  2. 中途切换的时机差异。询问模式的任务中途换成自主:从下一步起不再拦(已经弹出的卡片照常等回答);开工即自主的任务中途换成询问:留给下一个任务,因为一个任务内工具与提示词不变。

还有一层容易混淆的差异:自主模式下ConfirmationGate不存在,ConfirmDecision.judge的提前返回排在「不对 Mika 自己动手」检查之前,因此「不操作自己」这层保护在自主模式下不生效——docs/architecture.md对此有明确记录。这属于request_confirmation话题的边界知识:它保护的是「花钱」,不是所有风险。

九、兜底识别:模型没申报,也会被拦

原文档承诺「A spending step without approval is refused with confirmation_required」。但模型可能压根不调request_confirmation就去点「提交订单」。这套机制能成立,靠的是 IrreversibleDetector.kt 这层词表兜底——它不看模型的意图,只判断「这一步实际作用在哪个元素上」。

几处设计取舍直接解释了实际使用中的「为什么这个被拦了 / 那个没被拦」:

强词表(STRONG):支付、付款、转账、删除、撤回、下单、购买、结算、取消订单、确认收货、退款、转发。但前面是「待」「已」、后面跟「成功」「记录」「售后」「明细」「方式」「列表」「中心」「入口」「码」的,判为订单状态或功能入口,不算动作:

private val STATUS_PREFIXES = listOf('待', '已') val STATUS_SUFFIXES = listOf("成功", "记录", "售后", "明细", "方式", "列表", "中心", "入口", "码")

只认完全相等的:submit系列只有提交、确认提交、submit三个精确值,所以「提交搜索」不会被当成提交订单。这条规则在源码注释里写得很直白:「只有 submit 故意只认完全相等」。

通用确认按钮看上下文:确认/确定/ok/完成/confirm这类按钮只有在同屏存在强词或金额时才算动作。contextAction()的判定顺序是:同屏有付款词 →pay;否则第一个非下单类强词;有金额且像付款面板 →pay;再否则按下单。

付款面板识别做了降噪:「支持花呗付款」「货到付款」这类介绍性文字会先被PAYMENT_ADS正则剔除;页面上有「立即购买 / 加入购物车 / 立即下单」时不按付款面板字样判;「余额」「密码」「指纹」「零钱」这类弱信号只在下半屏(BOTTOM_REGION = 500)且同区有确认类按钮时才算。

输入框编辑不算提交:点输入框、点框内的清空 ×、点紧挨着框的「清空」按钮都被editsField()判为编辑。这是为了避免「点搜索框准备打字」被误判成发送。

这个检测器与模型申报的关系是「双保险但只有一把锁」:模型申报走request_confirmation,检测器负责在真正执行时确认——两者必须同时对上,用户才会被真正「消费」一次同意。

十、界面与留痕:用户看到的那张卡片从哪来

申报通过后,任务状态进入TaskState.WaitingConfirmation(r)。用户可能在三处看到它:对话页的确认卡片、悬浮胶囊的卡片、系统通知。文案由 ConfirmationCopy 统一生成,中文串在 strings_confirm.xml:

<string name="confirm_title">要你同意:%1$s</string> <string name="confirm_in_app">在 %1$s</string> <string name="confirm_target">对象:%1$s</string> <string name="confirm_content">内容:%1$s</string>

ConfirmationCopyTest给出了实际渲染样例:

标题:要你同意:下单 正文:在 外卖 · 对象:蜜雪冰城 · 内容:一杯柠檬水 · 金额:¥4 详情(通知大字):在 外卖\n对象:蜜雪冰城\n内容:一杯柠檬水\n金额:¥4

三个安全细节:

  • 模型写的字段当纯文本处理:换行压成空格,控制字符与方向控制符去掉,首尾空白去掉,杜绝用换行伪造字段;
  • 内容里的分隔符(·、:)会被替换成不易混淆的字符(/、∶),防止伪造「金额:¥0」这类结构;
  • 每条申报生成独立的 PendingIntent data(intentData(key, approve)),旧通知上的按钮不会被新申报劫持。

运行时留痕有两个事件(core/record/RunEvent.kt):RunEvent.Confirmation记录申报字段与决定(approved/declined/timed_out/mismatch/declined_unattended/declined_remote),RunEvent.Approval记录审批判定(来源、动作、风险、是否 payment、决定)。在「查看过程」的时间线里,这两类事件是复盘「为什么任务停在付款前」最直接的证据。

十一、脚本与批量:为什么同意不能「顺手复用」

原文档强调「do the step as your very next action on that screen」。这一条在批量与脚本场景下被额外加固:

  • do_steps:批量里的子步走同一个执行入口,命中花钱动作即整批停止;主循环随后会给出approved_step_first提示——「先单独执行已同意的那一步……not inside do_steps or web_script」。
  • web_script:页面脚本里出现不可逆动作会直接终止脚本,confirmation_required被视为致命错误(BrowserScript.kt 的FATAL集合),并给出明确指示:
That action was not run, and the script stopped. Irreversible actions never run from a script: call request_confirmation, then do that step as a normal tool call.

对应原文档里「the step as your very next action」的严格版本:这一步必须是普通工具调用,不能藏在批或脚本里。原因不难理解——脚本内部的中间动作在主循环看来是「一步」,MAX_STEPS = 3的窗口和「中间不许点别处」的约束都会失真。

十二、实操清单与故障对照

12.1 写出一次能通过的申报

  1. 先填完所有内容:地址、规格、数量、优惠券都已选好,页面显示最终应付金额;
  2. 读屏确认金额的确切写法(含币种、是否带千分位、是否被拆成多个元素),直接照抄;
  3. target用屏上原样文字(店名、卖家名),发送类必须用标题栏里显示的对方名字,且不少于两个字;
  4. content写清商品 + 规格 + 数量;发送类要与输入框内容逐字一致;
  5. app用手机包名(如com.taobao.taobao)或网页currentSite主机名;
  6. 调用后立刻做那一步,中间不点别处、不滑、不返回、不切 App;需要打字补充是允许的;
  7. 失败就按error+matched+reason修正,而不是原样重试。

12.2 故障对照表

现象最可能原因处置
ArgError: amount ... required想给「发送 / 删除」申请同意,或金额漏填非花钱动作直接做;金额必须含数字
payment_requires_takeoveraction写了pay改用hand_to_user,reason=payment
confirmation_required(模型没调工具就点)检测器命中花钱按钮补一次request_confirmation,字段照屏上写
confirmation_required(申报过仍被拦)同意已被消耗(上一匹配步骤已用掉)重新申报
confirmation_mismatch: amount not on the screen申报金额与屏上合计不一致,或合计在滚动区里读不出重新读屏、照抄合计;确认没有把「优惠后小计」当成合计
confirmation_mismatch: text to send is not exactly ...输入框内容与申报正文差异(多/少字、附链接)申报前先定稿文字
confirmation_mismatch: target not shown in the title area聊天对象名不在顶部(只在正文里)换成标题栏显示的会话名,或换到正确的会话
confirmation_mismatch: approved in X but the screen shows Y中途切了 App / 有效屏变化回到原 App 再申报
confirmation_mismatch: the approval expired (3 steps)中间隔了太多步紧邻执行
confirmation_declined用户拒绝 / 超时 / 无人值守 / 子任务 / 非本机来源不要重试;如实汇报现状
approval_unavailable本次运行没有确认关卡检查是否处于自主模式,或该来源不支持询问
unknown_tool(自主模式下)工具在有确认关卡时才提供自主模式不询问,按规则直接做

十三、证据索引:想继续深挖该看哪几个文件

  • 提示词原文:core/src/main/resources/prompts/tools/request_confirmation.md
  • 提示词分层规则(tools/是工具定义、feedback/是结果说明):core/src/main/resources/prompts/README.md
  • 工具实现与参数校验:core/src/main/kotlin/com/wangzan101/mika/core/general/RequestConfirmation.kt
  • 同意凭证与五重核对:core/src/main/kotlin/com/wangzan101/mika/core/gate/ConfirmationGate.kt
  • 词表兜底识别:core/src/main/kotlin/com/wangzan101/mika/core/gate/IrreversibleDetector.kt
  • 判定与审批接线:core/src/main/kotlin/com/wangzan101/mika/core/action/ConfirmDecision.kt、ActionExecutor.kt
  • 拒绝文案与错误码:core/src/main/kotlin/com/wangzan101/mika/core/action/ActionOutcome.kt
  • 用户交互与超时:core/src/main/kotlin/com/wangzan101/mika/core/agent/UserExchange.kt
  • 付款接管字段补全:core/src/main/kotlin/com/wangzan101/mika/core/general/OutcomeTools.kt
  • 主循环反馈文案:core/src/main/resources/prompts/feedback/loop.md
  • 下单 / 发送前的核对纪律:core/src/main/resources/prompts/capabilities/handover.md
  • 询问 / 自主开关:app/src/main/java/com/wangzan101/mika/settings/ApprovalControl.kt
  • 卡片与通知文案:app/src/main/java/com/wangzan101/mika/confirm/ConfirmationNotifier.kt、app/src/main/res/values-zh/strings_confirm.xml
  • 行为测试:core/src/test/kotlin/com/wangzan101/mika/core/action/PaymentOnlyAskTest.kt、core/src/test/kotlin/com/wangzan101/mika/core/gate/ConfirmationGateTest.kt
  • 工具定义快照:core/src/test/resources/prompt-surface/tools.txt
  • 全局架构中的确认链路说明:docs/architecture.md

一句话收束:这份提示词只有一段,但它描述的是一条从「模型申报」到「词表兜底」再到「凭证逐字段核对」的完整链路——request_confirmation的价值不在于「问了一句」,而在于让用户的每一次同意都精确绑定到屏幕上的那一个动作、那一个金额、那一串即将发出的字。

【免费下载链接】Mika

Mika — 你手机上的私人 AI 助理,替你操作 App、上网、把事办成。An on-device AI agent for Android that operates apps and the web to get things done for you.

项目地址:https://gitcode.com/gh_mirrors/mika2/Mika
点击查看免费下载

相关推荐

上一篇:ScrollMagic v3 维护手册:Issue 分流、PR 评审、发布流程与 v2 迁移裁决的完整指南
下一篇:PyO3 跨扩展模块共享 `[pyclass]` 类型:基于 Capsule 的 FFI 式 API 设计指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进