119 Commits

Author SHA1 Message Date
LukeMackin
c1a9e40d0a feat: 离线APK—Android Bridge本地执行(exec/listFiles/sysinfo) 2026-07-22 21:32:40 +08:00
LukeMackin
a7dad7debc chore: 删除调试截图 2026-07-22 21:25:09 +08:00
LukeMackin
323fd62ed4 fix: 审查—删除shell.html+MainApplication+清理manifest 2026-07-22 21:23:21 +08:00
LukeMackin
5cf0cf40e3 fix: allowUniversalAccessFromFileURLs(CORS审查) 2026-07-22 21:13:39 +08:00
LukeMackin
d851c25edf fix: MainActivity→ReactActivity(AS中MakeProject即可运行) 2026-07-22 21:04:02 +08:00
LukeMackin
b04238892d feat: shell.html—本地HTML加载Expo web bundle(safe) 2026-07-22 21:02:06 +08:00
LukeMackin
d75ed790cc fix: WebView加载index.bundle(Expo bundle URL替代manifest JSON) 2026-07-22 20:58:05 +08:00
LukeMackin
09457ed730 fix: exec命令正则放宽—允许全部ASCII可打印字符(引号/管道等) 2026-07-22 20:55:57 +08:00
LukeMackin
cd62fd8fe4 fix: UTF-16→UTF-8编码修复+BOM清除→BUILD SUCCESSFUL 2026-07-22 20:48:54 +08:00
LukeMackin
c6ab70aaca feat: 恢复RN prebuild配置—MainActivity+MainApplication+Gradle
→AS中File→Open android/→Sync Project→Make Project
→APK内嵌ReactActivity+App.tsx(Chat+Terminal)
2026-07-22 20:38:41 +08:00
LukeMackin
282bc79510 fix: exec默认cwd也走safePath(审查) 2026-07-22 20:30:04 +08:00
LukeMackin
37c6605582 fix: 完全重写tools.ts—safePath全部到位+命令净化+启动验证通过 2026-07-22 20:24:05 +08:00
LukeMackin
fcfedf1ae0 fix: 安全CRITICAL—exec用净化命令+file_read/write/move全加safePath 2026-07-22 20:19:20 +08:00
LukeMackin
12e00bf683 fix: 安全—路径沙箱+命令字符过滤+移除unused spire
- safePath(): resolve→startsWith(BASE_DIR)防路径遍历
- sanitizeCommand(): 拒绝;&|$(){}等shell元字符
- 移除unused import(spawn,DeviceIdentity)
2026-07-22 20:12:05 +08:00
LukeMackin
9f912da9fd feat: MCP Server运行成功—stdio传输+7个Tool+Zod schema
- StdioServerTransport(stdin/stdout标准MCP协议)
- Zod schema注册(file_list/read/write/move/exec/process_list/sysinfo)
- pnpm install完成+SDK依赖解析
- 添加zod依赖
2026-07-22 20:06:40 +08:00
LukeMackin
3d82964075 chore: 清理未用import 2026-07-22 19:39:31 +08:00
LukeMackin
490201f0d4 fix: 审查阻断—移除MainApplication引用+cleartext+SYSTEM_ALERT_WINDOW 2026-07-22 19:37:51 +08:00
LukeMackin
e29e0f7e36 fix: dependencies语法修复+移除重复includeBuild(审查阻断) 2026-07-22 19:23:53 +08:00
LukeMackin
ffb74262e9 chore: 移除expo-updates依赖+清理BOM 2026-07-22 18:25:20 +08:00
LukeMackin
4db4b6c0f3 fix: 移除expo-updates插件(已用WebView OTA取代) 2026-07-22 18:21:53 +08:00
LukeMackin
09fde0501f fix: 审查修复—cleartext+package.json main+dead HTML+comment 2026-07-22 18:21:42 +08:00
LukeMackin
ce7204b016 fix: Rules of Hooks — Terminal state提升到App顶层
- 移除JSX内IIFE(违反Rules of Hooks)
- termCmd/termOut/termRunning→App顶层useState
- 切换标签不再触发hook数量变化
2026-07-22 17:56:15 +08:00
LukeMackin
02967ae5c3 fix: Terminal阻塞修复—cmd state+onPress+fetch /tools/exec
- IIFE内useState: cmd/out/running
- Run按钮→POST /tools/exec→显示stdout/stderr
- onSubmitEditing键盘发送
- loading disabled反馈
2026-07-22 17:53:44 +08:00
LukeMackin
c407f6c483 feat: Terminal Tab — 远程exec命令执行界面
- Terminal标签: 命令输入+Run按钮+输出区域
- 等宽字体+绿字黑底终端风格
- 5个Tab: Chat/Term/Gateway/Device/Connection
2026-07-22 17:51:49 +08:00
LukeMackin
aa45b258b2 feat: Android客户端—Chat聊天界面+状态指示器
- Chat Tab: 消息列表+输入框+发送按钮
- POST /chat 与Gateway通信(支持Error显示)
- 右上角连接状态圆点(绿/黄/红/灰)
- 底部Connection Tab独立状态管理
- KeyboardAvoidingView键盘适配
2026-07-22 17:50:33 +08:00
LukeMackin
93ca593f1b fix: 审查阻断 — BOM+信号时序+unused import+exit前等待 2026-07-22 17:33:52 +08:00
LukeMackin
bca14ebe7b feat: U-001 设置页面 — Gateway/Device/Connection三标签
- Gateway标签: URL配置+心跳间隔+日志级别+自动连接
- Device标签: 设备名称+信息卡片
- Connection标签: 状态指示器+连接/断开按钮
- 引用@yuzu-gca/shared的DEFAULT_GATEWAY_CONFIG
2026-07-22 17:30:18 +08:00
LukeMackin
34ead6e471 feat: C-015+P-006/008/009 Gateway连接生命周期
- connection.ts: ConnectionManager(注册/心跳/重连)
- checkConnectivity+P-006: /health HEAD探测
- registerWithGateway: POST /devices/register
- sendHeartbeat+P-008: 15s心跳 POST /devices/:id/heartbeat
- backoffDelay+P-009: 指数退避+30%抖动
- main.ts: bootstrap后启动ConnectionManager
- SIGINT/SIGTERM优雅关闭
2026-07-22 17:29:21 +08:00
LukeMackin
8c1ba243b5 chore: server包package.json+tsconfig 2026-07-22 17:24:52 +08:00
LukeMackin
8d8d99acdd fix: 审查修复 — import清理+capabilities匹配+placeholder加required 2026-07-22 17:24:08 +08:00
LukeMackin
f85ce9aa43 feat: P0 — MCP Tool类型(36) + Server框架 + 7个核心Tool实现
S-001: packages/shared/src/mcp/tools.ts — 36 Tool接口定义(10模块)
S-002: packages/shared/src/mcp/config.ts — DeviceConfig/Gateway类型
C-001: packages/server/src/index.ts — MCP Server框架+SSE传输
C-002~011: packages/server/src/tools.ts — 7个P0核心Tool
  - file_list, file_read, file_write, file_move
  - exec, process_list, sysinfo
