智能助手网
标签聚合 Android

/tag/Android

linux.do · 2026-04-18 13:53:38+08:00 · tech

我发现最新Claude code已无法直接在Android termux用npm安装来直接使用,会有报错,肯定是Termux环境的兼容问题,毕竟不是标准的Linux。如何解决: 1.非proot方案(推荐) #!/data/data/com.termux/files/usr/bin/bash set -euo pipefail readonly SCRIPT_NAME="$(basename "$0")" readonly PREFIX_DIR="${PREFIX:-/data/data/com.termux/files/usr}" readonly STATE_DIR="${CLAUDE_CODE_HOME:-$HOME/.claude-code-termux}" readonly NODE_DIR="$STATE_DIR/node" readonly WRAPPER_BIN_DIR="$STATE_DIR/bin" readonly PATCH_DIR="$STATE_DIR/patches" readonly GLOBAL_PREFIX_DIR="$STATE_DIR/npm-global" readonly GLOBAL_BIN_DIR="$GLOBAL_PREFIX_DIR/bin" readonly NPM_CACHE_DIR="$STATE_DIR/npm-cache" readonly TMP_ROOT_DIR="${TMPDIR:-$PREFIX_DIR/tmp}" readonly GLIBC_LDSO="$PREFIX_DIR/glibc/lib/ld-linux-aarch64.so.1" readonly GLIBC_RUNNER_BIN="$PREFIX_DIR/bin/grun" readonly GLIBC_MARKER="$STATE_DIR/.glibc-arch" readonly HOST_CLAUDE_PATH="$PREFIX_DIR/bin/claude" readonly BACKUP_DIR="$STATE_DIR/backups" readonly CLAUDE_PACKAGE_NAME="@anthropic-ai/claude-code" readonly CLAUDE_PACKAGE_VERSION="${CLAUDE_CODE_VERSION:-latest}" readonly NODE_VERSION="${CLAUDE_CODE_NODE_VERSION:-22.22.0}" readonly NODE_TARBALL="node-v${NODE_VERSION}-linux-arm64.tar.xz" readonly NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}" readonly COMPAT_PATCH_PATH="$PATCH_DIR/claude-glibc-compat.js" readonly CLAUDE_EXE_PATH="$GLOBAL_PREFIX_DIR/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe" readonly HOST_WRAPPER_MARKER="# claude-code-termux-nonproot-wrapper" readonly C_BOLD_BLUE="\033[1;34m" readonly C_BOLD_GREEN="\033[1;32m" readonly C_BOLD_YELLOW="\033[1;33m" readonly C_BOLD_RED="\033[1;31m" readonly C_RESET="\033[0m" info() { printf '%b[INFO]%b %s\n' "$C_BOLD_BLUE" "$C_RESET" "$*" } success() { printf '%b[ OK ]%b %s\n' "$C_BOLD_GREEN" "$C_RESET" "$*" } warn() { printf '%b[WARN]%b %s\n' "$C_BOLD_YELLOW" "$C_RESET" "$*" >&2 } die() { printf '%b[ERR ]%b %s\n' "$C_BOLD_RED" "$C_RESET" "$*" >&2 exit 1 } usage() { cat <<EOF Usage: bash $SCRIPT_NAME What it does: 1. Installs Termux dependencies needed for a glibc-based Node runtime. 2. Installs glibc-runner through pacman (no proot distro). 3. Downloads official Node.js ${NODE_VERSION} linux-arm64. 4. Wraps node/npm with ld.so so they run on Termux. 5. Installs ${CLAUDE_PACKAGE_NAME} and exposes it as: $HOST_CLAUDE_PATH Environment overrides: CLAUDE_CODE_HOME install state dir, default: $STATE_DIR CLAUDE_CODE_VERSION npm package version/tag, default: $CLAUDE_PACKAGE_VERSION CLAUDE_CODE_NODE_VERSION Node.js linux-arm64 version, default: $NODE_VERSION Notes: - This follows the non-proot glibc-wrapper approach used by openclaw-android. - Only aarch64 Termux is supported. - Existing $HOST_CLAUDE_PATH will be backed up if it is not already managed. EOF } command_exists() { command -v "$1" >/dev/null 2>&1 } require_termux() { [ -d "$PREFIX_DIR" ] || die "This script must run in Termux." command_exists pkg || die "pkg not found. This script must run in Termux." } ensure_tmp_root() { mkdir -p "$TMP_ROOT_DIR" } ensure_state_dirs() { mkdir -p "$STATE_DIR" "$WRAPPER_BIN_DIR" "$PATCH_DIR" "$GLOBAL_PREFIX_DIR" \ "$GLOBAL_BIN_DIR" "$NPM_CACHE_DIR" "$BACKUP_DIR" } ensure_termux_package() { local package_name="$1" if dpkg -s "$package_name" >/dev/null 2>&1; then success "Termux package already installed: $package_name" return 0 fi info "Installing Termux package: $package_name" pkg install -y "$package_name" success "Installed Termux package: $package_name" } ensure_glibc_runner() { local arch local pacman_conf local siglevel_patched=0 arch="$(uname -m)" [ "$arch" = "aarch64" ] || die "glibc mode only supports aarch64, got: $arch" if [ -f "$GLIBC_MARKER" ] && [ -x "$GLIBC_LDSO" ]; then success "glibc-runner already available" return 0 fi ensure_termux_package "pacman" pacman_conf="$PREFIX_DIR/etc/pacman.conf" info "Initializing pacman for glibc-runner" if [ -f "$pacman_conf" ] && ! grep -q '^SigLevel = Never' "$pacman_conf"; then cp "$pacman_conf" "${pacman_conf}.bak" sed -i 's/^SigLevel\s*=.*/SigLevel = Never/' "$pacman_conf" siglevel_patched=1 warn "Applied temporary pacman SigLevel workaround" fi pacman-key --init 2>/dev/null || true pacman-key --populate 2>/dev/null || true info "Installing glibc-runner" if ! pacman -Sy glibc-runner --noconfirm --assume-installed bash,patchelf,resolv-conf; then if [ "$siglevel_patched" -eq 1 ] && [ -f "${pacman_conf}.bak" ]; then mv "${pacman_conf}.bak" "$pacman_conf" fi die "Failed to install glibc-runner" fi if [ "$siglevel_patched" -eq 1 ] && [ -f "${pacman_conf}.bak" ]; then mv "${pacman_conf}.bak" "$pacman_conf" success "Restored pacman SigLevel" fi [ -x "$GLIBC_LDSO" ] || die "glibc dynamic linker not found at $GLIBC_LDSO" touch "$GLIBC_MARKER" success "glibc-runner is ready" } write_compat_patch() { info "Writing Node compatibility patch" cat >"$COMPAT_PATCH_PATH" <<'EOF' 'use strict'; const childProcess = require('child_process'); const dns = require('dns'); const fs = require('fs'); const os = require('os'); const path = require('path'); const prefix = process.env.PREFIX || '/data/data/com.termux/files/usr'; const home = process.env.HOME || '/data/data/com.termux/files/home'; const wrapperPath = process.env._CLAUDE_WRAPPER_PATH || path.join(home, '.claude-code-termux', 'bin', 'node'); const termuxExec = path.join(prefix, 'lib', 'libtermux-exec-ld-preload.so'); const termuxShell = path.join(prefix, 'bin', 'sh'); try { if (fs.existsSync(wrapperPath)) { Object.defineProperty(process, 'execPath', { value: wrapperPath, writable: true, configurable: true, }); } } catch {} if (process.env._CLAUDE_ORIG_LD_PRELOAD) { process.env.LD_PRELOAD = process.env._CLAUDE_ORIG_LD_PRELOAD; delete process.env._CLAUDE_ORIG_LD_PRELOAD; } else if (!process.env.LD_PRELOAD) { try { if (fs.existsSync(termuxExec)) { process.env.LD_PRELOAD = termuxExec; } } catch {} } const originalCpus = os.cpus; os.cpus = function cpus() { try { const result = originalCpus.call(os); if (Array.isArray(result) && result.length > 0) { return result; } } catch {} return [{ model: 'unknown', speed: 0, times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 }, }]; }; const originalNetworkInterfaces = os.networkInterfaces; os.networkInterfaces = function networkInterfaces() { try { return originalNetworkInterfaces.call(os); } catch { return { lo: [{ address: '127.0.0.1', netmask: '255.0.0.0', family: 'IPv4', mac: '00:00:00:00:00:00', internal: true, cidr: '127.0.0.1/8', }], }; } }; if (!fs.existsSync('/bin/sh') && fs.existsSync(termuxShell)) { const originalExec = childProcess.exec; const originalExecSync = childProcess.execSync; childProcess.exec = function exec(command, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options = options || {}; if (!options.shell) { options.shell = termuxShell; } return originalExec.call(childProcess, command, options, callback); }; childProcess.execSync = function execSync(command, options) { options = options || {}; if (!options.shell) { options.shell = termuxShell; } return originalExecSync.call(childProcess, command, options); }; } try { let dnsServers = ['8.8.8.8', '8.8.4.4']; try { const resolvConf = fs.readFileSync(path.join(prefix, 'etc', 'resolv.conf'), 'utf8'); const matches = resolvConf.match(/^nameserver\s+(.+)$/gm); if (matches && matches.length > 0) { dnsServers = matches.map((line) => line.replace(/^nameserver\s+/, '').trim()); } } catch {} try { dns.setServers(dnsServers); } catch {} const originalLookup = dns.lookup; const originalLookupPromise = dns.promises.lookup; dns.lookup = function lookup(hostname, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } const originalOptions = options; const opts = typeof options === 'number' ? { family: options } : (options || {}); const wantAll = opts.all === true; const family = opts.family || 0; const resolveWith = (fam, done) => { const resolver = fam === 6 ? dns.resolve6 : dns.resolve4; resolver(hostname, done); }; const tryResolve = (fam) => { resolveWith(fam, (error, addresses) => { if (!error && Array.isArray(addresses) && addresses.length > 0) { const resolvedFamily = fam === 6 ? 6 : 4; if (wantAll) { callback(null, addresses.map((address) => ({ address, family: resolvedFamily, }))); return; } callback(null, addresses[0], resolvedFamily); return; } if (family === 0 && fam === 4) { tryResolve(6); return; } originalLookup.call(dns, hostname, originalOptions, callback); }); }; tryResolve(family === 6 ? 6 : 4); }; dns.promises.lookup = async function lookup(hostname, options) { const opts = typeof options === 'number' ? { family: options } : (options || {}); const wantAll = opts.all === true; const family = opts.family || 0; const resolveWith = family === 6 ? dns.promises.resolve6 : dns.promises.resolve4; try { const addresses = await resolveWith(hostname); if (addresses.length > 0) { const resolvedFamily = family === 6 ? 6 : 4; if (wantAll) { return addresses.map((address) => ({ address, family: resolvedFamily, })); } return { address: addresses[0], family: resolvedFamily, }; } } catch {} if (family === 0) { try { const addresses = await dns.promises.resolve6(hostname); if (addresses.length > 0) { if (wantAll) { return addresses.map((address) => ({ address, family: 6 })); } return { address: addresses[0], family: 6, }; } } catch {} } return originalLookupPromise.call(dns.promises, hostname, options); }; } catch {} EOF success "Compatibility patch written to $COMPAT_PATCH_PATH" } write_node_wrappers() { local node_bin_path local node_real_path node_bin_path="$NODE_DIR/bin/node" node_real_path="$NODE_DIR/bin/node.real" if [ -f "$node_real_path" ]; then : elif [ -f "$node_bin_path" ]; then mv "$node_bin_path" "$node_real_path" else die "Node binary missing at $node_bin_path" fi info "Writing node/npm wrappers" cat >"$WRAPPER_BIN_DIR/node" <<EOF #!$PREFIX_DIR/bin/bash [ -n "\${LD_PRELOAD:-}" ] && export _CLAUDE_ORIG_LD_PRELOAD="\$LD_PRELOAD" unset LD_PRELOAD export _CLAUDE_WRAPPER_PATH="$WRAPPER_BIN_DIR/node" export TMPDIR="\${TMPDIR:-$TMP_ROOT_DIR}" _CLAUDE_COMPAT="$COMPAT_PATCH_PATH" if [ -f "\$_CLAUDE_COMPAT" ]; then case "\${NODE_OPTIONS:-}" in *"\$_CLAUDE_COMPAT"*) ;; *) export NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }-r \$_CLAUDE_COMPAT" ;; esac fi exec "$GLIBC_LDSO" --library-path "$PREFIX_DIR/glibc/lib" "$NODE_DIR/bin/node.real" "\$@" EOF cat >"$WRAPPER_BIN_DIR/npm" <<EOF #!$PREFIX_DIR/bin/bash export PATH="$WRAPPER_BIN_DIR:$NODE_DIR/bin:\$PATH" export TMPDIR="\${TMPDIR:-$TMP_ROOT_DIR}" export NPM_CONFIG_PREFIX="$GLOBAL_PREFIX_DIR" export npm_config_prefix="$GLOBAL_PREFIX_DIR" export NPM_CONFIG_CACHE="$NPM_CACHE_DIR" export npm_config_cache="$NPM_CACHE_DIR" export NPM_CONFIG_SCRIPT_SHELL="$PREFIX_DIR/bin/sh" export npm_config_script_shell="$PREFIX_DIR/bin/sh" exec "$WRAPPER_BIN_DIR/node" "$NODE_DIR/lib/node_modules/npm/bin/npm-cli.js" "\$@" EOF cat >"$WRAPPER_BIN_DIR/npx" <<EOF #!$PREFIX_DIR/bin/bash export PATH="$WRAPPER_BIN_DIR:$NODE_DIR/bin:\$PATH" export TMPDIR="\${TMPDIR:-$TMP_ROOT_DIR}" export NPM_CONFIG_PREFIX="$GLOBAL_PREFIX_DIR" export npm_config_prefix="$GLOBAL_PREFIX_DIR" export NPM_CONFIG_CACHE="$NPM_CACHE_DIR" export npm_config_cache="$NPM_CACHE_DIR" export NPM_CONFIG_SCRIPT_SHELL="$PREFIX_DIR/bin/sh" export npm_config_script_shell="$PREFIX_DIR/bin/sh" exec "$WRAPPER_BIN_DIR/node" "$NODE_DIR/lib/node_modules/npm/bin/npx-cli.js" "\$@" EOF chmod 755 "$WRAPPER_BIN_DIR/node" "$WRAPPER_BIN_DIR/npm" "$WRAPPER_BIN_DIR/npx" success "node/npm wrappers are ready" } install_node_runtime() { local installed_version local tmp_dir local extract_dir local fresh_dir ensure_termux_package "curl" ensure_termux_package "xz-utils" if [ -x "$WRAPPER_BIN_DIR/node" ]; then installed_version="$("$WRAPPER_BIN_DIR/node" --version 2>/dev/null | sed 's/^v//')" if [ "$installed_version" = "$NODE_VERSION" ]; then success "Node.js already installed: v$installed_version" write_compat_patch write_node_wrappers return 0 fi fi info "Downloading official Node.js ${NODE_VERSION} linux-arm64" tmp_dir="$(mktemp -d "$TMP_ROOT_DIR/claude-node.XXXXXX")" curl -fL --max-time 300 "$NODE_URL" -o "$tmp_dir/$NODE_TARBALL" success "Downloaded $NODE_TARBALL" extract_dir="$tmp_dir/extract" fresh_dir="$tmp_dir/node-fresh" mkdir -p "$extract_dir" "$fresh_dir" tar -xJf "$tmp_dir/$NODE_TARBALL" -C "$extract_dir" mv "$extract_dir"/node-v"${NODE_VERSION}"-linux-arm64/* "$fresh_dir"/ rm -rf "$NODE_DIR" mkdir -p "$(dirname "$NODE_DIR")" mv "$fresh_dir" "$NODE_DIR" write_compat_patch write_node_wrappers rm -rf "$tmp_dir" success "Node.js runtime installed in $NODE_DIR" } install_claude_package() { local package_spec package_spec="$CLAUDE_PACKAGE_NAME" if [ "$CLAUDE_PACKAGE_VERSION" != "latest" ]; then package_spec="${CLAUDE_PACKAGE_NAME}@${CLAUDE_PACKAGE_VERSION}" fi info "Installing $package_spec" PATH="$WRAPPER_BIN_DIR:$GLOBAL_BIN_DIR:$PATH" "$WRAPPER_BIN_DIR/npm" install -g "$package_spec" [ -e "$GLOBAL_BIN_DIR/claude" ] || die "npm install completed, but $GLOBAL_BIN_DIR/claude was not created" [ -x "$CLAUDE_EXE_PATH" ] || die "Claude native binary missing at $CLAUDE_EXE_PATH" success "Claude Code is installed under $GLOBAL_PREFIX_DIR" } backup_existing_launcher() { local backup_path if [ ! -e "$HOST_CLAUDE_PATH" ]; then return 0 fi if grep -Fq "$HOST_WRAPPER_MARKER" "$HOST_CLAUDE_PATH" 2>/dev/null; then success "Managed host launcher already present" return 0 fi backup_path="$BACKUP_DIR/claude.host-backup.$(date +%Y%m%d_%H%M%S)" cp "$HOST_CLAUDE_PATH" "$backup_path" success "Backed up existing launcher to $backup_path" } install_host_wrapper() { local tmp_wrapper tmp_wrapper="$(mktemp "$TMP_ROOT_DIR/claude-wrapper.XXXXXX")" cat >"$tmp_wrapper" <<EOF #!$PREFIX_DIR/bin/bash $HOST_WRAPPER_MARKER export PATH="$WRAPPER_BIN_DIR:$GLOBAL_BIN_DIR:\$PATH" export TMPDIR="\${TMPDIR:-$TMP_ROOT_DIR}" exec "$GLIBC_RUNNER_BIN" -t "$CLAUDE_EXE_PATH" "\$@" EOF chmod 755 "$tmp_wrapper" cp "$tmp_wrapper" "$HOST_CLAUDE_PATH" chmod 755 "$HOST_CLAUDE_PATH" rm -f "$tmp_wrapper" success "Installed host launcher: $HOST_CLAUDE_PATH" } verify_install() { info "Verifying Node wrapper" "$WRAPPER_BIN_DIR/node" --version info "Verifying npm wrapper" "$WRAPPER_BIN_DIR/npm" --version info "Verifying Claude Code launcher" "$HOST_CLAUDE_PATH" --version success "Non-proot Claude Code setup completed" } main() { if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then usage exit 0 fi require_termux ensure_tmp_root ensure_state_dirs ensure_glibc_runner install_node_runtime install_claude_package backup_existing_launcher install_host_wrapper verify_install cat <<EOF Run Claude Code with: claude Current configuration: state dir: $STATE_DIR node version: $NODE_VERSION package version: $CLAUDE_PACKAGE_VERSION launcher: $HOST_CLAUDE_PATH EOF } main "$@" 2.proot方案 (会卡顿 卡就对了 卡了就等待) #!/data/data/com.termux/files/usr/bin/bash set -euo pipefail readonly SCRIPT_NAME="$(basename "$0")" readonly DISTRO_NAME="${CLAUDE_CODE_DISTRO:-debian}" readonly CLAUDE_PACKAGE_NAME="@anthropic-ai/claude-code" readonly CLAUDE_PACKAGE_VERSION="${CLAUDE_CODE_VERSION:-latest}" readonly PREFIX_DIR="${PREFIX:-/data/data/com.termux/files/usr}" readonly HOST_CLAUDE_PATH="$PREFIX_DIR/bin/claude" readonly PROOT_ROOT_DIR="$PREFIX_DIR/var/lib/proot-distro/installed-rootfs" readonly BACKUP_DIR="$HOME/.codex/tmp" readonly WRAPPER_MARKER="# claude-code-termux-wrapper" readonly C_BOLD_BLUE="\033[1;34m" readonly C_BOLD_GREEN="\033[1;32m" readonly C_BOLD_YELLOW="\033[1;33m" readonly C_BOLD_RED="\033[1;31m" readonly C_RESET="\033[0m" info() { printf '%b[INFO]%b %s\n' "$C_BOLD_BLUE" "$C_RESET" "$*" } success() { printf '%b[ OK ]%b %s\n' "$C_BOLD_GREEN" "$C_RESET" "$*" } warn() { printf '%b[WARN]%b %s\n' "$C_BOLD_YELLOW" "$C_RESET" "$*" >&2 } die() { printf '%b[ERR ]%b %s\n' "$C_BOLD_RED" "$C_RESET" "$*" >&2 exit 1 } usage() { cat <<EOF Usage: bash $SCRIPT_NAME What it does: 1. Installs proot-distro in Termux if needed. 2. Installs Debian userspace if needed. 3. Installs nodejs + npm inside Debian. 4. Installs ${CLAUDE_PACKAGE_NAME} inside Debian. 5. Replaces Termux's claude launcher with a wrapper that forwards into Debian. Environment overrides: CLAUDE_CODE_DISTRO proot distro alias, default: ${DISTRO_NAME} CLAUDE_CODE_VERSION npm package version/tag, default: ${CLAUDE_PACKAGE_VERSION} Notes: - Official Claude Code npm binaries do not support Termux's android-arm64 host. - This script uses Debian in proot as the supported Linux runtime. EOF } command_exists() { command -v "$1" >/dev/null 2>&1 } require_termux() { [ -d "$PREFIX_DIR" ] || die "This script must run in Termux." command_exists pkg || die "pkg not found. This script must run in Termux." } ensure_termux_package() { local package_name="$1" if dpkg -s "$package_name" >/dev/null 2>&1; then success "Termux package already installed: $package_name" return 0 fi info "Installing Termux package: $package_name" pkg install -y "$package_name" success "Installed Termux package: $package_name" } ensure_distro() { if [ -d "$PROOT_ROOT_DIR/$DISTRO_NAME" ]; then success "proot distro already installed: $DISTRO_NAME" return 0 fi info "Installing proot distro: $DISTRO_NAME" proot-distro install "$DISTRO_NAME" success "Installed proot distro: $DISTRO_NAME" } run_in_distro() { local command_text="$1" proot-distro login "$DISTRO_NAME" -- bash -lc "$command_text" } ensure_distro_packages() { info "Updating apt metadata inside $DISTRO_NAME" run_in_distro "env DEBIAN_FRONTEND=noninteractive apt-get update" info "Installing nodejs and npm inside $DISTRO_NAME" run_in_distro "env DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs npm" success "nodejs and npm are ready inside $DISTRO_NAME" } install_claude_in_distro() { local package_spec="$CLAUDE_PACKAGE_NAME" if [ "$CLAUDE_PACKAGE_VERSION" != "latest" ]; then package_spec="${CLAUDE_PACKAGE_NAME}@${CLAUDE_PACKAGE_VERSION}" fi info "Installing ${package_spec} inside $DISTRO_NAME" run_in_distro "npm install -g ${package_spec@Q}" success "Claude Code is installed inside $DISTRO_NAME" } backup_existing_launcher() { local backup_path mkdir -p "$BACKUP_DIR" if [ ! -e "$HOST_CLAUDE_PATH" ]; then return 0 fi if grep -Fq "$WRAPPER_MARKER" "$HOST_CLAUDE_PATH" 2>/dev/null; then success "Managed Termux launcher already present" return 0 fi backup_path="$BACKUP_DIR/claude.host-backup.$(date +%Y%m%d_%H%M%S)" cp "$HOST_CLAUDE_PATH" "$backup_path" success "Backed up existing launcher to $backup_path" } install_host_wrapper() { local tmp_wrapper tmp_wrapper="$(mktemp "${TMPDIR:-/tmp}/claude-wrapper.XXXXXX")" cat >"$tmp_wrapper" <<EOF #!/data/data/com.termux/files/usr/bin/sh $WRAPPER_MARKER work_dir=\$PWD if [ ! -d "\$work_dir" ]; then work_dir=/root fi exec proot-distro login --shared-tmp --work-dir "\$work_dir" $DISTRO_NAME -- /usr/local/bin/claude "\$@" EOF chmod 755 "$tmp_wrapper" cp "$tmp_wrapper" "$HOST_CLAUDE_PATH" chmod 755 "$HOST_CLAUDE_PATH" rm -f "$tmp_wrapper" success "Installed Termux launcher: $HOST_CLAUDE_PATH" } verify_install() { info "Verifying Claude inside $DISTRO_NAME" run_in_distro "claude --version" info "Verifying Termux launcher" "$HOST_CLAUDE_PATH" --version success "Claude Code setup completed" } main() { if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then usage exit 0 fi require_termux ensure_termux_package "proot-distro" ensure_distro ensure_distro_packages install_claude_in_distro backup_existing_launcher install_host_wrapper verify_install cat <<EOF Run Claude Code with: claude Current configuration: distro: $DISTRO_NAME host launcher: $HOST_CLAUDE_PATH EOF } main "$@" 非proot方案参考 GitHub - AidanPark/openclaw-android: Run OpenClaw on Android with a single command — no proot, no Linux · GitHub 1 个帖子 - 1 位参与者 阅读完整话题

