在 Termux 中构建本地 AI Web 应用
在运行 Termux 的 Android 设备上构建并运行本地 Web 应用程序,集成 AI API(尤其是 NaraRouter),并可选地使用 Hermes Agent 作为推理后端。
何时使用
- 用户希望在手机上运行一个本地 AI 助手/聊天界面
- 在 Termux 中用 Python/Flask 构建 Web 应用
- 通过描述驱动路由集成多个 AI API
- 通过浏览器 Web Speech API 添加语音模式
- 跨聊天会话管理会话和记忆
- 用户粘贴了开发工具(二进制发布版、Rust/cargo、npm/npx、pip)的安装说明,并指示安装和使用——首先评估在手机/Termux 环境中的可行性,然后在获得许可后安装
架构模式
浏览器 (Android 上的 Chrome) → Flask 后端 (Termux) → AI API
↓
Hermes Agent (可选,作为大脑)
关键原则: Web 应用是接口层。Hermes Agent(如果使用)是大脑。Flask 后端桥接它们。
快速开始
# 在 Termux 中
mkdir ~/sayip && cd ~/sayip
mkdir -p templates static/css static/js storage/sessions
# app.py — 最小 Flask 启动器
from flask import Flask, render_template, request, jsonify
from pathlib import Path
import json, time, uuid, os
app = Flask(__name__, template_folder="templates")
HERMES_HOME = Path(os.path.expanduser("~/.hermes"))
SAYIP_STORAGE = Path(os.path.expanduser("~/sayip/storage"))
@app.route("/")
def index():
return render_template("chat.html")
@app.route("/api/chat", methods=["POST"])
def chat():
data = request.get_json()
# ...处理消息,调用API,返回响应
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
# 运行
python3 app.py
# 在 Chrome 中打开: http://127.0.0.1:5000
API 集成模式
NaraRouter (主要推荐)
NaraRouter 是一个统一的 AI 网关,提供 53+ 个模型,提供免费套餐。
密钥格式: sk-nry-... (以 sk-nry- 开头)
端点 (均在同一个基础 URL 下,使用 Bearer 密钥):
| 端点 | 类型 | 用途 |
|---|---|---|
https://router.bynara.id/v1/chat/completions | OpenAI 兼容 | 聊天,通用任务 |
https://router.bynara.id/v1/messages | Anthropic 兼容 | Anthropic 格式请求 |
https://router.bynara.id/v1/responses | OpenAI Responses | 有状态响应 API |
https://router.bynara.id/v1/embeddings | OpenAI Embeddings | 向量嵌入 |
https://router.bynara.id/v1/models | LIST | 获取您套餐中可用的模型 |
https://api-images.bynara.id/v1/images/generations | 图像生成 | 文生图 |
https://api-images.bynara.id/v1/images/edits | 图像编辑 | 图像编辑 |
使用前测试密钥:
curl -s -X POST https://router.bynara.id/v1/chat/completions \
-H "Authorization: Bearer sk-nry-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"agnes-2.5-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":10}'
已知可用的免费套餐模型: agnes-2.5-flash, agnes-2.0-flash
检查您套餐中的模型:
curl -s https://router.bynara.id/v1/models \
-H "Authorization: Bearer sk-nry-YOUR_KEY" | python3 -c "import sys,json; [print(m['id']) for m in json.load(sys.stdin).get('data',[])]"
重要提示: 并非所有 53 个模型都在每个套餐中可用。deepseek-v4-pro-0813-bynara 需要积分。deepseek-v4-flash-free 可能未被包含。配置前务必进行测试。
NVIDIA NIM API (integrate.api.nvidia.com/v1)
NVIDIA NIM 通过一个兼容 OpenAI 的端点提供 82+ 个模型。
基础 URL: https://integrate.api.nvidia.com/v1
端点: https://integrate.api.nvidia.com/v1/chat/completions
密钥格式: nvapi- (例如 nvapi-_qJeGmEP...)
使用前测试密钥:
curl -s -X POST https://integrate.api.nvidia.com/v1/chat/completions \
-H "Authorization: Bearer nvapi-..." \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"model":"nvidia/nemotron-3.5-lightning-30b-a3b","messages":[{"role":"user","content":"hi"}],"max_tokens":10,"stream":true}'
已知可用的免费套餐模型 (已在 Termux 移动网络上测试):
| 模型 ID | 密钥 | 状态 | 备注 |
|---|---|---|---|
nvidia/nemotron-3.5-lightning-30b-a3b | nvapi-... | 可工作 | 快速(简单任务约 5-7 秒),推理能力强,支持思考模式。在移动设备上最可靠。 |
gpt-oss-20b | nvapi-... | 可工作 | 兼容 OpenAI |
deepseek-ai/deepseek-v4-pro-0813 | nvapi-... | 超时风险 | 在桌面上可工作,在移动 Termux 网络上经常超时 |
moonshotai/kimi-k3 | nvapi-... | 超时风险 | 支持视觉功能,但在移动网络上会超时 |
密钥不对称警告: 并非所有模型都在所有密钥上可用。密钥 1 (nvapi-_qJeGmEP...) 具有更广泛的访问权限。密钥 2 (nvapi-WDb7Rr...) 限制更多——某些模型会返回 404。在配置前,务必使用特定密钥测试模型。
失效密钥的迹象:
401 Unauthorized— 密钥无效/已过期404 Not Found于某个模型 — 该模型在此账户/密钥上不可用- 连接超时 (30秒+) — API 从该网络无法访问/过慢
- Teamorouter
sk-teamo-...密钥 → 在 OpenAI 端点 (https://api.teamorouter.com/v1) 上返回insufficient_balance;在 Anthropic 端点 (https://api.teamorouter.com) 上模型不受支持。已失效。提供deepseek-v4-pro-free模型但因余额不足无法使用。
详细信息请参见 references/nvidia-api.md:模型特定的 Python 模式(Nemotron 思考模式、DeepSeek 非流式、Kimi 视觉多模态)、流式 reasoning_content 提取、urllib 的基于线程的硬超时模式,以及视觉负载格式。
API 调度引擎
当配置了多个 API 时,构建一个调度器:
- 对任务进行分类(数学、编码、科学、通用)
- 根据描述文本选择最佳 API
- 调用它,返回结果
- 如果第一个失败,则回退到其他 API
集成细节请参见 references/nararouter-api.md。
使用标准库 urllib,而非 requests
在 Termux 中,pip install requests 可能会因为下载大包而超时。请改用 Python 的标准库 urllib——它始终可用,并且能很好地处理 JSON API:
import urllib.request, json
def call_api(url, payload, api_key):
data = json.dumps(payload).encode()
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
设计偏好 (用户特定)
极简、专业、非 AI 生成的外观:
- 深色主题,紧凑间距,无渐变,无装饰性阴影
- 适当使用等宽字体(代码、API 密钥、时间戳)
- 聊天气泡使用纯矩形——除非有功能需要,否则不要用圆角
- 侧边栏 = 纯列表,除非有意义否则不使用图标
- 搜索参考实现,而不是从头生成设计
- 有疑问时,删减功能而不是增加功能
具体反模式——这些会让 Web 应用看起来像 AI 生成的,请删除:
- 每个标题/磁贴/按钮中的表情符号(
🤖、⚙、🎭、🔌、🧠、💬、🎤)——移除。仅使用纯文本标签。 - 悬停时的强调发光/盒子阴影 (
box-shadow: 0 0 12px var(--accent-glow)) ——移除。仅使用纯色。 - 弹跳/脉冲/滑入的关键帧动画 (
@keyframes bounce、@keyframes pulse、@keyframes slideDown) ——移除。无动效设计。 - 带有图标的启动画面欢迎卡片 (包含
welcome-icon、welcome-hints网格的.welcome-screen) ——替换为输入区域中的单行提示占位符。 - Logo 旁的装饰性品牌图标 (带有表情符号的
.brand-icon) ——移除。 - 装饰标题的丝带、徽章、状态点 (
.health-badge、.health-dot) ——移除或使其纯粹功能性。 - 渐变背景、玻璃态效果、半透明叠加层——绝不使用。
经验法则: 在浏览器中打开页面,并问自己“一个人类开发者会为实用工具手写这种 CSS 吗?”如果答案是否定的(因为它有动效、发光或表情符号装饰),则删除它。一个正常的工具看起来像终端、笔记应用或极简聊天——而不是像着陆页。
语音模式:
- 使用 Web Speech API(
SpeechRecognition+SpeechSynthesis)——免费,内置于浏览器中 - 使用切换按钮在文本/语音模式之间切换
- 在语音模式下:麦克风持续监听,语音→文本→发送,响应→朗读
- 语音质量受限于 Android 上 Chrome 提供的功能——不要承诺“Ultron 声音”
工作流偏好 (用户特定)
安装前征求许可: 在运行 pip install 或 apt install 之前,先询问用户。这是不可商量的。
持续显示进度: 如果任务需要几分钟以上,请显示中间结果。不要长时间不输出。如果卡在某些事情上,请说明并告知正在尝试的方法。
无需安全说教: 用户了解 API 密钥的风险。除非明确要求,否则不要就密钥安全性、暴露或最佳实践进行说教。他们在相关时提供了自己的解释。
偏好可行的解决方案而非完美的架构: 先让某些东西跑起来。NaraRouter 密钥测试显示了这一点——测试密钥,找到可行的模型,配置它。不要过度规划。
存储是易失的——尽早备份到 GitHub: 在 Termux/Android 上,应用存储 (~/sayip/storage/) 可能在服务器重启、Termux 重启或设备重启时被清除。设置好 API 密钥和配置后,立即通过 SSH 将项目推送到 GitHub 仓库,以便恢复配置。在会话中,不要假设 storage/ 能在重启后幸存——在任何重启后重新检查它。通常需要使用 SSH-over-port-443 技巧(参见 references/termux-git-ssh.md),因为移动网络会屏蔽端口 22。
语音模式必须进行端到端测试才能宣布完成: chat.js 中的 Web Speech API 连接(setupVoice、startListening、speakText)与语音模式正常工作不是一回事。请通过实际对着手机麦克风说话并听到响应朗读出来进行测试。目标浏览器是 Android 上的 Chrome——重要的是它,而不是桌面 Chrome。
调试
端口 5000 已被占用:
fuser -k 5000/tcp # 杀死端口 5000 上的任何进程
# 或使用 ss 进行检查:
ss -tlnp | grep 5000
Flask 应用无法启动/崩溃:
- 检查语法:
python3 -c "import py_compile; py_compile.compile('app.py', doraise=True)" - 启动前检查端口是否空闲
- 使用
timeout 15 python3 app.py运行以查看启动输出而不阻塞
API 返回 401/403/payment_required:
- 密钥可能无效、已过期,或者模型不在您的套餐中
- 在应用配置前,直接使用 curl 测试密钥
- 检查
/v1/models以查看您的套餐实际包含哪些模型
Pip 安装超时:
- 移动设备上的 Termux 对于大型包的网络可能较慢/不可靠
- 尽可能使用标准库替代方案(使用 urllib 代替 requests)
- 如果必须安装,使用
pip3 install --timeout 30 <package>以快速失败
强制终止的后台 Flask 进程会短暂保留端口:
- 在
pkill -9 -f "[p]ython3 app.py"之后,等待 2-3 秒再重启。 - 如果遇到“Address already in use”,说明旧进程尚未完全释放套接字——等待更长时间。
在重启 Flask 服务器以加载代码更改之前,先杀死旧进程:
- 模式:
pkill -9 -f "[p]ython3 app.py" && sleep 3 && cd ~/sayip && python3 app.py & - 跳过杀死进程会导致“Address already in use”,新实例将无法启动。
- 重启后务必使用
curl -s --max-time 3 http://127.0.0.1:5000/api/health进行验证。
当用户报告 Web 应用损坏/JS 出错/无法工作时,按此顺序诊断:
- 确认服务器正在运行:
curl -s --max-time 3 http://127.0.0.1:5000/api/health - 检查静态文件是否返回 200:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5000/static/style.css(以及 app.js, chat.js, voice.js, settings.js) - 检查服务器实际提供的 HTML:
curl -s http://127.0.0.1:5000/ | grep -E 'static/|style.css|app.js|chat.js|voice.js'——确认 HTML 中的路径与 Flask 提供的路径匹配 - 如果静态文件返回 404 但 HTML 链接显示相对路径如
style.css而非/static/style.css,那就是错误——修补两个模板,使用/static/前缀 - 阅读 JS 文件,检查所有
document.querySelector/getElementById/.querySelector调用是否与 HTML 实际包含的内容匹配——缺少元素会导致整个脚本崩溃 - 检查空引用崩溃:在页面没有汉堡菜单元素的情况下调用
el("hamburger"),当.wrap不存在时调用messagesEl.querySelector(".wrap"),当.composer不存在时调用document.querySelector(".composer .wrap")——这些都会崩溃 IIFE 并导致整个页面的 JS 失效 - 保护元素查找:
var el = document.querySelector(selector); if (!el) return;后再使用结果
在任何 HTML/模板编辑后,通过检查服务器实际提供的内容来验证前端修复:
- 对页面 URL 执行 curl 并 grep 静态路径(
/static/style.css、/static/js/app.js)确认修复已生效。 - 直接对静态文件 URL 执行 curl,使用
-o /dev/null -w '%{http_code}'确认返回 200。 - 在两项检查都通过运行中的服务器之前,不要假设 HTML 编辑已生效。
此项目中最常见的“所有 JS 都坏了”的原因:
- HTML 模板使用了相对静态路径(
style.css、app.js、chat.js)而非 Flask 的/static/前缀(/static/style.css、/static/js/app.js、/static/js/chat.js)。 - 浏览器请求
http://127.0.0.1:5000/style.css→ 404。没有 CSS,没有 JS 加载 → 页面无样式且无法工作。 - 修复:修补两个
templates/文件(chat.html和settings.html),将所有相对静态路径替换为/static/前缀路径。