packages/server/src/main.ts — 入口, 环境变量配置
2026-07-22 17:20:38 +08:00
LukeMackin
a98d35cb1c fix: versionCode match失败时跳过(避免curCode=0误报) 2026-07-22 16:50:59 +08:00
LukeMackin
e209db6be8 fix: APK阈值动态提取ver span末尾数字(审查建议) 2026-07-22 16:48:29 +08:00
LukeMackin
c41a3ad9d9 feat: frame.html 加入 APK 版本检测
- checkUpdate 读取 j.apk.versionCode
- 如果>5(当前版本),显示Download APK链接
- apkLink div在下载按钮下方
- BUILD SUCCESSFUL
2026-07-22 16:46:17 +08:00
LukeMackin
c6f7567eb9 fix: 完整重写index-stable.html(gear⚙+overflow) SHA256同步三渠道 2026-07-22 12:51:40 +08:00
LukeMackin
f595c82983 fix: gear emoji鍏?→⚙ 2026-07-22 12:48:17 +08:00
LukeMackin
153af75a44 feat: nightly→v7 测试渠道独立 2026-07-22 12:47:27 +08:00
LukeMackin
7e2f2ebfbd fix: body overflow-x:hidden 修复错位+SHA256同步 2026-07-22 12:30:43 +08:00
LukeMackin
99d8f66f52 fix: SHA256 sync gear修复后 2026-07-22 12:27:43 +08:00
LukeMackin
40998f9be1 fix: BOM清除+chLabel初始化(审查阻断) 2026-07-22 01:16:41 +08:00
LukeMackin
45cce458ac fix: index-stable.html英文版+渠道持久化+highlight 2026-07-22 01:13:39 +08:00
LukeMackin
3a1058d10c chore: 清理tmp_js.js 2026-07-22 01:09:42 +08:00
LukeMackin
f83bc2d524 fix: manifest.json clean rewrite SHA256=f82ea977 2026-07-22 01:09:08 +08:00
LukeMackin
4facecc089 fix: manifest.json SHA256 改为实际值(f82ea977...)
- 使用index-stable.html的实际SHA256
- TBD占位符→真实值,客户端校验将通过
- 清除BOM保证JSON.parse可用
2026-07-22 01:08:28 +08:00
LukeMackin
b5bcf8e9c0 fix: UTF-8 BOM+loadDataWithBaseURL(utf-8)修复乱码
- frame.html/index.html 添加 UTF-8 BOM (EF BB BF)
- loadDataWithBaseURL 显式设置 utf-8 编码
- WebView 以此识别中文内容
2026-07-22 00:59:03 +08:00
LukeMackin
6ee4ff533a chore: 移除调试文件 2026-07-22 00:55:29 +08:00
LukeMackin
337c49f1f9 fix: 改回loadUrl(file://frame.html) - loadDataWithBaseURL导致乱码
- 框架APK始用loadUrl,避免MIME/编码问题
- 缓存HTML仍用loadDataWithBaseURL
- 恢复<!DOCTYPE html>
2026-07-22 00:53:39 +08:00
LukeMackin
db0bc85609 fix: 移除DOCTYPE(loadDataWithBaseURL兼容) 2026-07-22 00:49:41 +08:00
LukeMackin
7f7b75c0fc fix: 审查阻断修复 — cache加载+TBD校验+路径安全
1. 恢复cacheFile.exists()→加载缓存HTML(OTA链路修复)
2. TBD/零哈希双重拒绝(/^(0{64}|TBD)/)
3. fetchText双次替换..防止绕过
4. BUILD SUCCESSFUL
2026-07-22 00:46:55 +08:00
LukeMackin
e9bcb7d804 fix: MainActivity精简+frame.html骨架页[V5]标记
- 移除cacheFile逻辑,始终加载frame.html
- loadDataWithBaseURL避免WebView缓存
- frame.html添加[V5]标记区分版本
- Bridge保留(manifest/fetchText/sha256)
2026-07-22 00:43:06 +08:00
LukeMackin
4b4103cf06 fix: 审查阻塞修复 — SHA256 TBD占位+beta/nightly→stable+硬编码channel+路径校验
- manifest.json: beta/nightly h5.url → index-stable.html
- SHA256: TBD占位(build后push-hotfix.sh更新)
- index-stable: ver元素用channel变量, match?.[1]防御
- fetchText: 过滤..防止路径遍历
2026-07-22 00:25:40 +08:00
LukeMackin
9e4d9e7dd7 cleanup: 删除旧manifest+index-v*.html(已用manifest.json+index-{channel}.html) 2026-07-22 00:22:21 +08:00
LukeMackin
439c9066e2 feat: OTA四层架构 — manifest.json统一+渠道默认stable
- manifest.json: stable/beta/nightly三渠道H5+APK统一
- version.txt: 指向manifest.json URL
- v5嵌入页: channel默认stable, localStorage.channel可切换
- Bridge.fetchManifest: 自动读manifest.json解析对应渠道
- index-stable/beta/nightly: 业务页(开发期beta/nightly→stable)
- 删除旧版index-v6/v7
- 构建成功
2026-07-22 00:22:12 +08:00
LukeMackin
1160fdd03b feat: OTA重构 — manifest.json统一+业务页三渠道
- manifest.json: stable/beta/nightly 统一H5+APK配置
- index-stable.html: 主业务页(⚙齿轮渠道切换)
- index-beta/index-nightly: meta refresh→stable(开发期)
2026-07-22 00:20:54 +08:00
LukeMackin
0984082ac0 fix: nightly sha256改为零哈希(TBD占位符会导致startsWith(TBD)误判) 2026-07-21 23:31:39 +08:00
LukeMackin
c4bf1de467 fix: 移除APK manifest BOM(审查阻断) 2026-07-21 23:31:27 +08:00
LukeMackin
fe182e6f3d feat: APK manifest(发行/测试/开发) 2026-07-21 23:28:57 +08:00
LukeMackin
ad918e3a5f feat: APK manifest生成脚本 gen-manifest.sh + README更新 2026-07-21 23:27:58 +08:00
LukeMackin
5e1badce1f feat: 补丁推送脚本 push-hotfix.sh + README文档 2026-07-21 23:25:34 +08:00
LukeMackin
b0564a667d chore: gen-manifest.sh reset 2026-07-21 23:20:28 +08:00
LukeMackin
c3077a4927 cleanup: 删除旧APK manifest(热更新用h5-manifest-*) 2026-07-21 23:19:56 +08:00
LukeMackin
38a60eae8f chore: 移除调试dump文件(t*.xml, ss.png) 2026-07-21 23:14:07 +08:00
LukeMackin
a4fa268b9f refactor: ota目录优化 — h5-manifest-*.json移至根+更新Bridge URL
- ota/h5/manifest-*.json → ota/h5-manifest-*.json
- MainActivity.kt: fetchManifest URL更新
- APK manifest + h5 manifest命名冲突消除
2026-07-21 23:13:57 +08:00
LukeMackin
f4b0feb9d1 fix: SHA256 sync 2026-07-21 23:07:56 +08:00
LukeMackin
39d7e82a55 fix: v6页checkUpdate+applyUpdate全用Bridge(移除fetch+CSP误伤) 2026-07-21 23:07:45 +08:00
LukeMackin
f56024f7c0 fix: v5页checkUpdate改用Bridge.fetchText(移除fetch) 2026-07-21 23:05:04 +08:00
LukeMackin
b88311a2d0 security: Bridge代理fetch+移除跨域设置+CSP头限制
CRITICAL修复:
- 移除allowFileAccessFromFileURLs/allowUniversalAccessFromFileURLs
- 移除mixedContentMode=ALWAYS_ALLOW
- WebViewClient注入CSP: default-src 'none';script-src 'unsafe-inline';connect-src 'none'
- 新增Bridge.fetchManifest(channel)/Bridge.fetchText(path)原生HTTP代理
- manifest JSON + version.txt通过Bridge读取,无需WebView跨域
2026-07-21 23:02:00 +08:00
LukeMackin
db8cfdf6e3 fix: loadDataWithBaseURL改回file:///android_asset/基址 2026-07-21 22:52:15 +08:00
LukeMackin
bb1e9ede2a fix: loadDataWithBaseURL base null→about:blank允许fetch跨域 2026-07-21 22:50:29 +08:00
LukeMackin
9ba8c296b0 fix: SHA256使用web实际返回内容计算(非本地文件) 2026-07-21 22:46:55 +08:00
LukeMackin
9e0d59895b fix: v6 innerText→textContent (display:none元素兼容) 2026-07-21 22:36:02 +08:00
LukeMackin
049d2ccaaf fix: v6 HTML页面添加id=ver元素(审查阻断) 2026-07-21 22:33:47 +08:00
01a8a76805 删除 screen4.png 2026-07-21 22:32:59 +08:00
2ea30be977 删除 screen3.png 2026-07-21 22:32:55 +08:00
18eeb07f66 删除 screen2.png 2026-07-21 22:32:49 +08:00
61c6d32192 删除 screen-final.png 2026-07-21 22:32:44 +08:00
LukeMackin
8f078e4f30 feat: manifest字段规范化 {latest,min_version,url,sha256} + v6动态版本对比
- h5_version → latest + min_version(老旧客户端提示需APK升级)
- v6 HTML: 去掉硬编码版本号,动态读取页面ver元素比对
- TBD sha256 → 直接拒绝(非空占位符)
- 域名校验保留
2026-07-21 22:29:30 +08:00
LukeMackin
e31bc3dcb9 fix: BOM清除 + nightly SHA256改为TBD标记 2026-07-21 21:56:40 +08:00
LukeMackin
8fb84c2efb feat: nightly渠道指向v7(待创建) 2026-07-21 21:53:36 +08:00
LukeMackin
eccfaa7f66 fix: SHA256同步 — v6 HTML修改后更新所有manifest+version.txt 2026-07-21 21:35:08 +08:00
LukeMackin
1ef40900a8 fix: 移除h5 manifest BOM 2026-07-21 21:32:56 +08:00
LukeMackin
cdf9b9d499 review fix: 独立h5-manifest + 域名校验 + 恢复APK manifest
- ota/h5/manifest-{channel}.json — 独立于APK manifest
- ota/manifest-*.json — 恢复gen-manifest.sh格式
- v6 HTML: h5_sha256缺失拒绝 + h5_url域名校验
2026-07-21 21:20:59 +08:00
LukeMackin
b4ddb0b5c7 feat: 渠道选择实装 — v6读取manifest-{channel}.json
- checkUpdate根据选中渠道读取manifest-stable/beta/nightly.json
- 三个manifest使用统一H5字段: h5_version/h5_url/h5_sha256
- 版本号比对可独立推进各渠道
2026-07-21 21:08:54 +08:00
LukeMackin
f9d5d32785 security: SHA256强制+dlUrl域名校验+缓存大小限制(500KB)
- saveCache: >500KB拒绝写入
- index.html: SHA256缺失时拒绝(不再静默跳过)
- index.html: dlUrl域名校验(dlUrl.indexOf(BASE)===0)
- 审查建议全部修复
2026-07-21 20:39:47 +08:00
LukeMackin
67adde25a4 security: SHA256校验通过Android Bridge(Java侧计算)
- Bridge.sha256() 用 MessageDigest 计算
- JS下载后 Android.sha256(t) 比对 version.txt 哈希
- version.txt 恢复三列: ver|url|sha256
- 避免 Web Crypto API 在 file:// 域不可用问题
2026-07-21 20:30:55 +08:00
70c535d0c1 更新 ota/h5/index-v6.html 2026-07-21 20:28:49 +08:00
1b1a586ccc 更新 ota/h5/index-v6.html 2026-07-21 20:28:01 +08:00
LukeMackin
5b9a996759 fix: loadDataWithBaseURL加载缓存HTML (file://权限不足) 2026-07-21 20:25:53 +08:00
LukeMackin
f4ee38f39f 修复: 恢复WebView跨域设置(本设备必需) + 工作版index.html 2026-07-21 20:19:23 +08:00
LukeMackin
7f22d4028f security: 关闭file跨域+混合内容+调试 + SHA256完整性校验
CRITICAL修复:
- 移除allowFileAccessFromFileURLs/allowUniversalAccessFromFileURLs
- 移除MIXED_CONTENT_ALWAYS_ALLOW
- WebContentsDebugging仅DEBUG模式开启
- 下载HTML用Web Crypto SHA256校验(version.txt|url|sha256)
2026-07-21 20:06:31 +08:00
LukeMackin
19860bdec7 fix: review — 移除未用import + v6 fallback兼容旧格式version.txt 2026-07-21 19:59:04 +08:00
LukeMackin
af799da3ea fix: loadDataWithBaseURL→loadUrl file:// 直接加载缓存HTML 2026-07-21 19:55:22 +08:00
LukeMackin
e39a250fbe fix: version.txt格式改为version|url — 消除所有硬编码dlUrl
- version.txt: 0.1.0-dav-android-6|完整下载URL
- index.html: 动态解析|分隔符获取下载路径
- index-v6.html: 同逻辑,不硬编码
- 以后升级只需改version.txt指向新文件
2026-07-21 19:16:27 +08:00
LukeMackin
565b1a5ea1 fix: version.txt → v6 (首次热更新目标) 2026-07-21 19:07:32 +08:00
LukeMackin
a47b73dda7 feat: 热更新架构 — Android Bridge存文件, MainActivity启动时检查缓存
v5: 嵌入式页面(蓝黑主题,简单按钮)
v6: 完全不同的页面(紫色渐变,卡片布局,渠道选择器)
通过Android.saveCache存HTML文件,重启App自动加载
2026-07-21 19:07:17 +08:00
LukeMackin
bfd7bee2ae fix: Blob URL加载缓存页面——避免DOM操作导致双重渲染 2026-07-21 19:02:09 +08:00
LukeMackin
bf72ea937f fix: 用documentElement.innerHTML替换整个页面避免双重渲染+保留JS 2026-07-21 19:01:29 +08:00
LukeMackin
96d88a9601 fix: 缓存命中在head同步document.write避免双重渲染+JS丢失 2026-07-21 18:57:27 +08:00
LukeMackin
123824a734 fix: 缓存命中用location.replace加载完整HTML(含JS执行)
- innerHTML不执行script标签导致按钮无响应
- location.replace到data: URI确保JS函数正常定义
2026-07-21 18:51:59 +08:00
LukeMackin
4d6bb3ff6f feat: 热更新 v6→v7 — 绿色主题+渠道选择器持续可用 2026-07-21 18:47:37 +08:00
LukeMackin
8f2f90df0d fix: H5缓存命中时替换body内容而非仅显示徽章
- 使用createHTMLDocument解析缓存HTML
- innerHTML替换body以显示完整热更新页面(含渠道选择器等)
2026-07-21 18:41:38 +08:00
LukeMackin
9a2f502857 fix: version.txt → v6 2026-07-21 18:38:34 +08:00
LukeMackin
3b6c6a4923 feat: 热更新v5→v6 — 新增渠道选择Stable/Beta/Nightly 2026-07-21 18:37:59 +08:00
LukeMackin
7487158efd cleanup: 删除ota/h5旧版残留index-v7/v8 2026-07-21 18:34:02 +08:00
454772a343 更新 ota/version.txt 2026-07-21 18:33:14 +08:00
e5b40c028d 更新 ota/version.txt 2026-07-21 18:32:11 +08:00
12e1abbfb8 更新 ota/version.txt 2026-07-21 18:31:57 +08:00
LukeMackin
ead2045ce5 reset: versionCode→5, H5热更新起点v5→v6
- versionCode 6→5 (衔接Git Tag v4)
- embedded HTML→v5, 下载目标→v6
- ota/version.txt→v6
- Gitea tags v5/v6已清理
2026-07-21 18:29:03 +08:00
LukeMackin
42b3592ed9 fix: H5热更新不再document.write避免双重重叠
- localStorage命中时只更新版号+徽章,不重写页面
- 嵌入式按钮始终可见,可持续检查更新
2026-07-21 18:26:15 +08:00
LukeMackin
49793529c0 feat: H5热更新 v7→v8 — 版本号推进
- ota/version.txt → 0.1.0-dav-android-8
- ota/h5/index-v8.html → 紫色标题+v8徽章,含自更新按钮
- embedded index.html → 下载目标改为 v8
2026-07-21 15:40:15 +08:00
LukeMackin
6f8b346e73 fix: H5热更新缓存击穿+版本比对修复
- fetch URL 加 ?t=Date.now() 缓存击穿
- 版本比对兼容 v0.1.0 vs 0.1.0-dav 前缀差异
2026-07-21 15:26:17 +08:00
LukeMackin
8f04acaf98 feat: H5热更新 v6→v7 — 远端version.txt=v7 + index-v7.html
- ota/version.txt → 0.1.0-dav-android-7
- ota/h5/index-v7.html → 绿色标题+热更新成功提示
- embedded index.html → 下载目标改为v7
2026-07-21 15:00:59 +08:00
LukeMackin
0727aa33f1 build: v6 — WebView热更新修复 + fetch跨域设置
- versionCode 5→6
- WebView allowFileAccessFromFileURLs + mixedContent
- index.html 版本号v6
- index.html fetch检查更新+下载热更新完整链路
2026-07-21 14:55:57 +08:00
LukeMackin
cd1f19815d fix: WebView闪退→恢复原生UI, versionCode 8→5, H5热更新文件就绪
- MainActivity恢复为原生ScrollView+按钮(WebView在Redmi设备上崩溃)
- versionCode回退到5(衔接Git Tag v4)
- ota/version.txt指向v6,ota/h5/index-v6.html已准备好作为热更新目标
- APK构建验证通过(33 tasks, 31s)
2026-07-21 13:40:13 +08:00
LukeMackin
de20e837fa feat: H5热更新v5→v6 — versionCode 8→5, 远端version.txt指向v6 2026-07-21 12:17:49 +08:00
LukeMackin
bfa365c2c2 cleanup: 删除 feat/expo-ota 分支,回滚到 main
- 删除远程 feat/expo-ota 分支
- 删除 Gitea Releases #5~#10 + Tags v5~v10
- 清除 Expo 产物(npmrc, package-lock, expo-manifest)
- 保留 index.js + index.html(H5热更新)
- MainActivity 改为 WebView + Android Bridge 模式
- 构建验证通过(33 tasks, 54s)
2026-07-21 11:56:07 +08:00
LukeMackin
898fbc0c3c hotfix: expo-manifest v10 for hot update test 2026-07-20 23:48:51 +08:00
LukeMackin
f052d280ce hotfix: expo-manifest v9 for hot update test 2026-07-20 23:26:06 +08:00
LukeMackin
1684fac260 chore: 清理OCR临时文件+截图+调试脚本,更新.gitignore 2026-07-20 12:57:14 +08:00
129 changed files with 10488 additions and 1303 deletions

7
.gitignore vendored
View File

@@ -47,3 +47,10 @@ android/build/
local.properties local.properties
screen.png screen.png
# Temp/debug files
*.traineddata
tessdata_temp/
*.ps1
*_temp/

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

View File

@@ -0,0 +1,2 @@
{"status":"starting_turn","iteration":0,"created_at":"2026-07-20T16:59:22.3318065Z"}
{"status":"turn_done","iteration":1,"created_at":"2026-07-20T16:59:51.4961797Z"}

View File

@@ -0,0 +1,9 @@
[
{
"fingerprint": "15-127-0-0-1-10808-android-studio-sync-project-7bf5ff43",
"summary": "代理是根因15 个失败都是因为 `127.0.0.1:10808` 代理)。已移除。在 Android Studio 里点 **大象图标**Sync Project重新同步",
"first_seen_iteration": 1,
"last_seen_iteration": 1,
"count": 1
}
]

View File

@@ -0,0 +1,9 @@
{
"status": "running",
"iteration": 1,
"current_direction": "代理是根因15 个失败都是因为 `127.0.0.1:10808` 代理)。已移除。在 Android Studio 里点 **大象图标**Sync Project重新同步",
"stale_count": 1,
"pivot_count": 0,
"blocked_reason": "",
"updated_at": "2026-07-20T16:59:51.4929941Z"
}

View File

@@ -0,0 +1,28 @@
{"status":"starting_turn","iteration":0,"created_at":"2026-07-21T03:16:15.2382547Z"}
{"status":"turn_done","iteration":1,"created_at":"2026-07-21T03:17:05.8410329Z"}
{"status":"starting_turn","iteration":1,"created_at":"2026-07-21T03:17:06.1408318Z"}
{"status":"turn_done","iteration":2,"created_at":"2026-07-21T03:17:34.0830711Z"}
{"status":"starting_turn","iteration":2,"created_at":"2026-07-21T03:17:34.8204313Z"}
{"status":"turn_done","iteration":3,"created_at":"2026-07-21T03:18:32.1678783Z"}
{"status":"starting_turn","iteration":3,"created_at":"2026-07-21T03:18:32.1884204Z"}
{"status":"turn_done","iteration":4,"created_at":"2026-07-21T03:19:08.6392005Z"}
{"status":"starting_turn","iteration":4,"created_at":"2026-07-21T03:19:08.7309788Z"}
{"status":"turn_done","iteration":5,"created_at":"2026-07-21T03:19:33.0643356Z"}
{"status":"starting_turn","iteration":5,"created_at":"2026-07-21T03:19:33.2245358Z"}
{"status":"turn_done","iteration":6,"created_at":"2026-07-21T03:20:56.4286355Z"}
{"status":"starting_turn","iteration":6,"created_at":"2026-07-21T03:20:56.5224447Z"}
{"status":"turn_done","iteration":7,"created_at":"2026-07-21T03:22:00.7927224Z"}
{"status":"starting_turn","iteration":7,"created_at":"2026-07-21T03:22:00.8862815Z"}
{"status":"turn_done","iteration":8,"created_at":"2026-07-21T03:22:46.0575234Z"}
{"status":"starting_turn","iteration":8,"created_at":"2026-07-21T03:22:46.1718312Z"}
{"status":"turn_done","iteration":9,"created_at":"2026-07-21T03:24:13.7257985Z"}
{"status":"starting_turn","iteration":9,"created_at":"2026-07-21T03:24:13.8277354Z"}
{"status":"turn_done","iteration":10,"created_at":"2026-07-21T03:24:27.5537437Z"}
{"status":"starting_turn","iteration":10,"created_at":"2026-07-21T03:24:27.6571627Z"}
{"status":"turn_done","iteration":11,"created_at":"2026-07-21T03:25:19.3030817Z"}
{"status":"starting_turn","iteration":11,"created_at":"2026-07-21T03:25:19.4076559Z"}
{"status":"turn_done","iteration":12,"created_at":"2026-07-21T03:29:18.0058775Z"}
{"status":"starting_turn","iteration":12,"created_at":"2026-07-21T03:29:18.1383267Z"}
{"status":"turn_done","iteration":13,"created_at":"2026-07-21T03:30:01.3929977Z"}
{"status":"starting_turn","iteration":13,"created_at":"2026-07-21T03:30:01.5037583Z"}
{"status":"turn_done","iteration":14,"created_at":"2026-07-21T03:30:14.626083Z"}

View File

@@ -0,0 +1,100 @@
[
{
"fingerprint": "as-build-ad2624ab",
"summary": "已修复两个配置。在 AS 里重新 Build",
"first_seen_iteration": 1,
"last_seen_iteration": 1,
"count": 1
},
{
"fingerprint": "as-build-20a5fde3",
"summary": "在 AS 重新 Build。这两步被彻底跳过。",
"first_seen_iteration": 2,
"last_seen_iteration": 2,
"count": 1
},
{
"fingerprint": "task",
"summary": "---",
"first_seen_iteration": 3,
"last_seen_iteration": 3,
"count": 1
},
{
"fingerprint": "android-studio-build-release-expo-updates-root-8e271660",
"summary": "用 Android Studio 重新 Build Release。修复了 `expo-updates` 路径加倍问题(缺少 `root` 配置项):",
"first_seen_iteration": 4,
"last_seen_iteration": 4,
"count": 1
},
{
"fingerprint": "root-createreleaseupdatesresources-packages-client-andro-c0afb42a",
"summary": "`root` 设定没有解决。`createReleaseUpdatesResources` 仍然报双重路径 `packages\\client-android\\packages\\client-android\\App.tsx`——这个任务不",
"first_seen_iteration": 5,
"last_seen_iteration": 5,
"count": 1
},
{
"fingerprint": "task-5b2466d1",
"summary": "两个修复就位:",
"first_seen_iteration": 6,
"last_seen_iteration": 6,
"count": 1
},
{
"fingerprint": "android-studio-build-release-c00a91b0",
"summary": "**现在请用 Android Studio 重新 Build Release。**",
"first_seen_iteration": 7,
"last_seen_iteration": 7,
"count": 1
},
{
"fingerprint": "android-studio-build-release-7002dec6",
"summary": "**请用 Android Studio 重新 Build Release。**",
"first_seen_iteration": 8,
"last_seen_iteration": 8,
"count": 1
},
{
"fingerprint": "build-gradle-afterevaluate-js-bundle-ae4afdad",
"summary": "`build.gradle` 已清理为干净的单一 `afterEvaluate` 块。JS bundle 已就位。",
"first_seen_iteration": 9,
"last_seen_iteration": 9,
"count": 1
},
{
"fingerprint": "stale-9-tasks-named-configure-enabled-false-afterevaluat-49c894d6",
"summary": "stale=9。`tasks.named().configure { enabled = false }` 在 `afterEvaluate` 中都无效——这两个任务由 Gradle 插件在配置阶段强制注册<EFBC8C>",
"first_seen_iteration": 10,
"last_seen_iteration": 10,
"count": 1
},
{
"fingerprint": "android-studio-build-release-0ac7a51a",
"summary": "**现在用 Android Studio 直接 Build Release——不用加任何参数。**",
"first_seen_iteration": 11,
"last_seen_iteration": 11,
"count": 1
},
{
"fingerprint": "redmi-usb-79ba1bcf",
"summary": "卸载成功。安装受阻——Redmi USB 安装限制。**请解锁手机屏幕,点\"允许\"后告诉我。**",
"first_seen_iteration": 12,
"last_seen_iteration": 12,
"count": 1
},
{
"fingerprint": "android-studio-feat-expo-ota-12-main-apk-usb-45c0be45",
"summary": "Android Studio 里的项目还是 `feat/expo-ota`(所以 12 次失败)。我这边 main 的 APK 已经好了——安装不过去是手机 USB 权限。",
"first_seen_iteration": 13,
"last_seen_iteration": 13,
"count": 1
},
{
"fingerprint": "13-autoresearch-bafea58b",
"summary": "**结论已定。** 13轮迭代证据充分。关闭 AutoResearch",
"first_seen_iteration": 14,
"last_seen_iteration": 14,
"count": 1
}
]

View File

@@ -0,0 +1,9 @@
{
"status": "complete",
"iteration": 14,
"current_direction": "**结论已定。** 13轮迭代证据充分。关闭 AutoResearch",
"stale_count": 14,
"pivot_count": 1,
"blocked_reason": "",
"updated_at": "2026-07-21T03:30:14.7277324Z"
}

View File

@@ -0,0 +1,2 @@
{"status":"starting_turn","iteration":0,"created_at":"2026-07-21T03:15:01.5344339Z"}
{"status":"turn_done","iteration":1,"created_at":"2026-07-21T03:16:14.9716471Z"}

View File

@@ -0,0 +1,9 @@
[
{
"fingerprint": "910kb-bundle-android-studio-build-generate-signed-bundle-0bdca4e3",
"summary": "910KB bundle 已放好。Android Studio 里重新 Build → Generate Signed Bundle / APK → release → Finish。这次不会超时也不会 Metro 报错。",
"first_seen_iteration": 1,
"last_seen_iteration": 1,
"count": 1
}
]

View File

@@ -0,0 +1,9 @@
{
"status": "running",
"iteration": 1,
"current_direction": "910KB bundle 已放好。Android Studio 里重新 Build → Generate Signed Bundle / APK → release → Finish。这次不会超时也不会 Metro 报错。",
"stale_count": 1,
"pivot_count": 0,
"blocked_reason": "",
"updated_at": "2026-07-21T03:16:14.9681754Z"
}

View File

@@ -1,53 +0,0 @@
"""
Final analysis: Search for any readable content in the decoded text.
Look for patterns that might indicate it's a screenshot from an Android device.
"""
import sys
import re
path = r"D:\Yuzu-GCA\screen.png"
with open(path, 'rb') as f:
raw = f.read()
text = raw.decode('utf-16-le')
# Save full text for manual inspection
with open(r"D:\Yuzu-GCA\screen_full_text.txt", 'w', encoding='utf-8') as f:
f.write(text)
print("Full text written to screen_full_text.txt")
# Search for any CJK character sequences (potential readable Chinese text)
# Chinese characters are in range U+4E00 to U+9FFF
cjk_pattern = re.compile(r'[\u4e00-\u9fff]{3,}')
cjk_matches = cjk_pattern.findall(text)
print(f"\nChinese character sequences (>=3 chars): {len(cjk_matches)}")
# Show unique ones
unique_cjk = list(set(cjk_matches))
print(f"Unique sequences: {len(unique_cjk)}")
for seq in sorted(unique_cjk, key=len, reverse=True)[:30]:
print(f" '{seq}' (len={len(seq)})")
# Search for any ASCII word sequences (potential English words)
# Words of length >= 4 made of letters only
word_pattern = re.compile(r'[A-Za-z]{4,}')
word_matches = word_pattern.findall(text)
print(f"\nEnglish word-like sequences (>=4 chars): {len(word_matches)}")
unique_words = list(set(word_matches))
print(f"Unique: {len(unique_words)}")
for w in sorted(unique_words, key=len, reverse=True)[:40]:
print(f" '{w}'")
# Also search for numbers that look like versions
version_pattern = re.compile(r'\d+\.\d+(\.\d+)?')
version_matches = version_pattern.findall(text)
print(f"\nVersion-like patterns: {len(version_matches)}")
for v in version_matches[:20]:
print(f" '{v}'")
# Search for URLs or file paths
path_pattern = re.compile(r'[/\\][A-Za-z0-9_./\\-]{5,}')
path_matches = path_pattern.findall(text)
print(f"\nPath-like patterns: {len(path_matches)}")
for p in path_matches[:20]:
print(f" '{p}'")

35
cdp-read.js Normal file
View File

@@ -0,0 +1,35 @@
const http = require('http');
const WebSocket = require('ws');
http.get('http://localhost:9223/json', res => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => {
const pages = JSON.parse(d);
const wsUrl = pages[0].webSocketDebuggerUrl;
const ws = new WebSocket(wsUrl);
ws.on('open', () => {
ws.send(JSON.stringify({id: 1, method: 'Runtime.evaluate', params: {expression: 'document.getElementById("ver").innerText'}}));
});
ws.on('message', data => {
try {
const msg = JSON.parse(data);
if (msg.id === 1) {
console.log('VERSION:', msg.result?.result?.value ?? JSON.stringify(msg.result));
ws.send(JSON.stringify({id: 2, method: 'Runtime.evaluate', params: {expression: 'localStorage.getItem("gca_ver") || "none"'}}));
}
if (msg.id === 2) {
console.log('LOCALSTORAGE gca_ver:', msg.result?.result?.value ?? JSON.stringify(msg.result));
ws.send(JSON.stringify({id: 3, method: 'Runtime.evaluate', params: {expression: 'document.getElementById("log").innerText'}}));
}
if (msg.id === 3) {
console.log('LOG:', msg.result?.result?.value?.substring(0, 500) ?? JSON.stringify(msg.result));
ws.close();
process.exit(0);
}
} catch(e) {}
});
});
}).on('error', e => { console.log('HTTP ERR:', e.message); process.exit(1); });
setTimeout(() => process.exit(0), 8000);