www.ithome.com · 2026-04-17 18:42:29+08:00 · tech

IT之家 4 月 17 日消息,AI 编程如今无处不在,任何人都可以在掌握提示词(prompt)的能力下成为 Vibe Coding(氛围编程)开发者。如今 ChatGPT、Claude、Gemini 等工具甚至能直接将一个点子转化为完整应用,并发布到商店。 据科技媒体 PhoneArena 今天报道,AI 智能体确实可以高效开发移动 App,但模型从训练到实际使用往往存在时间差,导致 AI 往往无法掌握 Android 系统的最新变化。这就导致 AI 开发出的应用可能存在 Bug、安全隐患,不符合最新规范。 为解决上述问题,谷歌昨天向 AI 智能体开放最新 Android 开发指南访问权限,同时应用一系列新工具, 帮助 AI 掌握如何构建高质量 Android 应用 。 IT之家附上谷歌官方发言如下: 现在 AI 智能体已经可以通过持续更新的知识库,在 Android 开发文档、Firebase、Google Developers 以及 Kotlin 文档中寻找最新开发规范。即使某些大语言模型的训练数据已经过时一年,经过训练后仍能开发符合当前框架的应用。

www.ithome.com · 2026-04-17 09:05:04+08:00 · tech

IT之家 4 月 17 日消息,科技媒体 Android Authority 昨日(4 月 16 日)发布博文,报道称在安卓 17 Beta 4 更新中, 谷歌完善了彩蛋体验,用户需要连接星星解锁安卓 17 的 LOGO。 安卓 17 开发周期目前已进入尾声,Beta 3 已达到平台稳定性,开发者接口和系统行为基本定型;而 Beta 4 是最后一个计划内 Beta 版本,在功能层面没有太多新内容,对于用户而言主要完善彩蛋体验。 每年安卓新版本都会隐藏一个彩蛋,这已成为 Google 的传统。从早期版本的食物主题,到近年来的太空元素,彩蛋既是开发团队的趣味表达,也是用户探索系统的额外乐趣。 这个彩蛋的访问方式与往年相同:进入设置 > 关于手机,找到安卓版本选项,然后连续点击版本号。进入后,屏幕会出现一片星空,用户需要连接星星才能让安卓 17 的 LOGO 显现。 最简单的方法是画一个大圆圈,但用户也可以尝试任意连接方式,没有固定路径限制。IT之家附上相关视频如下: 安卓 17 的 LOGO 设计在过去几个 Beta 版本中逐步演变,谷歌一点点增加漩涡元素,让原本的行星风格设计变得更加动感。 现在这个设计终于定型,呈现出完整的漩涡造型。长按 LOGO 后,用户还能进入安卓 16 同款的太空小游戏,目前尚不确定是否有新增玩法。

