Compare commits

..

1 Commits

Author SHA1 Message Date
c0075543f0 feat: desktop build support and build list UI improvements
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 21:57:39 +08:00
66 changed files with 26 additions and 1022 deletions

View File

@@ -1,15 +0,0 @@
# Wrangler 本地开发密钥(复制为 .dev.vars 后填写真实值)
# cp .dev.vars.example .dev.vars
# GitHub Personal Access Token权限至少包含 repo、workflow
GITHUB_TOKEN=ghp_xxx
# 用于接收构建产物的 GitHub 仓库
GITHUB_OWNER=your-github-username
GITHUB_REPO=Web2App
# 触发 workflow 的默认分支(需与仓库默认分支一致)
DEFAULT_BRANCH=main
# 单次上传 zip 的最大体积MB
MAX_UPLOAD_MB=50

View File

@@ -1,41 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "../..");
const MARKER = "data-web2app-tauri-shell";
const PERMISSIONS_POLICY_MARKER = "data-web2app-permissions-policy";
const PERMISSIONS_POLICY_TAG = `<meta ${PERMISSIONS_POLICY_MARKER} http-equiv="Permissions-Policy" content="camera=*, microphone=*" />`;
const scriptPath = path.join(root, "template", "scripts", "tauri-shell.js");
export function injectTauriShell(distDir) {
const indexPath = path.join(distDir, "index.html");
if (!fs.existsSync(indexPath)) return;
let html = fs.readFileSync(indexPath, "utf8");
const needsPolicy = !html.includes(PERMISSIONS_POLICY_MARKER);
const needsScript = !html.includes(MARKER);
if (!needsPolicy && !needsScript) return;
const injections = [];
if (needsPolicy) injections.push(PERMISSIONS_POLICY_TAG);
if (needsScript) {
const script = fs.readFileSync(scriptPath, "utf8").trim();
injections.push(`<script ${MARKER}>${script}</script>`);
}
const block = `${injections.join("\n")}\n`;
if (html.includes("<head>")) {
html = html.replace("<head>", `<head>\n${block}`);
} else if (html.includes("</head>")) {
html = html.replace("</head>", `${block}</head>`);
} else if (html.includes("</body>")) {
html = html.replace("</body>", `${block}</body>`);
} else {
html += `\n${block}`;
}
fs.writeFileSync(indexPath, html);
console.log("Injected Tauri shell script into index.html");
}

View File

