Skip to content

Commit 82edc92

Browse files
committed
Improve WebUI agent readiness
1 parent 0dde2e6 commit 82edc92

8 files changed

Lines changed: 237 additions & 20 deletions

File tree

.github/workflows/desktop-nightly.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ on:
55
inputs:
66
app_url:
77
description: Web UI URL to package with Pake
8-
required: true
9-
default: https://lc.absinthe.life/
8+
required: false
109
pake_cli_version:
1110
description: pake-cli npm version or dist tag
1211
required: true
@@ -23,7 +22,6 @@ concurrency:
2322

2423
env:
2524
APP_NAME: LibChecker WebUI
26-
APP_URL: ${{ github.event.inputs.app_url || 'https://lc.absinthe.life/' }}
2725
PAKE_CLI_VERSION: ${{ github.event.inputs.pake_cli_version || 'latest' }}
2826
PAKE_WIDTH: "1280"
2927
PAKE_HEIGHT: "860"
@@ -56,6 +54,21 @@ jobs:
5654
node-version: 24
5755
cache: npm
5856

57+
- name: Resolve Web UI URL
58+
shell: bash
59+
env:
60+
INPUT_APP_URL: ${{ github.event.inputs.app_url }}
61+
WEBUI_SITE_URL_OVERRIDE: ${{ vars.WEBUI_SITE_URL }}
62+
run: |
63+
set -euo pipefail
64+
node --input-type=module <<'EOF'
65+
import { appendFileSync } from "node:fs";
66+
import { WEBUI_SITE_URL } from "./packages/apk-webui/site-config.mjs";
67+
68+
const appUrl = process.env.INPUT_APP_URL || process.env.WEBUI_SITE_URL_OVERRIDE || WEBUI_SITE_URL;
69+
appendFileSync(process.env.GITHUB_ENV, `APP_URL=${appUrl}\n`);
70+
EOF
71+
5972
- name: Install Rust
6073
uses: dtolnay/rust-toolchain@stable
6174

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ POST /admin/commands/delete
188188

189189
`.github/workflows/deploy.yml` checks both parts with `npm run check`, then deploys the Worker on pushes to `main` or `master`.
190190

191-
`.github/workflows/desktop-nightly.yml` packages `https://lc.absinthe.life/` with Pake into Windows x64 and macOS arm64 desktop installers. It runs nightly, can be triggered manually with a custom URL or `pake-cli` version, and replaces assets on the reusable `nightly` prerelease.
191+
`.github/workflows/desktop-nightly.yml` packages the Web UI URL from `packages/apk-webui/site-config.mjs` with Pake into Windows x64 and macOS arm64 desktop installers. It runs nightly, can be triggered manually with a custom URL or `pake-cli` version, supports a `WEBUI_SITE_URL` repository variable override, and replaces assets on the reusable `nightly` prerelease.
192192