linux.do · 2026-04-16 23:07:58+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 之前的软件介绍: 【开源项目】无需root,无需连电脑,随时随地,让AI到点自动操控你的手机完成任务,后台不杀,到点就干 开发调优 本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 昨天写了一… 之前的技术文章: 【开源】LXB-Framework:支持定时任务的端侧安卓自动化框架,用预置地图做页面路由,让大模型专注任务执行 开发调优 本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 项目地址: h… 项目链接: github.com GitHub - wuwei-crg/AutoLXB: 基于常驻守护进程的 Android 自动化框架,支持多任务定时执行。| Android... 基于常驻守护进程的 Android 自动化框架,支持多任务定时执行。| Android automation framework with persistent daemon and scheduled tasks. 一些演示: bilibili.com 【开源】我做了一个开源软件,让手机自己点咖啡,顺便帮我自动回下微信群_哔哩哔哩_bilibili 项目链接:https://github.com/wuwei-crg/LXB-Framework欢迎star!欢迎提issues!现在功能基本稳定了,后续估计维护放缓, 视频播放量 198、弹幕量 0、点赞数 13、投硬币枚数 8、收藏人数 4、转发人数 1, 视频作者 Wu_Weiii, 作者简介... 前言 上次发帖后收到不少反馈,于是这段时间进行了不少更新。为了好记点,项目改名叫 AutoLXB 了 剧透 。 这段时间根据大家提的意见,我把之前一些“不太好用”的地方做了改进。这次更新不整虚的,主要解决了执行慢、输入难和启动烦的问题。 聊聊这次的改动: 1. 路径记录与回放(重点) 之前版本全靠 AI 实时去“看”屏幕再做决策,说实话,挺慢的,而且很烧token,效率上完全不能称得上自动化工具。而且之前一直是在尝试为app构建应用级的路由图,但是实际上维护起来会非常困难,而且建图流程也需要处理各种异常状态。现在将 构建app级路由图的思路迁移到了构建任务级路由图 ,个人认为是比较好的解决方案。 新逻辑 :现在任务跑完一遍后,会记录上一次的操作路径( 记录方式是技术文章里说到的VLM-XML Fusion来实现控件定位,另外做了几种fallback ),你可以进入编辑器,把那些多余的、走错的操作删掉,留下一条 固定路径 。有点类似AutoJS?不过也不完全是。 收益 :下次跑任务,它会直接走这条“熟路”,不用每一步都调 VLM。只有到了路尽头或者需要动态决策的地方,AI 才介入。这应该是目前兼顾灵活性和确定性最务实的方案了。 例如,这是一个“帮我到bilibili发送一条动态,内容为test,标题为test”的任务路线,跑了一下记录路径后,现在可以按照路径回放来完成发动态任务了,全程不用调用VLM。 上面写的是点击坐标,实际上定位并 不是靠坐标定位 的,而是靠 控件属性 定位的 2. 通知触发任务 之前只能定时跑,现在支持 监听通知 了。 你可以设个规则,比如匹配特定的包名或标题关键词。 应用在后台会轮询通知栏,一旦对上,立马启动任务。比如收到微信(这是包名)的某个人(这是标题)的消息,帮忙回复。 3. 解决中文输入难题(支持 ADB Keyboard) 微信这类 App 屏蔽了系统剪切板,导致 AI 没法直接输入中文。 解决方案 :适配了 ADB Keyboard。安装ADB Keyboard 后,任务需要输入时,它会自动切到 ADB 键盘完成录入,完事再切回来。实测微信输入现在没问题了。 4. Root 手机不用再配对了 如果你手机有 Root 权限,现在可以直接启动后台核心,不用再去点那个麻烦的无线调试配对了。 5. 界面大改 之前的 UI 被吐槽有点丑,这次稍微优化了一下布局。 软件展示 首页引导 定时任务管理 通知触发任务管理 总结: 目前这个版本我认为在“省钱”和“稳定”上做了比较好的平衡。路径回放机制其实就是为了把那些重复的死板操作固定下来。 大家可以下载试试,如果有 Bug 或者想加什么功能,欢迎到github提issues,我看到都会回。觉得好用的佬友可以点点star!!! 1 个帖子 - 1 位参与者 阅读完整话题

