Skip to content

Commit e6151a5

Browse files
authored
Merge pull request #20 from ARTEMKOPIK/codegen-bot/ai-powered-automation-1766714812
🤖 AI-Powered Automation v1.1.0 - Крупное обновление!
2 parents 60b71f5 + 6a33885 commit e6151a5

File tree

5 files changed

+538
-5
lines changed

5 files changed

+538
-5
lines changed

CHANGELOG.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1-
## [1.0.21] - 2025-12-26\n\n### 🐛 Исправления
1+
## [1.1.0] - 2025-12-26
2+
3+
### 🤖 AI-POWERED AUTOMATION - Крупное обновление!
4+
5+
#### ✨ Новые возможности
6+
- **findText(text, timeout)** - умный поиск текста на экране используя ML Kit OCR
7+
- **findImage(imagePath, threshold)** - поиск изображений через template matching
8+
- **TemplateMatcher** - собственная реализация алгоритма Normalized Cross-Correlation
9+
- Полностью локальная работа AI - без внешних зависимостей и серверов
10+
- Автоматическое определение координат элементов
11+
12+
#### 📚 Улучшения документации
13+
- Расширенные примеры использования AI команд
14+
- Добавлены практические use-cases с findText и findImage
15+
- Обновлён README с подробной документацией
16+
17+
#### 🎯 Преимущества
18+
- Не нужно вручную искать координаты элементов
19+
- Скрипты работают на разных разрешениях экрана
20+
- 100% бесплатно, работает офлайн
21+
- Высокая точность распознавания
22+
23+
---
24+
25+
## [1.0.21] - 2025-12-26
26+
27+
### 🐛 Исправления
228
- Fix: Ошибка компиляции - убран лишний параметр versionName из createErrorNotification
329

430
### 📝 Другое
531
- Security: Добавлена проверка подписи APK перед установкой обновлений
632

7-

README.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Android приложение для автоматизации действий
1515

1616
## Функции скриптов
1717

18+
### Базовые команды
19+
1820
```javascript
1921
// Клик по координатам
2022
click(x, y)
@@ -47,7 +49,36 @@ pushToCb("текст")
4749
EXIT = true
4850
```
4951

50-
## Пример скрипта
52+
### 🤖 AI-Powered команды (NEW!)
53+
54+
```javascript
55+
// Найти текст на экране (возвращает координаты)
56+
val pos = findText("Кнопка")
57+
if (pos != null) {
58+
click(pos.first, pos.second)
59+
}
60+
61+
// Найти текст с таймаутом
62+
val pos = findText("Загрузка", 10000) // ждать до 10 секунд
63+
64+
// Найти изображение на экране
65+
val pos = findImage("/sdcard/template.png")
66+
if (pos != null) {
67+
click(pos.first, pos.second)
68+
}
69+
70+
// Найти изображение с порогом совпадения
71+
val pos = findImage("/sdcard/button.png", 0.9) // 90% совпадение
72+
73+
// Ожидать появления текста
74+
if (waitForText(0, 0, 1080, 1920, "Готово", 5000)) {
75+
log("Текст появился!")
76+
}
77+
```
78+
79+
## Примеры скриптов
80+
81+
### Базовый пример
5182

5283
```javascript
5384
log("Скрипт запущен")
@@ -70,6 +101,54 @@ while (!EXIT) {
70101
log("Скрипт остановлен")
71102
```
72103

104+
### 🤖 AI-пример: Автоматический поиск и клик
105+
106+
```javascript
107+
log("🤖 AI-скрипт запущен")
108+
109+
// Ищем кнопку "Начать" на экране
110+
val startButton = findText("Начать")
111+
if (startButton != null) {
112+
log("✅ Кнопка найдена!")
113+
click(startButton.first, startButton.second)
114+
sleep(1000)
115+
} else {
116+
log("❌ Кнопка не найдена")
117+
EXIT = true
118+
}
119+
120+
// Ждём появления текста "Готово"
121+
if (waitForText(0, 0, 1080, 1920, "Готово", 30000)) {
122+
log("✅ Процесс завершён!")
123+
sendTelegram("🎉 Задача выполнена!")
124+
} else {
125+
log("⏱️ Таймаут ожидания")
126+
}
127+
128+
log("Скрипт завершён")
129+
```
130+
131+
### 🖼️ Пример с поиском изображений
132+
133+
```javascript
134+
log("🖼️ Поиск изображения...")
135+
136+
// Сохраните эталонное изображение кнопки в /sdcard/button_template.png
137+
val buttonPos = findImage("/sdcard/button_template.png", 0.85)
138+
139+
if (buttonPos != null) {
140+
log("🎯 Кнопка найдена на экране!")
141+
click(buttonPos.first, buttonPos.second)
142+
sleep(500)
143+
} else {
144+
log("🔍 Кнопка не найдена, пробуем текстовый поиск...")
145+
val textPos = findText("ОК")
146+
if (textPos != null) {
147+
click(textPos.first, textPos.second)
148+
}
149+
}
150+
```
151+
73152
## Требуемые разрешения
74153

