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 0000000..cac6550 Binary files /dev/null and b/android-webview-app/app/src/main/res/drawable/ic_launcher_photo.jpg differ diff --git a/server/status.py b/server/status.py index c273fb6..c1d88d4 100644 --- a/server/status.py +++ b/server/status.py @@ -1,6 +1,8 @@ from __future__ import annotations import time -from flask import Blueprint, jsonify +import re +from pathlib import Path +from flask import Blueprint, jsonify, request, send_file from .auth_helpers import api_login_required, resolve_admin_policy from .context import with_terminal @@ -16,6 +18,42 @@ from config import AGENT_VERSION status_bp = Blueprint('status', __name__) +def _parse_android_version_from_gradle(project_root: Path) -> 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 @@