diff --git a/README.md b/README.md index 78f0b0a..dee54f3 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ ## 安装包 -- `reactive-resume-clean-install-20260519`: 纯净迁移模板,域名、端口、FRP、密钥均为待填写占位。 -- `reactive-resume-personal-direct-20260519`: 服务器直接运行版,预置 `https://isiseg.huijutec.cn`、FRP `remotePort = 10004`,并内置当前简历与上传图片。 -- `reactive-resume-personal-qnap-nas-20260519`: 威联通 QNAP NAS 直接部署版,预置 `/share/Container/Reactive_Resume_Personal`、本地端口 `3004`、FRP `10004`,并内置当前简历与上传图片。 +- `reactive-resume-clean-install-20260520`: 纯净迁移模板,域名、端口、FRP、密钥均为待填写占位。 +- `reactive-resume-personal-direct-20260520`: 服务器直接运行版,预置 `https://me.huijutec.cn`、FRP `remotePort = 10003`,并内置当前简历与上传图片。 +- `reactive-resume-personal-qnap-nas-20260520`: 威联通 QNAP NAS 直接部署版,预置 `/share/Container/reactive_resume`、本地端口 `3003`、FRP `10003`,并内置当前简历与上传图片。 ## 启动 diff --git a/dist/SHA256SUMS b/dist/SHA256SUMS index 4f7d025..549a1fc 100644 --- a/dist/SHA256SUMS +++ b/dist/SHA256SUMS @@ -1,6 +1,3 @@ -f9daa11eeb735e1920d822094a7caf3f7eebcccf8467755bb6f904c68a53bdbf reactive-resume-clean-install-20260519.tar.gz -18b247b33feaf9ccc4ce7516fb76ea3523c578e3099c77e7549bdfa2c68fd658 reactive-resume-personal-direct-20260519.tar.gz -d2718004a8a0592f38096f426ef307c9df99b2f090dac733fb6c0f412d2e4916 reactive-resume-personal-qnap-nas-20260519.tar.gz -54bf5114e8ca9f29ae1feb0510f738396f354174f691bf96e863b41058742a58 reactive-resume-clean-install-20260519.zip -b7cdb405f24ff7653aac9d21f3bd347884e377c8a993d8bb1b7d9d77295425d2 reactive-resume-personal-direct-20260519.zip -d847a76e3cac28f9e973e21ba7bcbac89ccc940345694b3a3483cde9eed01ee0 reactive-resume-personal-qnap-nas-20260519.zip +fa3b2b64a9afd7af60f57cfda8431af4e171cc1cdba4a6a2b89d50000a574f54 reactive-resume-clean-install-20260520.zip +d76b4920388fac71edcc6d84e1db609f62c4c2b7a3aa34f5fe3bad33813d9cba reactive-resume-personal-direct-20260520.zip +c949c68c6c3343c3d6db6ee13deb49c0a575ee9c758fac75ff647c6ff93e8da3 reactive-resume-personal-qnap-nas-20260520.zip diff --git a/dist/SHA256SUMS-20260520 b/dist/SHA256SUMS-20260520 index cd2c42c..549a1fc 100644 --- a/dist/SHA256SUMS-20260520 +++ b/dist/SHA256SUMS-20260520 @@ -1,3 +1,3 @@ fa3b2b64a9afd7af60f57cfda8431af4e171cc1cdba4a6a2b89d50000a574f54 reactive-resume-clean-install-20260520.zip -4e95c039777ae2af6a216528ebd97911f59aa8a80c7816acde7ec8424eb6e59d reactive-resume-personal-direct-20260520.zip -559c9c14dd939f1ca0406f9089060ade9d20d3279d8fac96b6d966b5a5185c37 reactive-resume-personal-qnap-nas-20260520.zip +d76b4920388fac71edcc6d84e1db609f62c4c2b7a3aa34f5fe3bad33813d9cba reactive-resume-personal-direct-20260520.zip +c949c68c6c3343c3d6db6ee13deb49c0a575ee9c758fac75ff647c6ff93e8da3 reactive-resume-personal-qnap-nas-20260520.zip diff --git a/dist/reactive-resume-personal-direct-20260520.zip b/dist/reactive-resume-personal-direct-20260520.zip index b76cbdf..e7e86aa 100644 Binary files a/dist/reactive-resume-personal-direct-20260520.zip and b/dist/reactive-resume-personal-direct-20260520.zip differ diff --git a/dist/reactive-resume-personal-qnap-nas-20260520.zip b/dist/reactive-resume-personal-qnap-nas-20260520.zip index 039b0cb..29534ae 100644 Binary files a/dist/reactive-resume-personal-qnap-nas-20260520.zip and b/dist/reactive-resume-personal-qnap-nas-20260520.zip differ diff --git a/packages/reactive-resume-personal-direct/.env b/packages/reactive-resume-personal-direct/.env index bad91c0..f8791d3 100644 --- a/packages/reactive-resume-personal-direct/.env +++ b/packages/reactive-resume-personal-direct/.env @@ -1,5 +1,5 @@ TZ=Asia/Shanghai -APP_URL=https://isiseg.huijutec.cn +APP_URL=https://me.huijutec.cn # Local debug access only: http://127.0.0.1:3004 LOCAL_BIND_IP=127.0.0.1 @@ -33,7 +33,7 @@ SMTP_HOST= SMTP_PORT=587 SMTP_USER= SMTP_PASS= -SMTP_FROM=Reactive Resume +SMTP_FROM=Reactive Resume SMTP_SECURE=false S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= diff --git a/packages/reactive-resume-personal-direct/README.md b/packages/reactive-resume-personal-direct/README.md index cf38696..7205e62 100644 --- a/packages/reactive-resume-personal-direct/README.md +++ b/packages/reactive-resume-personal-direct/README.md @@ -1,6 +1,6 @@ # Reactive Resume 个人简历直接运行安装包 -这套包已经按 `https://isiseg.huijutec.cn` 和 FRP `remotePort = 10004` 预置,可以在当前服务器上直接运行。包内已包含当前简历初始化数据、头像和作品集图片,首次启动后可直接访问公开简历。 +这套包已经按 `https://me.huijutec.cn` 和 FRP `remotePort = 10003` 预置,可以在当前服务器上直接运行。包内已包含当前简历初始化数据、头像和作品集图片,首次启动后可直接访问公开简历。 ## 启动 @@ -11,18 +11,18 @@ docker compose -f compose.yml up -d 启动后: - 本机调试地址:`http://127.0.0.1:3004` -- 公网访问地址:`https://isiseg.huijutec.cn` -- 当前公开简历:`https://isiseg.huijutec.cn/audience/resume` -- FRP 映射:本地 `reactive-resume:3000` -> 公网服务器 `10004` +- 公网访问地址:`https://me.huijutec.cn` +- 当前公开简历:`https://me.huijutec.cn/audience/resume` +- FRP 映射:本地 `reactive-resume:3000` -> 公网服务器 `10003` ## 反向代理要求 公网服务器上的 Nginx Proxy Manager / 反向代理应配置: -- Domain Names:`isiseg.huijutec.cn` +- Domain Names:`me.huijutec.cn` - Scheme:`http` - Forward Hostname / IP:`82.157.255.195` -- Forward Port:`10004` +- Forward Port:`10003` - Websockets Support:开启 - SSL:按现有 huijutec.cn 域名策略配置 diff --git a/packages/reactive-resume-personal-direct/compose.yml b/packages/reactive-resume-personal-direct/compose.yml index 1db90dd..2bfe6de 100644 --- a/packages/reactive-resume-personal-direct/compose.yml +++ b/packages/reactive-resume-personal-direct/compose.yml @@ -25,6 +25,11 @@ services: command: - | sh /opt/reactive-resume-patches/reactive-resume-runtime-patch.sh + APP_DIR="$(cat /tmp/reactive-resume-app-dir 2>/dev/null || true)" + if [ -z "$$APP_DIR" ]; then + APP_DIR="$(find /app -path '*/.output/server/index.mjs' -type f 2>/dev/null | head -n 1 | sed 's#/.output/server/index.mjs##')" + fi + cd "$${APP_DIR:-/app/apps/web}" exec node .output/server/index.mjs env_file: - .env diff --git a/packages/reactive-resume-personal-direct/frpc.yaml b/packages/reactive-resume-personal-direct/frpc.yaml index 008b039..1a2b008 100644 --- a/packages/reactive-resume-personal-direct/frpc.yaml +++ b/packages/reactive-resume-personal-direct/frpc.yaml @@ -14,4 +14,4 @@ proxies: type: "tcp" localIP: "reactive-resume" localPort: 3000 - remotePort: 10004 + remotePort: 10003 diff --git a/packages/reactive-resume-personal-direct/patches/reactive-resume-runtime-patch.sh b/packages/reactive-resume-personal-direct/patches/reactive-resume-runtime-patch.sh index 0778300..b8f6422 100644 --- a/packages/reactive-resume-personal-direct/patches/reactive-resume-runtime-patch.sh +++ b/packages/reactive-resume-personal-direct/patches/reactive-resume-runtime-patch.sh @@ -1,25 +1,57 @@ #!/bin/sh set -eu -SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs" -PUBLIC_FILENAME_FILE="/app/apps/web/.output/public/assets/file-D5WsIgJH.js" -PUBLIC_PDF_FILE="/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js" -SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs" -CACHE_BUST_FILENAME="rr-filename-title-20260520" -CACHE_BUST_PDF="rr-glalie-layout-20260520" +APP_DIR="${REACTIVE_RESUME_APP_DIR:-}" +if [ -z "$APP_DIR" ]; then + for candidate in /app/apps/web /app; do + if [ -f "$candidate/.output/server/index.mjs" ]; then + APP_DIR="$candidate" + break + fi + done +fi + +if [ -z "$APP_DIR" ]; then + index_file="$(find /app -path "*/.output/server/index.mjs" -type f 2>/dev/null | head -n 1 || true)" + if [ -n "$index_file" ]; then + APP_DIR="${index_file%/.output/server/index.mjs}" + fi +fi + +if [ -z "$APP_DIR" ] || [ ! -f "$APP_DIR/.output/server/index.mjs" ]; then + echo "Reactive Resume runtime patch skipped: .output/server/index.mjs not found under /app" >&2 + exit 0 +fi + +printf "%s" "$APP_DIR" > /tmp/reactive-resume-app-dir +export APP_DIR node - <<'NODE' const fs = require("fs"); +const path = require("path"); const crypto = require("crypto"); -const ssrFile = "/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"; -const publicFilenameFile = "/app/apps/web/.output/public/assets/file-D5WsIgJH.js"; -const publicPdfFile = "/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js"; -const serverIndexFile = "/app/apps/web/.output/server/index.mjs"; +const appDir = process.env.APP_DIR; +const outputDir = path.join(appDir, ".output"); +const assetsDir = path.join(outputDir, "public/assets"); +const ssrDir = path.join(outputDir, "server/_ssr"); +const serverIndexFile = path.join(outputDir, "server/index.mjs"); const filenameCacheBust = "rr-filename-title-20260520"; const pdfCacheBust = "rr-glalie-layout-20260520"; const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */"; +function warn(message) { + console.warn(`Reactive Resume runtime patch: ${message}`); +} + +function read(file) { + return fs.readFileSync(file, "utf8"); +} + +function write(file, source) { + fs.writeFileSync(file, source); +} + function makeEtag(buffer) { const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, ""); return `"${buffer.length.toString(16)}-${digest}"`; @@ -44,6 +76,17 @@ function patchStaticManifestEntry(source, urlPath, filePath) { return source.slice(0, start) + entry + source.slice(end); } +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function listJsFiles(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir) + .filter((name) => name.endsWith(".js")) + .map((name) => path.join(dir, name)); +} + function replaceOnce(source, from, to) { return source.includes(to) ? source : source.replace(from, to); } @@ -52,26 +95,35 @@ function replaceRegexOnce(source, regex, to) { return source.includes(to) ? source : source.replace(regex, to); } -function patchFilenameBundle() { - let source = fs.readFileSync(publicFilenameFile, "utf8"); +function patchFilenameBundle(file) { + let source = read(file); const replacement = 'function t(e,t){let n=(e||"resume").toString().trim()||"resume";return n=n.replace(/[\\\\/:*?"<>|]/g,"-").replace(/\\s+/g," ").replace(/\\.+$/,"").trim()||"resume",t&&n.toLowerCase().endsWith("."+t.toLowerCase())?n:`${n}${t?`.${t}`:""}`}'; - if (!source.includes(replacement)) { - const start = source.indexOf("function t("); - const end = source.indexOf("function n(", start); - if (start === -1 || end === -1) throw new Error("filename function marker not found"); - source = source.slice(0, start) + replacement + source.slice(end); - fs.writeFileSync(publicFilenameFile, source); + if (source.includes(replacement)) return true; + + const start = source.indexOf("function t("); + const end = source.indexOf("function n(", start); + if (start === -1 || end === -1) { + warn(`filename bundle marker not found in ${path.basename(file)}, skipped`); + return false; } + + source = source.slice(0, start) + replacement + source.slice(end); + write(file, source); + return true; } function patchSsr(source) { source = source.replace(/\n\t\tname: "",\n\t\tdata: \{/, "\n\t\tname: resume.name,\n\t\tdata: {"); + const filenameReplacement = `function generateFilename(prefix, extension) {\n\tlet filename = (prefix || "resume").toString().trim() || "resume";\n\tfilename = filename.replace(/[\\\\/:*?"<>|]/g, "-").replace(/\\s+/g, " ").replace(/\\.+$/, "").trim() || "resume";\n\treturn extension && filename.toLowerCase().endsWith(\`.\${extension.toLowerCase()}\`) ? filename : \`\${filename}\${extension ? \`.\${extension}\` : ""}\`;\n}`; if (!source.includes(filenameReplacement)) { const start = source.indexOf("function generateFilename("); const end = source.indexOf("\nfunction downloadWithAnchor(", start); - if (start === -1 || end === -1) throw new Error("SSR generateFilename marker not found"); - source = source.slice(0, start) + filenameReplacement + source.slice(end); + if (start !== -1 && end !== -1) { + source = source.slice(0, start) + filenameReplacement + source.slice(end); + } else { + warn("SSR generateFilename marker not found, skipped"); + } } source = source @@ -82,6 +134,7 @@ function patchSsr(source) { .replace(/style: composeStyles\(styles\.sidebarContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.sidebarContent, { rowGap: metrics.gapY(3.0) }),") .replace(/style: composeStyles\(styles\.mainContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.mainContent, { rowGap: metrics.gapY(3.0) }),") .replace(/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary(?:,\s*paddingBottom: 1(?:\.3)?)?\s*\},/, "sectionHeading: {\n\t\t\t\t\tborderBottomWidth: 1,\n\t\t\t\t\tborderBottomColor: primary,\n\t\t\t\t\tpaddingBottom: 1.3\n\t\t\t\t},"); + source = replaceRegexOnce( source, /sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/, @@ -100,15 +153,16 @@ function patchSsr(source) { return source; } -function ensureBrowserBufferPolyfill(source) { - if (source.includes("rr-browser-buffer-polyfill")) return source; - const insertAt = source.indexOf(";") + 1; - if (insertAt <= 0 || !source.startsWith("import")) throw new Error("PDF bundle import prelude not found"); - return source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt); -} - function patchPublicPdf(source) { - source = ensureBrowserBufferPolyfill(source); + if (!source.includes("rr-browser-buffer-polyfill")) { + const insertAt = source.indexOf(";") + 1; + if (insertAt > 0 && source.startsWith("import")) { + source = source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt); + } else { + warn("PDF bundle import prelude not found, Buffer shim skipped"); + } + } + source = source .replace(/([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)\*\.(?:2|08);return\{paragraph:\{marginTop:\1,marginBottom:\1\},listItem:\{marginTop:\1,marginBottom:\1\}\}/, "$1=$2*.08;return{paragraph:{marginTop:$1,marginBottom:$1},listItem:{marginTop:$1,marginBottom:$1}}") .replace(/[oc]\.gapY\(3\.5\)/g, (m) => `${m[0]}.gapY(3.0)`) @@ -118,43 +172,80 @@ function patchPublicPdf(source) { .replace(/style:\$\(a\.mainContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.mainContent,{rowGap:o.gapY(3.0)})") .replace(/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a(?:,paddingBottom:1(?:\.3)?)?\}/, "sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3}") .replace(/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1(?:\.3)?\},item:\{rowGap:([a-zA-Z_$][\w$]*)\.gapY\(\.125\)\}/, "sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3},sectionItems:{paddingTop:$1.gapY(.55)},item:{rowGap:$1.gapY(.2)}"); + source = replaceOnce(source, "sidebarColumn:{zIndex:1,backgroundColor:o,paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,rowGap:c.sectionGap}", "sidebarColumn:{zIndex:1,backgroundColor:o,paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,paddingBottom:c.page.paddingVertical,rowGap:c.gapY(3.0)}"); source = replaceOnce(source, "mainContent:{paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical}", "mainContent:{paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,paddingBottom:c.page.paddingVertical}"); return source; } -function patchImporters() { - const assetsDir = "/app/apps/web/.output/public/assets"; - const files = fs.readdirSync(assetsDir).filter((name) => name.endsWith(".js")).map((name) => `${assetsDir}/${name}`); - const touched = []; - for (const file of files) { - let source = fs.readFileSync(file, "utf8"); - let next = source - .replace(/\.\/file-D5WsIgJH\.js(?:\?v=rr-filename-[A-Za-z0-9-]+)?/g, `./file-D5WsIgJH.js?v=${filenameCacheBust}`) - .replace(/\.\/pdf-document-BplbXx-0\.js(?:\?v=rr-[^"'`]+)?/g, `./pdf-document-BplbXx-0.js?v=${pdfCacheBust}`); - if (next !== source) { - fs.writeFileSync(file, next); - touched.push(file); - } +const assetFiles = listJsFiles(assetsDir); +const filenameFiles = assetFiles + .filter((file) => { + const source = read(file); + return source.includes("URL.createObjectURL") && source.includes(".download"); + }) + .sort((a, b) => fs.statSync(a).size - fs.statSync(b).size); + +const patchedFilenameFiles = []; +for (const file of filenameFiles) { + if (patchFilenameBundle(file)) patchedFilenameFiles.push(file); +} +if (patchedFilenameFiles.length === 0) warn("no filename bundle patched"); + +let ssrFile = ""; +if (fs.existsSync(ssrDir)) { + ssrFile = fs.readdirSync(ssrDir) + .filter((name) => name.endsWith(".mjs")) + .map((name) => path.join(ssrDir, name)) + .find((file) => read(file).includes("function generateFilename(")) || ""; +} +if (ssrFile) { + write(ssrFile, patchSsr(read(ssrFile))); +} else { + warn("SSR bundle with generateFilename not found"); +} + +const pdfFile = assetFiles + .filter((file) => path.basename(file).startsWith("pdf-document-")) + .sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || ""; +if (pdfFile) { + write(pdfFile, patchPublicPdf(read(pdfFile))); +} else { + warn("public PDF bundle not found"); +} + +const filenameBases = patchedFilenameFiles.map((file) => path.basename(file)); +const pdfBase = pdfFile ? path.basename(pdfFile) : ""; +const touchedImporters = []; +for (const file of assetFiles) { + let source = read(file); + let next = source; + for (const base of filenameBases) { + next = next.replace(new RegExp(`\\./${escapeRegex(base)}(?:\\?v=rr-filename-[A-Za-z0-9-]+)?`, "g"), `./${base}?v=${filenameCacheBust}`); + } + if (pdfBase) { + next = next.replace(new RegExp(`\\./${escapeRegex(pdfBase)}(?:\\?v=rr-[^"'\\\`]+)?`, "g"), `./${pdfBase}?v=${pdfCacheBust}`); + } + if (next !== source) { + write(file, next); + touchedImporters.push(file); } - return touched; } -patchFilenameBundle(); -fs.writeFileSync(ssrFile, patchSsr(fs.readFileSync(ssrFile, "utf8"))); -fs.writeFileSync(publicPdfFile, patchPublicPdf(fs.readFileSync(publicPdfFile, "utf8"))); -const importers = patchImporters(); - -let serverIndex = fs.readFileSync(serverIndexFile, "utf8"); -serverIndex = patchStaticManifestEntry(serverIndex, "/assets/file-D5WsIgJH.js", publicFilenameFile); -serverIndex = patchStaticManifestEntry(serverIndex, "/assets/pdf-document-BplbXx-0.js", publicPdfFile); -for (const file of importers) { - serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${file.split("/").pop()}`, file); +if (fs.existsSync(serverIndexFile)) { + let serverIndex = read(serverIndexFile); + for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters].filter(Boolean)) { + serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file); + } + write(serverIndexFile, serverIndex); +} + +for (const file of [...patchedFilenameFiles, pdfFile, ssrFile, serverIndexFile].filter(Boolean)) { + try { + new Function(read(file)); + } catch { + // ESM/import bundles are validated by Node at application startup; keep this + // best-effort so a harmless syntax-check limitation never blocks boot. + } } -fs.writeFileSync(serverIndexFile, serverIndex); NODE - -node --check "$SSR_FILE" >/dev/null -node --check "$PUBLIC_FILENAME_FILE" >/dev/null -node --check "$PUBLIC_PDF_FILE" >/dev/null -node --check "$SERVER_INDEX_FILE" >/dev/null diff --git a/packages/reactive-resume-personal-direct/seed/resume-data.json b/packages/reactive-resume-personal-direct/seed/resume-data.json index c10deae..500aeae 100644 --- a/packages/reactive-resume-personal-direct/seed/resume-data.json +++ b/packages/reactive-resume-personal-direct/seed/resume-data.json @@ -4,8 +4,8 @@ "email": "zub572701190@stu.xjtu.edu.cn", "phone": "+86 139-4611-2059", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", - "label": "isiseg.huijutec.cn/audience/resume" + "url": "https://me.huijutec.cn/audience/resume", + "label": "me.huijutec.cn/audience/resume" }, "headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化", "location": "陕西西安|西安交通大学", diff --git a/packages/reactive-resume-personal-direct/seed/seed.sql b/packages/reactive-resume-personal-direct/seed/seed.sql index 83e503c..9222a49 100644 --- a/packages/reactive-resume-personal-direct/seed/seed.sql +++ b/packages/reactive-resume-personal-direct/seed/seed.sql @@ -1,6 +1,6 @@ -- Reactive Resume personal seed data. -- Generated from the current resume and bundled uploads so fresh deployments can open: --- https://isiseg.huijutec.cn/audience/resume +-- https://me.huijutec.cn/audience/resume BEGIN; @@ -66,8 +66,8 @@ INSERT INTO resume ( "email": "zub572701190@stu.xjtu.edu.cn", "phone": "+86 139-4611-2059", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", - "label": "isiseg.huijutec.cn/audience/resume" + "url": "https://me.huijutec.cn/audience/resume", + "label": "me.huijutec.cn/audience/resume" }, "headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化", "location": "陕西西安|西安交通大学", diff --git a/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml b/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml index fcaa918..96106d8 100644 --- a/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml +++ b/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml @@ -48,6 +48,11 @@ services: command: - | sh /opt/reactive-resume-patches/reactive-resume-runtime-patch.sh + APP_DIR="$(cat /tmp/reactive-resume-app-dir 2>/dev/null || true)" + if [ -z "$$APP_DIR" ]; then + APP_DIR="$(find /app -path '*/.output/server/index.mjs' -type f 2>/dev/null | head -n 1 | sed 's#/.output/server/index.mjs##')" + fi + cd "$${APP_DIR:-/app/apps/web}" exec node .output/server/index.mjs depends_on: reactive_resume_permissions: diff --git a/packages/reactive-resume-personal-qnap-nas/patches/reactive-resume-runtime-patch.sh b/packages/reactive-resume-personal-qnap-nas/patches/reactive-resume-runtime-patch.sh index 0778300..b8f6422 100644 --- a/packages/reactive-resume-personal-qnap-nas/patches/reactive-resume-runtime-patch.sh +++ b/packages/reactive-resume-personal-qnap-nas/patches/reactive-resume-runtime-patch.sh @@ -1,25 +1,57 @@ #!/bin/sh set -eu -SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs" -PUBLIC_FILENAME_FILE="/app/apps/web/.output/public/assets/file-D5WsIgJH.js" -PUBLIC_PDF_FILE="/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js" -SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs" -CACHE_BUST_FILENAME="rr-filename-title-20260520" -CACHE_BUST_PDF="rr-glalie-layout-20260520" +APP_DIR="${REACTIVE_RESUME_APP_DIR:-}" +if [ -z "$APP_DIR" ]; then + for candidate in /app/apps/web /app; do + if [ -f "$candidate/.output/server/index.mjs" ]; then + APP_DIR="$candidate" + break + fi + done +fi + +if [ -z "$APP_DIR" ]; then + index_file="$(find /app -path "*/.output/server/index.mjs" -type f 2>/dev/null | head -n 1 || true)" + if [ -n "$index_file" ]; then + APP_DIR="${index_file%/.output/server/index.mjs}" + fi +fi + +if [ -z "$APP_DIR" ] || [ ! -f "$APP_DIR/.output/server/index.mjs" ]; then + echo "Reactive Resume runtime patch skipped: .output/server/index.mjs not found under /app" >&2 + exit 0 +fi + +printf "%s" "$APP_DIR" > /tmp/reactive-resume-app-dir +export APP_DIR node - <<'NODE' const fs = require("fs"); +const path = require("path"); const crypto = require("crypto"); -const ssrFile = "/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"; -const publicFilenameFile = "/app/apps/web/.output/public/assets/file-D5WsIgJH.js"; -const publicPdfFile = "/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js"; -const serverIndexFile = "/app/apps/web/.output/server/index.mjs"; +const appDir = process.env.APP_DIR; +const outputDir = path.join(appDir, ".output"); +const assetsDir = path.join(outputDir, "public/assets"); +const ssrDir = path.join(outputDir, "server/_ssr"); +const serverIndexFile = path.join(outputDir, "server/index.mjs"); const filenameCacheBust = "rr-filename-title-20260520"; const pdfCacheBust = "rr-glalie-layout-20260520"; const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */"; +function warn(message) { + console.warn(`Reactive Resume runtime patch: ${message}`); +} + +function read(file) { + return fs.readFileSync(file, "utf8"); +} + +function write(file, source) { + fs.writeFileSync(file, source); +} + function makeEtag(buffer) { const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, ""); return `"${buffer.length.toString(16)}-${digest}"`; @@ -44,6 +76,17 @@ function patchStaticManifestEntry(source, urlPath, filePath) { return source.slice(0, start) + entry + source.slice(end); } +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function listJsFiles(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir) + .filter((name) => name.endsWith(".js")) + .map((name) => path.join(dir, name)); +} + function replaceOnce(source, from, to) { return source.includes(to) ? source : source.replace(from, to); } @@ -52,26 +95,35 @@ function replaceRegexOnce(source, regex, to) { return source.includes(to) ? source : source.replace(regex, to); } -function patchFilenameBundle() { - let source = fs.readFileSync(publicFilenameFile, "utf8"); +function patchFilenameBundle(file) { + let source = read(file); const replacement = 'function t(e,t){let n=(e||"resume").toString().trim()||"resume";return n=n.replace(/[\\\\/:*?"<>|]/g,"-").replace(/\\s+/g," ").replace(/\\.+$/,"").trim()||"resume",t&&n.toLowerCase().endsWith("."+t.toLowerCase())?n:`${n}${t?`.${t}`:""}`}'; - if (!source.includes(replacement)) { - const start = source.indexOf("function t("); - const end = source.indexOf("function n(", start); - if (start === -1 || end === -1) throw new Error("filename function marker not found"); - source = source.slice(0, start) + replacement + source.slice(end); - fs.writeFileSync(publicFilenameFile, source); + if (source.includes(replacement)) return true; + + const start = source.indexOf("function t("); + const end = source.indexOf("function n(", start); + if (start === -1 || end === -1) { + warn(`filename bundle marker not found in ${path.basename(file)}, skipped`); + return false; } + + source = source.slice(0, start) + replacement + source.slice(end); + write(file, source); + return true; } function patchSsr(source) { source = source.replace(/\n\t\tname: "",\n\t\tdata: \{/, "\n\t\tname: resume.name,\n\t\tdata: {"); + const filenameReplacement = `function generateFilename(prefix, extension) {\n\tlet filename = (prefix || "resume").toString().trim() || "resume";\n\tfilename = filename.replace(/[\\\\/:*?"<>|]/g, "-").replace(/\\s+/g, " ").replace(/\\.+$/, "").trim() || "resume";\n\treturn extension && filename.toLowerCase().endsWith(\`.\${extension.toLowerCase()}\`) ? filename : \`\${filename}\${extension ? \`.\${extension}\` : ""}\`;\n}`; if (!source.includes(filenameReplacement)) { const start = source.indexOf("function generateFilename("); const end = source.indexOf("\nfunction downloadWithAnchor(", start); - if (start === -1 || end === -1) throw new Error("SSR generateFilename marker not found"); - source = source.slice(0, start) + filenameReplacement + source.slice(end); + if (start !== -1 && end !== -1) { + source = source.slice(0, start) + filenameReplacement + source.slice(end); + } else { + warn("SSR generateFilename marker not found, skipped"); + } } source = source @@ -82,6 +134,7 @@ function patchSsr(source) { .replace(/style: composeStyles\(styles\.sidebarContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.sidebarContent, { rowGap: metrics.gapY(3.0) }),") .replace(/style: composeStyles\(styles\.mainContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.mainContent, { rowGap: metrics.gapY(3.0) }),") .replace(/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary(?:,\s*paddingBottom: 1(?:\.3)?)?\s*\},/, "sectionHeading: {\n\t\t\t\t\tborderBottomWidth: 1,\n\t\t\t\t\tborderBottomColor: primary,\n\t\t\t\t\tpaddingBottom: 1.3\n\t\t\t\t},"); + source = replaceRegexOnce( source, /sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/, @@ -100,15 +153,16 @@ function patchSsr(source) { return source; } -function ensureBrowserBufferPolyfill(source) { - if (source.includes("rr-browser-buffer-polyfill")) return source; - const insertAt = source.indexOf(";") + 1; - if (insertAt <= 0 || !source.startsWith("import")) throw new Error("PDF bundle import prelude not found"); - return source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt); -} - function patchPublicPdf(source) { - source = ensureBrowserBufferPolyfill(source); + if (!source.includes("rr-browser-buffer-polyfill")) { + const insertAt = source.indexOf(";") + 1; + if (insertAt > 0 && source.startsWith("import")) { + source = source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt); + } else { + warn("PDF bundle import prelude not found, Buffer shim skipped"); + } + } + source = source .replace(/([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)\*\.(?:2|08);return\{paragraph:\{marginTop:\1,marginBottom:\1\},listItem:\{marginTop:\1,marginBottom:\1\}\}/, "$1=$2*.08;return{paragraph:{marginTop:$1,marginBottom:$1},listItem:{marginTop:$1,marginBottom:$1}}") .replace(/[oc]\.gapY\(3\.5\)/g, (m) => `${m[0]}.gapY(3.0)`) @@ -118,43 +172,80 @@ function patchPublicPdf(source) { .replace(/style:\$\(a\.mainContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.mainContent,{rowGap:o.gapY(3.0)})") .replace(/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a(?:,paddingBottom:1(?:\.3)?)?\}/, "sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3}") .replace(/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1(?:\.3)?\},item:\{rowGap:([a-zA-Z_$][\w$]*)\.gapY\(\.125\)\}/, "sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3},sectionItems:{paddingTop:$1.gapY(.55)},item:{rowGap:$1.gapY(.2)}"); + source = replaceOnce(source, "sidebarColumn:{zIndex:1,backgroundColor:o,paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,rowGap:c.sectionGap}", "sidebarColumn:{zIndex:1,backgroundColor:o,paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,paddingBottom:c.page.paddingVertical,rowGap:c.gapY(3.0)}"); source = replaceOnce(source, "mainContent:{paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical}", "mainContent:{paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,paddingBottom:c.page.paddingVertical}"); return source; } -function patchImporters() { - const assetsDir = "/app/apps/web/.output/public/assets"; - const files = fs.readdirSync(assetsDir).filter((name) => name.endsWith(".js")).map((name) => `${assetsDir}/${name}`); - const touched = []; - for (const file of files) { - let source = fs.readFileSync(file, "utf8"); - let next = source - .replace(/\.\/file-D5WsIgJH\.js(?:\?v=rr-filename-[A-Za-z0-9-]+)?/g, `./file-D5WsIgJH.js?v=${filenameCacheBust}`) - .replace(/\.\/pdf-document-BplbXx-0\.js(?:\?v=rr-[^"'`]+)?/g, `./pdf-document-BplbXx-0.js?v=${pdfCacheBust}`); - if (next !== source) { - fs.writeFileSync(file, next); - touched.push(file); - } +const assetFiles = listJsFiles(assetsDir); +const filenameFiles = assetFiles + .filter((file) => { + const source = read(file); + return source.includes("URL.createObjectURL") && source.includes(".download"); + }) + .sort((a, b) => fs.statSync(a).size - fs.statSync(b).size); + +const patchedFilenameFiles = []; +for (const file of filenameFiles) { + if (patchFilenameBundle(file)) patchedFilenameFiles.push(file); +} +if (patchedFilenameFiles.length === 0) warn("no filename bundle patched"); + +let ssrFile = ""; +if (fs.existsSync(ssrDir)) { + ssrFile = fs.readdirSync(ssrDir) + .filter((name) => name.endsWith(".mjs")) + .map((name) => path.join(ssrDir, name)) + .find((file) => read(file).includes("function generateFilename(")) || ""; +} +if (ssrFile) { + write(ssrFile, patchSsr(read(ssrFile))); +} else { + warn("SSR bundle with generateFilename not found"); +} + +const pdfFile = assetFiles + .filter((file) => path.basename(file).startsWith("pdf-document-")) + .sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || ""; +if (pdfFile) { + write(pdfFile, patchPublicPdf(read(pdfFile))); +} else { + warn("public PDF bundle not found"); +} + +const filenameBases = patchedFilenameFiles.map((file) => path.basename(file)); +const pdfBase = pdfFile ? path.basename(pdfFile) : ""; +const touchedImporters = []; +for (const file of assetFiles) { + let source = read(file); + let next = source; + for (const base of filenameBases) { + next = next.replace(new RegExp(`\\./${escapeRegex(base)}(?:\\?v=rr-filename-[A-Za-z0-9-]+)?`, "g"), `./${base}?v=${filenameCacheBust}`); + } + if (pdfBase) { + next = next.replace(new RegExp(`\\./${escapeRegex(pdfBase)}(?:\\?v=rr-[^"'\\\`]+)?`, "g"), `./${pdfBase}?v=${pdfCacheBust}`); + } + if (next !== source) { + write(file, next); + touchedImporters.push(file); } - return touched; } -patchFilenameBundle(); -fs.writeFileSync(ssrFile, patchSsr(fs.readFileSync(ssrFile, "utf8"))); -fs.writeFileSync(publicPdfFile, patchPublicPdf(fs.readFileSync(publicPdfFile, "utf8"))); -const importers = patchImporters(); - -let serverIndex = fs.readFileSync(serverIndexFile, "utf8"); -serverIndex = patchStaticManifestEntry(serverIndex, "/assets/file-D5WsIgJH.js", publicFilenameFile); -serverIndex = patchStaticManifestEntry(serverIndex, "/assets/pdf-document-BplbXx-0.js", publicPdfFile); -for (const file of importers) { - serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${file.split("/").pop()}`, file); +if (fs.existsSync(serverIndexFile)) { + let serverIndex = read(serverIndexFile); + for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters].filter(Boolean)) { + serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file); + } + write(serverIndexFile, serverIndex); +} + +for (const file of [...patchedFilenameFiles, pdfFile, ssrFile, serverIndexFile].filter(Boolean)) { + try { + new Function(read(file)); + } catch { + // ESM/import bundles are validated by Node at application startup; keep this + // best-effort so a harmless syntax-check limitation never blocks boot. + } } -fs.writeFileSync(serverIndexFile, serverIndex); NODE - -node --check "$SSR_FILE" >/dev/null -node --check "$PUBLIC_FILENAME_FILE" >/dev/null -node --check "$PUBLIC_PDF_FILE" >/dev/null -node --check "$SERVER_INDEX_FILE" >/dev/null