193193
Required repository secrets:
194194

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
WEBUI_SITE_ORIGIN,
3+
WEBUI_SITE_URL,
4+
WEBUI_SOCIAL_PREVIEW_URL,
5+
WEBUI_SOURCE_REPOSITORY_URL,
6+
} from "../site-config.mjs";
7+
8+
const HOMEPAGE_PATHS = new Set(["/", "/index.html"]);
9+
10+
export const DISCOVERY_LINK_HEADER = [
11+
`<${WEBUI_SITE_ORIGIN}/sitemap.xml>; rel="sitemap"; type="application/xml"`,
12+
`<${WEBUI_SITE_ORIGIN}/index.md>; rel="alternate"; type="text/markdown"`,
13+
].join(", ");
14+
15+
export const HOMEPAGE_MARKDOWN = `---
16+
title: LibChecker WebUI
17+
description: Analyze APK/APKS/APKM/XAPK packages in your browser with local parsing, SDK markers, signatures, and reports.
18+
image: ${WEBUI_SOCIAL_PREVIEW_URL}
19+
---
20+
21+
# LibChecker WebUI
22+
23+
LibChecker WebUI is a browser-first Android package analyzer for APK, APKS, APKM, XAPK, and LCAPPS files.
24+
25+
## What It Does
26+
27+
- Parses package metadata, manifest data, permissions, components, signatures, native libraries, and SDK markers.
28+
- Runs file analysis locally in the browser with a Web Worker.
29+
- Supports direct APK link reports through the public URL report flow.
30+
- Renders shareable reports and package comparisons.
31+
- Uses the shared LibChecker rule and icon catalogs from the companion Telegram bot workspace.
32+
33+
## Agent Guidance
34+
35+
- Send users to the web app when they need to inspect an Android package manually.
36+
- Prefer local file analysis for complete package details.
37+
- Use link analysis only for direct APK URLs or supported containers where remote range parsing can read enough metadata.
38+
- Do not treat remote URL preview as a complete replacement for uploading the package.
39+
- Do not send credentials or private package data to the URL report endpoint.
40+
41+
## Important URLs
42+
43+
- Web UI: ${WEBUI_SITE_URL}
44+
- Sitemap: ${WEBUI_SITE_ORIGIN}/sitemap.xml
45+
- Source repository: ${WEBUI_SOURCE_REPOSITORY_URL}
46+
`;
47+
48+
export const HOMEPAGE_MARKDOWN_TOKENS = String(countApproximateTokens(HOMEPAGE_MARKDOWN));
49+
50+
export async function onRequest(context) {
51+
const markdownResponse = handleMarkdownRequest(context.request);
52+
if (markdownResponse) {
53+
return markdownResponse;
54+
}
55+
56+
const response = await context.next();
57+
if (!isHomepageRequest(context.request)) {
58+
return response;
59+
}
60+
61+
return withHomepageDiscoveryHeaders(response);
62+
}
63+
64+
export function handleMarkdownRequest(request) {
65+
if (!isHomepageRequest(request) || !["GET", "HEAD"].includes(request.method) || !acceptsMarkdown(request)) {
66+
return null;
67+
}
68+
69+
return createHomepageMarkdownResponse(request.method);
70+
}
71+
72+
export function createHomepageMarkdownResponse(method = "GET") {
73+
return new Response(method === "HEAD" ? null : HOMEPAGE_MARKDOWN, {
74+
headers: buildHomepageHeaders({
75+
"cache-control": "public, max-age=3600",
76+
"content-type": "text/markdown; charset=UTF-8",
77+
"x-markdown-tokens": HOMEPAGE_MARKDOWN_TOKENS,
78+
}),
79+
});
80+
}
81+
82+
function withHomepageDiscoveryHeaders(response) {
83+
return new Response(response.body, {
84+
status: response.status,
85+
statusText: response.statusText,
86+
headers: buildHomepageHeaders(response.headers),
87+
});
88+
}
89+
90+
function buildHomepageHeaders(sourceHeaders) {
91+
const headers = new Headers(sourceHeaders);
92+
headers.set("Link", DISCOVERY_LINK_HEADER);
93+
headers.set("Content-Signal", "search=yes,ai-input=yes,ai-train=no,use=reference");
94+
headers.set("Vary", mergeVary(headers.get("Vary"), "Accept"));
95+
return headers;
96+
}
97+
98+
function isHomepageRequest(request) {
99+
return HOMEPAGE_PATHS.has(new URL(request.url).pathname);
100+
}
101+
102+
function acceptsMarkdown(request) {
103+
return (request.headers.get("Accept") || "")
104+
.split(",")
105+
.some((part) => {
106+
const [mediaType, ...params] = part.split(";").map((value) => value.trim().toLowerCase());
107+
return mediaType === "text/markdown" && !params.some((param) => /^q=0(?:\.0+)?$/u.test(param));
108+
});
109+
}
110+
111+
function mergeVary(currentValue, headerName) {
112+
const values = new Set(
113+
(currentValue || "")
114+
.split(",")
115+
.map((value) => value.trim())
116+
.filter(Boolean),
117+
);
118+
values.add(headerName);
119+
return Array.from(values).join(", ");
120+
}
121+
122+
function countApproximateTokens(markdown) {
123+
return Math.ceil(markdown.trim().split(/\s+/u).length * 1.3);
124+
}

