Skip to main content

API 参考

调用已发布的 Mobile-Use Agent,支持标准版和专属版。提交任务文本和当前屏幕截图,通过 metadata 传入设备与应用上下文,以 SSE 返回下一步手机界面操作。界面操作由调用方执行,执行后携带会话 ID 和最新截图继续调用。

API 版本:2026-08-31。调用前请先完成实例创建、配置与发布。

接入前提

  • 在 SDK 中心 安装 SDK。
  • 已开通 Mobile-Use Agent 服务,准备已发布的 Mobile-Use Agent 实例 ID 及其访问密钥。
  • Mobile-Use Agent 提供标准版和专属版,按所选实例的版本调用。
  • 使用 SDK 完成 AccessKey 认证;实例访问密钥另放在 X-QI-Agent-Api-Key,不能替代 AccessKey 签名。
  • 设置 Content-Type: application/json、Accept: text/event-stream,stream 必须为 true。

调用方式

  • 方法与路径:POST /gui/v1/chat/completions。
  • X-QI-Instance-Id 指定实例,按已发布配置执行;请求体 model 是兼容占位,不能切换模型。
  • 响应为 SSE 增量事件,不是一个完整 JSON;持续读取到流结束,并检查流内错误。

会话管理

首轮可省略 X-QI-Session-Id,从 SSE 正文保存 qi_session_id,后续通过同名请求头续接。同一会话顺序调用;不同会话使用不同标识。不要依赖网关透传自定义响应头,实际标识以 SSE 正文为准。

基本信息

字段值
方法POST
路径/gui/v1/chat/completions
Content-Typeapplication/json
响应类型application/octet-stream

请求参数

Header 参数

参数类型必填说明
X-QI-Agent-Api-Keystring是Agent 实例访问密钥,用于 QICloud Customer 接口鉴权。
X-QI-Instance-Idstring是已发布的 Mobile-Use Agent 实例 ID,必须与 X-QI-Agent-Api-Key 对应。
X-QI-Session-Idstring否可选会话标识。首步可省略,由服务端生成;后续 Mobile-Use Agent 步骤必须传回同一值续接状态。

Body 参数

