Skip to content

Commit e535ac3

Browse files
committed
Merge branch 'dev'
2 parents 0b45ccd + 8e2f68c commit e535ac3

45 files changed

Lines changed: 1227 additions & 28 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/mobile-release.yml‎

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ jobs:
6262
needs: build
6363
runs-on: ubuntu-latest
6464
timeout-minutes: 15
65+
# secrets 不可用于 if 条件(同 release.yml),先注入 job 级 env 再在 step 用 env.* 引用
66+
env:
67+
DOWNLOAD_BASE_URL: ${{ secrets.DOWNLOAD_BASE_URL }}
68+
HAS_ALIYUN_AK: ${{ secrets.ALIYUN_ACCESS_KEY_ID != '' }}
6569
steps:
6670
- uses: actions/download-artifact@v4
6771
with:
@@ -85,7 +89,10 @@ jobs:
8589
strip_components: 1
8690
rm: true
8791
# scp rm:true 重建目录导致 bind mount 失效,须重启 nginx 容器恢复
88-
# (同 deploy-website.yml 模式);顺带写入版本标记 + 验证部署
92+
# (同 deploy-website.yml 模式);顺带写入版本标记 + 验证部署。
93+
# 自愈兜底:nginx 容器因故未运行(如服务器手动 compose 漏传
94+
# --env-file .env.production 致上游解析失败)时,用正确 env-file 重建,
95+
# 避免 restart 报错且全站持续不可用(2026-08-16 生产事故教训)
8996
- name: Restart Nginx, write version and verify
9097
uses: appleboy/ssh-action@v1
9198
with:
@@ -94,25 +101,56 @@ jobs:
94101
key: ${{ secrets.SSH_PRIVATE_KEY }}
95102
script: |
96103
echo "${{ github.ref_name }}" > /opt/Entropydecrease/pwa/version.txt
97-
docker restart entropy-decrease-nginx
104+
if docker ps -a --format '{{.Names}}' | grep -q '^entropy-decrease-nginx$'; then
105+
docker restart entropy-decrease-nginx
106+
else
107+
cd /opt/Entropydecrease/server
108+
docker compose -f docker-compose.prod.yml --env-file .env.production up -d nginx
109+
fi
98110
sleep 5
99111
curl -sf https://entropydecrease.com/pwa/ | grep -q "熵减" || exit 1
100112
echo "PWA deployed: ${{ github.ref_name }}"
101113
114+
# ---------------------------------------------------------------
115+
# CDN 预热:PWA 入口与元数据(manifest/sw.js)预热,移动端首开
116+
# 免回源等待(入口文件虽小,CDN 冷缓存回源仍受源站出网带宽限制)。
117+
# 与 release.yml 安装包预热共用同一套阿里云凭证;未配置时跳过。
118+
# ---------------------------------------------------------------
119+
- name: Prefetch CDN cache (PWA)
120+
if: env.DOWNLOAD_BASE_URL != '' && env.HAS_ALIYUN_AK == 'true'
121+
continue-on-error: true
122+
env:
123+
AK_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
124+
AK_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
125+
run: |
126+
set -eo pipefail
127+
if [ ! -x /tmp/aliyun ]; then
128+
curl -sL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz -o /tmp/aliyun.tgz
129+
tar -xzf /tmp/aliyun.tgz -C /tmp
130+
fi
131+
/tmp/aliyun configure set --profile cdnci --mode AK --region cn-hangzhou \
132+
--access-key-id "$AK_ID" --access-key-secret "$AK_SECRET"
133+
134+
OBJECTS="https://entropydecrease.com/pwa/"$'\n'"https://entropydecrease.com/pwa/manifest.webmanifest"$'\n'"https://entropydecrease.com/pwa/sw.js"
135+
echo "Prefetching:"
136+
echo "$OBJECTS"
137+
138+
/tmp/aliyun cdn PushObjectCache \
139+
--profile cdnci \
140+
--ObjectPath "$OBJECTS" \
141+
--Area domestic
142+
echo "PWA CDN prefetch done"
143+
102144
# ---------------------------------------------------------------------------
103145
# Android APK(B 方案:TWA 打包):bubblewrap 基于已部署的 PWA manifest
104146
# 生成 TWA 工程并构建签名 APK,上传 GitHub Release + 服务器下载目录
105147
# (官网「下载 Android APK」直链 = entropydecrease.com/downloads/entropydecrease.apk)。
106148
#
107-
# ⚠️ 首次设置(一次性,需 Android/Java 环境,完成后 CI 全自动):
108-
# 1. 生成 keystore:keytool -genkey -v -keystore entropydecrease.keystore
109-
# -alias entropydecrease -keyalg RSA -keysize 2048 -validity 10000
110-
# 2. 生成 TWA 工程(读 android/twa-manifest.json 配置):
111-
# npx @bubblewrap/cli init --manifest
112-
# https://entropydecrease.com/pwa/manifest.webmanifest --directory android/twa
113-
# 并把生成的 android/twa/ 提交到仓库(CI 只做非交互 update + build)
114-
# 3. GitHub Secrets:ANDROID_KEYSTORE_BASE64(keystore base64)、
115-
# ANDROID_KEYSTORE_PASS(keystore 密码)、ANDROID_KEY_PASS(key 密码)
149+
# ⚠️ 首次设置(2026-08-16 已完成:bubblewrap init 生成 android/twa/ + keystore):
150+
# keystore = android/twa/android.keystore(alias=android,packageId=com.entropydecrease.twa)
151+
# 1. 将 android/twa/android.keystore 转 base64 存入 Secret ANDROID_KEYSTORE_BASE64
152+
# 2. Secret ANDROID_KEYSTORE_PASS / ANDROID_KEY_PASS = keystore 密码(init 时 keytool 输入值)
153+
# CI 只做非交互 update + build;android/twa/ 提交仓库(*.keystore 已 gitignore)
116154
# ---------------------------------------------------------------------------
117155
build-apk:
118156
needs: deploy
@@ -154,7 +192,7 @@ jobs:
154192
bubblewrap build \
155193
--keystorePath /tmp/entropydecrease.keystore \
156194
--keystorePass "$KEYSTORE_PASS" \
157-
--keyAlias entropydecrease \
195+
--keyAlias android \
158196
--keyPass "$KEY_PASS"
159197
- name: Verify APK artifact
160198
run: test -f android/twa/app-release-signed.apk && echo "APK OK"
@@ -170,3 +208,14 @@ jobs:
170208
key: ${{ secrets.SSH_PRIVATE_KEY }}
171209
source: android/twa/app-release-signed.apk
172210
target: /opt/Entropydecrease/downloads
211+
strip_components: 2
212+
- name: Rename APK to canonical name
213+
uses: appleboy/ssh-action@v1
214+
with:
215+
host: ${{ secrets.SERVER_HOST }}
216+
username: ${{ secrets.SERVER_USER }}
217+
key: ${{ secrets.SSH_PRIVATE_KEY }}
218+
script: |
219+
mv -f /opt/Entropydecrease/downloads/app-release-signed.apk \
220+
/opt/Entropydecrease/downloads/entropydecrease.apk
221+
ls -la /opt/Entropydecrease/downloads/entropydecrease.apk

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,6 @@ client/public/sounds/_backup_original/
7676

