From fafb0d35ad8fdad5f2a2bb018d3f22f62ae66a30 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 6 Apr 2026 02:10:52 +0800 Subject: [PATCH] feat: add app update flow and apk deploy automation --- android-webview-app/APP_CHANGELOG.md | 33 +++ android-webview-app/app/build.gradle.kts | 26 +- .../app/src/main/AndroidManifest.xml | 4 +- .../main/java/com/cyjai/agent/MainActivity.kt | 91 ++++++- .../main/res/drawable/ic_launcher_photo.jpg | Bin 0 -> 3484 bytes server/status.py | 90 ++++++- .../personalization/PersonalizationDrawer.vue | 245 +++++++++++++++++- .../styles/components/overlays/_overlays.scss | 10 + upload_android_apk.sh | 136 ++++++++++ 9 files changed, 624 insertions(+), 11 deletions(-) create mode 100644 android-webview-app/APP_CHANGELOG.md create mode 100644 android-webview-app/app/src/main/res/drawable/ic_launcher_photo.jpg create mode 100755 upload_android_apk.sh diff --git a/android-webview-app/APP_CHANGELOG.md b/android-webview-app/APP_CHANGELOG.md new file mode 100644 index 0000000..ede081e --- /dev/null +++ b/android-webview-app/APP_CHANGELOG.md @@ -0,0 +1,33 @@ +# App 更新说明 + +# 1.0.6 +- 应用图标替换为指定 JPG 图片资源 +- 更新 AndroidManifest 图标引用(icon / roundIcon) + +# 1.0.5 +- 手机端「工作文件」侧栏最大宽度由 86% 提升到 100% +- 保持默认 60% 起始宽度,并在内容需要时继续自适应扩展 + +# 1.0.4 +- 调整手机端「工作文件」侧栏宽度策略:默认按 60% 屏宽展开 +- 当内容较宽时可自适应扩展侧栏宽度(上限 86%),减少内容被截断 +- 保持「对话记录」侧栏宽度逻辑不变 + +# 1.0.3 +- 修复 App 端版本读取时机,避免显示“当前版本未知(?)” +- 进一步隐藏 App 端页面滚动条(保留页面内部可滚动区域) + +# 1.0.2 +- 修复 App 端“当前版本显示未知”问题(版本号改为优先从 Android JS Bridge 读取) +- 优化“软件更新”页更新日志展示:长内容在卡片内滚动,不再无限拉长 +- 强化 App 页面滚动隐藏补丁,减少出现系统滚动条的情况 +- 更新上传脚本:支持自动打包 + 上传,并同步版本号与更新说明 + +# 1.0.1 +- 移除 App 页面上下滚动条(仅 App 壳生效) +- 优化 App 内页面样式补丁与资源版本参数 + +## 1.0.0 +- 首次发布 Android WebView 壳应用 +- 适配移动端抽屉宽度与状态栏主题联动 +- 新增个人空间「软件更新」页(仅 App 端显示) diff --git a/android-webview-app/app/build.gradle.kts b/android-webview-app/app/build.gradle.kts index 3f8210e..b59ddac 100644 --- a/android-webview-app/app/build.gradle.kts +++ b/android-webview-app/app/build.gradle.kts @@ -3,6 +3,11 @@ plugins { id("org.jetbrains.kotlin.android") } +fun resolveConfig(name: String): String? { + return (project.findProperty(name) as String?)?.takeIf { it.isNotBlank() } + ?: System.getenv(name)?.takeIf { it.isNotBlank() } +} + android { namespace = "com.cyjai.agent" compileSdk = 35 @@ -11,15 +16,32 @@ android { applicationId = "com.cyjai.agent" minSdk = 24 targetSdk = 35 - versionCode = 1 - versionName = "1.0.0" + versionCode = 7 + versionName = "1.0.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + signingConfigs { + create("release") { + val storeFilePath = resolveConfig("ANDROID_KEYSTORE_PATH") + val storePass = resolveConfig("ANDROID_KEYSTORE_PASSWORD") + val keyAliasValue = resolveConfig("ANDROID_KEY_ALIAS") + val keyPass = resolveConfig("ANDROID_KEY_PASSWORD") + + if (!storeFilePath.isNullOrBlank()) { + storeFile = file(storeFilePath) + } + storePassword = storePass + keyAlias = keyAliasValue + keyPassword = keyPass + } + } + buildTypes { release { isMinifyEnabled = false + signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" diff --git a/android-webview-app/app/src/main/AndroidManifest.xml b/android-webview-app/app/src/main/AndroidManifest.xml index 3eec7c5..a3eb7d8 100644 --- a/android-webview-app/app/src/main/AndroidManifest.xml +++ b/android-webview-app/app/src/main/AndroidManifest.xml @@ -5,10 +5,10 @@ diff --git a/android-webview-app/app/src/main/java/com/cyjai/agent/MainActivity.kt b/android-webview-app/app/src/main/java/com/cyjai/agent/MainActivity.kt index 970e5df..d8a1b82 100644 --- a/android-webview-app/app/src/main/java/com/cyjai/agent/MainActivity.kt +++ b/android-webview-app/app/src/main/java/com/cyjai/agent/MainActivity.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.view.ViewGroup +import android.view.View import android.webkit.CookieManager import android.webkit.JavascriptInterface import android.webkit.PermissionRequest @@ -37,6 +38,9 @@ class MainActivity : AppCompatActivity() { ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) + webView.isVerticalScrollBarEnabled = false + webView.isHorizontalScrollBarEnabled = false + webView.overScrollMode = View.OVER_SCROLL_NEVER setContentView(webView) ViewCompat.setOnApplyWindowInsetsListener(webView) { view, insets -> @@ -75,10 +79,16 @@ class MainActivity : AppCompatActivity() { super.onPageFinished(view, url) injectThemeObserver() injectMobileOverlayWidthPatch() + injectNoPageScrollPatch() } override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val url = request?.url?.toString() ?: return false + if (isApkUrl(url)) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + return true + } return !url.startsWith(HOME_URL) } } @@ -196,10 +206,57 @@ class MainActivity : AppCompatActivity() { (function() { var styleId = '__android_mobile_overlay_width_patch'; var css = [ - '.mobile-panel-sheet.mobile-panel-sheet--conversation,', - '.mobile-panel-sheet.mobile-panel-sheet--workspace {', + '.mobile-panel-sheet.mobile-panel-sheet--conversation {', ' width: min(420px, 65vw) !important;', ' max-width: 65vw !important;', + '}', + '.mobile-panel-sheet.mobile-panel-sheet--workspace {', + ' width: fit-content !important;', + ' min-width: min(420px, 60vw) !important;', + ' max-width: 100vw !important;', + '}' + ].join('\n'); + var existing = document.getElementById(styleId); + if (existing) { + existing.textContent = css; + return; + } + var style = document.createElement('style'); + style.id = styleId; + style.type = 'text/css'; + style.textContent = css; + (document.head || document.documentElement).appendChild(style); + })(); + """.trimIndent() + webView.evaluateJavascript(script, null) + } + + private fun injectNoPageScrollPatch() { + val script = """ + (function() { + var styleId = '__android_no_page_scroll_patch'; + var css = [ + 'html, body {', + ' overflow: hidden !important;', + ' height: 100% !important;', + ' max-height: 100% !important;', + '}', + 'body {', + ' position: fixed !important;', + ' width: 100% !important;', + ' inset: 0 !important;', + '}', + '* {', + ' scrollbar-width: none !important;', + '}', + '*::-webkit-scrollbar {', + ' width: 0 !important;', + ' height: 0 !important;', + ' display: none !important;', + '}', + '#app, .app-root, .chat-container {', + ' max-height: 100% !important;', + ' overflow: hidden !important;', '}' ].join('\n'); var existing = document.getElementById(styleId); @@ -224,15 +281,41 @@ class MainActivity : AppCompatActivity() { applySystemBarTheme(theme) } } + + @JavascriptInterface + fun getAppVersionCode(): String { + return getInstalledVersionCode().toString() + } + + @JavascriptInterface + fun getAppVersionName(): String { + return getInstalledVersionName() + } } companion object { private const val HOME_URL = "https://agent.cyjai.com" - private const val WEB_ASSET_VERSION = "20260405_3" + private const val WEB_ASSET_VERSION = "20260406_4" private const val FILE_CHOOSER_REQUEST_CODE = 9001 } private fun buildHomeUrl(): String { - return "$HOME_URL/?app_shell=$WEB_ASSET_VERSION" + val vc = getInstalledVersionCode() + val vn = Uri.encode(getInstalledVersionName()) + return "$HOME_URL/?app_shell=$WEB_ASSET_VERSION&app_vc=$vc&app_vn=$vn" + } + + private fun getInstalledVersionCode(): Long { + val pkgInfo = packageManager.getPackageInfo(packageName, 0) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pkgInfo.longVersionCode else @Suppress("DEPRECATION") pkgInfo.versionCode.toLong() + } + + private fun getInstalledVersionName(): String { + val pkgInfo = packageManager.getPackageInfo(packageName, 0) + return pkgInfo.versionName ?: "unknown" + } + + private fun isApkUrl(url: String): Boolean { + return url.lowercase(Locale.getDefault()).endsWith(".apk") || url.contains("/api/app/apk/latest") } } diff --git a/android-webview-app/app/src/main/res/drawable/ic_launcher_photo.jpg b/android-webview-app/app/src/main/res/drawable/ic_launcher_photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cac6550b7bea42f68bab1c673ca8fa4362781415 GIT binary patch literal 3484 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 JP tuple[int, str]: + """从 android-webview-app/app/build.gradle.kts 读取 versionCode/versionName。""" + gradle_file = project_root / "android-webview-app" / "app" / "build.gradle.kts" + if not gradle_file.exists(): + return 1, "1.0.0" + text = gradle_file.read_text(encoding="utf-8", errors="ignore") + vc_match = re.search(r"versionCode\s*=\s*(\d+)", text) + vn_match = re.search(r'versionName\s*=\s*"([^"]+)"', text) + version_code = int(vc_match.group(1)) if vc_match else 1 + version_name = vn_match.group(1) if vn_match else "1.0.0" + return version_code, version_name + + +def _resolve_android_apk_path(project_root: Path) -> Path: + """优先使用 release APK,其次 fallback 到 debug APK。""" + release_apk = project_root / "android-webview-app" / "app" / "release" / "app-release.apk" + if release_apk.exists(): + return release_apk + return project_root / "android-webview-app" / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk" + + +def _load_app_changelog(project_root: Path) -> str: + """读取 App 更新说明(优先 APP_CHANGELOG.md)。""" + candidates = [ + project_root / "android-webview-app" / "APP_CHANGELOG.md", + project_root / "android-webview-app" / "CHANGELOG.md", + ] + for path in candidates: + if not path.exists(): + continue + text = path.read_text(encoding="utf-8", errors="ignore").strip() + if text: + return text[:4000] + return "" + + @status_bp.route('/api/status') @api_login_required @with_terminal @@ -94,3 +132,53 @@ def get_project_storage(terminal, workspace, username): return jsonify({"success": True, "data": stale.get("data"), "stale": True}), 200 return jsonify({"success": False, "error": str(exc)}), 500 + +@status_bp.route('/api/app/version') +@api_login_required +def get_app_version_info(): + project_root = Path(__file__).resolve().parent.parent + version_code, version_name = _parse_android_version_from_gradle(project_root) + apk_path = _resolve_android_apk_path(project_root) + apk_exists = apk_path.exists() + file_size = apk_path.stat().st_size if apk_exists else 0 + published_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(apk_path.stat().st_mtime)) if apk_exists else None + changelog = _load_app_changelog(project_root) + apk_url = request.url_root.rstrip("/") + "/api/app/apk/latest" + + current_code_raw = request.args.get("currentVersionCode", "").strip() + current_code = int(current_code_raw) if current_code_raw.isdigit() else None + has_update = (version_code > current_code) if current_code is not None else None + + return jsonify({ + "success": True, + "data": { + "latestVersionCode": version_code, + "latestVersionName": version_name, + "apkUrl": apk_url, + "apkSha256": "", + "fileSizeBytes": file_size, + "changelog": changelog, + "forceUpdate": False, + "publishedAt": published_at, + "minSupportedVersionCode": 1, + "apkExists": apk_exists, + "hasUpdate": has_update, + } + }) + + +@status_bp.route('/api/app/apk/latest') +def download_latest_apk(): + project_root = Path(__file__).resolve().parent.parent + apk_path = _resolve_android_apk_path(project_root) + if not apk_path.exists(): + return jsonify({"success": False, "error": "APK 不存在,请先构建 release 包"}), 404 + return send_file( + apk_path, + as_attachment=True, + download_name="cyj-agent-latest.apk", + mimetype="application/vnd.android.package-archive", + conditional=False, + etag=False, + max_age=0, + ) diff --git a/static/src/components/personalization/PersonalizationDrawer.vue b/static/src/components/personalization/PersonalizationDrawer.vue index 24af5c4..5686f29 100644 --- a/static/src/components/personalization/PersonalizationDrawer.vue +++ b/static/src/components/personalization/PersonalizationDrawer.vue @@ -380,6 +380,61 @@ +
+
+
+
+

App 更新

+

软件更新

+

仅移动 App 可见。自动检查新版本,也可手动检查。

+

+ 当前版本:{{ appCurrentVersionText }} +

+
+
+ +
+
+
最新版本
+
{{ appLatestVersionText }}
+
+
+
安装包大小
+
{{ appLatestFileSizeText }}
+
+
+
更新状态
+
+ {{ appUpdateStateText }} +
+
+
+ +
+
更新说明
+
{{ appUpdateChangelogText }}
+
+ +
+ {{ appUpdateError }} + 正在检查版本... + {{ appUpdateCheckedText }} +
+ + +
+
+
+
@@ -767,7 +822,7 @@