hnrss.org · 2026-04-16 21:04:11+08:00 · tech

Hey HN! I'm a solo dev and I just wanted to share my latest Android game — Rollquation. The idea was to make math genuinely fun by combining it with navigation. You roll a sphere through maze-like levels, collecting numbered and operator cubes that must add up to exact values to unlock doors and reach the exit. I also wanted the game to feel like a nice place to be, so I built it around a nature scene with trees, mountains and a waterfall. That came with a cost though, performance on older/weaker phones was an issue during my first release. I ended up building an adaptive performance system that auto-selects a quality setting (low/medium/high/ultra) based on the device's RAM. It helped a lot, though I'm still fine-tuning it. The other big lesson was around controls. I originally used physics for the rolling sphere, which meant the ball would decelerate before changing direction which players found unresponsive and slow. Removing the physics and making movement instant transformed how the game felt. Unity UI is not my strength and players have mentioned that it could use some work. The UI is usable but I may end up changing it. Gameplay video: https://youtu.be/SGbQa3-ORhI Play Store: https://play.google.com/store/apps/details?id=com.JabGames.P... Any feedback or thoughts on the game is appreciated. Comments URL: https://news.ycombinator.com/item?id=47792388 Points: 2 # Comments: 0

hnrss.org · 2026-04-16 19:07:51+08:00 · tech