75154
- **Accessibility Service** - для выполнения кликов

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ android {
2828
applicationId = "com.autoclicker.app"
2929
minSdk = 24
3030
targetSdk = 35
31-
versionCode = 1021
32-
versionName = "1.0.21"
31+
versionCode = 1100
32+
versionName = "1.1.0"
3333

3434
// Crash reporting credentials (читаем из local.properties или environment)
3535
buildConfigField("String", "CRASH_BOT_TOKEN", "\"${getConfigValue("CRASH_BOT_TOKEN")}\"")

app/src/main/java/com/autoclicker/app/script/ScriptEngine.kt

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.Context
55
import android.graphics.Bitmap
66
import android.graphics.Color
77
import com.autoclicker.app.service.ClickerAccessibilityService
8+
import com.autoclicker.app.util.TemplateMatcher
89
import com.autoclicker.app.service.ScreenCaptureService
910
import com.autoclicker.app.util.Constants
1011
import com.autoclicker.app.util.CrashHandler
@@ -92,6 +93,12 @@ class ScriptEngine(
9293
private val REGEX_GET_VAR = Regex("""getVar\(["'](\w+)["']\)""")
9394
private val REGEX_INC_VAR = Regex("""incVar\(["'](\w+)["']\)""")
9495
private val REGEX_DEC_VAR = Regex("""decVar\(["'](\w+)["']\)""")
96+
97+
// AI-Powered Commands
98+
private val REGEX_FIND_TEXT = Regex("""findText\(["'](.*?)["']\)""")
99+
private val REGEX_FIND_TEXT_TIMEOUT = Regex("""findText\(["'](.*?)["'],\s*(\d+)\)""")
100+
private val REGEX_FIND_IMAGE = Regex("""findImage\(["'](.*?)["']\)""")
101+
private val REGEX_FIND_IMAGE_THRESHOLD = Regex("""findImage\(["'](.*?)["'],\s*([0-9.]+)\)""")
95102
}
96103

97104
fun execute(code: String) {
@@ -201,6 +208,8 @@ class ScriptEngine(
201208
line.contains("getText(") -> parseGetText(line)
202209
line.contains("getColor(") -> parseGetColor(line)
203210
line.contains("pushToCb(") -> parsePushToClipboard(line)
211+
line.contains("findText(") -> parseFindText(line)
212+
line.contains("findImage(") -> parseFindImage(line)
204213
line == "EXIT = true" -> EXIT = true
205214
line.startsWith("while") -> return executeWhileLoop(line, allLines, currentIndex)
206215
line.startsWith("if") -> return executeIfBlock(line, allLines, currentIndex)
@@ -1129,4 +1138,186 @@ class ScriptEngine(
11291138
}
11301139
log("Clipboard: $text")
11311140
}
1141+
1142+
// ==================== AI-POWERED METHODS ====================
1143+
1144+
/**
1145+
* Find text anywhere on screen using ML Kit OCR.
1146+
* Returns coordinates of found text or null if not found.
1147+
*
1148+
* @param text Text to search for
1149+
* @param timeout Maximum time to search in milliseconds (default 5000)
1150+
* @return Pair of (x, y) coordinates where text was found, or null
1151+
*/
1152+
fun findText(text: String, timeout: Long = 5000): Pair<Int, Int>? {
1153+
if (EXIT || Thread.currentThread().isInterrupted) return null
1154+
1155+
log("🔍 FindText: searching for '$text'...")
1156+
val startTime = System.currentTimeMillis()
1157+
1158+
while (!EXIT && !Thread.currentThread().isInterrupted) {
1159+
val elapsed = System.currentTimeMillis() - startTime
1160+
if (elapsed >= timeout) {
1161+
log("⏱️ FindText: timeout after ${timeout}ms")
1162+
return null
1163+
}
1164+
1165+
val screenshot = screenshot() ?: continue
1166+
1167+
try {
1168+
val image = InputImage.fromBitmap(screenshot, 0)
1169+
val latch = CountDownLatch(1)
1170+
var result: Pair<Int, Int>? = null
1171+
1172+
val recognizer = getTextRecognizer()
1173+
if (recognizer == null) {
1174+
log("❌ FindText: TextRecognizer not initialized")
1175+
screenshot.recycle()
1176+
return null
1177+
}
1178+
1179+
recognizer.process(image)
1180+
.addOnSuccessListener { visionText ->
1181+
for (block in visionText.textBlocks) {
1182+
if (block.text.contains(text, ignoreCase = true)) {
1183+
val rect = block.boundingBox
1184+
if (rect != null) {
1185+
result = Pair(rect.centerX(), rect.centerY())
1186+
log("✅ FindText: found at (${rect.centerX()}, ${rect.centerY()}) in ${elapsed}ms")
1187+
}
1188+
break
1189+
}
1190+
}
1191+
latch.countDown()
1192+
}
1193+
.addOnFailureListener { e ->
1194+
log("❌ FindText error: ${e.message}")
1195+
latch.countDown()
1196+
}
1197+
1198+
latch.await(2000, TimeUnit.MILLISECONDS)
1199+
screenshot.recycle()
1200+
1201+
if (result != null) {
1202+
return result
1203+
}
1204+
} catch (e: Exception) {
1205+
log("❌ FindText error: ${e.message}")
1206+
CrashHandler.logError("ScriptEngine", "Error in findText", e)
1207+
screenshot.recycle()
1208+
}
1209+
1210+
sleep(200)
1211+
}
1212+
1213+
return null
1214+
}
1215+
1216+
/**
1217+
* Find image on screen using template matching.
1218+
* Returns coordinates of best match or null if not found.
1219+
*
1220+
* @param imagePath Path to template image file
1221+
* @param threshold Confidence threshold (0.0 to 1.0, default 0.8)
1222+
* @return Pair of (x, y) coordinates where image was found, or null
1223+
*/
1224+
fun findImage(imagePath: String, threshold: Float = 0.8f): Pair<Int, Int>? {
1225+
if (EXIT || Thread.currentThread().isInterrupted) return null
1226+
1227+
log("🔍 FindImage: searching for '$imagePath' (threshold: $threshold)...")
1228+
1229+
try {
1230+
// Load template image
1231+
val templateFile = java.io.File(imagePath)
1232+
if (!templateFile.exists()) {
1233+
log("❌ FindImage: template file not found: $imagePath")
1234+
return null
1235+
}
1236+
1237+
val template = android.graphics.BitmapFactory.decodeFile(imagePath)
1238+
if (template == null) {
1239+
log("❌ FindImage: failed to decode template image")
1240+
return null
1241+
}
1242+
1243+
// Get current screenshot
1244+
val screenshot = screenshot()
1245+
if (screenshot == null) {
1246+
log("❌ FindImage: failed to capture screenshot")
1247+
template.recycle()
1248+
return null
1249+
}
1250+
1251+
// Perform template matching
1252+
val matcher = TemplateMatcher.getInstance()
1253+
val matches = matcher.match(screenshot, template, threshold, maxMatches = 1)
1254+
1255+
screenshot.recycle()
1256+
template.recycle()
1257+
1258+
if (matches.isNotEmpty()) {
1259+
val match = matches[0]
1260+
val centerX = match.x + template.width / 2
1261+
val centerY = match.y + template.height / 2
1262+
log("✅ FindImage: found at ($centerX, $centerY) with confidence ${match.confidence}")
1263+
return Pair(centerX, centerY)
1264+
} else {
1265+
log("❌ FindImage: not found (no matches above threshold)")
1266+
return null
1267+
}
1268+
1269+
} catch (e: Exception) {
1270+
log("❌ FindImage error: ${e.message}")
1271+
CrashHandler.logError("ScriptEngine", "Error in findImage", e)
1272+
return null
1273+
}
1274+
}
1275+
1276+
private fun parseFindText(line: String) {
1277+
val matchTimeout = REGEX_FIND_TEXT_TIMEOUT.find(line)
1278+
if (matchTimeout != null) {
1279+
val text = matchTimeout.groupValues[1]
1280+
val timeout = matchTimeout.groupValues[2].toLong()
1281+
val result = findText(text, timeout)
1282+
if (result != null && line.contains("=")) {
1283+
val varName = line.substringBefore("=").trim().removePrefix("val ").removePrefix("var ")
1284+
variables[varName] = result
1285+
}
1286+
return
1287+
}
1288+
1289+
val match = REGEX_FIND_TEXT.find(line)
1290+
if (match != null) {
1291+
val text = match.groupValues[1]
1292+
val result = findText(text)
1293+
if (result != null && line.contains("=")) {
1294+
val varName = line.substringBefore("=").trim().removePrefix("val ").removePrefix("var ")
1295+
variables[varName] = result
1296+
}
1297+
}
1298+
}
1299+
1300+
private fun parseFindImage(line: String) {
1301+
val matchThreshold = REGEX_FIND_IMAGE_THRESHOLD.find(line)
1302+
if (matchThreshold != null) {
1303+
val imagePath = matchThreshold.groupValues[1]
1304+
val threshold = matchThreshold.groupValues[2].toFloat()
1305+
val result = findImage(imagePath, threshold)
1306+
if (result != null && line.contains("=")) {
1307+
val varName = line.substringBefore("=").trim().removePrefix("val ").removePrefix("var ")
1308+
variables[varName] = result
1309+
}
1310+
return
1311+
}
1312+
1313+
val match = REGEX_FIND_IMAGE.find(line)
1314+
if (match != null) {
1315+
val imagePath = match.groupValues[1]
1316+
val result = findImage(imagePath)
1317+
if (result != null && line.contains("=")) {
1318+
val varName = line.substringBefore("=").trim().removePrefix("val ").removePrefix("var ")
1319+
variables[varName] = result
1320+
}
1321+
}
1322+
}
11321323
}

0 commit comments

Comments
 (0)