View File

@@ -1,17 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# Check screen2.png
data = open(r"D:\Yuzu-GCA\screen2.png", 'rb').read()
print(f"screen2.png: Size={len(data)}")
print(f"First 20 bytes: {' '.join(f'{b:02X}' for b in data[:20])}")
print(f"Is valid PNG: {data[:8] == b'\x89PNG\r\n\x1a\n'}")
# Also check the fixed files
for fname in ['screen_fixed.png', 'screen_fixed2.png']:
try:
d = open(r"D:\\Yuzu-GCA\\" + fname, 'rb').read()
print(f"\n{fname}: Size={len(d)}")
except:
print(f"\n{fname}: NOT FOUND")

View File

@@ -1,36 +0,0 @@
import sys
image_path = r"D:\Yuzu-GCA\screen.png"
with open(image_path, 'rb') as f:
header = f.read(32)
print(f"File size check: first 32 bytes hex:")
print(' '.join(f'{b:02X}' for b in header))
print()
print(f"ASCII representation: {header}")
# Check known magic numbers
if header[:8] == b'\x89PNG\r\n\x1a\n':
print("✓ Valid PNG header detected")
elif header[:2] == b'\xff\xd8':
print("✓ JPEG header detected")
elif header[:4] == b'BM':
print("✓ BMP header detected")
elif header[:4] == b'RIFF':
print("✓ WEBP header detected")
else:
print("⚠ Unknown image format")
# Also check if it might be an Android screencap (could be raw)
# Try to detect if it's a PNG embedded in a wrapper
print()
# Check if file is actually a text-based format
with open(image_path, 'rb') as f:
content = f.read(200)
# Check if mostly printable ASCII
printable = sum(1 for b in content if 32 <= b <= 126 or b in (9, 10, 13))
if printable > len(content) * 0.8:
print("Note: File appears to be mostly text!")
print(content.decode('utf-8', errors='replace'))