Mobile-Use Agent 调用请求体。首次调用提交当前目标和截图;续轮提交上一动作真实结果及新截图,不重放历史消息。Chat Completions 字段使用 snake_case,stream 设为 true。
参数类型必填说明
allowed_token_idsarray否允许生成的 Token ID 白名单。
allowed_token_ids[]integer否一个允许生成的 Token 整数 ID,需使用目标模型的词表 ID。
bad_wordsarray否禁止生成的词或字符串列表。
bad_words[]string否一个不希望模型生成的词或字符串。
chat_template_kwargsobject否vLLM/OpenAI-Compatible 模板参数对象。请求值会覆盖模型默认同名值并原样传给 Mobile-Use Agent 主模型。
chat_template_kwargs.enable_thinkingboolean否chat_template_kwargs.enable_thinking:请求级模型思考开关,覆盖同名默认值。Mobile-Use Agent 响应不会因开启该开关而返回 reasoning_content。
chat_template_kwargs.preserve_thinkingboolean否是否在续轮中保留已有思考上下文;按 vLLM/Qwen chat_template_kwargs 原样透传。
frequency_penaltynumber否频率惩罚参数,透传给 Mobile-Use Agent 主模型。
ignore_eosboolean否是否忽略 EOS 并继续生成。
include_reasoningboolean否是否请求上游生成推理字段;Mobile-Use Agent 对客结果不会直接暴露内部思考。
logprobsboolean否模型兼容参数,JSON 字段为 logprobs。是否接受及生效取决于实例所用模型;当前 Mobile-Use Agent 响应不提供 Token 对数概率明细。
max_completion_tokensinteger否OpenAI 兼容最大补全 Token 数,提供时必须大于 0。
max_tokensinteger否OpenAI 兼容最大生成 Token 数,提供时必须大于 0。
messagesarray是当前 Mobile-Use Agent 步骤消息。首步只能包含一条 user;后续步骤只能是一条 tool 后跟一条 user,不允许携带完整多轮历史。
messages[].contentarray否消息内容。user 使用内容数组,包含恰好一张当前截图,并可附加任务文字;截图支持公开 HTTPS URL 或 Base64 图片 Data URL。role=tool 时填上一动作的真实执行结果,可为非空字符串、对象或数组。
messages[].content[].image_datastring否input_image 的兼容内联图片字段;可传 Base64 Data URL。
messages[].content[].image_urlobject否截图引用。支持 image_url 字符串或包含 url 的对象;此处展示对象形式。
messages[].content[].image_url.urlstring否公开可访问的 HTTPS 图片 URL,或 Base64 图片 Data URL。每轮 user 消息必须且只能包含一张当前截图;已过期或无法访问的图片地址会被拒绝。
messages[].content[].textstring否文本内容,可描述当前操作目标或补充当前页面状态。
messages[].content[].typestring否内容项类型。文本使用 text 或 input_text;截图使用 image_url 或 input_image。
messages[].rolestring否消息角色。当前输入必须以 user 结尾;存在上一轮执行回执时前一条必须为 tool。
messages[].tool_call_idstring否role=tool 时必填,引用上一轮 Mobile-Use Agent 响应的 tool_call id。
metadataobject否Mobile-Use Agent 步骤元数据。支持 screen_width、screen_height、available_apps 和兼容字段 harness_message;tool 回执与 harness_message 不可同时传。
metadata.app_listarray否available_apps 的兼容别名;同样是 App 名称字符串数组。
metadata.app_list[]string否单个应用名称;与 available_apps 的数组项含义一致。
metadata.available_appsarray否Mobile-Use Agent 标准版 的应用名称补充列表,JSON 字段为 available_apps。新传入列表替换之前的补充项,但默认应用仍保留;空数组不清空默认应用。Mobile-Use Agent 专属版 不使用此列表。
metadata.available_apps[]string否单个应用名称,使用设备上的真实应用显示名。
metadata.harness_messagestring否旧客户端的 Mobile-Use Agent Harness 回执文本;与前置 tool 消息不能同时传。新客户端优先使用 [tool,user] 消息。
metadata.screen_heightinteger否屏幕高度正整数。base64 图片会从图片本身解析宽高;URL 图片时该值用于运行时屏幕尺寸。
metadata.screen_widthinteger否屏幕宽度正整数。base64 图片会从图片本身解析宽高;URL 图片时该值用于运行时屏幕尺寸。
min_pnumber否vLLM Min-P 采样参数。
min_tokensinteger否最少生成 Token 数。
mm_processor_kwargsobject否多模态预处理器参数,按键原样透传给 Mobile-Use Agent 主模型。
mm_processor_kwargs.max_dynamic_patchinteger否示例:视觉模型允许的最大动态图像 patch 数。
modelstring否为 OpenAI 客户端兼容保留;路由时忽略,模型由实例已发布配置决定,响应 model 固定为空字符串。
ninteger否建议设为 1。该参数是否被模型接受取决于模型;当前 Mobile-Use Agent 响应不会透出多个候选动作。
parallel_tool_callsboolean否模型兼容的并行工具调用开关,不会扩大本接口可用的 Mobile-Use Agent 动作集合。
presence_penaltynumber否存在惩罚参数,透传给 Mobile-Use Agent 主模型。
prompt_logprobsinteger否模型兼容参数,JSON 字段为 prompt_logprobs。是否接受取决于模型;当前 Mobile-Use Agent 响应不返回输入 Token 概率明细。
reasoning_effortstring否OpenAI 兼容推理强度,例如 low、medium、high。
repetition_penaltynumber否vLLM 重复惩罚参数。
response_formatobject否模型兼容的结构化输出选项,不改变本接口最终的 Mobile-Use Agent 工具调用结构。请避免设置与动作协议冲突的输出约束。
response_format.typestring否响应格式类型,例如 text、json_object 或 json_schema。
seedinteger否随机种子。
skip_special_tokensboolean否是否跳过特殊 Token。
stoparray否停止序列。兼容单个字符串或字符串数组;此处展示数组形式。
stop[]string否一个停止字符串。模型是否接受及停止行为取决于模型。
stop_token_idsarray否vLLM 停止 Token ID 列表。
stop_token_ids[]integer否一个停止 Token 的整数 ID,需使用目标模型的词表 ID。
streamboolean是是否使用 SSE 流式响应。本接口必须传 true。
stream_optionsobject否OpenAI 兼容流式选项。
stream_options.include_usageboolean否是否在 data:[DONE] 之前输出 usage 统计块。
structured_outputsobject否vLLM 结构化输出约束。
structured_outputs.choicearray否允许的枚举文本集合。
structured_outputs.choice[]string否一项允许模型选择的文本值。
temperaturenumber否采样温度,覆盖 Mobile-Use Agent 主模型默认值。
top_kinteger否vLLM Top-K 采样参数。
top_logprobsinteger否模型兼容参数,JSON 字段为 top_logprobs。是否接受取决于模型;当前 Mobile-Use Agent 响应不返回候选 Token 概率明细。
top_pnumber否Top-P 核采样参数,覆盖 Mobile-Use Agent 主模型默认值。

