chore: initialize WanderQ MiniAPP 2.0 repository

This commit is contained in:
inman
2026-07-23 15:48:34 +08:00
commit b4c0a1c516
93 changed files with 53525 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import { config as loadEnv } from "dotenv";
import OSS from "ali-oss";
import { spawn } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(scriptDir, "..");
const pythonScript = join(scriptDir, "prepare-static-webp.py");
const targetFiles = [
"src/App.tsx",
"src/content.ts",
"src/generated-products.json",
"src/search-page-defaults.ts",
"src/train-activity.ts",
"apps/admin/src/App.tsx",
"apps/api/src/home-module-defaults.ts",
"apps/api/src/routes/admin.ts",
"apps/api/src/routes/memory.ts",
"apps/api/src/search-page.ts",
];
loadEnv({ path: resolve(projectRoot, ".env") });
const defaultPublicBaseUrl = (() => {
const endpoint = String(process.env.OSS_ENDPOINT ?? "")
.replace(/^https?:\/\//i, "")
.replace(/\/$/, "")
.split("/")[0];
return process.env.OSS_BUCKET_NAME && endpoint ? `https://${process.env.OSS_BUCKET_NAME}.${endpoint}` : "";
})();
const configuredPublicBaseUrl = String(process.env.MEDIA_PUBLIC_BASE_URL ?? "").replace(/\/$/, "");
const publicBaseUrl = configuredPublicBaseUrl || defaultPublicBaseUrl;
if (!publicBaseUrl) throw new Error("无法推导 OSS 公共地址,请配置 MEDIA_PUBLIC_BASE_URL 或 OSS_BUCKET_NAME/OSS_ENDPOINT");
function endpointHost(endpoint) {
return endpoint.replace(/^https?:\/\//i, "").replace(/\/$/, "").split("/")[0];
}
function urlForKey(key) {
return `${publicBaseUrl}/${key.split("/").map((part) => encodeURIComponent(part)).join("/")}`;
}
function run(command, args) {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolvePromise(stdout.trim());
return;
}
reject(new Error(`${command} 退出码 ${code}: ${stderr || stdout}`));
});
});
}
function buildClient() {
const accessKeyId = process.env.OSS_ACCESS_KEY_ID;
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET;
const endpoint = process.env.OSS_ENDPOINT;
const bucket = process.env.OSS_BUCKET_NAME;
if (!accessKeyId || !accessKeySecret || !endpoint || !bucket) {
throw new Error("OSS 存储需要配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_ENDPOINT 和 OSS_BUCKET_NAME");
}
const host = endpointHost(endpoint);
return {
bucket,
client: new OSS({
region: process.env.OSS_REGION ?? host.split(".")[0],
endpoint: /^https?:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`,
accessKeyId,
accessKeySecret,
bucket,
secure: true,
}),
};
}
async function uploadAll(manifest, outputRoot) {
const { client } = buildClient();
let cursor = 0;
let uploaded = 0;
const concurrency = 4;
async function worker() {
while (cursor < manifest.length) {
const item = manifest[cursor++];
const buffer = await readFile(join(outputRoot, item.outputPath));
await client.put(item.objectKey, buffer, {
headers: {
"Content-Type": "image/webp",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
uploaded += 1;
if (uploaded % 20 === 0 || uploaded === manifest.length) {
console.log(`OSS 上传进度 ${uploaded}/${manifest.length}`);
}
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, manifest.length) }, () => worker()));
}
async function rewriteReferences(manifest) {
const replacements = new Map();
for (const item of manifest) {
if (item.reference) replacements.set(item.reference, urlForKey(item.objectKey));
}
for (const relativePath of targetFiles) {
const absolutePath = resolve(projectRoot, relativePath);
let source = await readFile(absolutePath, "utf8");
for (const [from, to] of replacements) source = source.split(from).join(to);
await writeFile(absolutePath, source);
}
}
async function main() {
const workDir = await mkdtemp(join(tmpdir(), "miniapp-static-webp-"));
const outputRoot = join(workDir, "webp");
const manifestPath = join(workDir, "manifest.json");
try {
const python = process.env.MINIAPP_IMAGE_PYTHON || process.env.PYTHON || "python3";
const prepareSummary = await run(python, [pythonScript, "--root", projectRoot, "--output", outputRoot, "--manifest", manifestPath]);
console.log(`WebP 转码完成:${prepareSummary}`);
const manifest = JSON.parse(await readFile(manifestPath, "utf8")).assets;
await uploadAll(manifest, outputRoot);
await rewriteReferences(manifest);
await writeFile(resolve(projectRoot, "scripts/static-asset-manifest.json"), JSON.stringify({
generatedAt: new Date().toISOString(),
publicBaseUrl,
assets: manifest.map((item) => ({
sourcePath: item.sourcePath,
reference: item.reference,
objectKey: item.objectKey,
url: urlForKey(item.objectKey),
lossless: item.lossless,
sourceBytes: item.sourceBytes,
outputBytes: item.outputBytes,
sha256: item.sha256,
})),
}, null, 2) + "\n");
console.log(JSON.stringify({ message: "静态图片已转 WebP 并上传 OSS", assets: manifest.length, publicBaseUrl }, null, 2));
} finally {
await rm(workDir, { recursive: true, force: true });
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});