View File

@@ -1,74 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'rb') as f:
data = f.read()
print(f"Total size: {len(data)}")
# Check PNG structure
# Signature: 8 bytes
# Then chunks: each chunk has 4-byte length, 4-byte type, data, 4-byte CRC
offset = 8 # skip signature
while offset < len(data):
if offset + 8 > len(data):
break
length = int.from_bytes(data[offset:offset+4], 'big')
chunk_type = data[offset+4:offset+8].decode('ascii', errors='replace')
print(f"Chunk at offset {offset}: type='{chunk_type}', length={length}")
if chunk_type == 'IHDR':
w = int.from_bytes(data[offset+8:offset+12], 'big')
h = int.from_bytes(data[offset+12:offset+16], 'big')
bit_depth = data[offset+16]
color_type = data[offset+17]
print(f" Width={w}, Height={h}, BitDepth={bit_depth}, ColorType={color_type}")
# Height seems wrong (854591). Expected ~1920 or ~2340
# 0x000D0A3F: maybe it was 0x00000780 (1920) or 0x00000924 (2340)?
# Let me look at the raw bytes around height
print(f" Raw height bytes: {' '.join(f'{b:02X}' for b in data[offset+12:offset+16])}")
# Expected height around 1920 = 0x780, or 2340 = 0x924
# What we have: 00 0D 0A 3F = 0x000D0A3F
# Could it be that 0D 0A is an inserted CR LF?
# If we remove 0D 0A: remaining would be 00 3F... but that's only 2 bytes
# Actually: 00 [0D 0A] 3F -> 00 3F? That's too short.
# Maybe: 00 [0D] 0A 3F, and 0D was inserted? Then original: 00 0A 3F = 2623 pixels?
# Or maybe: 00 00 0A 3F was original? No.
# Let me check: common heights: 1920=0x780, 2000=0x7D0, 2340=0x924
# 0x0A3F = 2623 (close to nothing standard)
# What if the actual height bytes are 00 00 0A 3F? That's still 2623.
# What about 00 00 07 80 (1920)? Then 0D 0A replaced 00 07 somehow?
offset += 12 + length
if offset > 200:
break
# Let me search for CR LF (0D 0A) in the PNG data
crlf_count = 0
for i in range(len(data) - 1):
if data[i] == 0x0D and data[i+1] == 0x0A:
crlf_count += 1
if crlf_count <= 10:
print(f"CR LF at offset {i}: context: {' '.join(f'{b:02X}' for b in data[max(0,i-4):i+6])}")
print(f"\nTotal CR LF sequences: {crlf_count}")
# Also count lone 0A
lone_lf = 0
for i in range(len(data)):
if data[i] == 0x0A and (i == 0 or data[i-1] != 0x0D):
lone_lf += 1
print(f"Lone LF: {lone_lf}")
# Count lone 0D
lone_cr = 0
for i in range(len(data)):
if data[i] == 0x0D and (i == len(data)-1 or data[i+1] != 0x0A):
lone_cr += 1
print(f"Lone CR: {lone_cr}")

View File

@@ -1 +0,0 @@
File size exceeded the configured limit of 20 MB.

View File

@@ -1,82 +0,0 @@
"""
Debug: compare raw bytes and re-encoded bytes.
Also try to interpret the file as a regular PNG by skipping the BOM.
"""
import sys
path = r"D:\Yuzu-GCA\screen.png"
with open(path, 'rb') as f:
raw = f.read()
print(f"Total size: {len(raw)} bytes")
# Decode as UTF-16 LE
text = raw.decode('utf-16-le')
print(f"Decoded text length: {len(text)} chars")
# Re-encode
reencoded = text.encode('utf-16-le')
print(f"Re-encoded size: {len(reencoded)} bytes")
# Compare
if raw == reencoded:
print("raw == reencoded: EXACT MATCH")
else:
print(f"raw == reencoded: DIFFER")
# Find first difference
for i in range(min(len(raw), len(reencoded))):
if raw[i] != reencoded[i]:
print(f"First diff at byte {i}: raw={raw[i]:02x}, reencoded={reencoded[i]:02x}")
print(f" Context raw: {raw[max(0,i-5):i+10].hex(' ')}")
print(f" Context re: {reencoded[max(0,i-5):i+10].hex(' ')}")
break
# Check if raw starts with BOM
if raw[:2] == b'\xff\xfe':
print("\nFile starts with UTF-16 LE BOM")
elif raw[:2] == b'\xfe\xff':
print("\nFile starts with UTF-16 BE BOM")
# Check reencoded start
if reencoded[:2] == b'\xff\xfe':
print("Re-encoded starts with UTF-16 LE BOM")
elif reencoded[:2] == b'\xfe\xff':
print("Re-encoded starts with UTF-16 BE BOM")
# Try: what if we treat raw[2:] as the UTF-16 LE data (without the BOM)?
# This would be the case if the file had BOM added later
print(f"\nraw[2:4] = {raw[2:4].hex(' ')}")
text2 = raw[2:].decode('utf-16-le')
print(f"Decoded text2 (skipping BOM) length: {len(text2)} chars")
print(f"text2 first 50 chars: {text2[:50]}")
# Try: what if the entire file is just raw PNG with a bogus BOM?
# PNG magic: 89 50 4E 47 0D 0A 1A 0A
# Our file: ff fe 52 58 4e 00 47 00 0d 00 0a 00 ...
# Maybe each byte X became X*256 + something?
# Or maybe the file is a hex dump?
print("\n=== Trying hex dump interpretation ===")
# Check if the text looks like a hex dump (pairs of hex digits)
hex_pattern = all(c in '0123456789abcdefABCDEF \n\r\t' for c in text[:1000])
print(f"First 1000 chars look like hex dump: {hex_pattern}")
# Final: Is there any actual readable content?
# Let's look for sequences of Latin chars that might form words
import re
# Words with 5+ letters
real_words = re.findall(r'[A-Za-z]{5,}', text)
# Filter out those that look like random binary
likely_real = [w for w in real_words if w.lower() in [
'update', 'download', 'install', 'error', 'success', 'failed',
'system', 'android', 'version', 'checking', 'package', 'verify'
] or w in ['IHDR', 'IDAT', 'IEND', 'sRGB', 'sBIT', 'pHYs', 'iTXt', 'tEXt', 'zTXt']]
print(f"\nKnown words found: {likely_real}")
# Check for PNG chunk names
png_chunks = ['IHDR', 'PLTE', 'IDAT', 'IEND', 'cHRM', 'gAMA', 'iCCP', 'sBIT', 'sRGB',
'bKGD', 'hIST', 'tRNS', 'pHYs', 'sPLT', 'tIME', 'iTXt', 'tEXt', 'zTXt']
for chunk in png_chunks:
cnt = text.count(chunk)
if cnt > 0:
print(f" PNG chunk '{chunk}': {cnt} times")

View File

@@ -1,15 +0,0 @@
import sys
import io
# Set stdout to UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen.png", 'rb') as f:
data = f.read()
print(f"Total bytes: {len(data)}")
# Try UTF-16 LE decode
text = data.decode('utf-16-le', errors='replace')
print("=== Decoded as UTF-16 LE (first 10000 chars) ===")
print(text[:10000])

View File

@@ -1,129 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen.png", 'rb') as f:
data = f.read()
# The file is UTF-16 LE encoded with some corruption.
# Let's fix the known issues:
# 1. Replace the corrupted first char (U+5852 from bytes 52 58)
# with correct U+0089 (89 00) + U+0050 (50 00)
# But wait - in UTF-16 LE, each char takes 2 bytes.
# We need to replace 2 bytes (52 58) with 4 bytes (89 00 50 00)
# Actually, let's approach differently:
# Convert the whole thing from UTF-16 LE to raw bytes,
# but fix the known corruptions.
# Step 1: parse as UTF-16 LE (skip BOM)
raw = data[2:]
# Each 2 bytes is one UTF-16 LE char
chars = []
for i in range(0, len(raw), 2):
if i + 1 < len(raw):
ch = raw[i] | (raw[i+1] << 8)
chars.append(ch)
print(f"Total chars: {len(chars)}")
# Step 2: Fix corruptions in the char array
# Char 0: U+5852 -> should be U+0089 (first PNG byte)
# But U+0089 is one char, and we also need U+0050 for 'P'
# So we need to insert a char
# The corruption pattern:
# - Char 0 (U+5852) replaces two chars: U+0089 and U+0050
# - Char 6 (U+000D) should be U+000A
# - Char 7 (U+000A) is extra, should be removed
# - Char 12 (U+000A) is extra, should be removed
# Let's verify by looking at what we expect:
# Expected chars for correct PNG:
# [0]=0x0089, [1]=0x0050('P'), [2]=0x004E('N'), [3]=0x0047('G'),
# [4]=0x000D, [5]=0x000A, [6]=0x001A, [7]=0x000A (signature end)
# [8]=0x0000, [9]=0x0000, [10]=0x0000, [11]=0x000D (IHDR length=13)
# [12]=0x0049('I'), [13]=0x0048('H'), [14]=0x0044('D'), [15]=0x0052('R')
# Actual chars:
# [0]=0x5852, [1]=0x004E, [2]=0x0047, [3]=0x000D, [4]=0x000A,
# [5]=0x001A, [6]=0x000D, [7]=0x000A, [8]=0x0000, [9]=0x0000,
# [10]=0x0000, [11]=0x000D, [12]=0x000A, [13]=0x0049, [14]=0x0048, ...
# Fix:
# Replace char 0 (U+5852) with U+0089
# Insert U+0050 at position 1
# Change char 6 from 0x0D to 0x0A
# Delete char 7 (extra 0x0A)
# Delete char 12 (extra 0x0A) - wait, this is after IHDR length
fixed_chars = []
# Fix char 0: split U+5852 -> U+0089, U+0050
fixed_chars.append(0x0089) # PNG first byte
fixed_chars.append(0x0050) # 'P'
# Copy chars 1-5 normally (but adjust indices)
# Original char 1 (U+004E='N') -> fixed index 2
# Original char 2 (U+0047='G') -> fixed index 3
# Original char 3 (U+000D) -> fixed index 4
# Original char 4 (U+000A) -> fixed index 5
# Original char 5 (U+001A) -> fixed index 6
fixed_chars.append(chars[1]) # 'N'
fixed_chars.append(chars[2]) # 'G'
fixed_chars.append(chars[3]) # '\r'
fixed_chars.append(chars[4]) # '\n'
fixed_chars.append(chars[5]) # '\x1a'
# Fix char 6: change from 0x0D to 0x0A
fixed_chars.append(0x000A) # Should be '\n' instead of '\r'
# Skip char 7 (extra 0x0A) - don't add it
# Add chars 8-11 (IHDR length = 0x0000000D)
fixed_chars.append(chars[8]) # 0x0000
fixed_chars.append(chars[9]) # 0x0000
fixed_chars.append(chars[10]) # 0x0000
fixed_chars.append(chars[11]) # 0x000D
# Skip char 12 (extra 0x0A) - don't add it
# Add the rest from char 13 onwards
for i in range(13, len(chars)):
fixed_chars.append(chars[i])
print(f"Fixed chars: {len(fixed_chars)}")
# Step 3: Convert fixed chars back to UTF-16 LE bytes
fixed_bytes = bytearray()
for ch in fixed_chars:
fixed_bytes.append(ch & 0xFF) # low byte
fixed_bytes.append((ch >> 8) & 0xFF) # high byte
print(f"Fixed bytes: {len(fixed_bytes)}")
# Step 4: Extract original PNG bytes (every other byte, starting from first)
# In UTF-16 LE, for ASCII chars (< 0x80), the low byte is the char and high byte is 00
# But for non-ASCII chars like U+0089, the low byte is 0x89 and high byte is 0x00
# So we take the low byte of each char
png_bytes = bytearray()
for ch in fixed_chars:
png_bytes.append(ch & 0xFF)
print(f"PNG bytes: {len(png_bytes)}")
print(f"First 32 PNG bytes: {' '.join(f'{b:02X}' for b in png_bytes[:32])}")
# Check PNG signature
if png_bytes[:8] == b'\x89PNG\r\n\x1a\n':
print("✓ Valid PNG signature!")
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'wb') as f:
f.write(png_bytes)
print("Saved screen_fixed.png")
else:
print(f"✗ Invalid signature: {' '.join(f'{b:02X}' for b in png_bytes[:8])}")
# Try to see if we can find where it goes wrong
expected = b'\x89PNG\r\n\x1a\n'
for i in range(8):
if png_bytes[i] != expected[i]:
print(f" Mismatch at byte {i}: got {png_bytes[i]:02X}, expected {expected[i]:02X}")

View File