JSON 字段写法

以下示例为 HTTP JSON 请求体。Chat Completions 字段使用 snake_case,例如 stream_options、include_usage、chat_template_kwargs、image_url;参数表的设计名称可能显示为驼峰,请按示例中的 JSON 键发送。

请求示例

以下请求体已使用 Python SDK 通过预发网关验证。实例 ID 和访问密钥通过请求头传入。此处使用 1×1 PNG 测试图,仅用于接口连通性验证;实际操作时必须替换为设备当前真实截图及其宽高,不能据此认为设备动作已执行。
{
  "model": "",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "观察当前屏幕并返回下一步操作"
        },
        {
          "type": "image_url",
          "image_url": {
            "url": "https://example.com/screenshot-current.png"
          }
        }
      ]
    }
  ],
  "metadata": {
    "screen_width": 1,
    "screen_height": 1,
    "available_apps": [
      "设置",
      "浏览器"
    ]
  },
  "tools": [],
  "chat_template_kwargs": {
    "enable_thinking": false
  },
  "stream": true,
  "stream_options": {
    "include_usage": true
  },
  "temperature": 0.2,
  "max_completion_tokens": 2048
}

截图与动作回执

  1. 首轮只传一条 user,包含任务目标和恰好一张当前截图。图片必须为有效的公开 HTTPS URL,或 Base64 图片 Data URL(data:image/png;base64,...)。
  2. 合并 SSE 中的 tool_calls,完整解析 function.arguments 后,由调用方执行动作;不要在参数尚未拼接完成时执行。
  3. 续轮使用返回的会话 ID,messages 仅传 [tool,user],不要重放历史 assistant 消息。tool_call_id 使用上一轮返回的 ID,content 填真实执行结果;user 带最新截图。
  4. 收到结束或用户接管动作时停止自动执行或转交用户,动作建议本身不代表设备已执行成功。
续轮 messages 格式(动作确实执行成功后才能填写成功回执):
[
  {
    "role": "user",
    "content": [
      # 注意:仅上轮为take_over和open时需要注入text信息,
      # 具体内容见:返回参数->补充说明
      {
        "type": "text",
        "text": "这是操作后的屏幕,请继续"
      },
      {
        "type": "image_url",
        "image_url": {
          "url": "https://example.com/screenshot-next.png"
        }
      }
    ]
  }
]
请替换截图地址、tool_call_id 和实际执行结果。

