diff --git a/dist/SHA256SUMS-20260520 b/dist/SHA256SUMS-20260520 new file mode 100644 index 0000000..21435ca --- /dev/null +++ b/dist/SHA256SUMS-20260520 @@ -0,0 +1,3 @@ +fa3b2b64a9afd7af60f57cfda8431af4e171cc1cdba4a6a2b89d50000a574f54 reactive-resume-clean-install-20260520.zip +4e95c039777ae2af6a216528ebd97911f59aa8a80c7816acde7ec8424eb6e59d reactive-resume-personal-direct-20260520.zip +708209d2ea066633ebd099dfe6554e41681be114f124e2656fc1233496aad534 reactive-resume-personal-qnap-nas-20260520.zip diff --git a/dist/reactive-resume-clean-install-20260520.zip b/dist/reactive-resume-clean-install-20260520.zip new file mode 100644 index 0000000..34d4055 Binary files /dev/null and b/dist/reactive-resume-clean-install-20260520.zip differ diff --git a/dist/reactive-resume-personal-direct-20260520.zip b/dist/reactive-resume-personal-direct-20260520.zip new file mode 100644 index 0000000..b76cbdf Binary files /dev/null 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 new file mode 100644 index 0000000..775aaaf Binary files /dev/null and b/dist/reactive-resume-personal-qnap-nas-20260520.zip differ diff --git a/packages/reactive-resume-personal-direct/README.md b/packages/reactive-resume-personal-direct/README.md index 9eeb554..cf38696 100644 --- a/packages/reactive-resume-personal-direct/README.md +++ b/packages/reactive-resume-personal-direct/README.md @@ -33,4 +33,4 @@ Compose 会创建独立项目名 `reactive-resume-personal`,默认使用 Docke - `reactive-resume-personal_postgres_data` - `reactive-resume-personal_reactive_resume_data` -`seed/` 目录会在首次启动时导入当前用户、公开简历和上传图片。后续如需迁移数据,请备份这些 volumes。 +`seed/` 目录会在首次启动时导入当前用户、公开简历和上传图片。`patches/` 目录会在应用启动时自动应用当前个人版需要的 PDF 渲染、Glalie 模板排版和“按简历标题下载 PDF”补丁。后续如需迁移数据,请备份这些 volumes。 diff --git a/packages/reactive-resume-personal-direct/compose.yml b/packages/reactive-resume-personal-direct/compose.yml index 663ef33..1db90dd 100644 --- a/packages/reactive-resume-personal-direct/compose.yml +++ b/packages/reactive-resume-personal-direct/compose.yml @@ -21,12 +21,18 @@ services: reactive-resume: image: amruthpillai/reactive-resume:latest restart: unless-stopped + entrypoint: ["/bin/sh", "-c"] + command: + - | + sh /opt/reactive-resume-patches/reactive-resume-runtime-patch.sh + exec node .output/server/index.mjs env_file: - .env ports: - "${LOCAL_BIND_IP}:${LOCAL_APP_PORT}:3000" volumes: - reactive_resume_data:/app/data + - ./patches/reactive-resume-runtime-patch.sh:/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh:ro networks: - resume_net depends_on: 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 new file mode 100644 index 0000000..0778300 --- /dev/null +++ b/packages/reactive-resume-personal-direct/patches/reactive-resume-runtime-patch.sh @@ -0,0 +1,160 @@ +#!/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" + +node - <<'NODE' +const fs = require("fs"); +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 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 makeEtag(buffer) { + const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, ""); + return `"${buffer.length.toString(16)}-${digest}"`; +} + +function patchStaticManifestEntry(source, urlPath, filePath) { + if (!fs.existsSync(filePath)) return source; + const buffer = fs.readFileSync(filePath); + const startMarker = `"${urlPath}": {`; + const start = source.indexOf(startMarker); + if (start === -1) return source; + const commaEnd = source.indexOf("\n\t},", start); + const objectEnd = source.indexOf("\n\t}", start); + const end = commaEnd === -1 ? objectEnd : Math.min(commaEnd, objectEnd); + if (end === -1) return source; + + let entry = source.slice(start, end); + entry = entry + .replace(/"etag": "(?:\\.|[^"\\])*"/, `"etag": ${JSON.stringify(makeEtag(buffer))}`) + .replace(/"mtime": "(?:\\.|[^"\\])*"/, `"mtime": ${JSON.stringify(new Date().toISOString())}`) + .replace(/"size": \d+/, `"size": ${buffer.length}`); + return source.slice(0, start) + entry + source.slice(end); +} + +function replaceOnce(source, from, to) { + return source.includes(to) ? source : source.replace(from, to); +} + +function replaceRegexOnce(source, regex, to) { + return source.includes(to) ? source : source.replace(regex, to); +} + +function patchFilenameBundle() { + let source = fs.readFileSync(publicFilenameFile, "utf8"); + 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); + } +} + +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); + } + + source = source + .replace(/const sideMargin = bodyLineHeight \* \.(?:2|08);/, "const sideMargin = bodyLineHeight * .08;") + .replace(/metrics\.gapY\(3\.5\)/g, "metrics.gapY(3.0)") + .replace(/metrics\.gapY\(2\.6\)/g, "metrics.gapY(3.0)") + .replace(/metrics\.gapY\(2\.2\)/g, "metrics.gapY(3.0)") + .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\) \},/, + "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},\n\t\t\t\tsectionItems: { paddingTop: metrics.gapY(.55) },\n\t\t\t\titem: { rowGap: metrics.gapY(.2) },", + ); + source = replaceRegexOnce( + source, + /sidebarColumn: \{\s*zIndex: 1,\s*backgroundColor: primaryTint,\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical,\s*)?rowGap: (?:metrics\.sectionGap|metrics\.gapY\([^)]+\))\s*\},/, + "sidebarColumn: {\n\t\t\t\t\tzIndex: 1,\n\t\t\t\t\tbackgroundColor: primaryTint,\n\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,\n\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,\n\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical,\n\t\t\t\t\trowGap: metrics.gapY(3.0)\n\t\t\t\t},", + ); + source = replaceRegexOnce( + source, + /mainContent: \{\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical\s*)?\},/, + "mainContent: {\n\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,\n\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,\n\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical\n\t\t\t\t},", + ); + 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); + 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)`) + .replace(/[oc]\.gapY\(2\.6\)/g, (m) => `${m[0]}.gapY(3.0)`) + .replace(/[oc]\.gapY\(2\.2\)/g, (m) => `${m[0]}.gapY(3.0)`) + .replace(/style:\$\(a\.sidebarContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.sidebarContent,{rowGap:o.gapY(3.0)})") + .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); + } + } + 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); +} +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 b6241ed..c10deae 100644 --- a/packages/reactive-resume-personal-direct/seed/resume-data.json +++ b/packages/reactive-resume-personal-direct/seed/resume-data.json @@ -31,7 +31,7 @@ ] }, "picture": { - "url": "https://isiseg.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", + "url": "https://me.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", "size": 62, "hidden": false, "rotation": 0, @@ -591,7 +591,7 @@ "period": "2025 - 2028", "company": "国家自然科学基金面上项目(82471190)", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, @@ -606,7 +606,7 @@ "period": "2023 - 2026", "company": "西安交通大学第一附属医院 医智慧研究院项目", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, diff --git a/packages/reactive-resume-personal-direct/seed/seed.sql b/packages/reactive-resume-personal-direct/seed/seed.sql index 4a46dc0..83e503c 100644 --- a/packages/reactive-resume-personal-direct/seed/seed.sql +++ b/packages/reactive-resume-personal-direct/seed/seed.sql @@ -93,7 +93,7 @@ INSERT INTO resume ( ] }, "picture": { - "url": "https://isiseg.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", + "url": "https://me.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", "size": 62, "hidden": false, "rotation": 0, @@ -653,7 +653,7 @@ INSERT INTO resume ( "period": "2025 - 2028", "company": "国家自然科学基金面上项目(82471190)", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, @@ -668,7 +668,7 @@ INSERT INTO resume ( "period": "2023 - 2026", "company": "西安交通大学第一附属医院 医智慧研究院项目", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, diff --git a/packages/reactive-resume-personal-qnap-nas/README.md b/packages/reactive-resume-personal-qnap-nas/README.md index 6febe1a..853f8c4 100644 --- a/packages/reactive-resume-personal-qnap-nas/README.md +++ b/packages/reactive-resume-personal-qnap-nas/README.md @@ -35,3 +35,4 @@ - PostgreSQL:`/share/Container/Reactive_Resume_Personal/data/postgres` - 上传与本地存储:`/share/Container/Reactive_Resume_Personal/data/uploads` - 初始化种子:`/share/Container/Reactive_Resume_Personal/seed` +- 运行时补丁:`/share/Container/Reactive_Resume_Personal/patches` diff --git a/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml b/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml index db6bd2d..282a3b3 100644 --- a/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml +++ b/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml @@ -31,6 +31,11 @@ services: reactive_resume_app: image: amruthpillai/reactive-resume:latest restart: unless-stopped + entrypoint: ["/bin/sh", "-c"] + command: + - | + sh /opt/reactive-resume-patches/reactive-resume-runtime-patch.sh + exec node .output/server/index.mjs depends_on: reactive_resume_permissions: condition: service_completed_successfully @@ -40,6 +45,7 @@ services: - "3004:3000" volumes: - /share/Container/Reactive_Resume_Personal/data/uploads:/app/data + - /share/Container/Reactive_Resume_Personal/patches/reactive-resume-runtime-patch.sh:/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh:ro environment: TZ: Asia/Shanghai APP_URL: https://isiseg.huijutec.cn 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 new file mode 100644 index 0000000..0778300 --- /dev/null +++ b/packages/reactive-resume-personal-qnap-nas/patches/reactive-resume-runtime-patch.sh @@ -0,0 +1,160 @@ +#!/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" + +node - <<'NODE' +const fs = require("fs"); +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 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 makeEtag(buffer) { + const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, ""); + return `"${buffer.length.toString(16)}-${digest}"`; +} + +function patchStaticManifestEntry(source, urlPath, filePath) { + if (!fs.existsSync(filePath)) return source; + const buffer = fs.readFileSync(filePath); + const startMarker = `"${urlPath}": {`; + const start = source.indexOf(startMarker); + if (start === -1) return source; + const commaEnd = source.indexOf("\n\t},", start); + const objectEnd = source.indexOf("\n\t}", start); + const end = commaEnd === -1 ? objectEnd : Math.min(commaEnd, objectEnd); + if (end === -1) return source; + + let entry = source.slice(start, end); + entry = entry + .replace(/"etag": "(?:\\.|[^"\\])*"/, `"etag": ${JSON.stringify(makeEtag(buffer))}`) + .replace(/"mtime": "(?:\\.|[^"\\])*"/, `"mtime": ${JSON.stringify(new Date().toISOString())}`) + .replace(/"size": \d+/, `"size": ${buffer.length}`); + return source.slice(0, start) + entry + source.slice(end); +} + +function replaceOnce(source, from, to) { + return source.includes(to) ? source : source.replace(from, to); +} + +function replaceRegexOnce(source, regex, to) { + return source.includes(to) ? source : source.replace(regex, to); +} + +function patchFilenameBundle() { + let source = fs.readFileSync(publicFilenameFile, "utf8"); + 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); + } +} + +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); + } + + source = source + .replace(/const sideMargin = bodyLineHeight \* \.(?:2|08);/, "const sideMargin = bodyLineHeight * .08;") + .replace(/metrics\.gapY\(3\.5\)/g, "metrics.gapY(3.0)") + .replace(/metrics\.gapY\(2\.6\)/g, "metrics.gapY(3.0)") + .replace(/metrics\.gapY\(2\.2\)/g, "metrics.gapY(3.0)") + .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\) \},/, + "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},\n\t\t\t\tsectionItems: { paddingTop: metrics.gapY(.55) },\n\t\t\t\titem: { rowGap: metrics.gapY(.2) },", + ); + source = replaceRegexOnce( + source, + /sidebarColumn: \{\s*zIndex: 1,\s*backgroundColor: primaryTint,\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical,\s*)?rowGap: (?:metrics\.sectionGap|metrics\.gapY\([^)]+\))\s*\},/, + "sidebarColumn: {\n\t\t\t\t\tzIndex: 1,\n\t\t\t\t\tbackgroundColor: primaryTint,\n\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,\n\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,\n\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical,\n\t\t\t\t\trowGap: metrics.gapY(3.0)\n\t\t\t\t},", + ); + source = replaceRegexOnce( + source, + /mainContent: \{\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical\s*)?\},/, + "mainContent: {\n\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,\n\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,\n\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical\n\t\t\t\t},", + ); + 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); + 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)`) + .replace(/[oc]\.gapY\(2\.6\)/g, (m) => `${m[0]}.gapY(3.0)`) + .replace(/[oc]\.gapY\(2\.2\)/g, (m) => `${m[0]}.gapY(3.0)`) + .replace(/style:\$\(a\.sidebarContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.sidebarContent,{rowGap:o.gapY(3.0)})") + .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); + } + } + 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); +} +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-qnap-nas/seed/resume-data.json b/packages/reactive-resume-personal-qnap-nas/seed/resume-data.json index b6241ed..c10deae 100644 --- a/packages/reactive-resume-personal-qnap-nas/seed/resume-data.json +++ b/packages/reactive-resume-personal-qnap-nas/seed/resume-data.json @@ -31,7 +31,7 @@ ] }, "picture": { - "url": "https://isiseg.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", + "url": "https://me.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", "size": 62, "hidden": false, "rotation": 0, @@ -591,7 +591,7 @@ "period": "2025 - 2028", "company": "国家自然科学基金面上项目(82471190)", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, @@ -606,7 +606,7 @@ "period": "2023 - 2026", "company": "西安交通大学第一附属医院 医智慧研究院项目", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, diff --git a/packages/reactive-resume-personal-qnap-nas/seed/seed.sql b/packages/reactive-resume-personal-qnap-nas/seed/seed.sql index 4a46dc0..83e503c 100644 --- a/packages/reactive-resume-personal-qnap-nas/seed/seed.sql +++ b/packages/reactive-resume-personal-qnap-nas/seed/seed.sql @@ -93,7 +93,7 @@ INSERT INTO resume ( ] }, "picture": { - "url": "https://isiseg.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", + "url": "https://me.huijutec.cn/uploads/019e2a40-5e4a-7303-a19a-c73de7a9b9b8/pictures/2025-profile.jpg", "size": 62, "hidden": false, "rotation": 0, @@ -653,7 +653,7 @@ INSERT INTO resume ( "period": "2025 - 2028", "company": "国家自然科学基金面上项目(82471190)", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true }, @@ -668,7 +668,7 @@ INSERT INTO resume ( "period": "2023 - 2026", "company": "西安交通大学第一附属医院 医智慧研究院项目", "website": { - "url": "https://isiseg.huijutec.cn/audience/resume", + "url": "https://me.huijutec.cn/audience/resume", "label": "", "inlineLink": true },