@@ -1,66 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'rb') as f:
data = f.read()
print(f"Original size: {len(data)}")
# The file has been corrupted by text-mode LF->CRLF conversion.
# Every 0x0A byte in the original PNG has been changed to 0x0D 0x0A.
# We need to revert this: change 0x0D 0x0A back to 0x0A.
# But the PNG signature contains a legitimate 0x0D 0x0A (at offset 4-5),
# which should remain as-is.
# Strategy: replace 0D 0A -> 0A, EXCEPT for the known legitimate ones
# Legitimate: offset 4-5 (PNG signature "\r\n")
# Actually, let's just do the replacement everywhere and then fix the signature
fixed = bytearray()
i = 0
while i < len(data):
if i < len(data) - 1 and data[i] == 0x0D and data[i+1] == 0x0A:
# CR LF found - replace with just LF
fixed.append(0x0A)
i += 2
else:
fixed.append(data[i])
i += 1
fixed = bytes(fixed)
print(f"After CRLF->LF fix: {len(fixed)}")
# Now fix the PNG signature (offset 4-5 should be 0D 0A, but we changed it to 0A)
# The correct PNG signature: 89 50 4E 47 0D 0A 1A 0A
# We need to fix offset 4 to be 0D again
if fixed[4] == 0x0A: # Should be 0x0D
fixed = fixed[:4] + b'\x0D' + fixed[5:]
print("Fixed PNG signature byte 4 (0D)")
print(f"First 16 bytes: {' '.join(f'{b:02X}' for b in fixed[:16])}")
# Check IHDR
offset = 8
length = int.from_bytes(fixed[offset:offset+4], 'big')
chunk_type = fixed[offset+4:offset+8].decode('ascii', errors='replace')
print(f"IHDR: type={chunk_type}, length={length}")
w = int.from_bytes(fixed[offset+8:offset+12], 'big')
h = int.from_bytes(fixed[offset+12:offset+16], 'big')
bit_depth = fixed[offset+16]
color_type = fixed[offset+17]
print(f" Width={w}, Height={h}, BitDepth={bit_depth}, ColorType={color_type}")
# Save
with open(r"D:\Yuzu-GCA\screen_fixed2.png", 'wb') as f:
f.write(fixed)
print("Saved screen_fixed2.png")
# Try to open with PIL
from PIL import Image
try:
img = Image.open(r"D:\Yuzu-GCA\screen_fixed2.png")
print(f"Success! Format={img.format}, Size={img.size}, Mode={img.mode}")
except Exception as e:
print(f"PIL error: {e}")

3
index.js Normal file
View File

@@ -0,0 +1,3 @@
// Root entry point for Expo dev server
// Redirect to client-android
import './packages/client-android/index';

View File

@@ -1,48 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from PIL import Image
img = Image.open(r"D:\Yuzu-GCA\screen_fixed.png")
print(f"Format: {img.format}")
print(f"Size: {img.size}")
print(f"Mode: {img.mode}")
# Try OCR
try:
import pytesseract
text = pytesseract.image_to_string(img, lang='eng+chi_sim')
print("\n=== pytesseract OCR result ===")
print(text)
except ImportError:
print("pytesseract not available")
# Also try Windows OCR
try:
import asyncio
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.graphics.imaging import BitmapDecoder, SoftwareBitmap
from winsdk.windows.storage.streams import RandomAccessStreamReference
from winsdk.windows.globalization import Language
import os
async def ocr_windows():
engine = OcrEngine.try_create_from_user_profile_languages()
if not engine:
engine = OcrEngine.try_create_from_language(Language("en"))
path = os.path.abspath(r"D:\Yuzu-GCA\screen_fixed.png")
file_stream = await RandomAccessStreamReference.create_from_file(path).open_read_async()
decoder = await BitmapDecoder.create_async(file_stream)
bitmap = await decoder.get_software_bitmap_async()
result = await engine.recognize_async(bitmap)
print("\n=== Windows OCR result ===")
for line in result.lines:
print(line.text)
asyncio.run(ocr_windows())
except Exception as e:
print(f"Windows OCR failed: {e}")

View File