设备元数据

  • screen_width、screen_height 填截图的实际像素宽高,URL 截图建议提供;Base64 图片可读取自身尺寸。
  • Mobile-Use Agent 标准版 的 available_apps(兼容别名 app_list)是应用名称补充列表。新列表替换之前的补充项,但默认应用仍保留;Mobile-Use Agent 专属版 不使用此列表。
  • 新接入请使用 tool 消息回执;旧方式 metadata.harness_message 不可与 tool 消息同时传。
  • tools 应省略或为空数组。Mobile-Use Agent 思考开关不会让响应返回 reasoning_content;自定义响应格式也不能改变 Mobile-Use Agent 对外动作协议。

模型兼容参数

请求中显式提供的采样参数优先于模型默认值,chat_template_kwargs 按键合并。请用嵌套形式 "chat_template_kwargs":{"enable_thinking":true},其中的键也保持 snake_case。扩展参数是否支持、允许范围和效果取决于实例所选模型;不支持的参数可能被模型拒绝。 Agent 不原样转发模型的完整响应:当前不返回 logprobs、top_logprobs 或 prompt_logprobs 明细;n 建议保持 1,不承诺返回多候选结果。

返回参数

SSE 流式响应元数据。业务事件连续返回,最终以 data:[DONE] 结束;完整格式见返回参数补充说明。
参数类型必填说明
request_idstring否由 POP 网关生成的请求标识,用于网关链路追踪;Agent 请求 ID 以 SSE 正文中的 qi_request_id 为准。

SSE 读取规则

响应 Content-Type 为 text/event-stream。以空行分隔事件,解析 data: 后的 JSON;忽略冒号开头的心跳注释。一次网络读取不一定是一个完整事件,需缓存并拼接后解析。

数据字段

字段使用方式
id / created响应标识与创建时间(Unix 秒)。
object / modelobject 为 chat.completion.chunk;model 为空字符串。
qi_session_id / qi_request_id保存会话与请求标识,用于续接及排查。
choices[0].delta.content按顺序拼接的正文增量。
choices[0].delta.tool_calls按 index 拼接动作调用,function.arguments 为 JSON 字符串,完整解析后由调用方执行。
choices[0].finish_reason非空表示内容结束,常见 stop、tool_calls、length;之后仍可能返回用量事件。
qi_event进度或错误信息,不拼入正文。
usageinclude_usage=true 时独立返回,choices 为空数组;prompt_tokens、completion_tokens、total_tokens 是 Token 用量,不是最终账单。

完成与异常

通常按角色、内容或进度、finish_reason、可选 usage、data:[DONE] 的顺序返回。事件数量及分片边界不固定。
  • progress 可展示 message,不要写死阶段数量。
  • error 表示执行失败,即使 HTTP 为 200。读取 code、http_status、message、retryable,部分错误还包含 param、reason。
  • [DONE] 仅代表流结束,不单独代表成功;断开且无结束标记时,结果可能不完整。
流内错误示例(只展示相关字段):
data:{"qi_event":{"type":"error","stage":"input_validation","code":"invalid_parameter","http_status":400,"message":"The reference image URL has expired. Upload the image again or provide a new signed URL.","retryable":false,"reason":"image_url_expired"}}

data:[DONE]
HTTP/网关错误参见错误码表;流已建立后的错误按 qi_event 处理。

UI 动作空间说明

模型通过工具调用返回动作。function.arguments 为动作 JSON,通过 action_type 区分动作类型,由客户端负责执行。

动作类型

action_type含义参数
click点击x、y:点击坐标
double_click双击x、y:双击坐标
long_press长按x、y:长按坐标
drag拖拽或滑动start_x、start_y:起点;end_x、end_y:终点
type向当前聚焦的输入框输入文本text:输入内容
open打开 Apptext:App 名称,客户端需将 App 名称映射为包名或启动方式
system_button按系统键button:back(返回)、home(桌面)、enter(回车)、menu(菜单)
wait等待duration:等待指定秒数
take_over暂停,等待人工介入text:提示或问题;tag:介入类型
terminate结束任务status:结果状态,取值为 success 或 failed;answer:最终回答

