d585ddf576
- AndroidManifest: три uses-native-library (libOpenCL/libvndksupport/libcdsprpc) ВНУТРИ <application> — aapt2 8.9.2 отбрасывает тег на уровне <manifest> - LocalLlm: enableSpeculativeDecoding=true, GPU/GPU/CPU бэкенды (как у автора) - DebugHttpServer: HTTP-отладка ассистента без UI - Log/PhoneApp: мелкие правки Подтверждено: движок поднимается, локальный чат отвечает на HONOR AXGL024B05001337
149 lines
6.1 KiB
Kotlin
149 lines
6.1 KiB
Kotlin
package pw.binom.viewmate.phone
|
|
|
|
import com.google.ai.edge.litertlm.Backend
|
|
import pw.binom.viewmate.phone.agent.LlmModelChoice
|
|
import pw.binom.viewmate.phone.agent.LocalLlmClient
|
|
import java.io.BufferedReader
|
|
import java.io.InputStreamReader
|
|
import java.net.InetAddress
|
|
import java.net.ServerSocket
|
|
|
|
/**
|
|
* Отладочный HTTP-сервер в приложении (только для разработки).
|
|
* Позволяет дёргать ассистента и переключать модель без UI:
|
|
*
|
|
* GET /status — выбор модели, готовность движка, размер файла
|
|
* POST /chat {"text":"привет"} — отправить фразу в ассистента (та же очередь, что UI)
|
|
* POST /model {"choice":"local"} — переключить модель (local|remote), персист
|
|
*
|
|
* Запускается на 127.0.0.1:8787. Доступ: `adb forward tcp:8787 tcp:8787`.
|
|
*/
|
|
object DebugHttpServer {
|
|
|
|
private const val PORT = 8787
|
|
private const val TAG = "httpd"
|
|
|
|
@Volatile
|
|
private var running = false
|
|
|
|
fun start(app: PhoneApp) {
|
|
if (running) return
|
|
running = true
|
|
Thread {
|
|
try {
|
|
val server = ServerSocket(PORT, 8, InetAddress.getByName("127.0.0.1"))
|
|
log(TAG, "debug-http на 127.0.0.1:$PORT (adb forward tcp:8787 tcp:8787)")
|
|
while (running) {
|
|
try {
|
|
val socket = server.accept()
|
|
Thread {
|
|
try {
|
|
handle(socket, app)
|
|
} catch (e: Exception) {
|
|
log(TAG, "обработка запроса: ${e.message}")
|
|
} finally {
|
|
runCatching { socket.close() }
|
|
}
|
|
}.apply { isDaemon = true }.start()
|
|
} catch (e: Exception) {
|
|
if (running) log(TAG, "accept: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
log(TAG, "не удалось запустить: ${e.message}")
|
|
}
|
|
}.apply { isDaemon = true; name = "viewmate-httpd" }.start()
|
|
}
|
|
|
|
private fun handle(socket: java.net.Socket, app: PhoneApp) {
|
|
val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8))
|
|
val requestLine = reader.readLine() ?: return
|
|
val parts = requestLine.split(" ")
|
|
if (parts.size < 2) return
|
|
val method = parts[0]
|
|
val path = parts[1]
|
|
|
|
// Тело (до Content-Length)
|
|
var contentLength = 0
|
|
while (true) {
|
|
val line = reader.readLine() ?: break
|
|
if (line.isEmpty()) break
|
|
if (line.startsWith("Content-Length:", true)) {
|
|
contentLength = line.substringAfter(':').trim().toIntOrNull() ?: 0
|
|
}
|
|
}
|
|
val body = if (contentLength > 0) {
|
|
val buf = CharArray(contentLength)
|
|
var read = 0
|
|
while (read < contentLength) {
|
|
val n = reader.read(buf, read, contentLength - read)
|
|
if (n < 0) break
|
|
read += n
|
|
}
|
|
String(buf, 0, read)
|
|
} else ""
|
|
|
|
val response: String
|
|
val code: Int
|
|
when {
|
|
method == "GET" && path.startsWith("/status") -> {
|
|
val choice = app.llmChoice.value
|
|
val local = app.localLlmClient()
|
|
val file = app.localModelFile()
|
|
response = """{"choice":"${choice.name.lowercase()}","local":{${localStatus(local, file)}}}"""
|
|
code = 200
|
|
}
|
|
method == "POST" && path.startsWith("/chat") -> {
|
|
val text = jsonField(body, "text") ?: ""
|
|
if (text.isBlank()) {
|
|
response = """{"error":"пустой text"}"""
|
|
code = 400
|
|
} else {
|
|
app.sendToAssistant(text)
|
|
response = """{"ok":true,"queued":"${text.take(60)}"}"""
|
|
code = 200
|
|
}
|
|
}
|
|
method == "POST" && path.startsWith("/model") -> {
|
|
val choice = jsonField(body, "choice")
|
|
if (choice == "local" || choice == "remote") {
|
|
app.setLlmModelChoice(if (choice == "local") LlmModelChoice.LOCAL else LlmModelChoice.REMOTE)
|
|
response = """{"ok":true,"choice":"$choice"}"""
|
|
code = 200
|
|
} else {
|
|
response = """{"error":"choice должен быть local|remote"}"""
|
|
code = 400
|
|
}
|
|
}
|
|
else -> {
|
|
response = """{"error":"нет такого пути: $method $path"}"""
|
|
code = 404
|
|
}
|
|
}
|
|
|
|
val out = socket.getOutputStream()
|
|
val payload = response.toByteArray(Charsets.UTF_8)
|
|
out.write(("HTTP/1.1 $code ${reason(code)}\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: ${payload.size}\r\nConnection: close\r\n\r\n").toByteArray(Charsets.UTF_8))
|
|
out.write(payload)
|
|
out.flush()
|
|
}
|
|
|
|
private fun localStatus(local: LocalLlmClient?, file: java.io.File): String {
|
|
if (local == null) return """"ready":false,"fileExists":${file.exists()},"fileBytes":${file.length()}"""
|
|
return """"ready":true,"modelDownloaded":${local.modelDownloaded()},"fileExists":${file.exists()},"fileBytes":${file.length()}"""
|
|
}
|
|
|
|
private fun jsonField(body: String, key: String): String? {
|
|
// {"text":"..."} — примитивный парсер, без кавычек внутри значения
|
|
val regex = Regex("\"$key\"\\s*:\\s*\"([^\"]*)\"")
|
|
return regex.find(body)?.groupValues?.get(1)
|
|
}
|
|
|
|
private fun reason(code: Int): String = when (code) {
|
|
200 -> "OK"
|
|
400 -> "Bad Request"
|
|
404 -> "Not Found"
|
|
else -> "Error"
|
|
}
|
|
}
|