I built HAINDY, a CLI that gives coding agents computer use across desktop, Android, and iOS. You install it as a normal CLI tool and can opt to install skills for it on Claude Code, Codex and OpenCode. It gives agents human-like control of devices. When the agent instructs it to click a button there's a screenshot + coordinate based computer use loop. No DOM or accessibility is used. I built it because I wanted agents to test things the way a human would, watching the screen and using it from a user perspective. Would love feedback on onboarding, real use cases, and whether this fits naturally into existing agent workflows. Comments URL: https://news.ycombinator.com/item?id=47791360 Points: 1 # Comments: 0

www.ithome.com · 2026-04-16 14:52:37+08:00 · tech

IT之家 4 月 16 日消息,科技媒体 Android Authority 今天(4 月 16 日)发布博文,报道称 Nothing 公司跨设备文件传输工具 Warp 上线仅数小时后, 突然从 Play Store 下架应用,同时撤回 Chrome 扩展和官方公告博客。 IT之家援引博文介绍,Warp 定位为跨平台文件传输工具,连接安卓手机与 macOS、Windows、Linux 桌面,用户可在登录同一谷歌账户的设备间传输文件、链接、图片和剪贴板文本。 在技术实现方面,Warp 依赖 Google Drive 作为临时中转桥梁,本质上自动化了用户可手动完成的操作。 不过在 Warp 上线数小时后便被紧急撤下,Play Store 应用、Chrome 扩展以及官方公告博客文章全部消失,公司未给出任何解释。 Android Authority 在发布当天完成测试,Warp 传输稳定且速度较快,但应用需要大量浏览器权限,这一点引发隐私顾虑。 Nothing 尚未回应撤回原因。考虑到下架速度之快,很可能发布过程中出现问题。可能性包括:临时修复 bug、应对隐私质疑,或重新评估产品策略。若涉及隐私问题,大量浏览器权限要求或是关键因素。