补充说明

  • 坐标以当前截图左上角为原点,x 轴向右、y 轴向下,单位为像素。
  • 普通动作执行后,客户端携带新截图请求下一轮;terminate 用于结束循环。
  • 执行 open 后,客户端检查目标 App 是否位于前台,并在下一轮 user 消息中同时携带新截图和以下文本反馈:
    • 已经成功打开{App名称}
    • 未成功打开{App名称}
  • 执行 take_over 后,客户端展示 text 并暂停。人工操作完成后,下一轮 user 消息中同时携带新截图和用户输入;无输入时反馈:
    • 用户已经完成操作,请继续执行
  • 以上反馈均作为下一轮 user 消息的文本内容发送,无需额外构造 tool 消息。
  • 详细流程见最小实现 Demo。

QI GUI Agent 客户接入最小实现

#!/usr/bin/env python3
"""QI GUI Agent —— 客户接入最小实现(单文件 · 仅标准库)。

接入模型:客户端负责「截图 + 执行」,QI 负责「决策」。
每一轮把当前截图和任务发给 QI,QI 返回一个动作(点击/滑动/输入…),
客户端在设备上执行后,带着新截图请求下一轮,直到 QI 返回 terminate。

    ┌─────────┐  session_id + query + screenshot   ┌─────────┐
    │ 客户端   │ ─────────────────────────────────▶ │   QI    │
    │(截图/执行)│ ◀───────────────────────────────── │ (决策)  │
    └─────────┘        action: click/swipe/...      └─────────┘

运行:
    export QI_AK_ID=your_ak_id
    export QI_AK_SECRET=your_ak_secret
    export GUI_API_KEY=qis_xxx
    export GUI_INSTANCE_ID=GUI_xxx
    python qi_min_demo.py --device emulator-5554 --task "打开设置,进入 WiFi 页面" --app 设置

单轮测试(无需真机,只发一张图看 QI 返回什么动作):
    python qi_min_demo.py --screenshot screen.png --task "打开设置"
"""
from __future__ import annotations

import argparse
import base64
import json
import hashlib
import hmac
from datetime import datetime, timezone
import os
import re
import subprocess
import time
import http.client
import urllib.parse
import uuid

ENDPOINT = os.environ.get("QI_ENDPOINT", "https://maasqiservice.cn-beijing.aliyuncs.com/gui/v1/chat/completions")
ACS_ACTION = os.environ.get("QI_ACS_ACTION", "GuiChatCompletionStream")
ACS_VERSION = os.environ.get("QI_ACS_VERSION", "2026-08-31")
# open 动作的 App 名 -> 包名映射(示例,客户按需补充, 不在表内则执行报错)。
APP_MAP = {
    "设置": "com.android.settings",
    "微信": "com.tencent.mm",
    "淘宝": "com.taobao.taobao",
}

def validate_config() -> None:
    """只报告缺失变量名,不输出凭据;在截图或设备操作前检查。"""
    required = ("QI_AK_ID", "QI_AK_SECRET", "GUI_API_KEY", "GUI_INSTANCE_ID")
    missing = [name for name in required if not os.environ.get(name, "").strip()]
    if missing:
        raise ValueError("缺少必填环境变量:" + ", ".join(missing))
    endpoint = urllib.parse.urlsplit(ENDPOINT)
    if (endpoint.scheme != "https" or not endpoint.hostname or endpoint.username is not None
            or endpoint.password is not None or endpoint.query or endpoint.fragment):
        raise ValueError("QI_ENDPOINT 必须是无账号、query 和 fragment 的 HTTPS URL")


