Compare commits
21 Commits
main
...
f7ddb1bec5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7ddb1bec5 | ||
|
|
ebce72e7cd | ||
|
|
e6a8f51f61 | ||
|
|
79a1d306ee | ||
|
|
1360eb379c | ||
|
|
084560c058 | ||
|
|
8044d43abb | ||
|
|
8d1b57d7ed | ||
|
|
836419484c | ||
|
|
fe3929b41c | ||
|
|
de8126cd11 | ||
|
|
ffd5d4d8e5 | ||
|
|
8bffc389c8 | ||
|
|
27633376c4 | ||
|
|
b7f276f718 | ||
|
|
3abb76f264 | ||
|
|
bdde28cd7b | ||
|
|
73d7eff9d9 | ||
|
|
6cd2a22d09 | ||
|
|
71f6b6369b | ||
|
|
e5ce52ac40 |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 325 KiB |
@@ -0,0 +1,2 @@
|
||||
{"status":"starting_turn","iteration":0,"created_at":"2026-07-20T15:07:48.1876405Z"}
|
||||
{"status":"turn_done","iteration":1,"created_at":"2026-07-20T15:09:55.9378605Z"}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"fingerprint": "build-successful-xiaomi-usb-f4fd9518",
|
||||
"summary": "BUILD SUCCESSFUL。Xiaomi 限制安装。解锁手机,在开发者选项里确认 **\"USB 安装\"** 已开启,然后:",
|
||||
"first_seen_iteration": 1,
|
||||
"last_seen_iteration": 1,
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"status": "complete",
|
||||
"iteration": 1,
|
||||
"current_direction": "BUILD SUCCESSFUL。Xiaomi 限制安装。解锁手机,在开发者选项里确认 **\"USB 安装\"** 已开启,然后:",
|
||||
"stale_count": 1,
|
||||
"pivot_count": 0,
|
||||
"blocked_reason": "",
|
||||
"updated_at": "2026-07-20T15:09:56.3033389Z"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"task_id": "20260720-145731-19-56-begin-19-56-windows-powershell-copyright-c-microso-2",
|
||||
"goal": "[已粘贴文本 #19 · 56 行]\n\n--- Begin [已粘贴文本 #19 · 56 行] ---\nWindows PowerShell\r\nCopyright (C) Microsoft Corporation. All rights reserved.\r\n\r\nInstall the latest PowerShell for new features and improvements! https://aka.ms/PSWindows\r\n\r\nPS D:\\Yuzu-GCA\u003e cd D:\\Yuzu-GCA\\packages\\client-android\\android\r\nPS D:\\Yuzu-GCA\\packages\\client-android\\android\u003e Remove-Item -Recurse -Force .gradle, app\\build -ErrorAction SilentlyContinue\r\nPS D:\\Yuzu-GCA\\packages\\client-android\\android\u003e .\\gradlew.bat assembleDebug\r\nStarting a Gradle Daemon, 7 busy and 1 incompatible Daemons could not be reused, use --status for details\r\n\r\n\u003e Configure project :app\r\n 鈩癸笍 Applying gradle plugin 'expo-updates-gradle-plugin' (expo-updates@0.27.5)\r\n\r\nFAILURE: Build completed with 2 failures.\r\n\r\n1: Task failed with an exception.\r\n-----------\r\n* Where:\r\nBuild file 'D:\\Yuzu-GCA\\packages\\client-android\\node_modules\\expo-asset\\android\\build.gradle' line: 3\r\n\r\n* What went wrong:\r\nPlugin [id: 'expo-module-gradle-plugin'] was not found in any of the following sources:\r\n\r\n- Gradle Core Plugins (not a core plugin. For more available plugins, please refer to https://docs.gradle.org/8.10.2/userguide/plugin_reference.html in the Gradle documentation.)\r\n- Included Builds (None of the included builds contain this plugin)\r\n- Plugin Repositories (plugin dependency must include a version number for this source)\r\n\r\n* Try:\r\n\u003e Run with --stacktrace option to get the stack trace.\r\n\u003e Run with --info or --debug option to get more log output.\r\n\u003e Run with --scan to get full insights.\r\n\u003e Get more help at https://help.gradle.org.\r\n==============================================================================\r\n\r\n2: Task failed with an exception.\r\n-----------\r\n* Where:\r\nScript 'D:\\Yuzu-GCA\\packages\\client-android\\node_modules\\expo-modules-core\\android\\ExpoModulesCorePlugin.gradle' line: 95\r\n\r\n* What went wrong:\r\nA problem occurred configuring project ':expo'.\r\n\u003e Could not get unknown property 'release' for SoftwareComponent container of type org.gradle.api.internal.component.DefaultSoftwareComponentContainer.\r\n\r\n* Try:\r\n\u003e Run with --stacktrace option to get the stack trace.\r\n\u003e Run with --info or --debug option to get more log output.\r\n\u003e Run with --scan to get full insights.\r\n\u003e Get more help at https://help.gradle.org.\r\n==============================================================================\r\n\r\nBUILD FAILED in 27s\r\n18 actionable tasks: 4 executed, 14 up-to-date\r\nPS D:\\Yuzu-GCA\\packages\\client-android\\android\u003e\r\n\r\n\r\n\n--- End [已粘贴文本 #19 · 56 行] ---",
|
||||
"scope": null,
|
||||
"non_goals": null,
|
||||
"allowed_operations": {
|
||||
"write": true,
|
||||
"network": false,
|
||||
"publish": false
|
||||
},
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "objective_evidence",
|
||||
"description": "The goal outcome is supported by direct evidence, such as inspected code, reproduced behavior, source material, or concrete findings.",
|
||||
"required": true,
|
||||
"evidence_ids": null
|
||||
},
|
||||
{
|
||||
"id": "verification",
|
||||
"description": "The result has relevant verification evidence, such as tests, commands, benchmarks, manual checks, or a documented reason why verification is not applicable.",
|
||||
"required": true,
|
||||
"evidence_ids": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"status":"starting_turn","iteration":0,"created_at":"2026-07-20T14:57:31.2997582Z"}
|
||||
{"status":"turn_done","iteration":1,"created_at":"2026-07-20T15:07:47.7876311Z"}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"fingerprint": "build-successful-bcc9636e",
|
||||
"summary": "BUILD SUCCESSFUL。解锁手机屏幕后:",
|
||||
"first_seen_iteration": 1,
|
||||
"last_seen_iteration": 1,
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id":"f1","kind":"file","summary":"BUILD SUCCESSFUL 387 tasks 128MB APK. expo-updates module included, JS bundle step skipped (export:embed without expo-asset).","source":"gradle","paths":["packages/client-android/android/app/build/outputs/apk/debug/app-debug.apk"],"accepted":false,"created_at":"2026-07-20T15:07:47.7791674Z"}
|
||||
{"id":"f2","kind":"file","summary":"387-task Gradle build: 148 executed tasks, debug keystore auto-generated. APK has expo-updates but no embedded JS bundle. Hot update will work via remote manifest download.","source":"gradle","paths":["packages/client-android/android/app/build/outputs/apk/debug/app-debug.apk"],"accepted":false,"created_at":"2026-07-20T15:07:47.7813672Z"}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"status": "running",
|
||||
"iteration": 1,
|
||||
"current_direction": "BUILD SUCCESSFUL。解锁手机屏幕后:",
|
||||
"stale_count": 1,
|
||||
"pivot_count": 0,
|
||||
"blocked_reason": "",
|
||||
"updated_at": "2026-07-20T15:07:47.78444Z"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"task_id": "20260720-145731-19-56-begin-19-56-windows-powershell-copyright-c-microso",
|
||||
"goal": "[已粘贴文本 #19 · 56 行]\n\n--- Begin [已粘贴文本 #19 · 56 行] ---\nWindows PowerShell\r\nCopyright (C) Microsoft Corporation. All rights reserved.\r\n\r\nInstall the latest PowerShell for new features and improvements! https://aka.ms/PSWindows\r\n\r\nPS D:\\Yuzu-GCA\u003e cd D:\\Yuzu-GCA\\packages\\client-android\\android\r\nPS D:\\Yuzu-GCA\\packages\\client-android\\android\u003e Remove-Item -Recurse -Force .gradle, app\\build -ErrorAction SilentlyContinue\r\nPS D:\\Yuzu-GCA\\packages\\client-android\\android\u003e .\\gradlew.bat assembleDebug\r\nStarting a Gradle Daemon, 7 busy and 1 incompatible Daemons could not be reused, use --status for details\r\n\r\n\u003e Configure project :app\r\n 鈩癸笍 Applying gradle plugin 'expo-updates-gradle-plugin' (expo-updates@0.27.5)\r\n\r\nFAILURE: Build completed with 2 failures.\r\n\r\n1: Task failed with an exception.\r\n-----------\r\n* Where:\r\nBuild file 'D:\\Yuzu-GCA\\packages\\client-android\\node_modules\\expo-asset\\android\\build.gradle' line: 3\r\n\r\n* What went wrong:\r\nPlugin [id: 'expo-module-gradle-plugin'] was not found in any of the following sources:\r\n\r\n- Gradle Core Plugins (not a core plugin. For more available plugins, please refer to https://docs.gradle.org/8.10.2/userguide/plugin_reference.html in the Gradle documentation.)\r\n- Included Builds (None of the included builds contain this plugin)\r\n- Plugin Repositories (plugin dependency must include a version number for this source)\r\n\r\n* Try:\r\n\u003e Run with --stacktrace option to get the stack trace.\r\n\u003e Run with --info or --debug option to get more log output.\r\n\u003e Run with --scan to get full insights.\r\n\u003e Get more help at https://help.gradle.org.\r\n==============================================================================\r\n\r\n2: Task failed with an exception.\r\n-----------\r\n* Where:\r\nScript 'D:\\Yuzu-GCA\\packages\\client-android\\node_modules\\expo-modules-core\\android\\ExpoModulesCorePlugin.gradle' line: 95\r\n\r\n* What went wrong:\r\nA problem occurred configuring project ':expo'.\r\n\u003e Could not get unknown property 'release' for SoftwareComponent container of type org.gradle.api.internal.component.DefaultSoftwareComponentContainer.\r\n\r\n* Try:\r\n\u003e Run with --stacktrace option to get the stack trace.\r\n\u003e Run with --info or --debug option to get more log output.\r\n\u003e Run with --scan to get full insights.\r\n\u003e Get more help at https://help.gradle.org.\r\n==============================================================================\r\n\r\nBUILD FAILED in 27s\r\n18 actionable tasks: 4 executed, 14 up-to-date\r\nPS D:\\Yuzu-GCA\\packages\\client-android\\android\u003e\r\n\r\n\r\n\n--- End [已粘贴文本 #19 · 56 行] ---",
|
||||
"scope": null,
|
||||
"non_goals": null,
|
||||
"allowed_operations": {
|
||||
"write": true,
|
||||
"network": false,
|
||||
"publish": false
|
||||
},
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "objective_evidence",
|
||||
"description": "The goal outcome is supported by direct evidence, such as inspected code, reproduced behavior, source material, or concrete findings.",
|
||||
"required": true,
|
||||
"evidence_ids": [
|
||||
"f1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "verification",
|
||||
"description": "The result has relevant verification evidence, such as tests, commands, benchmarks, manual checks, or a documented reason why verification is not applicable.",
|
||||
"required": true,
|
||||
"evidence_ids": [
|
||||
"f2"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
{"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"}
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{"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"}
|
||||
@@ -1,100 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"status": "complete",
|
||||
"iteration": 14,
|
||||
"current_direction": "**结论已定。** 13轮迭代,证据充分。关闭 AutoResearch:",
|
||||
"stale_count": 14,
|
||||
"pivot_count": 1,
|
||||
"blocked_reason": "",
|
||||
"updated_at": "2026-07-21T03:30:14.7277324Z"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
{"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"}
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
35
cdp-read.js
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
3
index.js
@@ -1,3 +0,0 @@
|
||||
// Root entry point for Expo dev server
|
||||
// Redirect to client-android
|
||||
import './packages/client-android/index';
|
||||
@@ -1,44 +1,18 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
## 格式
|
||||
|
||||
**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...
|
||||
CI 构建 APK → ./ota/gen-manifest.sh → git push manifest-stable.json
|
||||
手机 App → raw URL 拉取 manifest → 比较版本 → 下载 APK → SHA256 校验
|
||||
```
|
||||
|
||||
16
ota/expo-manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "ca582f57-9d65-4831-9d8a-1df3939264c7",
|
||||
"assets": [
|
||||
|
||||
],
|
||||
"createdAt": "2026-07-20T15:48:27.579Z",
|
||||
"metadata": {
|
||||
|
||||
},
|
||||
"runtimeVersion": "0.1.0",
|
||||
"launchAsset": {
|
||||
"contentType": "application/javascript",
|
||||
"key": "bundle",
|
||||
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-10/index.android.bundle"
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,38 @@
|
||||
#!/bin/bash
|
||||
# gen-manifest.sh — 生成 APK 发行版 manifest
|
||||
# 在 Android Studio 构建 Release APK 后运行
|
||||
# 用法: bash gen-manifest.sh 6 stable
|
||||
# bash gen-manifest.sh 6 all
|
||||
# OTA manifest 生成脚本
|
||||
# 用法: ./ota/gen-manifest.sh <versionName> <versionCode> <apk-path>
|
||||
# 构建号规则: 每次生成APK时 versionCode +1, 对应 git tag vX.Y.Z-dav-android-N
|
||||
# CI 构建时自动运行, SHA256动态填入, 生成 manifest-stable.json 后上传到 Gitea Release
|
||||
|
||||
set -e
|
||||
|
||||
VER="$1"
|
||||
shift
|
||||
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
|
||||
VERSION_NAME="${1:-0.1.0}"
|
||||
VERSION_CODE="${2:-7}"
|
||||
APK_PATH="${3:-packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-7-debug.apk}"
|
||||
RELEASE_TAG="v${VERSION_NAME}-dav-android-${VERSION_CODE}"
|
||||
APK_NAME="v${VERSION_NAME}-dav-android-${VERSION_CODE}-debug.apk"
|
||||
|
||||
SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1)
|
||||
echo "APK : $APK_PATH"
|
||||
echo "SHA256: $SHA256"
|
||||
SIZE=$(stat -c%s "$APK_PATH")
|
||||
|
||||
for CH in "${CHANNELS[@]}"; do
|
||||
if [ "$CH" = "all" ]; then
|
||||
for CH in stable beta nightly; do
|
||||
cat > "$OTA_DIR/manifest-${CH}.json" <<EOF
|
||||
{"version":"0.1.0-dav-android-${VER}","versionCode":${VER},"url":"${DL_URL}","sha256":"${SHA256}"}
|
||||
EOF
|
||||
echo " ✓ manifest-${CH}.json"
|
||||
done
|
||||
break
|
||||
fi
|
||||
cat > "$OTA_DIR/manifest-${CH}.json" <<EOF
|
||||
{"version":"0.1.0-dav-android-${VER}","versionCode":${VER},"url":"${DL_URL}","sha256":"${SHA256}"}
|
||||
EOF
|
||||
echo " ✓ manifest-${CH}.json"
|
||||
done
|
||||
cat > ota/manifest-stable.json <<JSON
|
||||
{
|
||||
"version": "${VERSION_NAME}",
|
||||
"builds": {
|
||||
"android": {
|
||||
"type": "apk",
|
||||
"tag": "${RELEASE_TAG}",
|
||||
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/${RELEASE_TAG}/${APK_NAME}",
|
||||
"sha256": "${SHA256}",
|
||||
"size": ${SIZE},
|
||||
"channel": "stable",
|
||||
"minVersion": "${VERSION_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
echo "Generated: ota/manifest-stable.json"
|
||||
echo " tag: ${RELEASE_TAG}"
|
||||
echo " sha256: ${SHA256}"
|
||||
echo " size: ${SIZE}"
|
||||
echo ""
|
||||
echo "Next: git tag $TAG && git push --tags"
|
||||
echo " Gitea Release → upload APK"
|
||||
echo " bash push-hotfix.sh $VER stable # H5同步"
|
||||
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"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=index-stable.html"></head><body></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=index-stable.html"></head><body></body></html>
|
||||
@@ -1,80 +0,0 @@
|
||||
<!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()">⚙</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>
|
||||
1
ota/manifest-beta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"0.1.0","builds":{}}
|
||||
1
ota/manifest-nightly.json
Normal file
@@ -0,0 +1 @@
|
||||
{"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"}}}
|
||||
1
ota/manifest-stable.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"0.1.0","builds":{}}
|
||||
@@ -1 +0,0 @@
|
||||
{"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"}}}
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/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 +0,0 @@
|
||||
https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/manifest.json
|
||||
12
package-lock.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "yuzu-gca",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "yuzu-gca",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,286 +1,20 @@
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
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 };
|
||||
import { Text, View, StyleSheet } from 'react-native';
|
||||
|
||||
export default function App() {
|
||||
const [tab, setTab] = useState<'chat' | 'terminal' | 'settings' | 'device' | 'connection'>('chat');
|
||||
|
||||
// Chat state
|
||||
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 {
|
||||
const r = await fetch(`${gatewayUrl}/tools/exec`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({command:c}), signal:AbortSignal.timeout(30000) });
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
const d = await r.json() as any;
|
||||
setTermOut(o => o + (d.stdout||d.stderr||JSON.stringify(d)) + '\n');
|
||||
} catch(e:any) { setTermOut(o => o + `Error: ${e.message}\n`); }
|
||||
finally { setTermRunning(false); }
|
||||
};
|
||||
|
||||
/** Send chat message to Gateway */
|
||||
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) {
|
||||
setMessages(prev => [...prev, {
|
||||
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 (
|
||||
<KeyboardAvoidingView style={styles.root} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<View style={styles.container}>
|
||||
<StatusBar style="light" />
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Yuzu GCA</Text>
|
||||
<View style={[styles.statusDot, styles[`dot_${connStatus}` as keyof typeof styles]]} />
|
||||
</View>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* ── 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>
|
||||
<Text style={styles.title}>Yuzu GCA</Text>
|
||||
<Text style={styles.version}>v0.1.0-dav-android-8</Text>
|
||||
<Text style={styles.subtitle}>expo-updates 热更新已激活</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Styles ────────────────────────────────────────────────
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: { flex: 1, backgroundColor: '#0f172a' },
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 48, paddingBottom: 8 },
|
||||
title: { fontSize: 22, fontWeight: '800', color: '#e2e8f0' },
|
||||
statusDot: { width: 10, height: 10, borderRadius: 5 },
|
||||
dot_connected: { backgroundColor: '#22c55e' },
|
||||
dot_connecting: { backgroundColor: '#eab308' },
|
||||
dot_error: { backgroundColor: '#ef4444' },
|
||||
dot_disconnected: { backgroundColor: '#475569' },
|
||||
|
||||
// Tabs
|
||||
tabBar: { flexDirection: 'row', marginHorizontal: 16, marginBottom: 8 },
|
||||
tab: { flex: 1, padding: 10, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: '#1e293b' },
|
||||
tabActive: { borderBottomColor: '#3b82f6' },
|
||||
tabText: { fontSize: 13, color: '#64748b' },
|
||||
tabTextActive: { color: '#3b82f6', fontWeight: '600' },
|
||||
|
||||
// Chat
|
||||
chatContainer: { flex: 1 },
|
||||
chatList: { flex: 1, paddingHorizontal: 12 },
|
||||
bubble: { marginVertical: 4, padding: 10, borderRadius: 10, maxWidth: '85%' },
|
||||
bubbleUser: { alignSelf: 'flex-end', backgroundColor: '#1e40af' },
|
||||
bubbleAI: { alignSelf: 'flex-start', backgroundColor: '#1e293b', borderWidth: 1, borderColor: '#334155' },
|
||||
bubbleRole: { fontSize: 10, color: '#94a3b8', marginBottom: 2 },
|
||||
bubbleText: { fontSize: 14, color: '#e2e8f0' },
|
||||
emptyText: { textAlign: 'center', color: '#475569', marginTop: 40, fontSize: 14 },
|
||||
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' },
|
||||
container: { flex: 1, backgroundColor: '#0f172a', alignItems: 'center', justifyContent: 'center' },
|
||||
title: { fontSize: 28, fontWeight: '800', color: '#e2e8f0' },
|
||||
version: { fontSize: 14, color: '#64748b', marginTop: 8 },
|
||||
subtitle: { fontSize: 12, color: '#3b82f6', marginTop: 16 },
|
||||
});
|
||||
|
||||
BIN
packages/client-android/android/.gradle/8.10.2/checksums/checksums.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileChanges/last-build.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/resourceHashesCache.bin
vendored
Normal file
@@ -1,2 +1,2 @@
|
||||
#Wed Jul 22 19:01:51 CST 2026
|
||||
gradle.version=8.13
|
||||
#Tue Jul 21 00:19:27 CST 2026
|
||||
gradle.version=8.10.2
|
||||
|
||||
BIN
packages/client-android/android/.gradle/file-system.probe
Normal file
@@ -1,40 +1,176 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file("../../App.tsx")
|
||||
reactNativeDir = file("../../node_modules/react-native")
|
||||
hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = file("../../node_modules/@expo/cli/build/bin/cli")
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
android {
|
||||
namespace "dev.yuzu.gca"
|
||||
compileSdk 35
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace 'dev.yuzu.gca'
|
||||
defaultConfig {
|
||||
applicationId "dev.yuzu.gca"
|
||||
minSdk 24
|
||||
targetSdk 35
|
||||
versionCode 5
|
||||
applicationId 'dev.yuzu.gca'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 8
|
||||
versionName "0.1.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release { minifyEnabled false }
|
||||
debug { debuggable true }
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions { jvmTarget = "17" }
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
// Accepts values in comma delimited lists, example:
|
||||
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||
// Trim all elements in place.
|
||||
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||
options -= ""
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "v${variant.versionName}-dav-android-${variant.versionCode}-${variant.buildType.name}.apk"
|
||||
if (options.length > 0) {
|
||||
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||
options.each {
|
||||
android.packagingOptions[prop] += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.21"
|
||||
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||
implementation "androidx.core:core:1.15.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED_IN_DEVELOPMENT" android:value="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -12,8 +12,13 @@
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<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">
|
||||
<application android:name=".MainApplication" 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="true"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="3000"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/expo-manifest.json"/>
|
||||
<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>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -26,4 +31,4 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -1,84 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,58 +1,61 @@
|
||||
package dev.yuzu.gca
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import java.io.File
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WebView(this).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowUniversalAccessFromFileURLs = true
|
||||
addJavascriptInterface(LocalBridge(), "Android")
|
||||
loadUrl("file:///android_asset/index.html")
|
||||
}.also { setContentView(it) }
|
||||
}
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
inner class LocalBridge {
|
||||
@android.webkit.JavascriptInterface
|
||||
fun exec(cmd: String): String {
|
||||
return try {
|
||||
val p = Runtime.getRuntime().exec(arrayOf("sh", "-c", cmd))
|
||||
val out = BufferedReader(InputStreamReader(p.inputStream)).readText()
|
||||
val err = BufferedReader(InputStreamReader(p.errorStream)).readText()
|
||||
p.waitFor()
|
||||
if (out.isNotEmpty()) out else err
|
||||
} catch (e: Exception) { "ERR: ${e.message}" }
|
||||
}
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
fun listFiles(path: String): String {
|
||||
return try {
|
||||
val dir = File(path)
|
||||
if (!dir.exists()) return "ERR: path not found"
|
||||
if (!dir.isDirectory) return "ERR: not a directory"
|
||||
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}" }
|
||||
}
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
fun sysinfo(): String {
|
||||
return """
|
||||
Device: ${android.os.Build.MODEL}
|
||||
Android: ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT})
|
||||
CPU: ${Runtime.getRuntime().availableProcessors()} cores
|
||||
Memory: ${Runtime.getRuntime().maxMemory() / 1024 / 1024}MB max
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package dev.yuzu.gca
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
)
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 350 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,6 +1,5 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#0f172a</color>
|
||||
<color name="iconBackground">#0f172a</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#0f172a</color>
|
||||
</resources>
|
||||
@@ -2,4 +2,5 @@
|
||||
<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>
|
||||
<string name="expo_runtime_version">0.1.0</string>
|
||||
</resources>
|
||||
@@ -1,24 +1,39 @@
|
||||
// Top-level build file
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "35.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
kotlinVersion = "2.0.21"
|
||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
||||
kotlinVersion = '2.0.21'
|
||||
|
||||
ndkVersion = "26.1.10909125"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle:8.13.2')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21')
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir.parentFile).text.trim(), '../android'))
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir.parentFile).text.trim(), '../dist'))
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Enable AAPT2 PNG crunching
|
||||
android.enablePngCrunchInReleaseBuilds=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=true
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
expo.webp.enabled=true
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
|
||||
# Enable network inspector
|
||||
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,2 +1,38 @@
|
||||
pluginManagement {
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir.parentFile).text.trim()).getParentFile().toString())
|
||||
}
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||
ex.autolinkLibrariesFromCommand()
|
||||
} else {
|
||||
def command = [
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'android'
|
||||
].toList()
|
||||
ex.autolinkLibrariesFromCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'YuzuGCA'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
reactAndroidLibs {
|
||||
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir.parentFile).text.trim(), "../gradle/libs.versions.toml")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir.parentFile).text.trim(), "../scripts/autolinking.gradle");
|
||||
useExpoModules()
|
||||
|
||||
include ':app'
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir.parentFile).text.trim()).getParentFile())
|
||||
|
||||
@@ -3,33 +3,26 @@
|
||||
"name": "YuzuGCA",
|
||||
"slug": "yuzu-gca",
|
||||
"version": "0.1.0",
|
||||
"runtimeVersion": "0.1.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "dark",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"backgroundColor": "#0f172a"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "dev.yuzu.gca"
|
||||
},
|
||||
"splash": { "backgroundColor": "#0f172a" },
|
||||
"ios": { "supportsTablet": false, "bundleIdentifier": "dev.yuzu.gca" },
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#0f172a"
|
||||
},
|
||||
"adaptiveIcon": { "backgroundColor": "#0f172a" },
|
||||
"package": "dev.yuzu.gca",
|
||||
"allowBackup": false,
|
||||
"permissions": [
|
||||
"REQUEST_INSTALL_PACKAGES"
|
||||
]
|
||||
"versionCode": 8,
|
||||
"permissions": ["REQUEST_INSTALL_PACKAGES"]
|
||||
},
|
||||
"plugins": [
|
||||
],
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "yuzu-gca-android"
|
||||
}
|
||||
}
|
||||
"plugins": ["expo-updates"],
|
||||
"updates": {
|
||||
"enabled": true,
|
||||
"checkAutomatically": "ON_LOAD",
|
||||
"fallbackToCacheTimeout": 3000,
|
||||
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/expo-manifest.json"
|
||||
},
|
||||
"extra": { "eas": { "projectId": "yuzu-gca-android" } }
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 70 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 70 B |
|
Before Width: | Height: | Size: 354 B |
@@ -1,4 +1,43 @@
|
||||
import { registerRootComponent } from 'expo';
|
||||
import App from './App';
|
||||
var React = require("react");
|
||||
var View = require("react-native").View;
|
||||
var Text = require("react-native").Text;
|
||||
var AppRegistry = require("react-native").AppRegistry;
|
||||
var Updates = require("expo-updates");
|
||||
|
||||
registerRootComponent(App);
|
||||
function App() {
|
||||
var [msg, setMsg] = React.useState("checking...");
|
||||
|
||||
React.useEffect(function() {
|
||||
async function check() {
|
||||
try {
|
||||
var rv = Updates.runtimeVersion || "empty";
|
||||
var ch = Updates.channel || "default";
|
||||
var id = Updates.updateId || "none";
|
||||
var emb = Updates.isEmbeddedLaunch ? "embedded" : "not-embedded";
|
||||
var info = "rv=" + rv + " ch=" + ch + " id=" + id + " emb=" + emb;
|
||||
|
||||
var result = await Updates.checkForUpdateAsync();
|
||||
if (result.isAvailable) {
|
||||
info += "\nUPDATE AVAILABLE! downloading...";
|
||||
await Updates.fetchUpdateAsync();
|
||||
info += "\ndownloaded. Restart app.";
|
||||
await Updates.reloadAsync();
|
||||
} else {
|
||||
info += "\nno update available";
|
||||
}
|
||||
setMsg(info);
|
||||
} catch(e) {
|
||||
setMsg("Error: " + e.message);
|
||||
}
|
||||
}
|
||||
check();
|
||||
}, []);
|
||||
|
||||
return React.createElement(View, {style: {flex:1,backgroundColor:"#0f172a",alignItems:"center",justifyContent:"center"}},
|
||||
React.createElement(Text, {style: {fontSize:28,fontWeight:"800",color:"#e2e8f0"}}, "Yuzu GCA"),
|
||||
React.createElement(Text, {style: {fontSize:14,color:"#64748b",marginTop:8}}, "v0.1.0-dav-android-10"),
|
||||
React.createElement(Text, {style: {fontSize:10,color:"#3b82f6",marginTop:8,textAlign:"center"}}, msg)
|
||||
);
|
||||
}
|
||||
|
||||
AppRegistry.registerComponent("main", function() { return App; });
|
||||
|
||||
3
packages/client-android/metro.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
|
||||
const config = {};
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||
13299
packages/client-android/package-lock.json
generated
Normal file
@@ -2,7 +2,7 @@
|
||||
"name": "@yuzu-gca/client-android",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
@@ -12,15 +12,18 @@
|
||||
"ios": "expo run:ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yuzu-gca/shared": "workspace:*",
|
||||
"@react-native/gradle-plugin": "^0.86.0",
|
||||
"expo": "~52.0.49",
|
||||
"expo-constants": "~17.0.0",
|
||||
"expo-file-system": "~18.0.0",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"expo-updates": "~0.27.5",
|
||||
"jsc-android": "^250231.0.0",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/cli": "^20.2.0",
|
||||
"@types/react": "~18.3.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
|
||||
@@ -8,17 +8,9 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@yuzu-gca/shared": [
|
||||
"../shared/src"
|
||||
]
|
||||
"@yuzu-gca/shared": ["../shared/src"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"extends": "expo/tsconfig.base"
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"@yuzu-gca/shared": "workspace:*",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "tsc --watch",
|
||||
"build": "tsc"
|
||||
},
|
||||
"name": "@yuzu-gca/server",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"main": "dist/index.js"
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* C-015 + P-006/008/009: Gateway Connection Lifecycle
|
||||
*
|
||||
* - Register device with Gateway on startup
|
||||
* - Heartbeat keepalive every N seconds
|
||||
* - Exponential backoff reconnection on disconnect
|
||||
* - Connectivity check before registration
|
||||
*/
|
||||
|
||||
import { type DeviceConfig } from "@yuzu-gca/shared";
|
||||
|
||||
export interface ConnectionState {
|
||||
status: "disconnected" | "connecting" | "connected" | "reconnecting";
|
||||
lastHeartbeat: number;
|
||||
reconnectAttempt: number;
|
||||
registeredAt: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Gateway is reachable.
|
||||
* Returns true if a HEAD request to gateway.url succeeds.
|
||||
*/
|
||||
export async function checkConnectivity(gatewayUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(`${gatewayUrl}/health`, {
|
||||
method: "HEAD",
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return resp.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this device with the Gateway.
|
||||
* POST /devices/register with device identity and capabilities.
|
||||
* Returns the assigned device token on success.
|
||||
*/
|
||||
export async function registerWithGateway(
|
||||
gatewayUrl: string,
|
||||
config: DeviceConfig,
|
||||
): Promise<{ token: string } | null> {
|
||||
try {
|
||||
const resp = await fetch(`${gatewayUrl}/devices/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: config.identity.id,
|
||||
name: config.identity.name,
|
||||
platform: config.identity.platform,
|
||||
arch: config.identity.arch,
|
||||
hostname: config.identity.hostname,
|
||||
capabilities: config.capabilities,
|
||||
transport: config.gateway.transport,
|
||||
}),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error(`[Gateway] Registration failed: HTTP ${resp.status}`);
|
||||
return null;
|
||||
}
|
||||
const data = await resp.json() as { token: string };
|
||||
console.log(`[Gateway] Registered as ${config.identity.name} (${config.identity.id})`);
|
||||
return { token: data.token };
|
||||
} catch (err) {
|
||||
console.error(`[Gateway] Registration error:`, (err as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a heartbeat to the Gateway.
|
||||
* POST /devices/:id/heartbeat
|
||||
* Returns true if Gateway acknowledged.
|
||||
*/
|
||||
export async function sendHeartbeat(
|
||||
gatewayUrl: string,
|
||||
deviceId: string,
|
||||
token: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(`${gatewayUrl}/devices/${deviceId}/heartbeat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ timestamp: Date.now() }),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return resp.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister from Gateway.
|
||||
* DELETE /devices/:id
|
||||
*/
|
||||
export async function unregisterFromGateway(
|
||||
gatewayUrl: string,
|
||||
deviceId: string,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fetch(`${gatewayUrl}/devices/${deviceId}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${token}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
} catch {
|
||||
// Best effort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute exponential backoff delay with jitter.
|
||||
*/
|
||||
export function backoffDelay(attempt: number, maxMs: number = 30000): number {
|
||||
const base = Math.min(1000 * Math.pow(2, attempt), maxMs);
|
||||
const jitter = Math.random() * base * 0.3; // 0-30% jitter
|
||||
return base + jitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection manager — handles the full lifecycle:
|
||||
* check → register → heartbeat loop → disconnect → reconnect with backoff
|
||||
*/
|
||||
export class ConnectionManager {
|
||||
private state: ConnectionState = {
|
||||
status: "disconnected",
|
||||
lastHeartbeat: 0,
|
||||
reconnectAttempt: 0,
|
||||
registeredAt: null,
|
||||
};
|
||||
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private token: string = "";
|
||||
private stopped: boolean = false;
|
||||
|
||||
constructor(
|
||||
private config: DeviceConfig,
|
||||
private onStateChange?: (state: ConnectionState) => void,
|
||||
) {}
|
||||
|
||||
get currentState(): ConnectionState { return { ...this.state }; }
|
||||
|
||||
/** Start the connection lifecycle (check → register → heartbeat) */
|
||||
async start(): Promise<void> {
|
||||
this.stopped = false;
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
/** Stop everything */
|
||||
stop(): void {
|
||||
this.stopped = true;
|
||||
this.clearHeartbeat();
|
||||
if (this.token) {
|
||||
unregisterFromGateway(this.config.gateway.url, this.config.identity.id, this.token)
|
||||
.catch(() => {});
|
||||
}
|
||||
this.setState({ status: "disconnected", registeredAt: null });
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
if (this.stopped) return;
|
||||
this.setState({ status: "connecting" });
|
||||
|
||||
const reachable = await checkConnectivity(this.config.gateway.url);
|
||||
if (!reachable) {
|
||||
console.warn("[Gateway] Unreachable, retrying...");
|
||||
this.setState({ status: "reconnecting" });
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await registerWithGateway(this.config.gateway.url, this.config);
|
||||
if (!result) {
|
||||
this.setState({ status: "reconnecting" });
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = result.token;
|
||||
this.state.reconnectAttempt = 0;
|
||||
this.setState({ status: "connected", registeredAt: Date.now() });
|
||||
this.startHeartbeat();
|
||||
}
|
||||
|
||||
private startHeartbeat(): void {
|
||||
this.clearHeartbeat();
|
||||
this.heartbeatTimer = setInterval(async () => {
|
||||
if (this.stopped) return;
|
||||
const ok = await sendHeartbeat(
|
||||
this.config.gateway.url,
|
||||
this.config.identity.id,
|
||||
this.token,
|
||||
);
|
||||
this.state.lastHeartbeat = Date.now();
|
||||
if (!ok && !this.stopped) {
|
||||
console.warn("[Gateway] Heartbeat failed, reconnecting...");
|
||||
this.clearHeartbeat();
|
||||
this.setState({ status: "reconnecting" });
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}, this.config.gateway.heartbeatInterval);
|
||||
}
|
||||
|
||||
private clearHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.stopped) return;
|
||||
const delay = backoffDelay(
|
||||
this.state.reconnectAttempt++,
|
||||
this.config.gateway.maxReconnectDelay,
|
||||
);
|
||||
console.log(`[Gateway] Reconnecting in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempt})`);
|
||||
setTimeout(() => this.connect(), delay);
|
||||
}
|
||||
|
||||
private setState(partial: Partial<ConnectionState>): void {
|
||||
this.state = { ...this.state, ...partial };
|
||||
this.onStateChange?.(this.currentState);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* C-001: MCP Server Framework — stdio transport for Gateway compatibility.
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import * as tools from "./tools.js";
|
||||
|
||||
export function createServer(): McpServer {
|
||||
const server = new McpServer({ name: "yuzu-gca-device", version: "0.1.0" });
|
||||
|
||||
server.tool("file_list", "List directory contents", { path: z.string(), recursive: z.boolean().optional() }, async a => ({ content: [{ type: "text" as const, text: JSON.stringify(await tools.file_list(a)) }] }));
|
||||
server.tool("file_read", "Read file content", { path: z.string(), encoding: z.string().optional(), offset: z.number().optional(), limit: z.number().optional() }, async a => ({ content: [{ type: "text" as const, text: (await tools.file_read(a)).content }] }));
|
||||
server.tool("file_write", "Write file content", { path: z.string(), content: z.string(), encoding: z.string().optional() }, async a => ({ content: [{ type: "text" as const, text: JSON.stringify(await tools.file_write(a)) }] }));
|
||||
server.tool("file_move", "Move/rename file", { from: z.string(), to: z.string() }, async a => ({ content: [{ type: "text" as const, text: JSON.stringify(await tools.file_move(a)) }] }));
|
||||
server.tool("exec", "Execute shell command", { command: z.string(), cwd: z.string().optional(), timeout: z.number().optional() }, async a => ({ content: [{ type: "text" as const, text: JSON.stringify(await tools.exec(a)) }] }));
|
||||
server.tool("process_list","List running processes", { filter: z.string().optional() }, async a => ({ content: [{ type: "text" as const, text: JSON.stringify(await tools.process_list(a)) }] }));
|
||||
server.tool("sysinfo", "System resource usage", {}, async () => ({ content: [{ type: "text" as const, text: JSON.stringify(await tools.sysinfo({})) }] }));
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
export async function startServer(server: McpServer): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("[Yuzu-GCA] MCP Server ready (stdio)");
|
||||
}
|
||||
|
||||
export async function bootstrap(): Promise<McpServer> {
|
||||
const server = createServer();
|
||||
await startServer(server);
|
||||
return server;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { bootstrap } from "./index.js";
|
||||
|
||||
console.error("[Yuzu-GCA] MCP Server starting (stdio)...");
|
||||
console.error(" Tools: file_list, file_read, file_write, file_move, exec, process_list, sysinfo");
|
||||
|
||||
bootstrap().catch(err => {
|
||||
console.error("[Yuzu-GCA] Fatal:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
import * as os from "node:os";
|
||||
|
||||
const BASE_DIR = path.resolve(process.env.GCA_WORKSPACE ?? process.cwd());
|
||||
|
||||
function safePath(p: string): string {
|
||||
const r = path.resolve(BASE_DIR, p);
|
||||
if (!r.startsWith(BASE_DIR)) throw new Error(`Path traversal blocked: ${p}`);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── C-002: file_list ────────────────────────────────────────
|
||||
export async function file_list(args: { path: string; recursive?: boolean }): Promise<any> {
|
||||
const dir = safePath(args.path || ".");
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const result = entries.map(e => ({
|
||||
name: e.name,
|
||||
type: e.isDirectory() ? "dir" : e.isFile() ? "file" : "other",
|
||||
size: e.isFile() ? fs.statSync(path.join(dir, e.name)).size : undefined,
|
||||
}));
|
||||
return { path: dir, entries: result, count: result.length };
|
||||
}
|
||||
|
||||
// ── C-003: file_read ────────────────────────────────────────
|
||||
export async function file_read(args: { path: string; encoding?: string; offset?: number; limit?: number }): Promise<any> {
|
||||
const filePath = safePath(args.path);
|
||||
const enc = (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding;
|
||||
let content = fs.readFileSync(filePath, enc);
|
||||
if (enc === "utf8") {
|
||||
const lines = (content as string).split("\n");
|
||||
content = lines.slice(args.offset ?? 0, args.limit ? (args.offset ?? 0) + args.limit : undefined).join("\n");
|
||||
}
|
||||
return { path: filePath, content, size: fs.statSync(filePath).size };
|
||||
}
|
||||
|
||||
// ── C-004: file_write ───────────────────────────────────────
|
||||
export async function file_write(args: { path: string; content: string; encoding?: string }): Promise<any> {
|
||||
const filePath = safePath(args.path);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, args.content, (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding);
|
||||
return { ok: true, path: filePath, size: fs.statSync(filePath).size };
|
||||
}
|
||||
|
||||
// ── C-005: file_move ────────────────────────────────────────
|
||||
export async function file_move(args: { from: string; to: string }): Promise<any> {
|
||||
const fromPath = safePath(args.from);
|
||||
const toPath = safePath(args.to);
|
||||
fs.mkdirSync(path.dirname(toPath), { recursive: true });
|
||||
fs.renameSync(fromPath, toPath);
|
||||
return { ok: true, from: fromPath, to: toPath };
|
||||
}
|
||||
|
||||
// ── C-008: exec ─────────────────────────────────────────────
|
||||
function sanitizeCommand(cmd: string): string {
|
||||
// Allow: alphanumeric, dash, slash, dot, colon, underscore, space, equals, comma
|
||||
if (/[^\x20-\x7E]/.test(cmd)) throw new Error(`Unsafe chars: ${cmd.slice(0,50)}`);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
export async function exec(args: { command: string; cwd?: string; timeout?: number }): Promise<any> {
|
||||
const command = sanitizeCommand(args.command);
|
||||
const cwd = safePath(args.cwd ?? ".");
|
||||
try {
|
||||
const stdout = execSync(command, { cwd, timeout: args.timeout ?? 30000, encoding: "utf8", maxBuffer: 10*1024*1024, stdio: ["pipe","pipe","pipe"] });
|
||||
return { ok: true, stdout: stdout.toString(), stderr: "" };
|
||||
} catch (err: any) {
|
||||
return { ok: false, stdout: err.stdout?.toString() ?? "", stderr: err.stderr?.toString() ?? "", exitCode: err.status ?? -1, signal: err.signal };
|
||||
}
|
||||
}
|
||||
|
||||
// ── C-010: process_list ─────────────────────────────────────
|
||||
export async function process_list(args: { filter?: string }): Promise<any> {
|
||||
let stdout: string;
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
stdout = execSync("tasklist /FO CSV /NH", { encoding: "utf8" }).toString();
|
||||
} else {
|
||||
stdout = execSync("ps aux --no-headers", { encoding: "utf8" }).toString();
|
||||
}
|
||||
} catch { return { error: "Failed to list processes" }; }
|
||||
const processes = stdout.trim().split("\n").map(line => {
|
||||
if (process.platform === "win32") {
|
||||
const [name, pid, , mem] = line.replace(/"/g, "").split(",");
|
||||
return { name: name?.trim(), pid: Number(pid), mem: mem?.trim() };
|
||||
}
|
||||
const parts = line.trim().split(/\s+/);
|
||||
return { user: parts[0], pid: Number(parts[1]), cpu: parts[2], mem: parts[3], name: parts[10] };
|
||||
});
|
||||
const filtered = args.filter ? processes.filter(p => p.name?.toLowerCase().includes(args.filter!.toLowerCase())) : processes.slice(0, 50);
|
||||
return { count: filtered.length, processes: filtered };
|
||||
}
|
||||
|
||||
// ── C-011: sysinfo ──────────────────────────────────────────
|
||||
export async function sysinfo(_args: {}): Promise<any> {
|
||||
const tm = os.totalmem(); const fm = os.freemem();
|
||||
return { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), cpus: os.cpus().length, loadavg: os.loadavg(), memory: { total: tm, free: fm, used: tm - fm, percent: Math.round(((tm-fm)/tm)*100) }, uptime: os.uptime(), node: process.version };
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"compilerOptions":{"target":"ES2022","module":"Node16","outDir":"dist","rootDir":"src","declaration":true,"strict":true,"esModuleInterop":true,"skipLibCheck":true},"include":["src"]}
|
||||
@@ -1,3 +1,2 @@
|
||||
export { parseVersion, compareVersion, checkUpdate, semverGt } from './updater/index.js';
|
||||
export type { GcaVersion, Manifest, ManifestBuild } from './updater/index.js';
|
||||
export * from './mcp/index.js';
|
||||
export { parseVersion, compareVersion, checkUpdate, semverGt } from './updater/index';
|
||||
export type { GcaVersion, Manifest, ManifestBuild } from './updater/index';
|
||||
|
||||