www.ithome.com · 2026-04-16 12:04:39+08:00 · tech

IT之家 4 月 16 日消息,科技媒体 Android Authority 今天(4 月 16 日)发布博文,通过 挖掘 Android Canary 2604 版本代码 , 发现安卓系统有望原生支持双卡差异化铃声。 IT之家援引博文介绍,谷歌持续优化双卡安卓体验,在 2025 年上线信号强度分栏显示后,谷歌有望原生支持为不同 SIM 卡设置不同铃声。 包括摩托罗拉等厂商在内,已经在自家系统中提供 SIM 卡独立铃声设置,但安卓原生系统始终缺失这一功能, 导致原生系统用户使用双卡时,无法通过铃声区分线路,只能依赖屏幕显示判断。 在 Android Canary 2604 版本中,该媒体发现了“unique ringtone for a specific SIM card”(为特定 SIM 卡设置专属铃声)和“Ringtone”(铃声)标题字符串,明确提及可以独立设置 SIM 卡铃声。 该功能预计同时覆盖 eSIM 和物理 SIM 卡,适配当前主流的双卡方案。目前尚不清楚该原生功能落地时间,但预估最快会在安卓 17 系统中实现。

www.solidot.org · 2026-04-15 21:43:09+08:00 · tech

RKS Global 的专家发现,30 款俄罗斯最流行 Android 应用​有 22 款能检测 VPN,其中 19 款会将 VPN 状态发送至服务器。这些应用包括:Yandex Browser、Yandex Maps、VKontakte、My MTS、Sberbank Online、T-Bank、VK Video、Wildberries、 Kinopoisk、Ozon、Samokat、RuStore、VTB Online、Yandex Music、Avito、Alfa-Bank、2GIS、MegaMarket、Odnoklassniki、MAX、Rutube 和 VK Music。俄罗斯数字发展部已经要求大型企业从 4 月 15 日起限制那些在设备上启用了 VPN 的用户的访问。调查发现,Avito 应用会检测设备上是否安装了逾 200 种外国应用,其中包括银行、加密货币钱包和 IM 等。