def sign_headers(body: bytes, session_id: str, request_id: str, *, stream: bool = False) -> dict:
    """ACS3 签名:body 必须与实际发送字节完全一致。会话 ID 复用,请求 ID 每轮更新。"""
    validate_config()
    endpoint = urllib.parse.urlsplit(ENDPOINT)
    body_hash = hashlib.sha256(body).hexdigest()
    headers = {
        "host": endpoint.netloc,
        "content-type": "application/json; charset=utf-8",
        "accept": "text/event-stream" if stream else "application/json",
        "x-acs-action": ACS_ACTION,
        "x-acs-version": ACS_VERSION,
        "x-acs-date": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "x-acs-signature-nonce": str(uuid.uuid4()),
        "x-acs-content-sha256": body_hash,
        "X-QI-Agent-Api-Key": os.environ["GUI_API_KEY"],
        "X-QI-Instance-Id": os.environ["GUI_INSTANCE_ID"],
        "X-QI-Session-Id": session_id,
        "X-QI-Request-Id": request_id,
    }
    token = os.environ.get("QI_STS_TOKEN", "")
    if token:
        headers["x-acs-security-token"] = token
    signed = dict(sorted((k.lower(), v.strip()) for k, v in headers.items()
                         if k.lower() in ("host", "content-type") or k.lower().startswith("x-acs-")))
    names = ";".join(signed)
    canonical = "\n".join([
        "POST", endpoint.path or "/", "",
        "".join(f"{k}:{v}\n" for k, v in signed.items()), names, body_hash,
    ])
    to_sign = "ACS3-HMAC-SHA256\n" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    signature = hmac.new(os.environ["QI_AK_SECRET"].encode("utf-8"),
                         to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
    headers["Authorization"] = (
        f"ACS3-HMAC-SHA256 Credential={os.environ['QI_AK_ID']},"
        f"SignedHeaders={names},Signature={signature}"
    )
    return headers


def resolve_package(name: str) -> str:
    """App 名解析成包名。"""
    assert name in APP_MAP, f"未知的 App 名:{name}"
    return APP_MAP[name]


# ======================================================================
# 1. QI 客户端:一次请求 = 一轮决策
# ======================================================================
def to_data_url(screenshot: str | bytes) -> str:
    """把截图(PNG/JPEG 字节或本地路径)转成 data URL。"""
    data = screenshot if isinstance(screenshot, bytes) else open(screenshot, "rb").read()
    mime = "image/png" if data.startswith(b"\x89PNG") else "image/jpeg"
    return f"data:{mime};base64,{base64.b64encode(data).decode()}"


def qi_request(messages: list, metadata: dict, session_id: str) -> dict:
    """调用 QI,返回本轮动作(dict,含 action_type)。

    AK 签名 + Agent API Key + Instance ID 鉴权;session_id 贯穿一个任务。
    """
    body = json.dumps({
        "model": "",
        "messages": messages,
        "metadata": metadata,
        "tools": [],
        "stream": False,
    }, ensure_ascii=False).encode("utf-8")

    request_id = f"round-{uuid.uuid4().hex}"
    # data=body 直接发送已签名的字节,不能再次 JSON 序列化。
    endpoint = urllib.parse.urlsplit(ENDPOINT)
    conn = http.client.HTTPSConnection(endpoint.hostname, endpoint.port, timeout=300)
    try:
        # urllib 会把 X-QI-* 改为 X-Qi-*;直接发送以保留网关要求的头名称。
        conn.request("POST", endpoint.path or "/", body=body,
                     headers=sign_headers(body, session_id, request_id, stream=False))
        resp = conn.getresponse()
        raw = resp.read()
        if resp.status >= 400:
            detail = raw.decode("utf-8", errors="replace")
            for name in ("QI_AK_ID", "QI_AK_SECRET", "GUI_API_KEY", "GUI_INSTANCE_ID", "QI_STS_TOKEN"):
                value = os.environ.get(name)
                if value:
                    detail = detail.replace(value, "[redacted]")
            raise RuntimeError(f"QI HTTP {resp.status}: {detail}")
        data = json.loads(raw)
    finally:
        conn.close()

    arguments = data["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
    return json.loads(arguments)


# ======================================================================
# 2. ADB 执行器:把 QI 的动作落到设备上
# ======================================================================
class AdbExecutor:
    def __init__(self, device: str):
        self.device = device

    def adb(self, *args: str, binary: bool = False):
        out = subprocess.run(["adb", "-s", self.device, *args], capture_output=True)
        return out.stdout if binary else out.stdout.decode(errors="replace").strip()

    def screenshot(self) -> bytes:
        return self.adb("exec-out", "screencap", "-p", binary=True)

    def foreground_package(self) -> str | None:
        """当前前台 App 的包名:从 dumpsys 里抓 `包名/Activity` token。"""
        out = self.adb("shell", "dumpsys", "activity", "activities")
        for line in out.splitlines():
            if "mResumedActivity" in line or "mFocusedApp" in line:
                m = re.search(r"([A-Za-z][A-Za-z0-9_.]*)/[A-Za-z0-9_.$]+", line)
                if m:
                    return m.group(1)
        return None

    def execute(self, action: dict) -> str:
        """执行一个 QI 动作,返回简短状态字符串。"""
        t = action["action_type"]
        if t == "click":
            self.adb("shell", "input", "tap", str(action["x"]), str(action["y"]))
        elif t == "double_click":
            x, y = str(action["x"]), str(action["y"])
            self.adb("shell", "input", "tap", x, y)
            time.sleep(0.1)
            self.adb("shell", "input", "tap", x, y)
        elif t == "long_press":
            x, y = str(action["x"]), str(action["y"])
            self.adb("shell", "input", "swipe", x, y, x, y, "1000")
        elif t == "drag":
            self.adb("shell", "input", "swipe",
                     str(action["start_x"]), str(action["start_y"]),
                     str(action["end_x"]), str(action["end_y"]), "400")
        elif t == "type":
            b64 = base64.b64encode(action["text"].encode()).decode()
            self.adb("shell", "am", "broadcast", "-a", "ADB_INPUT_B64", "--es", "msg", b64)
        elif t == "open":
            self.adb("shell", "monkey", "-p", resolve_package(action["text"]),
                     "-c", "android.intent.category.LAUNCHER", "1")
        elif t == "system_button":
            key = {"back": "KEYCODE_BACK", "home": "KEYCODE_HOME",
                   "enter": "KEYCODE_ENTER", "menu": "KEYCODE_MENU"}[action["button"]]
            self.adb("shell", "input", "keyevent", key)
        elif t == "wait":
            time.sleep(action.get("duration", 1))
        return f"{t} ok"


# ======================================================================
# 3. 主循环:截图 → 请求 QI → 执行动作 → 下一轮
# ======================================================================
def run_task(executor: AdbExecutor, task: str, apps: list, max_steps: int, delay: float):
    session_id = f"sess-{uuid.uuid4().hex[:16]}"
    print(f"session: {session_id} | task: {task} | apps: {apps}")

    # next_text:下一轮 user 消息里可选携带的一段文本。仅两种情况写入:
    #   1) open 后:前台 App 校验结果(已经/未成功打开 xx);
    #   2) take_over 恢复后:用户输入文本,或默认「用户已经完成操作,请继续执行」。
    next_text = None

    for step in range(1, max_steps + 1):
        shot = executor.screenshot()

        # 每轮只发一条 user 消息 + 一张当前截图,多轮上下文由 QI 按 session_id 维护。
        # 首轮 text 是任务;后续轮仅在需要时带 next_text。
        text = task if step == 1 else next_text
        content = ([{"type": "text", "text": text}] if text else [])
        content.append({"type": "image_url", "image_url": {"url": to_data_url(shot)}})
        messages = [{"role": "user", "content": content}]
        metadata = {"available_apps": apps} if step == 1 else {}
        next_text = None  # 本轮已消费,重置

        action = qi_request(messages, metadata, session_id)
        action_type = action["action_type"]
        print(f"[step {step}] {action_type}: {action}")

        if action_type == "terminate":  # 任务结束
            print(f"完成 status={action.get('status')} answer={action.get('answer')}")
            return

        if action_type == "take_over":
            # 阻塞等待用户输入。有输入则回传该文本,否则用默认继续文案。
            print(f"需要人工介入:{action.get('text')}")
            user_input = input(">>> 请输入补充信息后回车继续(直接回车表示已完成操作):").strip()
            next_text = user_input or "用户已经完成操作,请继续执行"
            print(f"已提交:{next_text}")
            continue

        executor.execute(action)
        time.sleep(delay)

        # open 动作后校验前台 App 是否已是目标,结果作为下一轮的文本回传。
        if action_type == "open":
            name = (action.get("text") or "").strip()
            ok = executor.foreground_package() == resolve_package(name)
            next_text = f"已经成功打开{name}" if ok else f"未成功打开{name}"
            print(f"前台校验:{next_text}")

    print(f"达到最大步数 {max_steps}")


def main():
    p = argparse.ArgumentParser(description="QI GUI Agent 最小接入 demo")
    p.add_argument("--task", required=True, help="任务指令,如:打开设置进入 WiFi 页面")
    p.add_argument("--app", nargs="*", default=[], help="设备可用 App 名列表")
    p.add_argument("--device", default=os.environ.get("ANDROID_DEVICE", "emulator-5554"))
    p.add_argument("--max-steps", type=int, default=100)
    p.add_argument("--step-delay", type=float, default=1.2)
    p.add_argument("--screenshot", help="单轮测试:只发这张图,打印 QI 返回的动作后退出")
    args = p.parse_args()
    try:
        validate_config()
    except ValueError as exc:
        p.error(str(exc))

    if args.screenshot:  # 无需真机的单轮 API 测试
        messages = [{"role": "user", "content": [
            {"type": "text", "text": args.task},
            {"type": "image_url", "image_url": {"url": to_data_url(args.screenshot)}}]}]
        print(json.dumps(qi_request(messages, {"available_apps": args.app},
                                    f"sess-{uuid.uuid4().hex[:16]}"),
                         ensure_ascii=False, indent=2))
        return

    run_task(AdbExecutor(args.device), args.task, args.app, args.max_steps, args.step_delay)


if __name__ == "__main__":
    main()

排查与重试

  • 未建立 SSE 时按 HTTP 状态和错误码表处理;AccessKey 签名认证与 Customer 实例密钥校验不同。
  • 参数错误、服务未开通、额度不足、会话冲突,先修正条件再调用。图片过期或无权限时更换有效链接。
  • 流内错误按 qi_event.retryable 判断是否退避重试;不要把 HTTP 200 或 [DONE] 单独当作成功。
  • 反馈问题时提供时间、接口名、qi_request_id 和网关 RequestId,不要提供密钥、完整签名链接或敏感输入。

返回示例

{
  "requestId": "request-20260904-001"
}

错误码

HTTP 状态码错误码错误信息说明
400ContentFilterYour request was blocked by the content filter.请求被内容安全策略拦截。
400InvalidParameterThe specified request parameter is invalid.指定的请求参数无效。
401InvalidApiKeyThe specified API key is invalid.指定的 API Key 无效。
403ServiceNotActivatedThe required Agent service is not activated for this account.当前账号未开通所请求的 Agent 服务,无法调用该接口。
404InvalidAgentNotFoundThe published Agent is not found.未找到已发布的 Agent。
409IdempotencyConflictThe request ID was reused with different request parameters.请求 ID 被用于不同的请求参数。
409SessionConflictThe session state conflicts with the request.Session 状态与请求冲突。
413RequestEntityTooLargeThe request body is too large.请求体过大。
429QuotaExhaustedThe Agent trial quota has been exhausted.Agent 试用额度已耗尽。
500InternalErrorAn internal server error occurred.发生内部服务器错误。
502UpstreamServiceErrorThe upstream Agent runtime failed to complete the request.上游 Agent Runtime 未能完成请求。
503ServiceUnavailableThe service is temporarily unavailable.服务暂时不可用。

变更历史

日期API 版本变更内容
2026-10-232026-08-31补充动作空间和客户接入最小实现。
2026-09-152026-08-31首次发布流式调用文档。