Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/boangs_rmkit.json
T

1 line
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "boangs/rmkit", "files": {"README.md": {"8a5c3038406ec2b7d235ce776414bb3a11eb9b72": {"t": 1785674233.955879, "n": 6306, "h": "5b644a06e1a2f20a", "c": "# rmkit-cn\n\nreMarkable 平板的中文化、AI、IME 与扩展工具集。\n\n通过 [xovi](https://github.com/asivery/xovi) 的 `LD_PRELOAD` + qmldiff 机制注入,\n不修改 ROM、不破坏 OTA、与系统 A/B 分区机制兼容。\n\n---\n\n## 功能\n\n- 🇨🇳 **系统中文化**:UI 文字翻译(设置 / 文件夹 / 工具栏)+ 中文键盘\n- ⌨️ **拼音输入法**:手写键盘 → 拼音浮动候选栏,FST 词库 (rime-frost)\n- 🤖 **文本 AI**:选中文字 → 润色 / 翻译 / 总结 / 问答(OpenAI 兼容)\n- ✍️ **手写 AI**:选中手写笔迹 → 视觉识别 → AI 回答 → 文字插入 OR 模拟笔迹回写\n- 📥 **手机扫码上传**:reMarkable 网页二维码 → 手机推送 PDF/字体/壁纸 → 设备自动应用\n- 🎨 **高级面板**:字体管理、壁纸切换、自定义启动器、AI 配置\n- 🎮 **小游戏**:象棋、五子棋、华容道、跳棋\n- 📚 **KOReader 启动器**:通过 [appload](https://github.com/asivery/rm-appload) 集成\n\n---\n\n## 设备支持\n\n| 代号 | 机型 | 架构 | 验证状态 |\n|---|---|---|---|\n| `rmpp-chiappa` | **reMarkable Paper Pro Move** | aarch64 | ✅ 主力测试 |\n| `rmpp-ferrari` | reMarkable Paper Pro | aarch64 | ⚠️ 同 aarch64 路径,未单独验证 |\n| `rm2` | reMarkable 2 | armv7l | ⚠️ 代码支持,未近期实测 |\n\n所有功能依赖 [xovi](https://github.com/asivery/xovi)(已自动检测安装)。\n\n---\n\n## 安装方式\n\n**前置条件**:\n1. reMarkable 设备已开启 Developer Mode(**注意:这会清空设备所有数据**)\n - Settings → General → About → Copyrights → 最底部记录 SSH 密码\n - Settings → General → Software → Enable Developer mode\n2. 设备已 USB-C 连接电脑,能 `ssh root@10.11.99.1`\n3. **建议先跑一次 `ssh-copy-id root@10.11.99.1`**(输入上一步看到的密码)。脚本内部会发起 10+ 次 ssh/scp 调用,没配公钥的话每次都要重新输密码,体验很糟\n\n### 推荐:下载 Release 完整包(无需 git / Go 工具链)\n\n```bash\ncurl -fLO https://github.com/boangs/rmkit/releases/latest/download/rmkit-cn-v1.1.1.tar.gz\ntar -xzf rmkit-cn-v1.1.1.tar.gz\ncd rmkit-cn-v1.1.1\nbash installer/install.sh\n```\n\n完整包 (~77MB) 包含 install.sh、所有架构预编译产物 (`dist/`)、xovi 依赖 (`vendor/`)。\n\n### 开发者:git clone\n\n```bash\ngit clone https://github.com/boangs/rmkit\ncd rmkit\nbash installer/install.sh\n```\n\n`dist/` 在 .gitignore 里 (80MB 二进制不入 git)。install.sh 检测到缺失时自动从最新 Release 下载 `dist.tar.gz` (~34MB) 补齐。\n\n`install.sh` 自动完成 7 步:\n1. 检测设备架构 + 固件版本\n2. 检查 xovi(缺失时自动解压 `vendor/xovi/xovi-{arch}.tar.gz` 部署)\n3. 编译 `qmd-src/*.qmd` 到对应固件 hashtab\n4. tar 流式推送 31MB payload(含 IME / AI / 字体 / 高级面板)\n5. 写入 systemd unit + xochitl drop-in(**双写 ext4 lower 持久化**,含 wants symlink)\n6. 第一次启动:临时 `LD_PRELOAD xochitl` 生成 hashtab → 设备端在线编译 .qmd\n7. `systemctl restart xochitl` 让 LD_PRELOAD 立即生效\n\n整个过程 **0 砖机**(详见下文砖机修复历史)。\n\n安装完成后几秒内可用:\n- 任意输入框点击 → 切换 \"中文\" 布局 → 拼音输入\n- 选中文字 → \"AI\" 按钮 → 选操作(润色/翻译/总结/问答)\n- 选中手写笔迹 → \"AI\" 按钮 → 同上\n- Settings 顶部多了 \"高级\" 入口(字体、壁纸、AI 配置)\n\n**OTA 升级后**:reMarkable 固件升级会重刷 rootfs,需要重跑一次 `bash installer/install.sh`。\n\n### 卸载\n\n```bash\nbash installer/install.sh --uninstall\n```\n\n---\n\n## AI 功能配置\n\n安装完成后默认 AI 配置为空。需要配置 OpenAI 兼容服务(dashscope / 千问 / OpenAI 等):\n\n**方法 1:扫码 Web UI(最简单)**\n\n设备 Settings → 高级 → 显示二维码 → 手机扫码 → 网页填入:\n- API URL:如 `https://dashscope.aliyuncs.com/compatible-mode/v1`\n- API Key:`sk-...`\n- Model:`qwen3.6-plus`(或你的 vision 兼容模型)\n\n**方法 2:SSH 直接改**\n\n```bash\nssh root@10.11.99.1 'cat > /home/root/rmkit-cn/upload-server/ai-config.json' <<EOF\n{\n \"kind\": \"openai\",\n \"url\": \"https://api.openai.com/v1\",\n \"key\": \"sk-...\",\n \"model\": \"gpt-4o-mini\"\n}\nEOF\nssh root@10.11.99.1 'systemctl restart rmkit-cn-upload'\n```\n\n---\n\n## 已知问题与限制\n\n| 问题 | 现象 | 状态 |\n|---|---|---|\n| 手写 AI 多行选区文字插入位置 | typingMode 默认把光标放第一行底部,多行选区时文字插入跟剩余行重叠 | 待修(reMarkable 没公开 view→scene API 转换) |\n| 手写过程中真笔靠近屏幕画 ghost 射线 | 真笔 hover events 跟我们虚拟笔 events 共享 event2,xochitl 状态机混淆 | 写字时建议笔放屏幕 10cm 外(reMarkable 单笔状态机的硬限制) |\n| OTA 升级后所有修改丢失 | rootfs 重刷 | 重跑 `install.sh` 一键恢复 |\n| 启用 Developer Mode 强制清数据 | reMarkable 安全设计 | 笔记建议先 cloud sync 备份 |\n\n---\n\n## 代码架构\n\n```\n.\n├── installer/ 部署脚本\n│ ├── install.sh macOS 端开发者部署(7 阶段,含砖机修复)\n│ ├── uninstall.sh\n│ ├── reenable.sh OTA 后一键恢复\n│ ├── fw-upgrade.sh 固件升级触发的 qmd 重编\n│ └── diagnose.sh 预检脚本\n├── qmd-src/ qmldiff 注入源代码\n│ ├── advanced_panel.qmd 高级面板(字体/AI/华容道/...)\n│ ├── ai_text_button.qmd 文字选区 AI 按钮\n│ ├── glyph_selection_ai.qmd 手写选区 AI 按钮 + 笔迹模拟\n│ └── language_zh_cn.qmd 系统中文化\n├── qmd/ 不经编译的 qmldiff\n│ ├── pinyin_interceptor.qmd\n│ └── zh_CN.rcc\n├── ime-go/ 拼音 IME 引擎(Go + rime-frost FST)\n├── intercept/ xochitl IME hook(C++ → ime_hook.so)\n├── upload-server-go/ 文件上传 + AI 后端 + 截图(Go)\n│ ├── internal/handwriting/ 笔迹模拟引擎(hover 状态机)\n│ ├── internal/server/ ai_glyph / screenshot_rmpp / evdev_input\n│ └── static/ Web UI(qr.html / index.html)\n├── tools/qmd-tool/ qmd 编译 + hash 校验(Go)\n├── systemd/ *.service / *.path\n├── assets/chess/ 游戏图标资源\n├── vendor/\n│ ├── xovi/ 上游 xovi tarball\n│ └── extensions/ librarian / xovi-message-broker\n└── docs/ architecture / upgrade-sop / devices\n```\n\n---\n\n## 升级安全规则(铁律)\n\n历史 N 次砖机事故全部源于**不当的部署+重启时序**。规则:\n\n1. **永远不要**在同一个 SSH session 里\"部署 + 立即 restart xochitl\"\n2. install.sh 先写 `.last_fw_version`,**再**调 reenable.sh(防 fw-upgrade.sh race)\n3. 所有 .qmd 部署前用 `qmd-tool check` 校验 hash 命中\n4. xochitl drop-in 用 `After=home.mount` + `ConditionPathExists=` 守卫\n5. systemd unit + `multi-user.target.wants/` symlink **双写 ext4 lower**(mount --bind / 后必须 remount,rw)\n6. `daemon-reload` 必须在 `umount /tmp/lc` **之后**(mount 期间会 dbus race)\n7. tar 推 payload 用 `--uid 0 --gid 0` + `--no-same-owner` + chown 兜底(防 /home/root owner 被 macOS uid 502 污染)\n\n详见 [`docs/upgrade-sop.md`](docs/upgrade-sop.md) 和 [`docs/architecture.md`](docs/architecture.md)。\n\n---\n\n## 贡献\n\n详见 [`CONTRIBUTING.md`](CONTRIBUTING.md):\n\n- 改 `.qmd`:先 `dist/qmd-tool check` 校验 hash 命中\n- 改 Go:`cd ime-go && go vet ./... && go test ./...`(或 upload-server-go / tools/qmd-tool)\n- 改 systemd unit:`systemd-analyze verify systemd/*.service`\n- 提交前 `bash -n` + `shellcheck`\n\n---\n\n## 第三方代码致谢\n\n本项目使用了以下开源代码,详细 license 标注与归属见 [`NOTICE.md`](NOTICE.md):\n\n| 项目 | License | 用途 |\n|---|---|---|\n| [asivery/xovi](https://github.com/asivery/xovi) | GPL-3.0 | LD_PRELOAD + qmldiff 注入框架 |\n| [asivery/rm-appload](https://github.com/asivery/rm-appload) | GPL-3.0 | 应用加载器 + qtfb-shim(KOReader) |\n| [awwaiid/ghostwriter](https://github.com/awwaiid/ghostwriter) | MIT | 笔迹模拟参考 + handstrokes.json 字体源 |\n| [FouzR/xovi-extensions](https://github.com/FouzR/xovi-extensions) | GPL-3.0 | qmldiff 注入参考代码 |\n| [gaboolic/rime-frost](https://github.com/gaboolic/rime-frost) | GPL-3.0 | 雾凇拼音词库 (FST 转换源) |\n\n各上游 LICENSE 全文存放在 [`third-party-licenses/`](third-party-licenses/) 目录。\n\n---\n\n## License\n\n本项目采用 [**GNU General Public License v3.0**](LICENSE)。\n\n选择 GPL-3.0 的原因:我们的项目通过 `LD_PRELOAD` 装载 GPL-3.0 协议的 xovi.so,\n根据 GPL \"传染\"条款衍生作品也必须使用兼容协议。\n"}}, "scripts/translate_ts.py": {"8a5c3038406ec2b7d235ce776414bb3a11eb9b72": {"t": 1785674248.4146354, "n": 7091, "h": "68e39a2146be546a", "c": "#!/usr/bin/env python3\n\"\"\"Translate Qt .ts file to Chinese using DashScope (Qwen) OpenAI-compatible API.\n\nUsage:\n DASHSCOPE_API_KEY=sk-xxx python3 translate_ts.py input.ts output.ts \\\n --model qwen3.6-plus --batch 30 --concurrency 5\n\nStrategy: send each batch as JSON {id: source}; ask model to return JSON\n{id: translation}. Placeholders like %1 %n <br> must be preserved verbatim.\n\"\"\"\n\nimport argparse\nimport concurrent.futures\nimport json\nimport os\nimport re\nimport sys\nimport time\nimport urllib.request\nimport urllib.error\nfrom xml.etree import ElementTree as ET\n\n\nDEFAULT_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\"\n\nSYSTEM_PROMPT = \"\"\"You are a professional zh-CN (Simplified Chinese) UI translator for a reMarkable e-ink tablet.\n\nRules:\n- Translate the \"source\" text to natural, concise Simplified Chinese suitable for a consumer UI.\n- Keep placeholders like %1 %2 %n %L1 EXACTLY as-is (position may be adjusted to match Chinese grammar).\n- Keep HTML tags like <br> <b> <i> <nobr> verbatim, including attributes.\n- Keep URLs, email-like tokens, and code identifiers unchanged.\n- If source is only placeholders or punctuation (e.g. \"%1\", \"99+\"), return it unchanged.\n- Do NOT translate brand names: reMarkable, Google, Dropbox, OneDrive, Wi-Fi.\n- Match typical iOS/Android Chinese UI conventions (e.g. \"Back\" -> \"返回\", \"Settings\" -> \"设置\", \"Cancel\" -> \"取消\", \"Done\" -> \"完成\").\n- Use the \"context\" as a hint for disambiguation; do not include it in your output.\n- Return ONLY a single JSON object mapping each id to its Chinese translation — no prose, no code fences.\"\"\"\n\n\ndef load_ts(path):\n tree = ET.parse(path)\n root = tree.getroot()\n root.set(\"language\", \"zh_CN\")\n items = []\n for ctx in root.findall(\"context\"):\n ctx_name = ctx.findtext(\"name\") or \"\"\n for msg in ctx.findall(\"message\"):\n src_el = msg.find(\"source\")\n tr_el = msg.find(\"translation\")\n if src_el is None or tr_el is None:\n continue\n src = src_el.text or \"\"\n if tr_el.get(\"type\") == \"obsolete\" or tr_el.get(\"type\") == \"vanished\":\n continue\n items.append({\n \"ctx\": ctx_name,\n \"source\": src,\n \"tr_el\": tr_el,\n \"comment\": msg.findtext(\"comment\") or \"\",\n \"extracomment\": msg.findtext(\"extracomment\") or \"\",\n })\n return tree, root, items\n\n\nSKIP_RE = re.compile(r\"^[\\s%0-9+\\-:/.,()\\[\\]]*$\")\n\n\ndef should_skip(src):\n if not src.strip():\n return True\n if SKIP_RE.match(src):\n return True\n return False\n\n\ndef call_api(url, api_key, model, payload):\n body = json.dumps(payload).encode(\"utf-8\")\n req = urllib.request.Request(\n url,\n data=body,\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n with urllib.request.urlopen(req, timeout=120) as resp:\n return json.loads(resp.read())\n\n\ndef translate_batch(url, api_key, model, batch):\n payload_items = {}\n for i, it in enumerate(batch):\n entry = {\"source\": it[\"source\"], \"context\": it[\"ctx\"]}\n if it[\"comment\"]:\n entry[\"comment\"] = it[\"comment\"]\n if it[\"extracomment\"]:\n entry[\"extracomment\"] = it[\"extracomment\"]\n payload_items[str(i)] = entry\n user_prompt = (\n \"Translate these UI strings to Simplified Chinese. Return a single JSON object.\\n\\n\"\n + json.dumps(payload_items, ensure_ascii=False, indent=2)\n )\n body = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_prompt},\n ],\n \"response_format\": {\"type\": \"json_object\"},\n \"temperature\": 0.2,\n }\n\n last_err = None\n for attempt in range(4):\n try:\n result = call_api(url, api_key, model, body)\n content = result[\"choices\"][0][\"message\"][\"content\"]\n data = json.loads(content)\n return {int(k): v for k, v in data.items()}\n except (urllib.error.HTTPError, urllib.error.URLError, KeyError, json.JSONDecodeError) as e:\n last_err = e\n time.sleep(2 ** attempt)\n raise RuntimeError(f\"Batch failed after retries: {last_err}\")\n\n\ndef main():\n ap = argparse.ArgumentParser()\n ap.add_argument(\"input\")\n ap.add_argument(\"output\")\n ap.add_argument(\"--model\", default=\"qwen3.6-plus\")\n ap.add_argument(\"--url\", default=DEFAULT_URL)\n ap.add_argument(\"--batch\", type=int, default=30)\n ap.add_argument(\"--concurrency\", type=int, default=5)\n args = ap.parse_args()\n\n api_key = os.environ.get(\"DASHSCOPE_API_KEY\")\n if not api_key:\n print(\"DASHSCOPE_API_KEY env var required\", file=sys.stderr)\n sys.exit(1)\n\n tree, root, items = load_ts(args.input)\n total = len(items)\n to_translate = []\n already = 0\n for idx, it in enumerate(items):\n if it[\"tr_el\"].text: # already translated — leave it\n already += 1\n continue\n if should_skip(it[\"source\"]):\n it[\"tr_el\"].text = it[\"source\"]\n else:\n to_translate.append((idx, it))\n print(f\"already translated: {already}\", file=sys.stderr)\n print(f\"total messages: {total}, skipping trivial: {total - len(to_translate)}, \"\n f\"to translate: {len(to_translate)}\", file=sys.stderr)\n\n batches = []\n for i in range(0, len(to_translate), args.batch):\n batches.append(to_translate[i:i + args.batch])\n print(f\"batches: {len(batches)} (size {args.batch}, concurrency {args.concurrency})\",\n file=sys.stderr)\n\n done = 0\n with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as ex:\n futures = {\n ex.submit(translate_batch, args.url, api_key, args.model,\n [b[1] for b in batch]): batch\n for batch in batches\n }\n for fut in concurrent.futures.as_completed(futures):\n batch = futures[fut]\n try:\n result = fut.result()\n except Exception as e:\n print(f\"batch error: {e}\", file=sys.stderr)\n continue\n for local_i, (orig_idx, it) in enumerate(batch):\n tr = result.get(local_i)\n if tr is None:\n print(f\"missing translation for idx {orig_idx}: {it['source'][:60]!r}\",\n file=sys.stderr)\n continue\n it[\"tr_el\"].text = tr\n done += 1\n print(f\" progress {done}/{len(batches)}\", file=sys.stderr)\n\n # Strip type=\"unfinished\" from filled translations\n for it in items:\n if it[\"tr_el\"].text:\n if it[\"tr_el\"].get(\"type\") == \"unfinished\":\n del it[\"tr_el\"].attrib[\"type\"]\n\n tree.write(args.output, encoding=\"utf-8\", xml_declaration=True)\n print(f\"wrote {args.output}\", file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n main()\n"}}}}