www.ithome.com · 2026-04-15 14:16:38+08:00 · tech

IT之家 4 月 15 日消息,科技媒体 Android Authority 今天(4 月 15 日)发布博文,报道称谷歌面向 Pixel 8 系列及以上设备, 推送 Android Canary 2604 实验性版本更新,编号为 ZP11.260320.007。 谷歌明确表示 Android Canary 并不稳定,强烈建议用户不要在主力设备上安装该版本,而且为了加速新特性验证流程,本月发布时间还较上月(2026 年 3 月)提前。 安装 Canary 版本后将无法轻易回退至稳定版。如需停止接收测试版更新,必须刷入非 Canary 版本镜像,且该操作会导致数据全部清除。谷歌提醒开发者提前备份重要数据,谨慎评估测试风险。 此次更新聚焦优化界面交互,通知面板调整文本描述,在清空所有通知后会显示“无通知”文本,而新版提升状态反馈的直观性, 调整为“已全部看完”。 IT之家附上相关截图如下: 新版为提升界面整洁度,重新设计应用图标长按菜单,采用更紧凑的布局风格。 新版本深度调整快捷操作菜单的逻辑。快捷方式默认折叠于“快捷方式”开关后,用户需点击展开查看。展开后的操作菜单同样默认折叠,需通过“操作”开关访问。 除 Pixel 8 系列外,本次更新暂未覆盖旧款机型。谷歌表示 Pixel 7a、Pixel 7 Pro、Pixel 6a、Pixel Fold 及 Pixel Tablet 等设备的系统镜像将在稍后时间发布。开发者可根据测试需求,选择对应设备进行适配验证。

hnrss.org · 2026-04-15 01:29:34+08:00 · tech

As a developer who has an android phone it is very exhausting making apps and widgets even for simple tasks such as fetching and displaying data from the internet. Appy allows you to develop your own homescreen widgets with only python, with: - Simple and intuitive UI library - Easy to use logging and debug tools - Full access to java and android libraries - Extensive wiki even Claude can understand Feedback is welcome! Comments URL: https://news.ycombinator.com/item?id=47768556 Points: 2 # Comments: 0

www.ithome.com · 2026-04-14 20:09:33+08:00 · tech

微信 8.0.71 for Android 内测来了!就在今天(4 月 14 日)上午, 微信发布了安卓平台 8.0.71 测试版更新。 IT之家抓取安装包获悉,本次文件的大小为 243.3 MB。 说来也是少见,这次 8.0.71 版本的测试节奏, 竟然是安卓先于 iOS 启动 。 而经实测发现,本次安卓最新版本的变化力度,也要比前一个版本大得多。 具体的更新情况,小编接下来逐一向各位展示。 一、“添加好友”界面增加新选项 在安卓 8.0.71 测试版中,微信于“添加好友”界面中增加了一个新选项 —— 服务号。 依次点击“微信 - + - 添加朋友”就能看到该选项,标注有“获取更多购物信息和服务”,选择后可搜索服务号。 但需要注意的是,目前这处“服务号”有个小问题, 显示的图标为旧版样式而非新版 。 在今年 2 月,微信变更了“服务号”的图标,由红色“购物袋 + 小票”变为浅蓝色“双菱形”。 二、“听全文”悬浮窗优化 更新至新版本后,微信对“听全文”的悬浮窗进行了优化。 在之前的版本中,于任一公众号文章选择听全文,悬浮窗的样式为上方显示封面缩略图,下方有播放和关闭两个选项。 反观在新版本,悬浮窗的样式就变得很简约, 只有一个“音符”图标 。 点击音符后,才会展开显示文章标题的前几个字,并露出播放和关闭选项。 三、“听全文”播放器界面改版 同样是与“听全文”相关的改动,其音频播放器界面迎来改版。 原来界面的中间位置,公众号名称和进度条之间没有任何内容。 但是现在的新版本中, 微信塞入了字幕 ,会根据语音播报的进度自动滚动。 点击字幕区域,会开启字幕,以大字的显示形式呈现,可手动滑动查看,实则就是文章原文。 四、公众号个人中心有新选项 这处上新位于“公众号 - 个人中心”里,出现了“稍后听和播放历史”选项。 点击该选项, 界面分为“稍后听”和“播放历史”两个类别 。 前者会显示添加来的公众号内容,后者则会记录已经听过的公众号内容。 五、“发贴图”图标焕新 同样是在“公众号 - 个人中心”的界面中,下方“我的公众号”区域内的“发贴图”图标焕新。 此前的图片是一张照片的样式, 现今变更为双菱形 。 经查询发现,目前 iOS 端在同一位置的“发贴图”图标仍然是照片,以及公众号后台的“贴图”也还是照片,唯有安卓端的这个地方变样。 有一说一,此处发贴图的图标,倒是很神似服务号变更后的新样式。 六、公众号主页改版 安卓端公众号主页在 8.0.71 测试版中实现改版,具体体现在两个方面。 一个是主页中上方的分类,由四个变为五个,在原有的全部、贴图、视频号、服务的基础上,增加了“文章”。 在文章类别下, 仅显示公众号“文章”形式的推文 ,并且头条封面会完整显示,效果和信息流界面一致。 作为对比,iOS 端虽然也只显示文章,但所有封面都是缩略图,并且没有根据发布日期做明显的分隔。 至于另一个方面,则是和“贴图”有关, 贴图在公众号主页的“全部”类别下也有了独立的地盘 ,且每篇贴图以转发卡片的样式展示。 反观先前的版本中,贴图是和文章混在一起,并且显示样式为消息列表。 七、公众号主页滑动显示头像 需要注意的是,在改版后的公众号主页中,安卓端还有一处细节变化。 于公众号主页中向下滑动屏幕, 上方中间的公众号名称旁新增头像的显示 。 而在安卓的旧版本和 iOS 的现有版本中,都只显示公众号名称,并未有头像。 八、总结 关于微信安卓 8.0.71 测试版的更新情况,IT之家小编挖掘到的如上所述,预估半个月后会转为正式版。 至于 iOS 的 8.0.71 版本,可能会在近日开启内测,也可能会在稍晚直接上架正式版。 还是那句话,鉴于 微信新功能是通过灰度放量的形式上线 ,若发现自己在更新后没有新特性,唯有再耐心等待,当然了,即便不更新,也有概率获得新变化。 总之,甭管是安卓,还是 iOS,还有鸿蒙,都希望微信能带来更多实用功能。 大家在 IT之家微信号 回复“ 微信 ”两字,即可获取当前最新官方内部版微信下载。