@@ -32,8 +32,6 @@ function patchMainActivity(filePath) {
const content = `package ${pkg}
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebView
import androidx.core.view.WindowCompat
class MainActivity : TauriActivity() {
@@ -41,13 +39,6 @@ class MainActivity : TauriActivity() {
super.onCreate(savedInstanceState)
// 内容不延伸到状态栏 / 导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, true)
CookieManager.getInstance().setAcceptCookie(true)
}
override fun onWebViewCreate(webView: WebView) {
super.onWebViewCreate(webView)
// Android WebView 默认拦截跨站 Cookie桌面壳请求远程 API 时需放行
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
}
}
`;
@@ -136,34 +127,16 @@ function ensureNetworkSecurityXml(androidRoot) {
console.log(`Wrote ${path.relative(root, target)}`);
}
const ANDROID_PERMISSIONS = [
"android.permission.INTERNET",
"android.permission.CAMERA",
];
function ensureUsesPermission(xml, permission) {
if (xml.includes(permission)) return xml;
const tag = ` <uses-permission android:name="${permission}" />\n`;
if (xml.includes("<application")) {
return xml.replace(/<application/, `${tag}\n <application`);
}
return xml.replace(/<\/manifest>/, `${tag}</manifest>`);
}
function patchAndroidManifest(androidRoot) {
for (const file of walk(androidRoot, (p) => p.endsWith("AndroidManifest.xml"))) {
const normalized = file.replace(/\\/g, "/");
if (!normalized.includes("/src/main/")) continue;
let xml = fs.readFileSync(file, "utf8");
for (const permission of ANDROID_PERMISSIONS) {
xml = ensureUsesPermission(xml, permission);
}
if (!xml.includes("android.hardware.camera")) {
if (!xml.includes("android.permission.INTERNET")) {
xml = xml.replace(
/<application/,
' <uses-feature android:name="android.hardware.camera" android:required="false" />\n\n <application',
' <uses-permission android:name="android.permission.INTERNET" />\n\n <application',
);
}
@@ -224,4 +197,4 @@ ensureNetworkSecurityXml(androidRoot);
patchAndroidManifest(androidRoot);
patchGradleCleartext(androidRoot);
console.log("Android app patches applied (system bars + network + camera)");
console.log("Android app patches applied (system bars + network)");

View File

@@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url";
import AdmZip from "adm-zip";
import { BUNDLE_ICONS, generateAppIcons } from "./generate-app-icons.mjs";
import { injectInAppNav } from "./inject-in-app-nav.mjs";
import { injectTauriShell } from "./inject-tauri-shell.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "../..");
@@ -110,7 +109,6 @@ function patchTauriConfig(filePath) {
fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`);
}
injectTauriShell(distDir);
injectInAppNav(distDir);
patchTauriConfig(confPath);

View File

@@ -19,10 +19,6 @@ on:
description: Application bundle identifier
required: true
type: string
app_version:
description: Application version string
required: true
type: string
permissions:
contents: write
@@ -71,17 +67,14 @@ jobs:
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path artifacts/windows | Out-Null
$nsisDir = "template/src-tauri/target/release/bundle/nsis"
if (-not (Test-Path $nsisDir)) {
Write-Error "NSIS bundle directory not found: $nsisDir"
exit 1
$bundleRoot = "template/src-tauri/target/release/bundle"
if (Test-Path $bundleRoot) {
Get-ChildItem -Path $bundleRoot -Recurse -Include *setup.exe,*.exe,*.msi -ErrorAction SilentlyContinue |
Copy-Item -Destination artifacts/windows -Force
}
$installers = Get-ChildItem -Path $nsisDir -Filter "*-setup.exe"
if (-not $installers) {
Write-Error "No NSIS installer (*-setup.exe) found in $nsisDir"
exit 1
}
$installers | Copy-Item -Destination artifacts/windows -Force
Get-ChildItem -Path "template/src-tauri/target/release" -Filter "*.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch "wix|candle|light" } |
Copy-Item -Destination artifacts/windows -Force
Get-ChildItem artifacts/windows
- uses: actions/upload-artifact@v4
@@ -219,7 +212,7 @@ jobs:
body: |
Automated build for **${{ inputs.app_name }}** (English: `${{ inputs.app_name_en }}`, version: `${{ inputs.app_version }}`, ID: `${{ inputs.app_identifier }}`).
- Windows NSIS installer (*-setup.exe)
- Windows desktop installer/portable binary
- Android APK (arm64-v8a, signed for sideload install)
draft: false
prerelease: true

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 shumengya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1 +0,0 @@
2026.6.6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1 +0,0 @@
2026.6.6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1 +0,0 @@
2026.6.6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1 +0,0 @@
2026.7.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

View File

@@ -1 +0,0 @@
2026.6.6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -1 +0,0 @@
2026.7.4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

View File

@@ -1 +0,0 @@
2026.6.6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1 +0,0 @@
2026.7.18

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1 +0,0 @@
2026.7.12

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1 +0,0 @@
2026.6.6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1 +0,0 @@
2026.6.7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 KiB

View File

@@ -1 +0,0 @@
2026.6.8

Binary file not shown.

View File

@@ -1 +0,0 @@
2026.7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1 +0,0 @@
2026.7.3

View File

@@ -1,369 +0,0 @@
(function () {
if (window.__web2appShellInstalled) return;
window.__web2appShellInstalled = true;
function isTauriShell() {
var protocol = location.protocol;
var host = location.hostname;
return (
protocol === "tauri:" ||
protocol === "file:" ||
host === "asset.localhost" ||
host === "tauri.localhost"
);
}
if (!isTauriShell()) return;
var TOKEN_KEYS = ["sprout2fa_session", "__web2app_bearer_token"];
/**
* Bodies larger than this stay on the browser fetch path (needs server CORS
* for https://tauri.localhost). Keep high enough for typical R2 uploads.
*/
var PROXY_BODY_LIMIT = 64 * 1024 * 1024;
/** Soft warn threshold for huge proxied responses (base64 over IPC). */
var PROXY_RESPONSE_LIMIT = 64 * 1024 * 1024;
function readToken() {
try {
for (var i = 0; i < TOKEN_KEYS.length; i++) {
var value = localStorage.getItem(TOKEN_KEYS[i]);
if (value) return value;
}
} catch (_) {
/* ignore */
}
return null;
}
function writeToken(token) {
if (!token) return;
try {
for (var i = 0; i < TOKEN_KEYS.length; i++) {
localStorage.setItem(TOKEN_KEYS[i], token);
}
} catch (_) {
/* ignore */
}
}
function clearToken() {
try {
for (var i = 0; i < TOKEN_KEYS.length; i++) {
localStorage.removeItem(TOKEN_KEYS[i]);
}
} catch (_) {
/* ignore */
}
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker.getRegistrations().then(function (regs) {
for (var i = 0; i < regs.length; i++) regs[i].unregister();
});
navigator.serviceWorker.register = function () {
return Promise.resolve({
active: null,
installing: null,
waiting: null,
unregister: function () {
return Promise.resolve(true);
},
update: function () {
return Promise.resolve();
},
});
};
}
function getInvoke() {
try {
var core = window.__TAURI__ && window.__TAURI__.core;
if (core && typeof core.invoke === "function") {
return core.invoke.bind(core);
}
} catch (_) {
/* ignore */
}
return null;
}
function resolveUrl(input) {
if (typeof input === "string") return input;
if (input instanceof URL) return input.href;
if (input && typeof input.url === "string") return input.url;
return "";
}
function resolveMethod(input, init) {
if (init && init.method) return String(init.method).toUpperCase();
if (input instanceof Request) return input.method.toUpperCase();
return "GET";
}
/**
* Collect headers without inventing an empty Headers that would later
* overwrite a signed Request (aws4fetch passes fetch(Request) only).
*/
function collectHeaders(input, init) {
var headers = new Headers();
if (input instanceof Request) {
input.headers.forEach(function (value, key) {
headers.set(key, value);
});
}
if (init && init.headers) {
new Headers(init.headers).forEach(function (value, key) {
headers.set(key, value);
});
}
return headers;
}
function headersToObject(headers) {
var out = {};
headers.forEach(function (value, key) {
// Host is set by the real client; hop-by-hop headers are stripped natively.
if (key.toLowerCase() === "host") return;
out[key] = value;
});
return out;
}
function arrayBufferToBase64(buffer) {
var bytes = new Uint8Array(buffer);
var binary = "";
var chunk = 0x8000;
for (var i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + chunk),
);
}
return btoa(binary);
}
function base64ToUint8Array(b64) {
var binary = atob(b64 || "");
var len = binary.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
function bodyToArrayBuffer(input, init) {
if (init && init.body != null) {
var body = init.body;
if (body === "") return Promise.resolve(null);
if (typeof body === "string") {
return Promise.resolve(new TextEncoder().encode(body).buffer);
}
if (body instanceof ArrayBuffer) return Promise.resolve(body);
if (ArrayBuffer.isView(body)) {
return Promise.resolve(
body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength),
);
}
if (typeof Blob !== "undefined" && body instanceof Blob) {
return body.arrayBuffer();
}
if (
typeof URLSearchParams !== "undefined" &&
body instanceof URLSearchParams
) {
return Promise.resolve(new TextEncoder().encode(body.toString()).buffer);
}
if (typeof FormData !== "undefined" && body instanceof FormData) {
// FormData needs multipart framing; fall back to browser fetch.
return Promise.reject(new Error("FORMDATA_UNSUPPORTED"));
}
if (
typeof ReadableStream !== "undefined" &&
body instanceof ReadableStream
) {
return new Response(body).arrayBuffer();
}
return Promise.resolve(null);
}
if (input instanceof Request) {
// Clone so we do not lock the original Request body stream.
return input
.clone()
.arrayBuffer()
.then(function (buf) {
return buf && buf.byteLength > 0 ? buf : null;
})
.catch(function () {
return null;
});
}
return Promise.resolve(null);
}
function maybeInjectToken(headers) {
if (headers.has("Authorization")) return;
var token = readToken();
if (token) headers.set("Authorization", "Bearer " + token);
}
function trackAuthResponse(requestUrl, response) {
if (!response || !response.ok) return response;
var path = requestUrl.pathname.replace(/\/$/, "");
if (/\/auth\/login$/i.test(path)) {
response
.clone()
.json()
.then(function (data) {
if (data && typeof data.token === "string" && data.token) {
writeToken(data.token);
}
})
.catch(function () {
/* ignore */
});
}
if (/\/auth\/logout$/i.test(path)) {
clearToken();
}
return response;
}
function callOriginalFetch(input, init) {
if (init === undefined) return originalFetch.call(window, input);
return originalFetch.call(window, input, init);
}
/**
* Proxy cross-origin HTTP(S) through Rust so WebView CORS never applies.
* Origin of packaged apps is https://tauri.localhost (etc.), which remote
* APIs (R2/S3) usually do not list — even when AllowedOrigins is ["*"],
* signed preflights and some WebView builds still fail.
*/
function proxyFetch(input, init) {
var invoke = getInvoke();
if (!invoke) return null;
var url = resolveUrl(input);
var method = resolveMethod(input, init);
var headers = collectHeaders(input, init);
maybeInjectToken(headers);
return bodyToArrayBuffer(input, init)
.then(function (buf) {
if (buf && buf.byteLength > PROXY_BODY_LIMIT) {
// Large upload: browser path (needs R2 CORS for tauri.localhost).
return callOriginalFetch(url, {
method: method,
headers: headers,
body: buf,
});
}
var bodyBase64 =
buf && buf.byteLength > 0 ? arrayBufferToBase64(buf) : null;
return invoke("proxy_fetch", {
method: method,
url: url,
headers: headersToObject(headers),
bodyBase64: bodyBase64,
}).then(function (result) {
if (!result || typeof result.status !== "number") {
throw new TypeError("Failed to fetch");
}
if (
result.bodyBase64 &&
result.bodyBase64.length * 0.75 > PROXY_RESPONSE_LIMIT
) {
console.warn(
"[web2app] proxied response is very large; consider presigned URLs",
);
}
var bytes = base64ToUint8Array(result.bodyBase64 || "");
var responseHeaders = new Headers();
var pairs = result.headers || [];
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i];
if (pair && pair.length >= 2) {
try {
responseHeaders.append(pair[0], pair[1]);
} catch (_) {
/* invalid header name/value */
}
}
}
return new Response(bytes, {
status: result.status,
statusText: result.statusText || "",
headers: responseHeaders,
});
});
})
.catch(function (err) {
if (err && err.message === "FORMDATA_UNSUPPORTED") {
return callOriginalFetch(url, {
method: method,
headers: headers,
body: init && init.body,
});
}
throw err;
});
}
var originalFetch = window.fetch;
window.fetch = function (input, init) {
var url = resolveUrl(input);
var requestUrl;
try {
requestUrl = new URL(url, location.href);
} catch (_) {
return callOriginalFetch(input, init);
}
var crossOrigin = requestUrl.origin !== location.origin;
var isHttp =
requestUrl.protocol === "http:" || requestUrl.protocol === "https:";
// Same-origin / non-HTTP: never rewrite (preserves Request signatures).
if (!crossOrigin || !isHttp) {
return callOriginalFetch(input, init);
}
var proxied = proxyFetch(input, init);
if (proxied) {
return proxied.then(function (response) {
return trackAuthResponse(requestUrl, response);
});
}
// Tauri IPC not ready yet: pass through without clobbering Request headers.
// Only add a bearer token when Authorization is absent.
var headers = collectHeaders(input, init);
var hadAuth = headers.has("Authorization");
maybeInjectToken(headers);
if (!hadAuth && headers.has("Authorization")) {
var passInit = Object.assign({}, init || {}, {
method: resolveMethod(input, init),
headers: headers,
});
if (passInit.body == null && input instanceof Request) {
// Keep original Request if we only needed headers from it and body unset.
return callOriginalFetch(input.url, Object.assign({}, passInit, {
body: undefined,
})).then(function (response) {
return trackAuthResponse(requestUrl, response);
});
}
return callOriginalFetch(requestUrl.href, passInit).then(function (
response,
) {
return trackAuthResponse(requestUrl, response);
});
}
return callOriginalFetch(input, init).then(function (response) {
return trackAuthResponse(requestUrl, response);
});
};
})();

View File

@@ -47,18 +47,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "async-compression"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -321,23 +309,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -360,24 +331,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "compression-codecs"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
dependencies = [
"brotli",
"compression-core",
"flate2",
"memchr",
]
[[package]]
name = "compression-core"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
[[package]]
name = "cookie"
version = "0.18.1"
@@ -437,15 +390,6 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1057,10 +1001,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -1082,13 +1024,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core",
"wasip2",
"wasip3",
"wasm-bindgen",
]
[[package]]
@@ -1347,22 +1286,6 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1776,12 +1699,6 @@ version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "markup5ever"
version = "0.38.0"
@@ -2390,62 +2307,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
dependencies = [
"bytes",
"getrandom 0.4.2",
"lru-slab",
"rand",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "quote"
version = "1.0.45"
@@ -2467,32 +2328,6 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -2568,44 +2403,6 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]]
name = "reqwest"
version = "0.13.4"
@@ -2640,20 +2437,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -2669,53 +2452,12 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
version = "0.23.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -2906,18 +2648,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_with"
version = "3.20.0"
@@ -2988,7 +2718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"digest",
]
@@ -3116,12 +2846,6 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -3274,7 +2998,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest 0.13.4",
"reqwest",
"serde",
"serde_json",
"serde_repr",
@@ -3577,16 +3301,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -3741,17 +3455,12 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"async-compression",
"bitflags 2.11.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
@@ -3888,12 +3597,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -4130,22 +3833,10 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web2app-template"
version = "1.0.0"
dependencies = [
"base64 0.22.1",
"reqwest 0.12.28",
"serde",
"serde_json",
"tauri",
@@ -4208,15 +3899,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -4447,15 +4129,6 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4856,12 +4529,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.4"

View File

@@ -16,9 +16,6 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip", "brotli", "deflate"] }
base64 = "0.22"
[profile.release]
codegen-units = 1

View File

@@ -3,5 +3,5 @@
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": ["core:default", "allow-proxy-fetch"]
"permissions": ["core:default"]
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capability for the main window","local":true,"windows":["main"],"permissions":["core:default","allow-proxy-fetch"]}}
{"default":{"identifier":"default","description":"Default capability for the main window","local":true,"windows":["main"],"permissions":["core:default"]}}

View File

@@ -176,12 +176,6 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "Allow the app webview to perform native HTTP requests (CORS bypass for packaged sites).",
"type": "string",
"const": "allow-proxy-fetch",
"markdownDescription": "Allow the app webview to perform native HTTP requests (CORS bypass for packaged sites)."
},
{
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
"type": "string",

View File

@@ -176,12 +176,6 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "Allow the app webview to perform native HTTP requests (CORS bypass for packaged sites).",
"type": "string",
"const": "allow-proxy-fetch",
"markdownDescription": "Allow the app webview to perform native HTTP requests (CORS bypass for packaged sites)."
},
{
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
"type": "string",

View File

@@ -1,4 +0,0 @@
[[permission]]
identifier = "allow-proxy-fetch"
description = "Allow the app webview to perform native HTTP requests (CORS bypass for packaged sites)."
commands.allow = ["proxy_fetch"]

View File

@@ -1,10 +1,5 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use serde::Serialize;
use std::collections::HashMap;
use tauri::Manager;
const TAURI_SHELL_SCRIPT: &str = include_str!("../../scripts/tauri-shell.js");
const IN_APP_NAV_SCRIPT: &str = r##"
(function () {
if (window.__web2appNavInstalled) return;
@@ -68,125 +63,17 @@ const IN_APP_NAV_SCRIPT: &str = r##"
})();
"##;
/// Hop-by-hop / forbidden request headers (RFC 7230 / fetch).
const STRIP_REQUEST_HEADERS: &[&str] = &[
"accept-encoding",
"connection",
"content-length",
"host",
"keep-alive",
"proxy-connection",
"te",
"trailer",
"transfer-encoding",
"upgrade",
];
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ProxyFetchResponse {
status: u16,
status_text: String,
headers: Vec<(String, String)>,
body_base64: String,
}
/// Native HTTP client used to bypass WebView CORS for packaged apps.
/// Front-end origin is typically `https://tauri.localhost`, which remote APIs
/// (Cloudflare R2, S3, custom backends) rarely allow even with `AllowedOrigins: ["*"]`.
#[tauri::command]
async fn proxy_fetch(
method: String,
url: String,
headers: HashMap<String, String>,
body_base64: Option<String>,
) -> Result<ProxyFetchResponse, String> {
let method = method.trim().to_uppercase();
if method.is_empty() {
return Err("method is required".into());
}
let url = url.trim();
if url.is_empty() {
return Err("url is required".into());
}
let parsed = reqwest::Url::parse(url).map_err(|e| format!("invalid url: {e}"))?;
if parsed.scheme() != "http" && parsed.scheme() != "https" {
return Err(format!("unsupported scheme: {}", parsed.scheme()));
}
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.map_err(|e| format!("http client error: {e}"))?;
let http_method = reqwest::Method::from_bytes(method.as_bytes())
.map_err(|_| format!("invalid method: {method}"))?;
let mut builder = client.request(http_method, parsed);
for (name, value) in &headers {
let lower = name.to_ascii_lowercase();
if STRIP_REQUEST_HEADERS.iter().any(|h| *h == lower.as_str()) {
continue;
}
builder = builder.header(name.as_str(), value.as_str());
}
if let Some(b64) = body_base64.as_deref() {
if !b64.is_empty() {
let bytes = BASE64
.decode(b64)
.map_err(|e| format!("invalid body base64: {e}"))?;
builder = builder.body(bytes);
}
}
let response = builder
.send()
.await
.map_err(|e| format!("request failed: {e}"))?;
let status = response.status();
let status_text = status.canonical_reason().unwrap_or("").to_string();
let headers: Vec<(String, String)> = response
.headers()
.iter()
.map(|(k, v)| {
(
k.as_str().to_string(),
v.to_str().unwrap_or("").to_string(),
)
})
.collect();
let bytes = response
.bytes()
.await
.map_err(|e| format!("read body failed: {e}"))?;
Ok(ProxyFetchResponse {
status: status.as_u16(),
status_text,
headers,
body_base64: BASE64.encode(&bytes),
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![proxy_fetch])
.setup(|app| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.eval(TAURI_SHELL_SCRIPT);
let _ = window.eval(IN_APP_NAV_SCRIPT);
}
Ok(())
})
.on_page_load(|webview, payload| {
if payload.event() == tauri::webview::PageLoadEvent::Finished {
let _ = webview.eval(TAURI_SHELL_SCRIPT);
let _ = webview.eval(IN_APP_NAV_SCRIPT);
}
})

View File

@@ -22,18 +22,6 @@ export default {
return await handleBuildsRequest(request, env, url);
}
// 静态资源 / SPA 回退(需 wrangler.toml [assets] binding = "ASSETS"
if (!env.ASSETS) {
return jsonResponseWithCors(
request,
{
error:
"ASSETS binding is not configured. Set [assets] binding = \"ASSETS\" in wrangler.toml and redeploy.",
},
500,
);
}
return env.ASSETS.fetch(request);
} catch (error) {
console.error("Unhandled worker error:", error);

View File

@@ -1,10 +1,4 @@
import { customAlphabet } from "nanoid";
// GitHub published releases reject tag names ending with '-'.
const generateJobId = customAlphabet(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
10,
);
import { nanoid } from "nanoid";
import type { Env } from "../env";
import {
getBuild,
@@ -118,7 +112,7 @@ async function createBuild(request: Request, env: Env): Promise<Response> {
const buffer = new Uint8Array(await file.arrayBuffer());
const { normalizedBuffer } = validateZipBuffer(buffer, maxUploadBytes(env));
const jobId = generateJobId();
const jobId = nanoid(10);
const slug = slugifyIdentifier(appNameEn) || jobId.toLowerCase();
const appIdentifier = normalizeAppIdentifier(
identifierInput || `com.web2app.${slug}`,
@@ -154,7 +148,6 @@ async function createBuild(request: Request, env: Env): Promise<Response> {
appName: appNameZh,
appNameEn,
appIdentifier,
appVersion,
});
await updateBuild(env, jobId, {

View File

@@ -115,7 +115,6 @@ export async function triggerBuildWorkflow(
appName: string;
appNameEn: string;
appIdentifier: string;
appVersion: string;
},
): Promise<number> {
const { owner, repo, branch } = getGitHubConfig(env);
@@ -134,7 +133,6 @@ export async function triggerBuildWorkflow(
app_name: input.appName,
app_name_en: input.appNameEn,
app_identifier: input.appIdentifier,
app_version: input.appVersion,
},
}),
},
@@ -219,19 +217,18 @@ export async function getReleaseAssets(
assets: Array<{ name: string; browser_download_url: string }>;
};
let windowsInstallerUrl: string | null = null;
let windowsFallbackUrl: string | null = null;
let windowsUrl: string | null = null;
let androidUrl: string | null = null;
for (const asset of release.assets) {
const name = asset.name.toLowerCase();
if (name.endsWith("-setup.exe") || name.endsWith(".msi")) {
windowsInstallerUrl = asset.browser_download_url;
} else if (
!windowsFallbackUrl &&
name.endsWith(".exe")
if (
!windowsUrl &&
(name.endsWith(".exe") ||
name.endsWith(".msi") ||
name.includes("windows"))
) {
windowsFallbackUrl = asset.browser_download_url;
windowsUrl = asset.browser_download_url;
}
if (
!androidUrl &&
@@ -241,10 +238,7 @@ export async function getReleaseAssets(
}
}
return {
windowsUrl: windowsInstallerUrl ?? windowsFallbackUrl,
androidUrl,
};
return { windowsUrl, androidUrl };
}
export function getActionsRunUrl(env: Env, runId: number | null): string | null {

View File

@@ -4,8 +4,6 @@ compatibility_date = "2024-11-01"
[assets]
directory = "./frontend/dist"
binding = "ASSETS"
# SPA未匹配的前端路由如 /jobs/:id经 env.ASSETS.fetch 回退到 index.html
not_found_handling = "single-page-application"
# 运行 `wrangler d1 create web2app` 后将输出的 database_id 替换下方占位符