问一下,利用在线 DeepSeek 等 API 服务实现一个答题 APP
简介
这是一个利用 Android 无障碍功能 + 悬浮窗 + 大模型的搜题应用
原理就是利用无障碍读取屏幕内容,然后通过悬浮窗来显示答案
众所周知我是一个学渣,所以在搜答案方面颇有成就
大概是在 4 年前,我写了这样一个脚本
GitHub:截图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.service
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.buguagaoshu.scan.search.config.StaticVariableConfig
import com.buguagaoshu.scan.search.data.ScanSearchData
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
class 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
转载请注明出处
问一下,利用在线 DeepSeek 等 API 服务实现一个答题 APP的更多相关文章
- 利用JNDI的命名与服务功能来满足企业级API对命名与服务的访问
包含了大量的命名和目录服务,使用通用接口来访问不同种类的服务: 可以同时连接到多个命名或目录服务上: 建立起逻辑关联,允许把名称同Java对象或资源关联起来,而不必知道对象或资源的物理ID. JNDI ...
- 利用Vert.x构建简单的API 服务、分布式服务
目前已经使用Vertx已经一年多了,虽然没有太多的造诣,但也已在项目中推广了下:从最初的vertx搭建web服务,到项目上线运营,还算比较稳定.再到后来尝试搭建基于vertx的分布式服务,一路下来也积 ...
- Wami Map Project – 开源的 OSM API 服务
Wami 地图项目把 OSM 数据分享给所有的人,很容易使用.他们利用 MongoDB 的潜力进行大数据管理来实现从 OSM 数据来源搜索相关的数据.它们的 API 使人们有可能检索不同格式的 POI ...
- [AI开发]Python+Tensorflow打造自己的计算机视觉API服务
"与其停留在概念理论层面,不如动手去实现一个简单demo ." ——鲁迅 没有源码都是耍流氓github 前言 目前提供AI开发相关API接口的公司有很多,国外如微软. ...
- 使用CodeIgniter框架搭建RESTful API服务
使用CodeIgniter框架搭建RESTful API服务 发表于 2014-07-12 | 分类于 翻译笔记 | 6条评论 在2011年8月的时候,我写了一篇博客<使用Cod ...
- 利用PhantomJS搭建Highcharts export服务
利用PhantomJS搭建Highcharts export服务 一直在使用Highcharts做web图表的展示, 但是当发送定时的报表邮件的遇到了这个问题. 为了保证邮件图表和web页图表样式一致 ...
- Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)
技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢. 当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问 ...
- .net core实践系列之短信服务-Sikiro.SMS.Api服务的实现
前言 上篇<.net core实践系列之短信服务-架构设计>介绍了我对短信服务的架构设计,同时针对场景解析了我的设计理念.本篇继续讲解Api服务的实现过程. 源码地址:https://gi ...
- API服务网关(Zuul)
技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢. 当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问 ...
- Web Api 内部数据思考 和 利用http缓存优化 Api
在上篇<Web Api 端点设计 与 Oauth>后,接着我们思考Web Api 的内部数据: 其他文章:<API接口安全加强设计方法> 第一 实际使用应该返回怎样的数据 ? ...
随机推荐
- HUAWEI SECURITY 2023 山东大学专场 WP
Crypto by Smera1d0 1.ezrsa 题干如下: from Crypto.Util.number import getPrime from secret import flag p = ...
- 全网最适合入门的面向对象编程教程:60 Python面向对象综合实例-传感器数据实时绘图器
全网最适合入门的面向对象编程教程:60 Python 面向对象综合实例-传感器数据实时绘图器 摘要: 本文将结合之前内容实现模拟一个传感器系统软件,包括三个线程:传感器线程生成数据并通过串口发送给主机 ...
- 图片渲染 API:极速生成电商、社媒、营销、横幅、证书等图片!
不知道还有没有同学还记得,当时自己开发智能体时,有一个自动生成证书图片的功能,既方便又实用.今天我们就来带大家回顾一下,如何快速生成图片,并且最重要的是,完全无需通过 HTTP 调用,极大提高了操作的 ...
- uni-app项目uview的表单验证在小程序上不生效
前情 uni-app是我比较喜欢的跨平台框架,它能开发小程序/H5/APP(安卓/iOS),重要的是对前端开发友好,自带的IDE让开发体验非常棒,公司项目就是主推uni-app,在uniapp生态中u ...
- 【Web前端】【开源分享】H5登陆界面 - 2021年12月30日
下载地址 Gitee下载 后续更新关注本文评论区作者萌狼蓝天的回复
- Qt编写可视化大屏电子看板系统23-模块1产量汇总
一.前言 大屏系统采用结构模块化的分层设计思路,一个表对应一个最小模块比如模具产量.零件产量,数据库采集的时候采集对应的表,拿到数据后按照对应的数据规则传给控件绘制,其中模具产量.零件产量两个模块采用 ...
- golang两个协程交替打印出1-100
基于channel实现的,两个协程交替打印出1-100 package main import ( "fmt" "sync" ) var ( toOdd = m ...
- 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-9- 浏览器的相关操作 (详细教程)
1.简介 在自动化测试领域,元素定位是非常重要的一环.正确定位页面元素是测试用例能否成功执行的关键因素之一.playwright是一种自动化测试工具,它提供了丰富的元素定位方法,可以满足不同场景下的定 ...
- Solution Set -「NOIP Simu.」20221113
\(\mathscr{A}\sim\) 游戏 Cover:「ARC 087E」Prefix-free Game. Tags:「A.博弈-SG 函数」「A.数据结构-Trie」 想了半天 ( ...
- Note -「q-analog」组合意义灭天地
(搁置, 填坑看心情.) \[\mathfrak{Defining~\LaTeX~macros\cdots} \newcommand{\qnum}[1]{\lbrack{#1}\rbrack_q} ...