7777
# Android 签名密钥(严禁提交)
7878
android/keystore/
79+
80+
# TWA 签名密钥(bubblewrap 生成,严禁提交)
81+
android/twa/*.keystore

‎android/twa/app/build.gradle‎

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright 2019 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import groovy.xml.MarkupBuilder
18+
19+
plugins {
20+
id 'com.android.application'
21+
}
22+
23+
def twaManifest = [
24+
applicationId: 'com.entropydecrease.twa',
25+
hostName: 'entropydecrease.com', // The domain being opened in the TWA.
26+
launchUrl: '/pwa/', // The start path for the TWA. Must be relative to the domain.
27+
name: '熵减 - 学习伴侣', // The application name.
28+
launcherName: '熵减', // The name shown on the Android Launcher.
29+
themeColor: '#3B82F6', // The color used for the status bar.
30+
themeColorDark: '#000000', // The color used for the dark status bar.
31+
navigationColor: '#000000', // The color used for the navigation bar.
32+
navigationColorDark: '#000000', // The color used for the dark navbar.
33+
navigationDividerColor: '#000000', // The navbar divider color.
34+
navigationDividerColorDark: '#000000', // The dark navbar divider color.
35+
backgroundColor: '#111827', // The color used for the splash screen background.
36+
enableNotifications: true, // Set to true to enable notification delegation.
37+
// Every shortcut must include the following fields:
38+
// - name: String that will show up in the shortcut.
39+
// - short_name: Shorter string used if |name| is too long.
40+
// - url: Absolute path of the URL to launch the app with (e.g '/create').
41+
// - icon: Name of the resource in the drawable folder to use as an icon.
42+
shortcuts: [],
43+
// The duration of fade out animation in milliseconds to be played when removing splash screen.
44+
splashScreenFadeOutDuration: 300,
45+
generatorApp: 'bubblewrap-cli', // Application that generated the Android Project
46+
// The fallback strategy for when Trusted Web Activity is not available. Possible values are
47+
// 'customtabs' and 'webview'.
48+
fallbackType: 'customtabs',
49+
enableSiteSettingsShortcut: 'true',
50+
orientation: 'portrait-primary',
51+
]
52+
53+
android {
54+
compileSdkVersion 36
55+
namespace "com.entropydecrease.twa"
56+
defaultConfig {
57+
applicationId "com.entropydecrease.twa"
58+
minSdkVersion 21
59+
targetSdkVersion 36
60+
versionCode 1
61+
versionName "1"
62+
63+
// The name for the application
64+
resValue "string", "appName", twaManifest.name
65+
66+
// The name for the application on the Android Launcher
67+
resValue "string", "launcherName", twaManifest.launcherName
68+
69+
// The URL that will be used when launching the TWA from the Android Launcher
70+
def launchUrl = "https://" + twaManifest.hostName + twaManifest.launchUrl
71+
resValue "string", "launchUrl", launchUrl
72+
73+
74+
75+
76+
// The URL the Web Manifest for the Progressive Web App that the TWA points to. This
77+
// is used by Chrome OS and Meta Quest to open the Web version of the PWA instead of
78+
// the TWA, as it will probably give a better user experience for non-mobile devices.
79+
resValue "string", "webManifestUrl", 'https://entropydecrease.com/pwa/manifest.webmanifest'
80+
81+
82+
83+
// This is used by Meta Quest.
84+
resValue "string", "fullScopeUrl", 'https://entropydecrease.com/pwa/'
85+
86+
87+
88+
89+
// The hostname is used when building the intent-filter, so the TWA is able to
90+
// handle Intents to open host url of the application.
91+
resValue "string", "hostName", twaManifest.hostName
92+
93+
// This attribute sets the status bar color for the TWA. It can be either set here or in
94+
// `res/values/colors.xml`. Setting in both places is an error and the app will not
95+
// compile. If not set, the status bar color defaults to #FFFFFF - white.
96+
resValue "color", "colorPrimary", twaManifest.themeColor
97+
98+
// This attribute sets the dark status bar color for the TWA. It can be either set here or in
99+
// `res/values/colors.xml`. Setting in both places is an error and the app will not
100+
// compile. If not set, the status bar color defaults to #000000 - white.
101+
resValue "color", "colorPrimaryDark", twaManifest.themeColorDark
102+
103+
// This attribute sets the navigation bar color for the TWA. It can be either set here or
104+
// in `res/values/colors.xml`. Setting in both places is an error and the app will not
105+
// compile. If not set, the navigation bar color defaults to #FFFFFF - white.
106+
resValue "color", "navigationColor", twaManifest.navigationColor
107+
108+
// This attribute sets the dark navigation bar color for the TWA. It can be either set here
109+
// or in `res/values/colors.xml`. Setting in both places is an error and the app will not
110+
// compile. If not set, the navigation bar color defaults to #000000 - black.
111+
resValue "color", "navigationColorDark", twaManifest.navigationColorDark
112+
113+
// This attribute sets the navbar divider color for the TWA. It can be either
114+
// set here or in `res/values/colors.xml`. Setting in both places is an error and the app
115+
// will not compile. If not set, the divider color defaults to #00000000 - transparent.
116+
resValue "color", "navigationDividerColor", twaManifest.navigationDividerColor
117+
118+
// This attribute sets the dark navbar divider color for the TWA. It can be either
119+
// set here or in `res/values/colors.xml`. Setting in both places is an error and the
120+
//app will not compile. If not set, the divider color defaults to #000000 - black.
121+
resValue "color", "navigationDividerColorDark", twaManifest.navigationDividerColorDark
122+
123+
// Sets the color for the background used for the splash screen when launching the
124+
// Trusted Web Activity.
125+
resValue "color", "backgroundColor", twaManifest.backgroundColor
126+
127+
// Defines a provider authority for the Splash Screen
128+
resValue "string", "providerAuthority", twaManifest.applicationId + '.fileprovider'
129+
130+
// The enableNotification resource is used to enable or disable the
131+
// TrustedWebActivityService, by changing the android:enabled and android:exported
132+
// attributes
133+
resValue "bool", "enableNotification", twaManifest.enableNotifications.toString()
134+
135+
twaManifest.shortcuts.eachWithIndex { shortcut, index ->
136+
resValue "string", "shortcut_name_$index", "$shortcut.name"
137+
resValue "string", "shortcut_short_name_$index", "$shortcut.short_name"
138+
}
139+
140+
// The splashScreenFadeOutDuration resource is used to set the duration of fade out animation in milliseconds
141+
// to be played when removing splash screen. The default is 0 (no animation).
142+
resValue "integer", "splashScreenFadeOutDuration", twaManifest.splashScreenFadeOutDuration.toString()
143+
144+
resValue "string", "generatorApp", twaManifest.generatorApp
145+
146+
resValue "string", "fallbackType", twaManifest.fallbackType
147+
148+
resValue "bool", "enableSiteSettingsShortcut", twaManifest.enableSiteSettingsShortcut
149+
resValue "string", "orientation", twaManifest.orientation
150+
151+
152+
}
153+
buildTypes {
154+
release {
155+
minifyEnabled true
156+
}
157+
}
158+
compileOptions {
159+
sourceCompatibility JavaVersion.VERSION_1_8
160+
targetCompatibility JavaVersion.VERSION_1_8
161+
}
162+
lintOptions {
163+
checkReleaseBuilds false
164+
}
165+
}
166+
167+
task generateShorcutsFile {
168+
assert twaManifest.shortcuts.size() < 5, "You can have at most 4 shortcuts."
169+
twaManifest.shortcuts.eachWithIndex { s, i ->
170+
assert s.name != null, 'Missing `name` in shortcut #' + i
171+
assert s.short_name != null, 'Missing `short_name` in shortcut #' + i
172+
assert s.url != null, 'Missing `icon` in shortcut #' + i
173+
assert s.icon != null, 'Missing `url` in shortcut #' + i
174+
}
175+
176+
def shortcutsFile = new File("$projectDir/src/main/res/xml", "shortcuts.xml")
177+
178+
def xmlWriter = new StringWriter()
179+
def xmlMarkup = new MarkupBuilder(new IndentPrinter(xmlWriter, " ", true))
180+
181+
xmlMarkup
182+
.'shortcuts'('xmlns:android': 'http://schemas.android.com/apk/res/android') {
183+
twaManifest.shortcuts.eachWithIndex { s, i ->
184+
'shortcut'(
185+
'android:shortcutId': 'shortcut' + i,
186+
'android:enabled': 'true',
187+
'android:icon': '@drawable/' + s.icon,
188+
'android:shortcutShortLabel': '@string/shortcut_short_name_' + i,
189+
'android:shortcutLongLabel': '@string/shortcut_name_' + i) {
190+
'intent'(
191+
'android:action': 'android.intent.action.MAIN',
192+
'android:targetPackage': twaManifest.applicationId,
193+
'android:targetClass': twaManifest.applicationId + '.LauncherActivity',
194+
'android:data': s.url)
195+
'categories'('android:name': 'android.intent.category.LAUNCHER')
196+
}
197+
}
198+
}
199+
shortcutsFile.text = xmlWriter.toString() + '\n'
200+
}
201+
202+
preBuild.dependsOn(generateShorcutsFile)
203+
204+
repositories {
205+
206+
}
207+
208+
dependencies {
209+
implementation fileTree(include: ['*.jar'], dir: 'libs')
210+
211+
implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.6.2'
212+
213+
}

0 commit comments

Comments
 (0)