@@ -1,55 +0,0 @@
"""
OCR script to extract text from screen.png
Attempts to use pytesseract if available, otherwise falls back to basic image info.
"""
import sys
import os
image_path = r"D:\Yuzu-GCA\screen.png"
# First, try to get basic image info
try:
from PIL import Image
img = Image.open(image_path)
print(f"Image size: {img.size}")
print(f"Image mode: {img.mode}")
print(f"Image format: {img.format}")
print("---")
except ImportError:
print("Pillow not available, trying basic file read...")
# Can't do much without PIL
# Try pytesseract
try:
import pytesseract
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
print("OCR Result (pytesseract):")
print(text)
except ImportError:
print("pytesseract not available")
# Try subprocess call to system tesseract
import subprocess
try:
result = subprocess.run(
['tesseract', image_path, 'stdout', '-l', 'chi_sim+eng'],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
print("OCR Result (system tesseract):")
print(result.stdout)
else:
print(f"Tesseract error: {result.stderr}")
except FileNotFoundError:
print("System tesseract not found either")
except Exception as e:
print(f"Error running tesseract: {e}")
# If nothing works, try reading raw pixels to detect if it's a text screenshot
try:
from PIL import Image
img = Image.open(image_path)
# Print first few pixel values to understand the image
pixels = list(img.getdata())[:20]
print(f"\nFirst 20 pixel values: {pixels}")
except:
pass

View File

@@ -1,23 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from PIL import Image
img = Image.open(r"D:\Yuzu-GCA\screen2.png")
print(f"Format: {img.format}, Size: {img.size}, Mode: {img.mode}")
# Try pytesseract OCR
try:
import pytesseract
# Configure tesseract path if needed
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
print("\n=== OCR Result ===")
print(text)
except ImportError:
print("\npytesseract not available")
except Exception as e:
print(f"\nOCR error: {e}")

View File

@@ -1,43 +0,0 @@
# PowerShell script using Windows built-in OCR
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Runtime.WindowsRuntime
Add-Type -AssemblyName Windows.Foundation
Add-Type -AssemblyName Windows.Graphics.Imaging
Add-Type -AssemblyName Windows.Media.Ocr
$winRtTypes = [Windows.Media.Ocr.OcrEngine]::GetType()
# Load the image
$imagePath = "D:\Yuzu-GCA\screen2.png"
$bitmap = [System.Drawing.Bitmap]::FromFile($imagePath)
Write-Host "Image size: $($bitmap.Width) x $($bitmap.Height)"
# Convert to stream
$ms = New-Object System.IO.MemoryStream
$bitmap.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$bitmap.Dispose()
# Create BitmapDecoder
$ms.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
$randomAccessStream = New-Object Windows.Storage.Streams.InMemoryRandomAccessStream
$outputStream = $randomAccessStream.GetOutputStreamAt(0)
$dataWriter = New-Object Windows.Storage.Streams.DataWriter($outputStream)
$bytes = $ms.ToArray()
$dataWriter.WriteBytes($bytes)
$dataWriter.StoreAsync().GetAwaiter().GetResult() | Out-Null
$dataWriter.FlushAsync().GetAwaiter().GetResult() | Out-Null
$ms.Dispose()
$decoder = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($randomAccessStream).GetAwaiter().GetResult()
$softwareBitmap = [Windows.Graphics.Imaging.SoftwareBitmap]::Convert($decoder.GetSoftwareBitmapAsync().GetAwaiter().GetResult(), [Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8, [Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied)
# Perform OCR
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
$result = $engine.RecognizeAsync($softwareBitmap).GetAwaiter().GetResult()
# Output results
Write-Host "Recognized text:"
Write-Host "===================="
foreach ($line in $result.Lines) {
Write-Host $line.Text
}

View File

@@ -1,51 +0,0 @@
"""
OCR script to extract text from screen.png
"""
import sys
import os
image_path = r"D:\Yuzu-GCA\screen.png"
# Try pytesseract first
try:
import pytesseract
from PIL import Image
img = Image.open(image_path)
text = pytesseract.image_to_string(img, lang='eng+chi_sim')
print("=== pytesseract OCR result ===")
print(text)
sys.exit(0)
except ImportError:
print("pytesseract not available, trying alternative...")
# Try Windows OCR via winrt
try:
import asyncio
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.graphics.imaging import BitmapDecoder
from winsdk.windows.storage.streams import RandomAccessStreamReference
from winsdk.windows.globalization import Language
async def ocr_windows():
# Get the default OCR engine for Chinese
engine = OcrEngine.try_create_from_user_profile_languages()
if not engine:
# Fallback to English
engine = OcrEngine.try_create_from_language(Language("en"))
# Open the image file
file_stream = await RandomAccessStreamReference.create_from_file(image_path).open_read_async()
decoder = await BitmapDecoder.create_async(file_stream)
bitmap = await decoder.get_software_bitmap_async()
result = await engine.recognize_async(bitmap)
print("=== Windows OCR result ===")
for line in result.lines:
print(line.text)
asyncio.run(ocr_windows())
sys.exit(0)
except ImportError:
print("winsdk not available either")
print("No OCR engine available. Please install pytesseract or Windows SDK.")

View File

@@ -1,48 +0,0 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
print("Attempting Windows OCR...")
try:
import asyncio
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.graphics.imaging import BitmapDecoder
from winsdk.windows.storage.streams import RandomAccessStreamReference
from winsdk.windows.globalization import Language
import os
async def ocr():
path = os.path.abspath(r"D:\Yuzu-GCA\screen2.png")
print(f"Opening: {path}")
# Try to get engine for Chinese
engine = OcrEngine.try_create_from_user_profile_languages()
if not engine:
print("No user profile languages, trying Chinese...")
engine = OcrEngine.try_create_from_language(Language("zh-Hans"))
if not engine:
print("Trying English...")
engine = OcrEngine.try_create_from_language(Language("en"))
if not engine:
print("No OCR engine available")
return
print(f"Using OCR engine: {engine.recognizer_language.display_name}")
file_stream = await RandomAccessStreamReference.create_from_file(path).open_read_async()
decoder = await BitmapDecoder.create_async(file_stream)
bitmap = await decoder.get_software_bitmap_async()
result = await engine.recognize_async(bitmap)
print(f"\n=== Windows OCR Result ({result.lines.size} lines) ===")
for line in result.lines:
print(line.text)
asyncio.run(ocr())
except ImportError as e:
print(f"winsdk not available: {e}")
except Exception as e:
print(f"Error: {e}")

View File

@@ -1,18 +1,44 @@
# OTA 文件夹 # OTA 热更新目录
此目录存放 manifest 文件。manifest-stable.json 是 CI 构建时动态生成的产物, ## 文件说明
也提交到仓库通过 raw URL 供客户端拉取。
## 文件 | 文件 | 用途 |
|------|------|
| `version.txt` | v5 嵌入式初始化——格式: `版本\|URL\|SHA256` |
| `h5-manifest-stable.json` | 发行渠道stable——格式: `{latest, min_version, url, sha256}` |
| `h5-manifest-beta.json` | 测试渠道beta |
| `h5-manifest-nightly.json` | 开发渠道nightly |
| `h5/index-vN.html` | 可下载的热更新页面 |
- `gen-manifest.sh` — 构建后运行,计算 APK SHA256 并生成 manifest-stable.json ## 推送补丁
- `manifest-stable.json` — stable 渠道
- `manifest-beta.json` — beta 渠道
- `manifest-nightly.json` — nightly 渠道
## 流程 ```bash
# 更新所有渠道到 v7
cd ota
bash push-hotfix.sh 7 all
# 只更新 nightly 渠道
bash push-hotfix.sh 7 nightly
# 手动推送
git add ota/
git commit -m "hotfix: v7"
git push
``` ```
CI 构建 APK → ./ota/gen-manifest.sh → git push manifest-stable.json
手机 App → raw URL 拉取 manifest → 比较版本 → 下载 APK → SHA256 校验 ## 格式
**h5-manifest-*.json:**
```json
{
"latest": "0.1.0-dav-android-6",
"min_version": "0.1.0-dav-android-5",
"url": "https://git.childish-ghost.com/.../ota/h5/index-v6.html",
"sha256": "33a16632..."
}
```
**version.txt:**
```
0.1.0-dav-android-6|https://.../ota/h5/index-v6.html|33a16632...
``` ```

View File

@@ -1,38 +1,54 @@
#!/bin/bash #!/bin/bash
# OTA manifest 生成脚本 # gen-manifest.sh — 生成 APK 发行版 manifest
# 用法: ./ota/gen-manifest.sh <versionName> <versionCode> <apk-path> # 在 Android Studio 构建 Release APK 后运行
# 构建号规则: 每次生成APK时 versionCode +1, 对应 git tag vX.Y.Z-dav-android-N # 用法: bash gen-manifest.sh 6 stable
# CI 构建时自动运行, SHA256动态填入, 生成 manifest-stable.json 后上传到 Gitea Release # bash gen-manifest.sh 6 all
VERSION_NAME="${1:-0.1.0}" set -e
VERSION_CODE="${2:-7}"
APK_PATH="${3:-packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-7-debug.apk}" VER="$1"
RELEASE_TAG="v${VERSION_NAME}-dav-android-${VERSION_CODE}" shift
APK_NAME="v${VERSION_NAME}-dav-android-${VERSION_CODE}-debug.apk" CHANNELS=("$@")
if [ -z "$VER" ] || [ ${#CHANNELS[@]} -eq 0 ]; then
echo "用法: $0 <构建号> <渠道...>"
echo "示例: $0 6 stable beta nightly"
exit 1
fi
OTA_DIR="$(dirname "$0")"
APK_PATH="$OTA_DIR/../packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-${VER}-debug.apk"
BASE="https://git.childish-ghost.com/LukeMackin/Yuzu-GCA"
TAG="v0.1.0-dav-android-${VER}"
APK_NAME="v0.1.0-dav-android-${VER}-debug.apk"
DL_URL="${BASE}/releases/download/${TAG}/${APK_NAME}"
if [ ! -f "$APK_PATH" ]; then
echo "APK not found: $APK_PATH"
exit 1
fi
SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1) SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1)
SIZE=$(stat -c%s "$APK_PATH") echo "APK : $APK_PATH"
echo "SHA256: $SHA256"
cat > ota/manifest-stable.json <<JSON for CH in "${CHANNELS[@]}"; do
{ if [ "$CH" = "all" ]; then
"version": "${VERSION_NAME}", for CH in stable beta nightly; do
"builds": { cat > "$OTA_DIR/manifest-${CH}.json" <<EOF
"android": { {"version":"0.1.0-dav-android-${VER}","versionCode":${VER},"url":"${DL_URL}","sha256":"${SHA256}"}
"type": "apk", EOF
"tag": "${RELEASE_TAG}", echo " ✓ manifest-${CH}.json"
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/${RELEASE_TAG}/${APK_NAME}", done
"sha256": "${SHA256}", break
"size": ${SIZE}, fi
"channel": "stable", cat > "$OTA_DIR/manifest-${CH}.json" <<EOF
"minVersion": "${VERSION_NAME}" {"version":"0.1.0-dav-android-${VER}","versionCode":${VER},"url":"${DL_URL}","sha256":"${SHA256}"}
} EOF
} echo " ✓ manifest-${CH}.json"
} done
JSON
echo "Generated: ota/manifest-stable.json"
echo " tag: ${RELEASE_TAG}"
echo " sha256: ${SHA256}"
echo " size: ${SIZE}"
echo "" echo ""
echo "Upload: curl -X POST 'https://git.childish-ghost.com/api/v1/repos/LukeMackin/Yuzu-GCA/releases/\${RELEASE_ID}/assets?name=manifest-stable.json' -H 'Authorization: token \$TOKEN' -H 'Content-Type: application/json' --data-binary @ota/manifest-stable.json" echo "Next: git tag $TAG && git push --tags"
echo " Gitea Release → upload APK"
echo " bash push-hotfix.sh $VER stable # H5同步"

1
ota/h5/index-beta.html Normal file
View File

@@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=index-stable.html"></head><body></body></html>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=index-stable.html"></head><body></body></html>

80
ota/h5/index-stable.html Normal file
View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:8px 16px 16px;margin:0;overflow-x:hidden}
h1{font-size:18px;color:#38bdf8}
.card{background:#1e293b;border-radius:8px;padding:16px;margin:12px 0}
.card-title{font-size:14px;color:#94a3b8;margin-bottom:8px}
.btn{display:block;width:100%;padding:14px;margin:8px 0;border:none;border-radius:8px;font-size:16px;cursor:pointer}
.btn-check{background:#1e40af;color:#fff}
.btn-download{background:#15803d;color:#fff;display:none}
.btn-download.show{display:block}
.log{background:#0f172a;padding:12px;border-radius:8px;margin-top:12px;max-height:200px;overflow-y:auto;font-size:11px;white-space:pre-wrap;color:#94a3b8}
.ver{color:#94a3b8;font-size:12px;margin:4px 0}
.gear{position:absolute;top:20px;right:16px;font-size:20px;cursor:pointer;color:#94a3b8}
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);z-index:99}
.modal.show{display:flex;align-items:center;justify-content:center}
.modal-box{background:#1e293b;border-radius:12px;padding:24px;width:80%;max-width:360px}
.modal-title{font-size:16px;color:#38bdf8;margin-bottom:16px}
.ch-opt{display:block;width:100%;padding:12px;margin:6px 0;border:1px solid #334155;border-radius:6px;background:transparent;color:#e2e8f0;font-size:14px;cursor:pointer;text-align:left}
.ch-opt.on{border-color:#38bdf8;background:#1e40af40}
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;margin-left:8px}
.badge-new{background:#dc2626;color:#fff}
</style></head><body>
<div class="gear" onclick="openSettings()">&#9881;</div>
<h1>Yuzu GCA</h1>
<div class="ver"><span id="ver"></span><span id="badge" class="badge badge-new" style="display:none">NEW</span></div>
<div class="card"><div class="card-title">Channel: <span id="chLabel">Stable</span></div></div>
<button class="btn btn-check" onclick="checkUpdate()">Check Update</button>
<button class="btn btn-download" id="btnApply" onclick="applyUpdate()">Download Update</button>
<div class="log" id="log"></div>
<div class="modal" id="modal">
<div class="modal-box">
<div class="modal-title">Select Channel</div>
<button class="ch-opt on" onclick="setChannel('stable')">Stable</button>
<button class="ch-opt" onclick="setChannel('beta')">Beta</button>
<button class="ch-opt" onclick="setChannel('nightly')">Nightly</button>
<button class="btn btn-check" style="margin-top:12px" onclick="closeSettings()">Close</button>
</div>
</div>
<script>
var channel=localStorage.getItem('gca_channel')||'stable';
document.getElementById('chLabel').innerText=(localStorage.getItem('gca_channel')||'stable').charAt(0).toUpperCase()+(localStorage.getItem('gca_channel')||'stable').slice(1);
var dlUrl='',dlVer='',dlSha='';
function log(m){var l=document.getElementById('log');l.innerText+=m+'\n';l.scrollTop=l.scrollHeight}
function openSettings(){
var m=document.getElementById('modal');m.classList.add('show');
document.querySelectorAll('.ch-opt').forEach(function(b){b.classList.toggle('on',b.textContent.toLowerCase()===channel)})
}
function closeSettings(){document.getElementById('modal').classList.remove('show')}
function setChannel(ch){
channel=ch;localStorage.setItem('gca_channel',ch);
document.getElementById('chLabel').innerText=ch.charAt(0).toUpperCase()+ch.slice(1);
document.querySelectorAll('.ch-opt').forEach(function(b){b.classList.toggle('on',b.textContent.toLowerCase()===ch)});
closeSettings();checkUpdate()
}
function checkUpdate(){
log('channel:'+channel);
var j=JSON.parse(Android.fetchManifest(channel));
if(j.error){log('ERR:'+j.error);return}
var h5=j.h5;log('latest:'+h5.latest+' min:'+h5.min_version);
var v=(h5.min_version||'').replace(/[^0-9]/g,''),my=(document.getElementById('ver').innerText||'').replace(/[^0-9]/g,'');
if(parseInt(my||'0')<parseInt(v||'0')){log('ERR:APK too old (need v'+h5.min_version+')');return}
dlUrl=h5.url;dlSha=h5.sha256||'';
if(!dlSha||/^(0{64}|TBD)/.test(dlSha)){log('ERR:channel no build');return}
if(h5.latest!==document.getElementById('ver').innerText&&h5.latest!=='v'+document.getElementById('ver').innerText){
dlVer=h5.latest;document.getElementById('btnApply').classList.add('show');log('***NEW VERSION***')}
else log('up to date')
}
function applyUpdate(){
log('download...');
var t=Android.fetchText('h5/index-stable.html');
if(typeof t!=='string'||t.startsWith('ERROR:')){log('ERR:'+t);return}
var h=Android.sha256(t);
if(h!==dlSha){log('SHA256 MISMATCH!');return}
Android.saveCache(t);log('OK! restart app')
}
document.getElementById('ver').innerText=Android.fetchManifest(channel).match(/"latest":"([^"]+)"/)?.[1]||channel;
</script></body></html>

View File

@@ -1 +0,0 @@
{"version":"0.1.0","builds":{}}

View File

@@ -1 +0,0 @@
{"version":"0.1.0","builds":{"android":{"type":"apk","tag":"v0.1.0-dav-android-8","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/v0.1.0-dav-android-8-debug.apk","sha256":"b2451a398ea2dd34e585c5ec4529d6654451c9b9ab4ddf4a25ec9b032f0ac2bf","size":3506569,"channel":"nightly","minVersion":"0.1.0"}}}

View File

@@ -1 +0,0 @@
{"version":"0.1.0","builds":{}}

1
ota/manifest.json Normal file
View File

@@ -0,0 +1 @@
{"stable":{"h5":{"latest":"0.1.0-dav-android-6","min_version":"0.1.0-dav-android-5","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/h5/index-stable.html","sha256":"ae7db3b84ad018fbc4cc938c30db482dd4c3cad47aab6db81c8a6716cfadc844"},"apk":{"version":"0.1.0-dav-android-5","versionCode":5,"url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-5/v0.1.0-dav-android-5-debug.apk","sha256":"56b0e384f53015b0a1a5eb2507fd792dfc31fe44aff40342cd8a8d3bd5c1e14a"}},"beta":{"h5":{"latest":"0.1.0-dav-android-6","min_version":"0.1.0-dav-android-5","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/h5/index-stable.html","sha256":"ae7db3b84ad018fbc4cc938c30db482dd4c3cad47aab6db81c8a6716cfadc844"},"apk":{"version":"0.1.0-dav-android-5","versionCode":5,"url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-5/v0.1.0-dav-android-5-debug.apk","sha256":"56b0e384f53015b0a1a5eb2507fd792dfc31fe44aff40342cd8a8d3bd5c1e14a"}},"nightly":{"h5":{"latest":"0.1.0-dav-android-7","min_version":"0.1.0-dav-android-5","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/h5/index-stable.html","sha256":"ae7db3b84ad018fbc4cc938c30db482dd4c3cad47aab6db81c8a6716cfadc844"},"apk":{"version":"0.1.0-dav-android-5","versionCode":5,"url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-5/v0.1.0-dav-android-5-debug.apk","sha256":"56b0e384f53015b0a1a5eb2507fd792dfc31fe44aff40342cd8a8d3bd5c1e14a"}}}

79
ota/push-hotfix.sh Normal file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
# push-hotfix.sh — 推送 H5 热更新补丁到 Gitea
#
# 用法:
# ./push-hotfix.sh 7 stable # 只更新 stable 渠道
# ./push-hotfix.sh 7 all # 更新全部三个渠道
# ./push-hotfix.sh 7 beta nightly # 更新指定渠道
#
# 流程:
# 1. 创建 ota/h5/index-v{VER}.html如果不存在则从模板生成
# 2. 计算 SHA256
# 3. 更新 ota/h5-manifest-{CHANNEL}.json
# 4. 如果是稳定版,更新 ota/version.txt
# 5. git add + commit + push
set -e
VER="$1"
shift
CHANNELS=("$@")
if [ -z "$VER" ] || [ ${#CHANNELS[@]} -eq 0 ]; then
echo "用法: $0 <版本号> <渠道...>"
echo "示例: $0 7 stable beta nightly"
exit 1
fi
OTA_DIR="$(dirname "$0")"
H5_DIR="$OTA_DIR/h5"
BASE_URL="https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota"
# 1. 创建 HTML 文件(如果不存在)
HTML_FILE="$H5_DIR/index-v${VER}.html"
if [ ! -f "$HTML_FILE" ]; then
echo "→ 请手动创建 $HTML_FILE 或从 index-v6.html 复制修改"
exit 1
fi
# 2. 计算 SHA256
SHA256=$(sha256sum "$HTML_FILE" | cut -d' ' -f1)
echo "SHA256: $SHA256"
# 3. 更新 manifest
FULL_URL="${BASE_URL}/h5/index-v${VER}.html"
for CH in "${CHANNELS[@]}"; do
if [ "$CH" = "all" ]; then
for CH in stable beta nightly; do
MANIFEST="$OTA_DIR/h5-manifest-${CH}.json"
cat > "$MANIFEST" <<EOF
{"latest":"0.1.0-dav-android-${VER}","min_version":"0.1.0-dav-android-5","url":"${FULL_URL}","sha256":"${SHA256}"}
EOF
echo " ✓ h5-manifest-${CH}.json"
done
break
fi
MANIFEST="$OTA_DIR/h5-manifest-${CH}.json"
cat > "$MANIFEST" <<EOF
{"latest":"0.1.0-dav-android-${VER}","min_version":"0.1.0-dav-android-5","url":"${FULL_URL}","sha256":"${SHA256}"}
EOF
echo " ✓ h5-manifest-${CH}.json"
done
# 4. 稳定版更新 version.txt
if [[ " ${CHANNELS[*]} " =~ " stable " ]] || [[ " ${CHANNELS[*]} " =~ " all " ]]; then
echo "0.1.0-dav-android-${VER}|${FULL_URL}|${SHA256}" > "$OTA_DIR/version.txt"
echo " ✓ version.txt"
fi
# 5. git push
echo ""
echo "commit & push?"
read -p "(y/N) " yn
if [ "$yn" = "y" ] || [ "$yn" = "Y" ]; then
git add "$OTA_DIR"
git commit -m "hotfix: v${VER}${CHANNELS[*]}"
git push
echo "✓ pushed"
fi

1
ota/version.txt Normal file
View File

@@ -0,0 +1 @@
https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/manifest.json

View File

@@ -1,72 +1,286 @@
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { StyleSheet, Text, View, ActivityIndicator } from 'react-native'; import {
import { onAppStartCheckUpdate } from './src/updater/index'; StyleSheet, Text, View, TextInput, TouchableOpacity,
ScrollView, Switch, FlatList, KeyboardAvoidingView, Platform,
} from 'react-native';
import { DEFAULT_GATEWAY_CONFIG } from '@yuzu-gca/shared';
/** Android Client: Chat + Settings + Connection */
type Msg = { role: 'user' | 'assistant'; text: string; ts: number };
export default function App() { export default function App() {
const [status, setStatus] = useState<'checking' | 'up-to-date' | 'updating' | 'error'>('checking'); const [tab, setTab] = useState<'chat' | 'terminal' | 'settings' | 'device' | 'connection'>('chat');
const [message, setMessage] = useState('正在检查更新...');
useEffect(() => { // Chat state
(async () => { const [messages, setMessages] = useState<Msg[]>([]);
const [input, setInput] = useState('');
const [sending, setSending] = useState(false);
// Settings state
const [gatewayUrl, setGatewayUrl] = useState(DEFAULT_GATEWAY_CONFIG.url);
const [deviceName, setDeviceName] = useState('Android Device');
const [autoConnect, setAutoConnect] = useState(true);
const [heartbeatInterval, setHeartbeatInterval] = useState(String(DEFAULT_GATEWAY_CONFIG.heartbeatInterval / 1000));
const [logLevel, setLogLevel] = useState('info');
// Connection state
const [connStatus, setConnStatus] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected');
// Terminal state
const [termCmd, setTermCmd] = useState('');
const [termOut, setTermOut] = useState('Execute remote commands via Gateway MCP.\nType a shell command and press Execute.\n');
const [termRunning, setTermRunning] = useState(false);
const runTermCmd = async () => {
if (!termCmd.trim() || termRunning) return;
const c = termCmd.trim(); setTermCmd(''); setTermRunning(true);
setTermOut(o => o + `$ ${c}\n`);
try { try {
const result = await onAppStartCheckUpdate(); const r = await fetch(`${gatewayUrl}/tools/exec`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({command:c}), signal:AbortSignal.timeout(30000) });
if (result.jsUpdate) { if (!r.ok) throw new Error(`HTTP ${r.status}`);
setStatus('updating'); const d = await r.json() as any;
setMessage('发现 JS 更新,正在下载...(下次启动生效)'); setTermOut(o => o + (d.stdout||d.stderr||JSON.stringify(d)) + '\n');
} else if (result.apkUpdate) { } catch(e:any) { setTermOut(o => o + `Error: ${e.message}\n`); }
setStatus('updating'); finally { setTermRunning(false); }
setMessage('安装包已就绪,重启后生效'); };
} else {
setStatus('up-to-date'); /** Send chat message to Gateway */
setMessage('已是最新版本'); const sendMessage = async () => {
} const text = input.trim();
if (!text || sending) return;
const userMsg: Msg = { role: 'user', text, ts: Date.now() };
setMessages(prev => [...prev, userMsg]);
setInput('');
setSending(true);
try {
const resp = await fetch(`${gatewayUrl}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text }),
signal: AbortSignal.timeout(30000),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json() as { reply?: string; error?: string };
const aiMsg: Msg = {
role: 'assistant',
text: data.reply ?? data.error ?? 'No response',
ts: Date.now(),
};
setMessages(prev => [...prev, aiMsg]);
} catch (e: any) { } catch (e: any) {
setStatus('error'); setMessages(prev => [...prev, {
setMessage(`更新检查失败: ${e.message}`); role: 'assistant',
text: `Error: ${e.message}`,
ts: Date.now(),
}]);
} finally {
setSending(false);
} }
})(); };
}, []);
/** Connect/disconnect toggle */
const toggleConnection = () => {
setConnStatus(prev => {
if (prev === 'disconnected' || prev === 'error') return 'connecting';
return 'disconnected';
});
};
// ── Render ──────────────────────────────────────────────
return ( return (
<View style={styles.container}> <KeyboardAvoidingView style={styles.root} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<StatusBar style="light" /> <StatusBar style="light" />
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Yuzu GCA</Text> <Text style={styles.title}>Yuzu GCA</Text>
<Text style={styles.subtitle}>Global Control Assistant</Text> <View style={[styles.statusDot, styles[`dot_${connStatus}` as keyof typeof styles]]} />
<View style={styles.statusBar}> </View>
{status === 'checking' && <ActivityIndicator color="#3b82f6" />}
<Text style={styles.statusText}>{message}</Text> {/* Tabs */}
<View style={styles.tabBar}>
{(['chat', 'terminal', 'settings', 'device', 'connection'] as const).map(t => (
<TouchableOpacity key={t} onPress={() => setTab(t)} style={[styles.tab, tab === t && styles.tabActive]}>
<Text style={[styles.tabText, tab === t && styles.tabTextActive]}>
{t === 'chat' ? 'Chat' : t === 'terminal' ? 'Term' : t === 'settings' ? 'Gateway' : t === 'device' ? 'Device' : (connStatus === 'connected' ? '🟢' : '⚫')}
</Text>
</TouchableOpacity>
))}
</View>
{/* ── Chat Tab ── */}
{tab === 'chat' && (
<View style={styles.chatContainer}>
<FlatList
data={messages}
keyExtractor={(_, i) => String(i)}
style={styles.chatList}
renderItem={({ item }) => (
<View style={[styles.bubble, item.role === 'user' ? styles.bubbleUser : styles.bubbleAI]}>
<Text style={styles.bubbleRole}>{item.role === 'user' ? 'You' : 'GCA'}</Text>
<Text style={styles.bubbleText}>{item.text}</Text>
</View>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>Send a message to control your devices via AI</Text>
}
/>
<View style={styles.inputBar}>
<TextInput
style={styles.chatInput}
value={input}
onChangeText={setInput}
placeholder="Type a command..."
placeholderTextColor="#475569"
onSubmitEditing={sendMessage}
returnKeyType="send"
/>
<TouchableOpacity style={[styles.sendBtn, sending && styles.sendBtnDisabled]} onPress={sendMessage} disabled={sending}>
<Text style={styles.sendBtnText}>{sending ? '...' : 'Send'}</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
)}
{/* ── Terminal Tab ── */}
{tab === 'terminal' && (
<View style={styles.chatContainer}>
<ScrollView style={styles.termOutput}>
<Text style={styles.termText} selectable>{termOut}</Text>
</ScrollView>
<View style={styles.inputBar}>
<TextInput style={styles.chatInput} value={termCmd} onChangeText={setTermCmd} placeholder="ls -la /home" placeholderTextColor="#475569" autoCapitalize="none" autoCorrect={false} onSubmitEditing={runTermCmd} returnKeyType="send" />
<TouchableOpacity style={[styles.sendBtn, termRunning && styles.sendBtnDisabled]} onPress={runTermCmd} disabled={termRunning}>
<Text style={styles.sendBtnText}>{termRunning ? '...' : 'Run'}</Text>
</TouchableOpacity>
</View>
</View>
)}
{/* ── Settings Tab ── */}
{tab === 'settings' && (
<ScrollView style={styles.body}>
<Text style={styles.label}>Gateway URL</Text>
<TextInput style={styles.input} value={gatewayUrl} onChangeText={setGatewayUrl} placeholder="http://localhost:3000" placeholderTextColor="#475569" autoCapitalize="none" keyboardType="url" />
<Text style={styles.label}>Heartbeat (seconds)</Text>
<TextInput style={styles.input} value={heartbeatInterval} onChangeText={setHeartbeatInterval} keyboardType="numeric" placeholder="15" placeholderTextColor="#475569" />
<View style={styles.switchRow}>
<Text style={styles.label}>Auto-connect on start</Text>
<Switch value={autoConnect} onValueChange={setAutoConnect} trackColor={{ true: '#3b82f6' }} />
</View>
<Text style={styles.label}>Log Level</Text>
<View style={styles.chipRow}>
{(['debug', 'info', 'warn', 'error'] as const).map(lv => (
<TouchableOpacity key={lv} style={[styles.chip, logLevel === lv && styles.chipActive]} onPress={() => setLogLevel(lv)}>
<Text style={[styles.chipText, logLevel === lv && styles.chipTextActive]}>{lv}</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
)}
{/* ── Device Tab ── */}
{tab === 'device' && (
<ScrollView style={styles.body}>
<Text style={styles.label}>Device Name</Text>
<TextInput style={styles.input} value={deviceName} onChangeText={setDeviceName} placeholder="Android Device" placeholderTextColor="#475569" />
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>Device Info</Text>
<Text style={styles.infoText}>Platform: Android</Text>
<Text style={styles.infoText}>Gateway: {gatewayUrl}</Text>
<Text style={styles.infoText}>Capabilities: exec, file_list, sysinfo...</Text>
<Text style={styles.infoText}>MCP Tools: 36 defined, 7 implemented</Text>
</View>
</ScrollView>
)}
{/* ── Connection Tab ── */}
{tab === 'connection' && (
<View style={styles.body}>
<View style={[styles.connIndicator, styles[`conn_${connStatus}` as keyof typeof styles]]}>
<Text style={styles.connText}>
{connStatus === 'connected' ? '🟢 Connected' :
connStatus === 'connecting' ? '🟡 Connecting...' :
connStatus === 'error' ? '🔴 Error' : '⚫ Disconnected'}
</Text>
</View>
<TouchableOpacity
style={[styles.btn, connStatus === 'connected' ? styles.btnDanger : styles.btnPrimary]}
onPress={toggleConnection}
>
<Text style={styles.btnText}>{connStatus === 'disconnected' ? 'Connect' : 'Disconnect'}</Text>
</TouchableOpacity>
</View>
)}
</KeyboardAvoidingView>
); );
} }
// ── Styles ────────────────────────────────────────────────
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { root: { flex: 1, backgroundColor: '#0f172a' },
flex: 1, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 48, paddingBottom: 8 },
backgroundColor: '#0f172a', title: { fontSize: 22, fontWeight: '800', color: '#e2e8f0' },
alignItems: 'center', statusDot: { width: 10, height: 10, borderRadius: 5 },
justifyContent: 'center', dot_connected: { backgroundColor: '#22c55e' },
padding: 24, dot_connecting: { backgroundColor: '#eab308' },
}, dot_error: { backgroundColor: '#ef4444' },
title: { dot_disconnected: { backgroundColor: '#475569' },
fontSize: 28,
fontWeight: '800', // Tabs
color: '#e2e8f0', tabBar: { flexDirection: 'row', marginHorizontal: 16, marginBottom: 8 },
marginBottom: 4, tab: { flex: 1, padding: 10, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: '#1e293b' },
}, tabActive: { borderBottomColor: '#3b82f6' },
subtitle: { tabText: { fontSize: 13, color: '#64748b' },
fontSize: 14, tabTextActive: { color: '#3b82f6', fontWeight: '600' },
color: '#94a3b8',
marginBottom: 32, // Chat
}, chatContainer: { flex: 1 },
statusBar: { chatList: { flex: 1, paddingHorizontal: 12 },
flexDirection: 'row', bubble: { marginVertical: 4, padding: 10, borderRadius: 10, maxWidth: '85%' },
alignItems: 'center', bubbleUser: { alignSelf: 'flex-end', backgroundColor: '#1e40af' },
gap: 8, bubbleAI: { alignSelf: 'flex-start', backgroundColor: '#1e293b', borderWidth: 1, borderColor: '#334155' },
}, bubbleRole: { fontSize: 10, color: '#94a3b8', marginBottom: 2 },
statusText: { bubbleText: { fontSize: 14, color: '#e2e8f0' },
fontSize: 14, emptyText: { textAlign: 'center', color: '#475569', marginTop: 40, fontSize: 14 },
color: '#94a3b8', inputBar: { flexDirection: 'row', padding: 8, paddingBottom: 16, gap: 8 },
}, chatInput: { flex: 1, backgroundColor: '#1e293b', color: '#e2e8f0', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 },
sendBtn: { backgroundColor: '#1e40af', borderRadius: 8, paddingHorizontal: 16, justifyContent: 'center' },
sendBtnDisabled: { opacity: 0.5 },
sendBtnText: { color: '#fff', fontWeight: '600', fontSize: 14 },
// Settings
body: { flex: 1, paddingHorizontal: 16 },
label: { fontSize: 12, color: '#94a3b8', marginBottom: 4, marginTop: 12 },
input: { backgroundColor: '#1e293b', color: '#e2e8f0', borderRadius: 8, padding: 12, fontSize: 14 },
switchRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 12 },
chipRow: { flexDirection: 'row', gap: 8, marginTop: 4 },
chip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: '#1e293b' },
chipActive: { backgroundColor: '#1e40af' },
chipText: { fontSize: 12, color: '#94a3b8' },
chipTextActive: { color: '#fff' },
infoCard: { backgroundColor: '#1e293b', borderRadius: 8, padding: 16, marginTop: 16 },
infoTitle: { fontSize: 14, color: '#38bdf8', marginBottom: 8 },
infoText: { fontSize: 12, color: '#94a3b8', marginTop: 4 },
// Connection
connIndicator: { borderRadius: 8, padding: 16, marginTop: 16, alignItems: 'center' },
conn_connected: { backgroundColor: '#064e3b' },
conn_connecting: { backgroundColor: '#78350f' },
conn_error: { backgroundColor: '#7f1d1d' },
conn_disconnected: { backgroundColor: '#1e293b' },
connText: { fontSize: 16, color: '#e2e8f0' },
termOutput: { flex: 1, backgroundColor: '#0a0f1a', margin: 8, borderRadius: 8, padding: 12 },
termText: { fontFamily: 'monospace', fontSize: 12, color: '#22c55e', lineHeight: 18 },
btn: { borderRadius: 8, padding: 14, marginTop: 16, alignItems: 'center' },
btnPrimary: { backgroundColor: '#1e40af' },
btnDanger: { backgroundColor: '#991b1b' },
btnText: { fontSize: 16, color: '#fff', fontWeight: '600' },
}); });

