【免费下载链接】Mika
Mika — 你手机上的私人 AI 助理,替你操作 App、上网、把事办成。An on-device AI agent for Android that operates apps and the web to get things done for you.
本篇以 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 | 步数窗口 ≤ 3 | the 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) } }这里有两条实操结论:
- 申报的
content必须与输入框里的字逐字相同。测试里「同意『明天见』,却打进去『明天见 + 链接』」判Mismatch;聊天正文里恰好有一条「明天见」,而输入框是别的字,同样判Mismatch——因为只认输入框。 - 若
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_takeover | action = "pay" | 指向hand_to_user | true |
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.
代码里三处呼应:
- 申报阶段拦截
pay(payment_requires_takeover),并把这次申报暂存到ctx.refusedPay; - 闸门
declare()对pay直接返回Unsupported; - 即便存在别的同意,
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()两个副作用值得写进排查清单:
- 工具本身不可见。AgentLoop.kt 只在有确认关卡时提供这个工具:
return (confirmations != null || verb != Verb.RequestConfirmation) && ...所以自主模式下调它只会得到
unknown_tool。而UserExchange.confirm在confirmations == null时的返回也是同类错误(put("tool", RequestConfirmation.name))。 - 中途切换的时机差异。询问模式的任务中途换成自主:从下一步起不再拦(已经弹出的卡片照常等回答);开工即自主的任务中途换成询问:留给下一个任务,因为一个任务内工具与提示词不变。
还有一层容易混淆的差异:自主模式下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 写出一次能通过的申报
- 先填完所有内容:地址、规格、数量、优惠券都已选好,页面显示最终应付金额;
- 读屏确认金额的确切写法(含币种、是否带千分位、是否被拆成多个元素),直接照抄;
target用屏上原样文字(店名、卖家名),发送类必须用标题栏里显示的对方名字,且不少于两个字;content写清商品 + 规格 + 数量;发送类要与输入框内容逐字一致;app用手机包名(如com.taobao.taobao)或网页currentSite主机名;- 调用后立刻做那一步,中间不点别处、不滑、不返回、不切 App;需要打字补充是允许的;
- 失败就按
error+matched+reason修正,而不是原样重试。
12.2 故障对照表
| 现象 | 最可能原因 | 处置 |
|---|---|---|
ArgError: amount ... required | 想给「发送 / 删除」申请同意,或金额漏填 | 非花钱动作直接做;金额必须含数字 |
payment_requires_takeover | action写了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.
相关推荐
不花一分钱:让免费Spotify彻底告别广告的秘密工具
不花一分钱:让免费Spotify彻底告别广告的秘密工具 你是否也曾被Spotify的广告逼到崩溃? 想象一下:加班到深夜想靠音乐提神,却在两首歌之间被理财广告强
桌面应用不花一分钱也能访谈用户:user-research-skill合成用户(AI Persona)功能全解析
不花一分钱也能访谈用户:user research skill合成用户 AI Persona 功能全解析 想访谈用户,却不想花一分钱招募真实参与者? user
如何零成本获取1000+用户:工程师必备的终极营销指南
如何零成本获取1000+用户:工程师必备的终极营销指南 对于技术创业者和工程师来说,零预算营销是获取早期用户的必备技能。Marketing for Engine
教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考