packages/apk-webui/scripts/build.mjs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
22
import { dirname, resolve } from "node:path";
33
import { fileURLToPath } from "node:url";
44
import { build } from "vite";
5+
import { DISCOVERY_LINK_HEADER, HOMEPAGE_MARKDOWN } from "../functions/_middleware.js";
6+
import { WEBUI_SITE_ORIGIN } from "../site-config.mjs";
57

68
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
79
const distDir = resolve(projectDir, "dist");
8-
const siteUrl = "https://lc.absinthe.life";
910

1011
await build({
1112
configFile: resolve(projectDir, "vite.config.mjs"),
@@ -14,6 +15,7 @@ await build({
1415
await disableRocketLoaderForExternalScripts(resolve(distDir, "index.html"));
1516
await copyStableSocialPreview();
1617
await writeSeoFiles();
18+
await writeAgentFiles();
1719

1820
await writeFile(
1921
resolve(distDir, "_headers"),
@@ -22,6 +24,7 @@ await writeFile(
2224
" X-Content-Type-Options: nosniff",
2325
" Referrer-Policy: strict-origin-when-cross-origin",
2426
" Permissions-Policy: camera=(), microphone=(), geolocation=()",
27+
` Link: ${DISCOVERY_LINK_HEADER}`,
2528
"",
2629
"/index.html",
2730
" Cache-Control: no-cache",
@@ -35,13 +38,25 @@ await writeFile(
3538
"/sitemap.xml",
3639
" Cache-Control: public, max-age=3600",
3740
"",
41+
"/index.md",
42+
" Content-Type: text/markdown; charset=UTF-8",
43+
" Cache-Control: public, max-age=3600",
44+
"",
3845
"/assets/*",
3946
" Cache-Control: public, max-age=31536000, immutable",
4047
"",
4148
].join("\n"),
4249
);
4350

4451
await writeFile(resolve(distDir, "_redirects"), "/* /index.html 200\n");
52+
await writeFile(
53+
resolve(distDir, "_routes.json"),
54+
`${JSON.stringify({
55+
version: 1,
56+
include: ["/", "/index.html", "/url-report", "/analytics"],
57+
exclude: [],
58+
}, null, 2)}\n`,
59+
);
4560

4661
console.log(`Built Cloudflare Pages site at ${distDir}`);
4762

@@ -71,7 +86,7 @@ async function writeSeoFiles() {
7186
"User-agent: *",
7287
"Allow: /",
7388
"Disallow: /url-report",
74-
`Sitemap: ${siteUrl}/sitemap.xml`,
89+
`Sitemap: ${WEBUI_SITE_ORIGIN}/sitemap.xml`,
7590
"",
7691
].join("\n"),
7792
);
@@ -82,7 +97,7 @@ async function writeSeoFiles() {
8297
'<?xml version="1.0" encoding="UTF-8"?>',
8398
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
8499
" <url>",
85-
` <loc>${siteUrl}/</loc>`,
100+
` <loc>${WEBUI_SITE_ORIGIN}/</loc>`,
86101
" <changefreq>monthly</changefreq>",
87102
" <priority>1.0</priority>",
88103
" </url>",
@@ -91,3 +106,7 @@ async function writeSeoFiles() {
91106
].join("\n"),
92107
);
93108
}
109+
110+
async function writeAgentFiles() {
111+
await writeFile(resolve(distDir, "index.md"), HOMEPAGE_MARKDOWN);
112+
}

packages/apk-webui/site-config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const WEBUI_SITE_ORIGIN = "https://lc.absinthe.life";
2+
export const WEBUI_SITE_URL = `${WEBUI_SITE_ORIGIN}/`;
3+
export const WEBUI_SOCIAL_PREVIEW_URL = `${WEBUI_SITE_ORIGIN}/social-preview.png`;
4+
export const WEBUI_SOURCE_REPOSITORY_URL = "https://github.com/LibChecker/tgbot";

packages/apk-webui/src/index.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@
1414
<meta property="og:site_name" content="LibChecker WebUI">
1515
<meta property="og:title" content="LibChecker WebUI">
1616
<meta property="og:description" content="Analyze APK/APKS/APKM/XAPK packages in your browser with local parsing, SDK markers, signatures and reports.">
17-
<meta property="og:url" content="https://lc.absinthe.life/">
18-
<meta property="og:image" content="https://lc.absinthe.life/social-preview.png">
19-
<meta property="og:image:secure_url" content="https://lc.absinthe.life/social-preview.png">
17+
<meta property="og:url" content="%WEBUI_SITE_URL%">
18+
<meta property="og:image" content="%WEBUI_SOCIAL_PREVIEW_URL%">
19+
<meta property="og:image:secure_url" content="%WEBUI_SOCIAL_PREVIEW_URL%">
2020
<meta property="og:image:type" content="image/png">
2121
<meta property="og:image:width" content="1200">
2222
<meta property="og:image:height" content="630">
2323
<meta property="og:image:alt" content="LibChecker WebUI Android Package Analyzer preview">
2424
<meta name="twitter:card" content="summary_large_image">
2525
<meta name="twitter:title" content="LibChecker WebUI">
2626
<meta name="twitter:description" content="Analyze APK/APKS/APKM/XAPK packages in your browser with local parsing, SDK markers, signatures and reports.">
27-
<meta name="twitter:image" content="https://lc.absinthe.life/social-preview.png">
28-
<link rel="canonical" href="https://lc.absinthe.life/">
27+
<meta name="twitter:image" content="%WEBUI_SOCIAL_PREVIEW_URL%">
28+
<link rel="canonical" href="%WEBUI_SITE_URL%">
2929
<title>LibChecker WebUI</title>
3030
<link rel="icon" type="image/svg+xml" href="./assets/icon_round.svg">
31-
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebApplication","name":"LibChecker WebUI","alternateName":"Android Package Analyzer","url":"https://lc.absinthe.life/","description":"Analyze APK/APKS/APKM/XAPK packages in your browser with local parsing, SDK markers, signatures and reports.","applicationCategory":"DeveloperApplication","operatingSystem":"Any","browserRequirements":"Requires a modern browser with JavaScript enabled","image":"https://lc.absinthe.life/social-preview.png","isAccessibleForFree":true,"inLanguage":"en","sameAs":"https://github.com/LibChecker/tgbot"}</script>
31+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebApplication","name":"LibChecker WebUI","alternateName":"Android Package Analyzer","url":"%WEBUI_SITE_URL%","description":"Analyze APK/APKS/APKM/XAPK packages in your browser with local parsing, SDK markers, signatures and reports.","applicationCategory":"DeveloperApplication","operatingSystem":"Any","browserRequirements":"Requires a modern browser with JavaScript enabled","image":"%WEBUI_SOCIAL_PREVIEW_URL%","isAccessibleForFree":true,"inLanguage":"en","sameAs":"%WEBUI_SOURCE_REPOSITORY_URL%"}</script>
3232
<script>
3333
(() => {
3434
const key = "apk-webui-theme";

packages/apk-webui/vite.config.mjs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { readFileSync } from "node:fs";
33
import { dirname, resolve } from "node:path";
44
import { fileURLToPath } from "node:url";
55
import { defineConfig, minify } from "vite";
6+
import {
7+
WEBUI_SITE_URL,
8+
WEBUI_SOCIAL_PREVIEW_URL,
9+
WEBUI_SOURCE_REPOSITORY_URL,
10+
} from "./site-config.mjs";
611

712
const projectDir = dirname(fileURLToPath(import.meta.url));
813
const repoDir = resolve(projectDir, "../..");
@@ -57,6 +62,7 @@ export default defineConfig({
5762
base: "./",
5863
publicDir: false,
5964
plugins: [
65+
siteMetadataHtmlReplacements(),
6066
pagesFunctionDevProxy(),
6167
minifyGeneratedJsAssets(),
6268
],
@@ -104,11 +110,46 @@ export default defineConfig({
104110
},
105111
});
106112

113+
function siteMetadataHtmlReplacements() {
114+
const replacements = {
115+
"%WEBUI_SITE_URL%": WEBUI_SITE_URL,
116+
"%WEBUI_SOCIAL_PREVIEW_URL%": WEBUI_SOCIAL_PREVIEW_URL,
117+
"%WEBUI_SOURCE_REPOSITORY_URL%": WEBUI_SOURCE_REPOSITORY_URL,
118+
};
119+
120+
return {
121+
name: "site-metadata-html-replacements",
122+
transformIndexHtml(html) {
123+
return Object.entries(replacements).reduce(
124+
(result, [placeholder, value]) => result.replaceAll(placeholder, value),
125+
html,
126+
);
127+
},
128+
};
129+
}
130+
107131
function pagesFunctionDevProxy() {
108132
return {
109133
name: "pages-function-dev-proxy",
110134
apply: "serve",
111135
configureServer(server) {
136+
server.middlewares.use(async (req, res, next) => {
137+
try {
138+
const { handleMarkdownRequest } = await import("./functions/_middleware.js");
139+
const response = handleMarkdownRequest(createMetadataOnlyDevRequest(req));
140+
if (!response) {
141+
next();
142+
return;
143+
}
144+
await sendDevResponse(res, response);
145+
} catch (error) {
146+
const message = error instanceof Error ? error.message : "Failed to handle Markdown response";
147+
res.statusCode = 500;
148+
res.setHeader("content-type", "text/plain; charset=UTF-8");
149+
res.end(message);
150+
}
151+
});
152+
112153
server.middlewares.use("/url-report", async (req, res) => {
113154
try {
114155
const { onRequest } = await import("./functions/url-report.js");
@@ -128,7 +169,27 @@ function pagesFunctionDevProxy() {
128169
};
129170
}
130171

172+
function createMetadataOnlyDevRequest(req) {
173+
const method = req.method || "GET";
174+
const url = new URL(req.url || "/", "http://127.0.0.1");
175+
return new Request(url, {
176+
method,
177+
headers: createDevHeaders(req),
178+
});
179+
}
180+
131181
async function createDevRequest(req) {
182+
const headers = createDevHeaders(req);
183+
const method = req.method || "GET";
184+
const body = method === "GET" || method === "HEAD" ? undefined : await readDevRequestBody(req);
185+
return new Request("http://127.0.0.1/url-report", {
186+
method,
187+
headers,
188+
body,
189+
});
190+
}
191+
192+
function createDevHeaders(req) {
132193
const headers = new Headers();
133194
for (const [key, value] of Object.entries(req.headers)) {
134195
if (Array.isArray(value)) {
@@ -140,13 +201,7 @@ async function createDevRequest(req) {
140201
}
141202
}
142203

143-
const method = req.method || "GET";
144-
const body = method === "GET" || method === "HEAD" ? undefined : await readDevRequestBody(req);
145-
return new Request("http://127.0.0.1/url-report", {
146-
method,
147-
headers,
148-
body,
149-
});
204+
return headers;
150205
}
151206

152207
async function readDevRequestBody(req) {

0 commit comments

Comments
 (0)