linux.do · 2026-04-14 10:25:27+08:00 · tech

Teknium 发布开源 AI Agent 框架 Hermes Agent v0.9.0,包含 487 个 commit 和 269 个合并 PR。这是一次以「平台扩展」为主题的大版本更新,此前 Hermes 只能通过终端和配置文件管理,此版本首次提供浏览器端 Web Dashboard,并首次支持在 Android 手机上通过 Termux 原生运行。 Web Dashboard 让用户可以在浏览器中配置设置、监控会话、浏览 Skill、管理网关,不再需要编辑配置文件或操作终端。Termux 适配则针对移动端做了安装路径、TUI 界面、语音后端和 /image 命令的优化。 通讯平台方面新增三个:通过 BlueBubbles 接入 iMessage,通过 iLink Bot API 接入微信,以及新增企业微信回调模式适配器(v0.6.0 已有企业微信基础支持,此次新增的是面向自建应用的回调模式)。加上此前已支持的 Telegram、Discord、Slack、WhatsApp、Signal、Matrix、飞书、钉钉等,Hermes 现已覆盖 16 个通讯平台。 其他主要新功能: Fast Mode( /fast 命令),接入 OpenAI Priority Processing 和 Anthropic 快速通道,降低推理延迟 后台进程输出模式匹配( watch_patterns ),可设置关键词实时监控后台进程输出并触发通知,区别于 v0.8.0 的任务完成通知 可插拔上下文引擎,通过插件系统自定义 Agent 每轮看到的上下文内容,支持过滤、摘要或领域特定注入 xAI(Grok)和小米 MiMo 升级为一级提供商,此前 v0.8.0 中 xAI 仅支持 prompt caching、MiMo 仅作为 Nous Portal 免费模型提供 统一代理支持,SOCKS 代理、平台专用代理和系统代理自动检测,企业防火墙环境下可直接使用 hermes backup 和 hermes import 命令,支持完整备份和恢复配置、会话、Skill 和记忆 GitHub Release Hermes Agent v0.9.0 (v2026.4.13) · NousResearch/hermes-agent Hermes Agent v0.9.0 (v2026.4.13) Release Date: April 13, 2026 Since v0.8.0: 487 commits · 269 merged PRs · 167 resolved issues · 493 files changed · 63,281 insertions · 24 contributors The everywh... 1 个帖子 - 1 位参与者 阅读完整话题

hnrss.org · 2026-04-12 19:48:16+08:00 · tech

Hi HN, We built Sova AI https://ayconic.io/sova , an Android assistant agent that actually controls and operates your apps. It's not a chat and not another LLM wrapper. We were incredibly frustrated with the current state of mobile AI. Built-in assistants like Gemini are deeply integrated into the OS, yet if you ask them to "Order an Uber to the airport", they mostly just give you web search results or a button to open the app yourself. They don't do the work. (The Perplexity "assistant" is just a browser agent :/ ) So, we built an agent that does operate your phone. (NO root, NO adb, NO PC, NO appium/whatever, NO usb, NO browser) How it works: You give Sova a prompt - either voice or text, you can make it a default assistant if you like. Instead of relying on non-existent official app APIs, Sova acts as a virtual human - clicks, scrolls, types etc. It uses the Android Accessibility API to read the screen's UI node tree. About AI models - currently we support main AI cloud providers (OpenAI, Gemini, Anthropic, Deepseek etc etc) and working towards support of local AI models on your host - Ollama, LM studio, etc. Pricing: 100% Free / Bring Your Own Key (BYOK) We aren't charging for the Sova engine right now. We built a BYOK system: you plug in your own API key (OpenAI, Claude, whatever you prefer), and you only pay the provider for the tokens you use. We figured out how to do this entirely on-device as a standard Kotlin app. No tethering to a PC, no Appium, no Root, and no Shizuku/ADB workarounds. Just an app even your granny can use. The Google Play Ban: Because we use the Accessibility API for "universal automation" (literally mapping and clicking other apps), Google Play rejected our submission. It’s ironic: they banned us for building the exact agentic behavior that Gemini promises but fails to deliver. So, we are hosting the APK ourselves: https://sova.ayconic.io We’d love for you to download the APK, plug in your key, and try to break it. What apps completely confuse the agent? Roadmap: support of local models with Ollama, LM studio or another tools, predefined rules and personas for your tasks, detailed statistics for you, support for Openrouter, enterprise Amazon Bedrock, Google Vertex and Azure Foundry models, support for IOS. What would you like to see more? We'd be happy to hear your feedback, success and failure stories. Video demo is here https://www.youtube.com/watch?v=r-x6hRmtBy0 and APK is here: https://ayconic.io/sova We are here to answer your questions and listen to feedback in Telegram and Discord. It's not perfect yet, but it does its work. Comments URL: https://news.ycombinator.com/item?id=47738583 Points: 2 # Comments: 1