问一下,利用在线 DeepSeek 等 API 服务实现一个答题 APP
简介这是一个利用 Android 无障碍功能 + 悬浮窗 + 大模型的搜题应用
原理就是利用无障碍读取屏幕内容,然后通过悬浮窗来显示答案
众所周知我是一个学渣,所以在搜答案方面颇有成就
大概是在 4 年前,我写了这样一个脚本
GitHub:截图OCR识别后搜索题目获取答案
码云:截图OCR识别后搜索题目获取答案
利用 ADB 对屏幕截图后进行 OCR 识别,然后将识别到的结果用搜索引擎和本地题库进行搜索,然后快速获取答案
前几天,看着我手机里面的李跳跳和 DeepSeek,我突然发现,我可以利用无障碍读取屏幕数据,将读取到的题目发送给 DeepSeek 等大模型进行解答,利用Android 悬浮窗 来显示答案
说干就干,感觉代码不是特别多,于是就有了这个项目
因为没有做历史记录,提问每次只能问一下,所以这个 APP 就叫问一下了
运行展示
https://www.bilibili.com/video/BV1PrNweHEDX
源码
GitHub:https://github.com/PuZhiweizuishuai/ScanSearch
码云:https://gitee.com/puzhiweizuishuai/ScanSearch
注意:无障碍权限属于敏感权限,请确认软件来源后再安装或者自己编译安装,避免造成损失
技术实现
无障碍
首先到 AndroidManifest.xml 配置无障碍服务
<!-- 注册无障碍服务 --> <service android:name=".service.ScreenReaderService" android:exported="true" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>创建 app/src/main/res/xml/accessibility_service_config.xml 无障碍配置文件
<?xml version="1.0" encoding="utf-8"?><accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagDefault" android:accessibilityFeedbackType="feedbackGeneric" android:canRetrieveWindowContent="true" android:notificationTimeout="100" />编写 无障碍服务代码
package com.buguagaoshu.scan.search.serviceimport android.accessibilityservice.AccessibilityServiceimport android.view.accessibility.AccessibilityEventimport android.view.accessibility.AccessibilityNodeInfoimport com.buguagaoshu.scan.search.config.StaticVariableConfigimport com.buguagaoshu.scan.search.data.ScanSearchDataimport kotlin.uuid.ExperimentalUuidApiimport kotlin.uuid.Uuidclass ScreenReaderService : AccessibilityService() { // 指定按钮的包名和类名,需要根据实际情况修改 private val targetButtonPackageName = "com.buguagaoshu.scan.search" private val targetButtonClassName = "androidx.compose.material3.Button" // 指定按钮的文本内容,需要根据实际情况修改 private val targetButtonText = "读取屏幕" override fun onAccessibilityEvent(event: AccessibilityEvent) { if (!StaticVariableConfig.openScan) { return } if (event.packageName == targetButtonPackageName) { return } // 监听滑动事件 if (event.eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED || event.eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) { // 判断事件类型是否为点击事件 val source = event.source // 检查点击的节点是否符合目标按钮的条件 if (source != null) { // TODO 增加指定包名过滤配置 println(source.packageName) } // 获取根节点信息 val rootNode = rootInActiveWindow if (rootNode != null) { // 清除之前的数据 StaticVariableConfig.screenTextList.clear(); // 遍历节点并读取内容 traverseNodeLoop(rootNode) } } } @OptIn(ExperimentalUuidApi::class) private fun traverseNodeLoop(node: AccessibilityNodeInfo) { val stack = mutableListOf<AccessibilityNodeInfo>() stack.add(node) while (stack.isNotEmpty()) { val currentNode = stack.removeAt(stack.size - 1) // 读取节点的文本内容 val text = currentNode.text if (text != null && text.isNotEmpty()) { // 存储数据 StaticVariableConfig.screenTextList.add( ScanSearchData(Uuid.random().toString(), text.toString()) ) } // 遍历子节点并将它们添加到栈中 for (i in currentNode.childCount - 1 downTo 0) { val child = currentNode.getChild(i) if (child != null) { stack.add(child) } } } } private fun traverseNode(node: AccessibilityNodeInfo) { // 读取节点的文本内容 val text = node.text if (text != null && text.isNotEmpty()) { println(text) } // 遍历子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { traverseNode(child) } } } override fun onInterrupt() { // 服务中断时的处理 }}监控滑动事件,跳过对自身 APP 的监控,将读取到的屏幕数据保存到缓存中,方便后期读取加载
流式响应
因为目前兼容 Open API 的服务都支持流式输出,提升用户体验,避免出现长时间的等待
所以在使用 okhttp 发送请求的时候不能使用 call.execute(),而需要使用 client.newCall(req).enqueue
完整代码实现如下
fun sendStream( sendData: SendData, url: String, key: String, onChunkReceived: (String) -> Unit, onComplete: () -> Unit, onError: (String) -> Unit ) { CoroutineScope(Dispatchers.IO).launch { val body = Json.encodeToString(sendData).toRequestBody(contentType) // Authorization: Bearer $DASHSCOPE_API_KEY val req = Request .Builder() .url(url) .post(body) .addHeader("Authorization", "Bearer $key") .build() client.newCall(req).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { e.message?.let { onError(it) } } override fun onResponse(call: Call, response: Response) { response.use { if (!response.isSuccessful) { return } val reader = response.body.charStream() reader.forEachLine { line -> if (line.isNotBlank()) { onChunkReceived(line) } } onComplete() } } }) } }其它
UI实现大部分都是通过AI写的,也没有什么要注意的了
使用指南
一、配置无障碍权限与悬浮窗权限
第一次使用会弹出无障碍权限配置菜单
在已下载应用内点击问一下
给予问一下无障碍权限
二、配置 API 服务商
首先你需要配置好你的 AI 服务商,当前你也可以不用配置这个,直接使用悬浮窗内打开网页进行搜索,不过由于 Android Webview 控件我不太会用,显示的效果有问题
由于 DeepSeek 的服务目前用不了,暂时用阿里通义的 API 替代
API申请地址:https://platform.deepseek.com/usage
登陆后点击侧边栏 API keys 生成一个 API_KEY
然后到 APP 内填写你需要调用的大模型名称、 API 地址、和 API-KEY 即可使用
其它可以白嫖的API服务地址
字节火山:https://www.volcengine.com/product/doubao
目前免费送 50 万 TOKE,支持满血 DeepSeek R1 模型
阿里通义:https://www.aliyun.com/minisite/goods?userCode=4i6gwidx
免费送 100 万 TOKEN
三、开始使用
打开悬浮窗后,进入你要搜索的应用
由于只监听了滑动事件,所以进入应用后请先在屏幕上划两下,然后再点击加载数据
这是应该就可以读取到屏幕上显示内容了
将你要搜索的题目进行勾选
点击确定
这样题目就会自动出现再搜索框
⚠️注意:如果需要对题目进行编辑,请先点击打开 ⌨️ 键盘获取焦点,不然无法输入,修改完成后请点击关闭键盘,读取屏幕数据会无法读取到当前屏幕信息
为避免滑动事件冲突,如果需要挪动窗口位置,请先点击右上角的锁,挪动完成后,再点击一下锁就可以再次滑动显示内容
最后点击提问即可获取答案
四、其它功能
点击打开网页可以调用 秘塔AI搜索,不过由于显示界面太小,所以显示会有一些问题
点击最小化按钮可以缩小悬浮窗,这时你可以利用手机系统自带的 AI 功能对屏幕进行识别,获取问题信息
版权
本文首发于:https://www.buguagaoshu.com/archives/wen-yi-xia
转载请注明出处
页:
[1]