feat: add app update flow and apk deploy automation
This commit is contained in:
parent
6f2f854f6d
commit
fafb0d35ad
33
android-webview-app/APP_CHANGELOG.md
Normal file
33
android-webview-app/APP_CHANGELOG.md
Normal file
@ -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 端显示)
|
||||||
@ -3,6 +3,11 @@ plugins {
|
|||||||
id("org.jetbrains.kotlin.android")
|
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 {
|
android {
|
||||||
namespace = "com.cyjai.agent"
|
namespace = "com.cyjai.agent"
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
@ -11,15 +16,32 @@ android {
|
|||||||
applicationId = "com.cyjai.agent"
|
applicationId = "com.cyjai.agent"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 7
|
||||||
versionName = "1.0.0"
|
versionName = "1.0.6"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_bot_icon"
|
android:icon="@drawable/ic_launcher_photo"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@drawable/ic_bot_icon"
|
android:roundIcon="@drawable/ic_launcher_photo"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AgentWebView"
|
android:theme="@style/Theme.AgentWebView"
|
||||||
android:usesCleartextTraffic="false">
|
android:usesCleartextTraffic="false">
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.View
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.PermissionRequest
|
import android.webkit.PermissionRequest
|
||||||
@ -37,6 +38,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
|
webView.isVerticalScrollBarEnabled = false
|
||||||
|
webView.isHorizontalScrollBarEnabled = false
|
||||||
|
webView.overScrollMode = View.OVER_SCROLL_NEVER
|
||||||
setContentView(webView)
|
setContentView(webView)
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(webView) { view, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(webView) { view, insets ->
|
||||||
@ -75,10 +79,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
injectThemeObserver()
|
injectThemeObserver()
|
||||||
injectMobileOverlayWidthPatch()
|
injectMobileOverlayWidthPatch()
|
||||||
|
injectNoPageScrollPatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||||
val url = request?.url?.toString() ?: return false
|
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)
|
return !url.startsWith(HOME_URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,10 +206,57 @@ class MainActivity : AppCompatActivity() {
|
|||||||
(function() {
|
(function() {
|
||||||
var styleId = '__android_mobile_overlay_width_patch';
|
var styleId = '__android_mobile_overlay_width_patch';
|
||||||
var css = [
|
var css = [
|
||||||
'.mobile-panel-sheet.mobile-panel-sheet--conversation,',
|
'.mobile-panel-sheet.mobile-panel-sheet--conversation {',
|
||||||
'.mobile-panel-sheet.mobile-panel-sheet--workspace {',
|
|
||||||
' width: min(420px, 65vw) !important;',
|
' width: min(420px, 65vw) !important;',
|
||||||
' max-width: 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');
|
].join('\n');
|
||||||
var existing = document.getElementById(styleId);
|
var existing = document.getElementById(styleId);
|
||||||
@ -224,15 +281,41 @@ class MainActivity : AppCompatActivity() {
|
|||||||
applySystemBarTheme(theme)
|
applySystemBarTheme(theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getAppVersionCode(): String {
|
||||||
|
return getInstalledVersionCode().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getAppVersionName(): String {
|
||||||
|
return getInstalledVersionName()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val HOME_URL = "https://agent.cyjai.com"
|
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 const val FILE_CHOOSER_REQUEST_CODE = 9001
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildHomeUrl(): String {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import time
|
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 .auth_helpers import api_login_required, resolve_admin_policy
|
||||||
from .context import with_terminal
|
from .context import with_terminal
|
||||||
@ -16,6 +18,42 @@ from config import AGENT_VERSION
|
|||||||
status_bp = Blueprint('status', __name__)
|
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')
|
@status_bp.route('/api/status')
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@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": True, "data": stale.get("data"), "stale": True}), 200
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@ -380,6 +380,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-else-if="activeTab === 'app-update'" key="app-update" class="personal-page app-update-page">
|
||||||
|
<div class="usage-summary-card">
|
||||||
|
<div class="usage-summary-header">
|
||||||
|
<div>
|
||||||
|
<p class="usage-summary-eyebrow">App 更新</p>
|
||||||
|
<h3>软件更新</h3>
|
||||||
|
<p class="usage-summary-desc">仅移动 App 可见。自动检查新版本,也可手动检查。</p>
|
||||||
|
<p class="usage-summary-note">
|
||||||
|
当前版本:{{ appCurrentVersionText }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usage-summary-grid usage-summary-grid--counts">
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">最新版本</div>
|
||||||
|
<div class="value">{{ appLatestVersionText }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">安装包大小</div>
|
||||||
|
<div class="value">{{ appLatestFileSizeText }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">更新状态</div>
|
||||||
|
<div class="value" :class="{ 'value--warning': appHasUpdate }">
|
||||||
|
{{ appUpdateStateText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-update-changelog" v-if="appUpdateChangelogText">
|
||||||
|
<div class="app-update-changelog-title">更新说明</div>
|
||||||
|
<pre class="app-update-changelog-content">{{ appUpdateChangelogText }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usage-summary-meta">
|
||||||
|
<span v-if="appUpdateError" class="usage-summary-error">{{ appUpdateError }}</span>
|
||||||
|
<span v-else-if="appUpdateChecking">正在检查版本...</span>
|
||||||
|
<span v-else>{{ appUpdateCheckedText }}</span>
|
||||||
|
<div class="app-update-actions">
|
||||||
|
<button type="button" class="usage-summary-refresh" @click="checkAppUpdate" :disabled="appUpdateChecking">
|
||||||
|
{{ appUpdateChecking ? '检查中...' : '手动检查' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usage-summary-refresh"
|
||||||
|
:disabled="!appHasUpdate || !appDownloadUrl"
|
||||||
|
@click="downloadLatestApp"
|
||||||
|
>
|
||||||
|
下载更新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section v-else-if="activeTab === 'behavior'" key="behavior" class="personal-page behavior-page">
|
<section v-else-if="activeTab === 'behavior'" key="behavior" class="personal-page behavior-page">
|
||||||
<div class="behavior-section">
|
<div class="behavior-section">
|
||||||
<div class="behavior-field">
|
<div class="behavior-field">
|
||||||
@ -767,7 +822,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { usePersonalizationStore, type BlockDisplayMode } from '@/stores/personalization';
|
import { usePersonalizationStore, type BlockDisplayMode } from '@/stores/personalization';
|
||||||
import { useResourceStore } from '@/stores/resource';
|
import { useResourceStore } from '@/stores/resource';
|
||||||
@ -804,6 +859,7 @@ const baseTabs = [
|
|||||||
{ id: 'preferences', label: '个性化设置', description: '称呼、语气与注意事项' },
|
{ id: 'preferences', label: '个性化设置', description: '称呼、语气与注意事项' },
|
||||||
{ id: 'model', label: '模型偏好', description: '默认模型选择' },
|
{ id: 'model', label: '模型偏好', description: '默认模型选择' },
|
||||||
{ id: 'usage', label: '用量统计', description: '对话累计数据' },
|
{ id: 'usage', label: '用量统计', description: '对话累计数据' },
|
||||||
|
{ id: 'app-update', label: '软件更新', description: '检查并下载新版 APK' },
|
||||||
{ id: 'behavior', label: '模型行为', description: '工具提示与界面表现' },
|
{ id: 'behavior', label: '模型行为', description: '工具提示与界面表现' },
|
||||||
{ id: 'skills', label: 'Skills', description: '可用技能开关' },
|
{ id: 'skills', label: 'Skills', description: '可用技能开关' },
|
||||||
{ id: 'image', label: '图片压缩', description: '发送图片的尺寸策略' },
|
{ id: 'image', label: '图片压缩', description: '发送图片的尺寸策略' },
|
||||||
@ -815,6 +871,7 @@ type PersonalTab =
|
|||||||
| 'preferences'
|
| 'preferences'
|
||||||
| 'model'
|
| 'model'
|
||||||
| 'usage'
|
| 'usage'
|
||||||
|
| 'app-update'
|
||||||
| 'behavior'
|
| 'behavior'
|
||||||
| 'skills'
|
| 'skills'
|
||||||
| 'image'
|
| 'image'
|
||||||
@ -823,9 +880,15 @@ type PersonalTab =
|
|||||||
| 'admin-monitor';
|
| 'admin-monitor';
|
||||||
|
|
||||||
const isAdmin = computed(() => (resourceStore.usageQuota.role || '').toLowerCase() === 'admin');
|
const isAdmin = computed(() => (resourceStore.usageQuota.role || '').toLowerCase() === 'admin');
|
||||||
|
const isAppShell = computed(() => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.has('app_shell')) return true;
|
||||||
|
return Boolean((window as any)?.AndroidThemeBridge);
|
||||||
|
});
|
||||||
|
|
||||||
const personalTabs = computed(() => {
|
const personalTabs = computed(() => {
|
||||||
const tabs = [...baseTabs];
|
const tabs = baseTabs.filter((tab) => (tab.id === 'app-update' ? isAppShell.value : true));
|
||||||
if (isAdmin.value) {
|
if (isAdmin.value) {
|
||||||
tabs.push({ id: 'admin-monitor' as const, label: '管理员监控', description: '系统总览' });
|
tabs.push({ id: 'admin-monitor' as const, label: '管理员监控', description: '系统总览' });
|
||||||
}
|
}
|
||||||
@ -900,6 +963,96 @@ const usageUpdatedText = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appCurrentVersionCode = ref<number | null>(null);
|
||||||
|
const appCurrentVersionName = ref<string>('');
|
||||||
|
const appUpdateChecking = ref(false);
|
||||||
|
const appUpdateError = ref('');
|
||||||
|
const appUpdateCheckedAt = ref<string | null>(null);
|
||||||
|
const appUpdateInfo = ref<{
|
||||||
|
latestVersionCode: number;
|
||||||
|
latestVersionName: string;
|
||||||
|
apkUrl: string;
|
||||||
|
fileSizeBytes: number;
|
||||||
|
changelog?: string;
|
||||||
|
publishedAt?: string | null;
|
||||||
|
hasUpdate?: boolean | null;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const vcRaw = params.get('app_vc') || '';
|
||||||
|
const vnRaw = params.get('app_vn') || '';
|
||||||
|
const bridge = (window as any)?.AndroidThemeBridge;
|
||||||
|
const bridgeVcRaw = bridge && typeof bridge.getAppVersionCode === 'function' ? String(bridge.getAppVersionCode() || '') : '';
|
||||||
|
const bridgeVnRaw = bridge && typeof bridge.getAppVersionName === 'function' ? String(bridge.getAppVersionName() || '') : '';
|
||||||
|
const finalVcRaw = vcRaw || bridgeVcRaw;
|
||||||
|
const finalVnRaw = vnRaw || bridgeVnRaw;
|
||||||
|
appCurrentVersionCode.value = /^\d+$/.test(finalVcRaw) ? Number(finalVcRaw) : null;
|
||||||
|
appCurrentVersionName.value = finalVnRaw || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateAppVersionFromBridge = () => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const bridge = (window as any)?.AndroidThemeBridge;
|
||||||
|
if (!bridge) return;
|
||||||
|
try {
|
||||||
|
const vcRaw = typeof bridge.getAppVersionCode === 'function' ? String(bridge.getAppVersionCode() || '') : '';
|
||||||
|
const vnRaw = typeof bridge.getAppVersionName === 'function' ? String(bridge.getAppVersionName() || '') : '';
|
||||||
|
if (vcRaw && /^\d+$/.test(vcRaw)) {
|
||||||
|
appCurrentVersionCode.value = Number(vcRaw);
|
||||||
|
}
|
||||||
|
if (vnRaw) {
|
||||||
|
appCurrentVersionName.value = vnRaw;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore bridge read failures
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const appCurrentVersionText = computed(() => {
|
||||||
|
const vn = appCurrentVersionName.value || '未知';
|
||||||
|
const vc = appCurrentVersionCode.value ?? '?';
|
||||||
|
return `${vn} (${vc})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appLatestVersionText = computed(() => {
|
||||||
|
if (!appUpdateInfo.value) return '--';
|
||||||
|
return `${appUpdateInfo.value.latestVersionName || '未知'} (${appUpdateInfo.value.latestVersionCode ?? '?'})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appLatestFileSizeText = computed(() => {
|
||||||
|
const bytes = Number(appUpdateInfo.value?.fileSizeBytes || 0);
|
||||||
|
if (!bytes) return '--';
|
||||||
|
const mb = bytes / 1024 / 1024;
|
||||||
|
return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appUpdateChangelogText = computed(() => {
|
||||||
|
const text = (appUpdateInfo.value?.changelog || '').trim();
|
||||||
|
return text || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const appHasUpdate = computed(() => {
|
||||||
|
if (!appUpdateInfo.value) return false;
|
||||||
|
if (typeof appUpdateInfo.value.hasUpdate === 'boolean') return appUpdateInfo.value.hasUpdate;
|
||||||
|
if (appCurrentVersionCode.value == null) return false;
|
||||||
|
return Number(appUpdateInfo.value.latestVersionCode || 0) > appCurrentVersionCode.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appDownloadUrl = computed(() => appUpdateInfo.value?.apkUrl || '');
|
||||||
|
|
||||||
|
const appUpdateStateText = computed(() => {
|
||||||
|
if (!appUpdateInfo.value) return '未检查';
|
||||||
|
return appHasUpdate.value ? '发现新版本' : '已是最新';
|
||||||
|
});
|
||||||
|
|
||||||
|
const appUpdateCheckedText = computed(() => {
|
||||||
|
if (!appUpdateCheckedAt.value) return '尚未检查更新';
|
||||||
|
const d = new Date(appUpdateCheckedAt.value);
|
||||||
|
if (Number.isNaN(d.getTime())) return '刚刚检查过';
|
||||||
|
return `最近检查:${d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
||||||
|
});
|
||||||
|
|
||||||
const fetchUsageSummary = async () => {
|
const fetchUsageSummary = async () => {
|
||||||
if (usageLoading.value) {
|
if (usageLoading.value) {
|
||||||
return;
|
return;
|
||||||
@ -929,12 +1082,62 @@ const fetchUsageSummary = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkAppUpdate = async () => {
|
||||||
|
if (appUpdateChecking.value) return;
|
||||||
|
hydrateAppVersionFromBridge();
|
||||||
|
appUpdateChecking.value = true;
|
||||||
|
appUpdateError.value = '';
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (appCurrentVersionCode.value != null) {
|
||||||
|
params.set('currentVersionCode', String(appCurrentVersionCode.value));
|
||||||
|
}
|
||||||
|
const suffix = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const resp = await fetch(`/api/app/version${suffix}`);
|
||||||
|
const payload = await resp.json();
|
||||||
|
if (!resp.ok || !payload?.success) {
|
||||||
|
throw new Error(payload?.error || '检查更新失败');
|
||||||
|
}
|
||||||
|
const data = payload?.data || {};
|
||||||
|
appUpdateInfo.value = {
|
||||||
|
latestVersionCode: Number(data.latestVersionCode || 0),
|
||||||
|
latestVersionName: String(data.latestVersionName || ''),
|
||||||
|
apkUrl: String(data.apkUrl || ''),
|
||||||
|
fileSizeBytes: Number(data.fileSizeBytes || 0),
|
||||||
|
changelog: String(data.changelog || ''),
|
||||||
|
publishedAt: data.publishedAt || null,
|
||||||
|
hasUpdate: typeof data.hasUpdate === 'boolean' ? data.hasUpdate : null
|
||||||
|
};
|
||||||
|
appUpdateCheckedAt.value = new Date().toISOString();
|
||||||
|
} catch (error: any) {
|
||||||
|
appUpdateError.value = error?.message || '检查更新失败';
|
||||||
|
} finally {
|
||||||
|
appUpdateChecking.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadLatestApp = () => {
|
||||||
|
const url = appDownloadUrl.value;
|
||||||
|
if (!url) {
|
||||||
|
appUpdateError.value = '未拿到下载地址';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
hydrateAppVersionFromBridge();
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [activeTab.value, visible.value],
|
() => [activeTab.value, visible.value],
|
||||||
([tab, isVisible]) => {
|
([tab, isVisible]) => {
|
||||||
if (isVisible && tab === 'usage') {
|
if (isVisible && tab === 'usage') {
|
||||||
fetchUsageSummary();
|
fetchUsageSummary();
|
||||||
}
|
}
|
||||||
|
if (isVisible && tab === 'app-update' && isAppShell.value && !appUpdateInfo.value && !appUpdateChecking.value) {
|
||||||
|
checkAppUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1198,6 +1401,44 @@ const applyThemeOption = (theme: ThemeKey) => {
|
|||||||
box-shadow: 0 14px 28px rgba(61, 57, 41, 0.22);
|
box-shadow: 0 14px 28px rgba(61, 57, 41, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-update-page .app-update-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-update-changelog {
|
||||||
|
border: 1px solid var(--claude-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--claude-panel);
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-update-changelog-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--claude-text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-update-changelog-content {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.usage-summary-grid {
|
.usage-summary-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|||||||
@ -1736,10 +1736,20 @@
|
|||||||
.mobile-panel-sheet--conversation,
|
.mobile-panel-sheet--conversation,
|
||||||
.mobile-panel-sheet--workspace {
|
.mobile-panel-sheet--workspace {
|
||||||
background: var(--claude-left-rail);
|
background: var(--claude-left-rail);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-panel-sheet--conversation {
|
||||||
|
background: var(--claude-left-rail);
|
||||||
width: min(420px, 65vw);
|
width: min(420px, 65vw);
|
||||||
max-width: 65vw;
|
max-width: 65vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-panel-sheet--workspace {
|
||||||
|
width: fit-content;
|
||||||
|
min-width: min(420px, 60vw);
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-panel-sheet--focus {
|
.mobile-panel-sheet--focus {
|
||||||
background: var(--claude-panel);
|
background: var(--claude-panel);
|
||||||
}
|
}
|
||||||
|
|||||||
136
upload_android_apk.sh
Executable file
136
upload_android_apk.sh
Executable file
@ -0,0 +1,136 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 专用:上传本地 APK 到服务器,覆盖
|
||||||
|
# /opt/agent/agents/android-webview-app/app/release/app-release.apk
|
||||||
|
# 并同步版本元数据(build.gradle.kts + APP_CHANGELOG.md)
|
||||||
|
# 支持命令行自动打包(assembleRelease)+ 上传
|
||||||
|
|
||||||
|
# ===== 可按需覆盖(环境变量优先)=====
|
||||||
|
REMOTE_HOST="${REMOTE_HOST:-59.110.19.30}"
|
||||||
|
REMOTE_USER="${REMOTE_USER:-root}"
|
||||||
|
REMOTE_PORT="${REMOTE_PORT:-22}"
|
||||||
|
REMOTE_APK_PATH="${REMOTE_APK_PATH:-/opt/agent/agents/android-webview-app/app/release/app-release.apk}"
|
||||||
|
REMOTE_GRADLE_PATH="${REMOTE_GRADLE_PATH:-/opt/agent/agents/android-webview-app/app/build.gradle.kts}"
|
||||||
|
REMOTE_CHANGELOG_PATH="${REMOTE_CHANGELOG_PATH:-/opt/agent/agents/android-webview-app/APP_CHANGELOG.md}"
|
||||||
|
REMOTE_PASS="${REMOTE_PASS:-}"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ANDROID_APP_DIR="${ANDROID_APP_DIR:-$SCRIPT_DIR/android-webview-app}"
|
||||||
|
LOCAL_APK_PATH="${LOCAL_APK_PATH:-}"
|
||||||
|
LOCAL_GRADLE_PATH="${LOCAL_GRADLE_PATH:-$SCRIPT_DIR/android-webview-app/app/build.gradle.kts}"
|
||||||
|
LOCAL_CHANGELOG_PATH="${LOCAL_CHANGELOG_PATH:-$SCRIPT_DIR/android-webview-app/APP_CHANGELOG.md}"
|
||||||
|
BUILD_APK="${BUILD_APK:-1}" # 1=先打包再上传;0=仅上传
|
||||||
|
|
||||||
|
SSH_OPTS=(
|
||||||
|
-o StrictHostKeyChecking=no
|
||||||
|
-o UserKnownHostsFile=/dev/null
|
||||||
|
)
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || { echo "缺少命令: $1"; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
need_cmd ssh
|
||||||
|
need_cmd scp
|
||||||
|
|
||||||
|
if [[ "$BUILD_APK" == "1" ]]; then
|
||||||
|
if [[ ! -d "$ANDROID_APP_DIR" ]]; then
|
||||||
|
echo "Android 工程目录不存在: $ANDROID_APP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$ANDROID_APP_DIR/gradlew" ]]; then
|
||||||
|
echo "未找到可执行的 gradlew: $ANDROID_APP_DIR/gradlew"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "先执行 Android 打包: assembleRelease"
|
||||||
|
(
|
||||||
|
cd "$ANDROID_APP_DIR"
|
||||||
|
./gradlew assembleRelease
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 自动探测默认 APK 路径(按优先级)
|
||||||
|
if [[ -z "$LOCAL_APK_PATH" ]]; then
|
||||||
|
if [[ -f "$ANDROID_APP_DIR/app/build/outputs/apk/release/app-release.apk" ]]; then
|
||||||
|
LOCAL_APK_PATH="$ANDROID_APP_DIR/app/build/outputs/apk/release/app-release.apk"
|
||||||
|
elif [[ -f "$ANDROID_APP_DIR/app/build/outputs/apk/release/app-release-unsigned.apk" ]]; then
|
||||||
|
LOCAL_APK_PATH="$ANDROID_APP_DIR/app/build/outputs/apk/release/app-release-unsigned.apk"
|
||||||
|
elif [[ -f "$ANDROID_APP_DIR/app/release/app-release.apk" ]]; then
|
||||||
|
LOCAL_APK_PATH="$ANDROID_APP_DIR/app/release/app-release.apk"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$LOCAL_APK_PATH" ]]; then
|
||||||
|
echo "本地 APK 不存在: $LOCAL_APK_PATH"
|
||||||
|
echo "尝试过路径:"
|
||||||
|
echo " - $ANDROID_APP_DIR/app/build/outputs/apk/release/app-release.apk"
|
||||||
|
echo " - $ANDROID_APP_DIR/app/build/outputs/apk/release/app-release-unsigned.apk"
|
||||||
|
echo " - $ANDROID_APP_DIR/app/release/app-release.apk"
|
||||||
|
echo "可用方式: BUILD_APK=1 bash upload_android_apk.sh"
|
||||||
|
echo "或指定现成包: BUILD_APK=0 LOCAL_APK_PATH=/your/app-release.apk bash upload_android_apk.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$LOCAL_GRADLE_PATH" ]]; then
|
||||||
|
echo "本地版本文件不存在: $LOCAL_GRADLE_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$LOCAL_CHANGELOG_PATH" ]]; then
|
||||||
|
echo "本地更新说明不存在: $LOCAL_CHANGELOG_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "本地 APK: $LOCAL_APK_PATH"
|
||||||
|
if [[ "$LOCAL_APK_PATH" == *"-unsigned.apk" ]]; then
|
||||||
|
log "错误:当前是 unsigned APK,无法用于覆盖安装更新。请先配置签名后重新打包。"
|
||||||
|
log "需要环境变量:ANDROID_KEYSTORE_PATH / ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "本地版本文件: $LOCAL_GRADLE_PATH"
|
||||||
|
log "本地更新说明: $LOCAL_CHANGELOG_PATH"
|
||||||
|
log "目标服务器: $REMOTE_USER@$REMOTE_HOST:$REMOTE_APK_PATH"
|
||||||
|
|
||||||
|
local remote_apk_dir remote_gradle_dir remote_changelog_dir
|
||||||
|
remote_apk_dir="$(dirname "$REMOTE_APK_PATH")"
|
||||||
|
remote_gradle_dir="$(dirname "$REMOTE_GRADLE_PATH")"
|
||||||
|
remote_changelog_dir="$(dirname "$REMOTE_CHANGELOG_PATH")"
|
||||||
|
|
||||||
|
if [[ -n "$REMOTE_PASS" ]]; then
|
||||||
|
need_cmd sshpass
|
||||||
|
log "创建远程目录: $remote_apk_dir / $remote_gradle_dir / $remote_changelog_dir"
|
||||||
|
sshpass -p "$REMOTE_PASS" ssh -p "$REMOTE_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
|
"mkdir -p '$remote_apk_dir' '$remote_gradle_dir' '$remote_changelog_dir'"
|
||||||
|
|
||||||
|
log "上传并覆盖 APK + 版本元数据"
|
||||||
|
sshpass -p "$REMOTE_PASS" scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$LOCAL_APK_PATH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_APK_PATH"
|
||||||
|
sshpass -p "$REMOTE_PASS" scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$LOCAL_GRADLE_PATH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_GRADLE_PATH"
|
||||||
|
sshpass -p "$REMOTE_PASS" scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$LOCAL_CHANGELOG_PATH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_CHANGELOG_PATH"
|
||||||
|
|
||||||
|
log "远程文件检查"
|
||||||
|
sshpass -p "$REMOTE_PASS" ssh -p "$REMOTE_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
|
"ls -lh '$REMOTE_APK_PATH' '$REMOTE_GRADLE_PATH' '$REMOTE_CHANGELOG_PATH'"
|
||||||
|
else
|
||||||
|
log "使用 SSH Key 模式(未提供 REMOTE_PASS)"
|
||||||
|
log "创建远程目录: $remote_apk_dir / $remote_gradle_dir / $remote_changelog_dir"
|
||||||
|
ssh -p "$REMOTE_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
|
"mkdir -p '$remote_apk_dir' '$remote_gradle_dir' '$remote_changelog_dir'"
|
||||||
|
|
||||||
|
log "上传并覆盖 APK + 版本元数据"
|
||||||
|
scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$LOCAL_APK_PATH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_APK_PATH"
|
||||||
|
scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$LOCAL_GRADLE_PATH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_GRADLE_PATH"
|
||||||
|
scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$LOCAL_CHANGELOG_PATH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_CHANGELOG_PATH"
|
||||||
|
|
||||||
|
log "远程文件检查"
|
||||||
|
ssh -p "$REMOTE_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
|
"ls -lh '$REMOTE_APK_PATH' '$REMOTE_GRADLE_PATH' '$REMOTE_CHANGELOG_PATH'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "完成:服务器 APK 与版本信息已更新"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Loading…
Reference in New Issue
Block a user