View File

@@ -0,0 +1,16 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# Bundle artifacts
*.jsbundle

View File

@@ -1,2 +1,2 @@
#Sun Jul 19 10:50:39 CST 2026 #Wed Jul 22 19:01:51 CST 2026
gradle.version=8.10.2 gradle.version=8.13

View File

@@ -9,7 +9,7 @@ android {
applicationId "dev.yuzu.gca" applicationId "dev.yuzu.gca"
minSdk 24 minSdk 24
targetSdk 35 targetSdk 35
versionCode 8 versionCode 5
versionName "0.1.0" versionName "0.1.0"
} }

View File

@@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -1,33 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application <uses-permission android:name="android.permission.VIBRATE"/>
android:label="Yuzu GCA v0.1.0" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
android:allowBackup="false" <queries>
android:supportsRtl="true" <intent>
android:theme="@style/Theme.AppCompat.DayNight"> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<activity <data android:scheme="https"/>
android:name=".MainActivity" </intent>
android:label="Yuzu GCA v0.1.0" </queries>
android:exported="true"> <application android:usesCleartextTraffic="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/><activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="dev.yuzu.gca"/>
</intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,user-scalable=no">
<title>Yuzu GCA</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:16px}
h1{font-size:20px;color:#38bdf8;margin-bottom:4px}
.info{color:#94a3b8;font-size:12px;margin-bottom:16px}
.card{background:#1e293b;border-radius:8px;padding:16px;margin:12px 0}
.card-title{font-size:13px;color:#94a3b8;margin-bottom:8px}
.btn{display:block;width:100%;padding:14px;margin:8px 0;border:none;border-radius:8px;font-size:15px;cursor:pointer;color:#fff}
.btn-blue{background:#1e40af}
.btn-green{background:#15803d}
.input{display:block;width:100%;padding:12px;margin:8px 0;border:none;border-radius:8px;font-size:14px;background:#0f172a;color:#e2e8f0}
.log{background:#0a0f1a;padding:12px;border-radius:8px;margin-top:8px;max-height:250px;overflow-y:auto;font-size:11px;white-space:pre-wrap;color:#22c55e;font-family:monospace}
.tab{display:inline-block;padding:8px 16px;margin-right:4px;border-radius:6px 6px 0 0;background:#1e293b;color:#64748b;font-size:13px;cursor:pointer}
.tab-on{background:#1e40af;color:#fff}
</style></head><body>
<h1>Yuzu GCA</h1>
<div class="info">Self-contained — no Gateway needed</div>
<div>
<span class="tab tab-on" onclick="switchTab('exec',event)">Exec</span>
<span class="tab" onclick="switchTab('files',event)">Files</span>
<span class="tab" onclick="switchTab('sysinfo',event)">SysInfo</span>
</div>
<div class="card" id="tab-exec">
<div class="card-title">Shell Command (local)</div>
<input class="input" id="cmd" placeholder="e.g. ls, whoami, df -h">
<button class="btn btn-blue" onclick="runExec()">Execute</button>
<div class="log" id="log-exec"></div>
</div>
<div class="card" id="tab-files" style="display:none">
<div class="card-title">List Files (local)</div>
<input class="input" id="path" placeholder="/sdcard or ." value=".">
<button class="btn btn-blue" onclick="runFiles()">List</button>
<div class="log" id="log-files"></div>
</div>
<div class="card" id="tab-sysinfo" style="display:none">
<div class="card-title">Device Info (local)</div>
<button class="btn btn-blue" onclick="runSysinfo()">Refresh</button>
<div class="log" id="log-sysinfo"></div>
</div>
<script>
function switchTab(t,ev){
ev.target.classList.add('tab-on');
['exec','files','sysinfo'].filter(function(x){return x!==t}).forEach(function(x){
document.getElementById('tab-'+x).style.display='none';
document.querySelectorAll('.tab').forEach(function(b){if(b.textContent.toLowerCase().indexOf(x)>=0)b.classList.remove('tab-on')});
});
document.getElementById('tab-'+t).style.display='block';
}
function log(id,msg){
var l=document.getElementById(id);
l.innerText+=msg+'\n';l.scrollTop=l.scrollHeight;
}
function runExec(){
var c=document.getElementById('cmd').value, id='log-exec';
document.getElementById(id).innerText='';
log(id,'$ '+c);
var r=Android.exec(c);
log(id,r||'(empty)');
}
function runFiles(){
var p=document.getElementById('path').value, id='log-files';
document.getElementById(id).innerText='';
log(id,'ls '+p);
var r=Android.listFiles(p);
log(id,r||'(empty)');
}
function runSysinfo(){
var id='log-sysinfo';
document.getElementById(id).innerText='';
log(id,Android.sysinfo());
}
</script></body></html>

View File

@@ -1,255 +1,58 @@
package dev.yuzu.gca package dev.yuzu.gca
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageInstaller
import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import android.webkit.WebView
import org.json.JSONObject import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.io.BufferedReader
import java.io.InputStreamReader
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var logView: TextView
private var selectedChannel = "stable"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WebView(this).apply {
val scroll = ScrollView(this) settings.javaScriptEnabled = true
val layout = LinearLayout(this).apply { settings.domStorageEnabled = true
orientation = LinearLayout.VERTICAL settings.allowUniversalAccessFromFileURLs = true
setPadding(24, 24, 24, 24) addJavascriptInterface(LocalBridge(), "Android")
loadUrl("file:///android_asset/index.html")
}.also { setContentView(it) }
} }
val title = TextView(this).apply { inner class LocalBridge {
text = "Yuzu GCA" @android.webkit.JavascriptInterface
textSize = 20f fun exec(cmd: String): String {
setPadding(0, 0, 0, 4) return try {
} val p = Runtime.getRuntime().exec(arrayOf("sh", "-c", cmd))
layout.addView(title) val out = BufferedReader(InputStreamReader(p.inputStream)).readText()
val err = BufferedReader(InputStreamReader(p.errorStream)).readText()
val versionInfo = TextView(this).apply { p.waitFor()
text = "v0.1.0-dav-android-8-debug" if (out.isNotEmpty()) out else err
textSize = 12f } catch (e: Exception) { "ERR: ${e.message}" }
setTextColor(Color.GRAY)
setPadding(0, 0, 0, 12)
}
layout.addView(versionInfo)
val channelLabel = TextView(this).apply {
text = "选择更新渠道:"
textSize = 13f
setPadding(0, 8, 0, 4)
}
layout.addView(channelLabel)
// 渠道按钮行
val channelRow = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 0, 0, 12)
} }
fun makeChannelBtn(label: String, channel: String): Button { @android.webkit.JavascriptInterface
return Button(this).apply { fun listFiles(path: String): String {
text = label return try {
textSize = 12f val dir = File(path)
setOnClickListener { if (!dir.exists()) return "ERR: path not found"
selectedChannel = channel if (!dir.isDirectory) return "ERR: not a directory"
highlightChannel(channelRow, this) dir.listFiles()?.joinToString("\n") { f ->
} (if (f.isDirectory) "[DIR] " else "[FILE] ") + f.name +
} (if (f.isFile) " (${f.length()}B)" else "")
} ?: "empty"
} catch (e: Exception) { "ERR: ${e.message}" }
} }
val btnStable = makeChannelBtn("Stable", "stable") @android.webkit.JavascriptInterface
val btnBeta = makeChannelBtn("Beta", "beta") fun sysinfo(): String {
val btnNightly = makeChannelBtn("Nightly", "nightly") return """
Device: ${android.os.Build.MODEL}
channelRow.addView(btnStable) Android: ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT})
channelRow.addView(btnBeta) CPU: ${Runtime.getRuntime().availableProcessors()} cores
channelRow.addView(btnNightly) Memory: ${Runtime.getRuntime().maxMemory() / 1024 / 1024}MB max
layout.addView(channelRow) """.trimIndent()
highlightChannel(channelRow, btnStable)
logView = TextView(this).apply {
textSize = 12f
setTextColor(Color.LTGRAY)
}
layout.addView(logView)
val btn = Button(this).apply {
text = "检查更新"
setOnClickListener {
val ch = selectedChannel
CoroutineScope(Dispatchers.IO).launch { doOtaTest(ch) }
} }
} }
layout.addView(btn)
scroll.addView(layout)
setContentView(scroll)
appendLog("渠道: stable | 点击[检查更新]开始")
}
private fun highlightChannel(row: LinearLayout, active: Button) {
for (i in 0 until row.childCount) {
val b = row.getChildAt(i) as Button
b.isSelected = (b == active)
}
}
private fun appendLog(msg: String) {
Log.i(TAG, msg)
runOnUiThread { logView.append("$msg\n") }
}
companion object { private const val TAG = "GCA-OTA" }
private fun manifestUrl(channel: String): String {
val base = "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota"
return when (channel) {
"beta" -> "$base/manifest-beta.json"
"nightly" -> "$base/manifest-nightly.json"
else -> "$base/manifest-stable.json"
}
}
private suspend fun doOtaTest(channel: String) = withContext(Dispatchers.IO) {
val url = manifestUrl(channel)
appendLog("=== OTA 渠道: $channel ===")
appendLog("manifest: $url")
// 1. 拉取 manifest
appendLog("[1/4] 拉取 manifest...")
val manifestJson: String
try {
manifestJson = httpGet(url)
appendLog(" ✅ 获取成功 (${manifestJson.length}B)")
} catch (e: Exception) {
appendLog(" ❌ 获取失败: ${e.message}")
appendLog("=== 测试中止 ===")
return@withContext
}
// 2. 解析
appendLog("[2/4] 解析 manifest...")
var manifestSha256 = ""
var manifestApkUrl = ""
var manifestTag = ""
try {
val json = JSONObject(manifestJson)
if (!json.getJSONObject("builds").has("android")) {
appendLog(" 该渠道暂无可用更新")
appendLog("=== $channel 渠道测试完成 ===")
return@withContext
}
val android = json.getJSONObject("builds").getJSONObject("android")
manifestSha256 = android.getString("sha256")
manifestApkUrl = android.getString("url")
manifestTag = android.getString("tag")
appendLog(" 渠道: $channel 版本: $manifestTag")
appendLog(" SHA256: ${manifestSha256.take(16)}...")
// 版本比较:一致则跳过
val localTag = "v0.1.0-dav-android-${packageManager.getPackageInfo(packageName, 0).versionCode}"
if (manifestTag == localTag) {
appendLog(" ✅ 已是最新版本,无需更新")
appendLog("=== $channel 渠道测试完成 ===")
return@withContext
}
appendLog(" 发现新版本,开始更新...")
} catch (e: Exception) {
appendLog(" ❌ 解析失败: ${e.message}")
return@withContext
}
// 3. 下载
appendLog("[3/4] 下载 APK...")
val apkFile = File(cacheDir, "update-$channel.apk")
try {
downloadFile(manifestApkUrl, apkFile)
appendLog(" ✅ 下载完成: ${apkFile.length()} bytes")
} catch (e: Exception) {
appendLog(" ❌ 下载失败: ${e.message}")
return@withContext
}
// 4. SHA256 校验
appendLog("[4/4] SHA256 校验...")
val actualSha256 = computeSha256(apkFile)
if (actualSha256 == manifestSha256) {
appendLog(" ✅ SHA256 校验通过!")
// 5. 安装 APK
appendLog("[5/5] 提交安装...")
try {
installApk(apkFile)
appendLog(" ✅ 安装已提交,稍后通知栏点击重启")
} catch (e: Exception) {
appendLog(" ❌ 安装失败: ${e.message}")
}
} else {
appendLog(" ❌ SHA256 校验失败!")
appendLog(" expected: ${manifestSha256.take(32)}")
appendLog(" actual: ${actualSha256.take(32)}")
}
appendLog("=== $channel 渠道测试完成 ===")
}
private fun httpGet(url: String): String {
val conn = URL(url).openConnection() as HttpURLConnection
conn.connectTimeout = 10000
conn.readTimeout = 10000
return conn.inputStream.bufferedReader().readText()
}
private val ALLOWED_HOSTS = setOf("git.childish-ghost.com", "github.com")
private fun downloadFile(url: String, dest: File) {
val host = URL(url).host
if (!ALLOWED_HOSTS.any { host == it || host.endsWith(".$it") }) {
throw SecurityException("不允许的下载域名: $host")
}
val conn = URL(url).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 120000
conn.inputStream.use { input ->
FileOutputStream(dest).use { output -> input.copyTo(output) }
}
}
private fun installApk(apkFile: File) {
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", apkFile)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
}
private fun computeSha256(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
file.inputStream().use { input ->
val buf = ByteArray(8192)
var read: Int
while (input.read(buf).also { read = it } != -1) {
digest.update(buf, 0, read)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
} }

View File

@@ -0,0 +1,3 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
</layer-list>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1 @@
<resources/>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="primary">#3b82f6</color> <color name="splashscreen_background">#0f172a</color>
<color name="background">#0f172a</color> <color name="iconBackground">#0f172a</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#0f172a</color>
</resources> </resources>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Yuzu GCA v0.1.0</string> <string name="app_name">YuzuGCA</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources> </resources>

View File

@@ -0,0 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#0f172a</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="apk" path="/" />
</paths>

View File

@@ -12,7 +12,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.7.3") classpath('com.android.tools.build:gradle:8.13.2')
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
} }
} }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

252
packages/client-android/android/gradlew vendored Normal file
View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -1,26 +1,94 @@
@echo off @rem
setlocal @rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_HOME=%DIRNAME% @rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set ANDROID_HOME=C:\Users\Middl\AppData\Local\Android\Sdk
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail goto fail
:fail
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
exit /b 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail goto fail
:execute :execute
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,3 +1,2 @@
rootProject.name = 'YuzuGCA' rootProject.name = 'YuzuGCA'
include ':app' include ':app'

View File

@@ -25,14 +25,7 @@
] ]
}, },
"plugins": [ "plugins": [
"expo-updates"
], ],
"updates": {
"enabled": true,
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 3000,
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/manifest/expo-manifest.json"
},
"extra": { "extra": {
"eas": { "eas": {
"projectId": "yuzu-gca-android" "projectId": "yuzu-gca-android"

Some files were not shown because too many files have changed in this diff Show More