Compare commits
9 Commits
v2026.05.1
...
v2026.05.2
| Author | SHA1 | Date | |
|---|---|---|---|
| fb149b4e00 | |||
| c7ed88b2d8 | |||
| a3569a52e7 | |||
| d77954ba94 | |||
| d2edebecda | |||
| 999a1314a8 | |||
| 602f00262b | |||
| 475bab8bf6 | |||
| beb14bf834 |
3
dist/SHA256SUMS-20260520
vendored
Normal file
3
dist/SHA256SUMS-20260520
vendored
Normal file
@@ -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
|
||||
BIN
dist/reactive-resume-clean-install-20260520.zip
vendored
Normal file
BIN
dist/reactive-resume-clean-install-20260520.zip
vendored
Normal file
Binary file not shown.
BIN
dist/reactive-resume-personal-direct-20260520.zip
vendored
Normal file
BIN
dist/reactive-resume-personal-direct-20260520.zip
vendored
Normal file
Binary file not shown.
BIN
dist/reactive-resume-personal-qnap-nas-20260520.zip
vendored
Normal file
BIN
dist/reactive-resume-personal-qnap-nas-20260520.zip
vendored
Normal file
Binary file not shown.
@@ -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。
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -8,27 +8,114 @@ set -eu
|
||||
|
||||
PUBLIC_FILE="/app/apps/web/.output/public/assets/file-D5WsIgJH.js"
|
||||
SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"
|
||||
SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs"
|
||||
CACHE_BUST="rr-filename-title-20260519"
|
||||
|
||||
cp "$PUBLIC_FILE" "$PUBLIC_FILE.bak-filename" 2>/dev/null || true
|
||||
cp "$SSR_FILE" "$SSR_FILE.bak-filename" 2>/dev/null || true
|
||||
cp "$SERVER_INDEX_FILE" "$SERVER_INDEX_FILE.bak-filename" 2>/dev/null || true
|
||||
|
||||
node - <<'NODE'
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const publicFile = '/app/apps/web/.output/public/assets/file-D5WsIgJH.js';
|
||||
const ssrFile = '/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs';
|
||||
const serverIndexFile = '/app/apps/web/.output/server/index.mjs';
|
||||
const cacheBust = 'rr-filename-title-20260519';
|
||||
|
||||
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) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const startMarker = `"${urlPath}": {`;
|
||||
const start = source.indexOf(startMarker);
|
||||
if (start === -1) throw new Error(`Static manifest entry not found for ${urlPath}`);
|
||||
|
||||
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) throw new Error(`Static manifest entry end not found for ${urlPath}`);
|
||||
|
||||
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 patchPublicImporters() {
|
||||
const assetsDir = '/app/apps/web/.output/public/assets';
|
||||
const files = fs
|
||||
.readdirSync(assetsDir)
|
||||
.filter((name) => name.endsWith('.js'))
|
||||
.map((name) => `${assetsDir}/${name}`)
|
||||
.filter((file) => fs.readFileSync(file, 'utf8').includes('file-D5WsIgJH.js'));
|
||||
|
||||
for (const file of files) {
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
source = source.replace(/\.\/file-D5WsIgJH\.js(?:\?v=rr-filename-[A-Za-z0-9-]+)?/g, `./file-D5WsIgJH.js?v=${cacheBust}`);
|
||||
fs.writeFileSync(file, source);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const publicFilenameFunction = '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}`:""}`}';
|
||||
|
||||
let publicJs = fs.readFileSync(publicFile, 'utf8');
|
||||
publicJs = publicJs.replace(
|
||||
/function t\(t,n\)\{return`\$\{e\(t\)\}\$\{n\?`\.\$\{n\}`:""\}`\}/,
|
||||
'function t(e,t){let n=(e||"resume").toString().trim()||"resume";return`${n}${t?`.${t}`:""}`}'
|
||||
);
|
||||
const publicReplacement = publicFilenameFunction;
|
||||
if (!publicJs.includes(publicReplacement)) {
|
||||
const start = publicJs.indexOf('function t(');
|
||||
const end = publicJs.indexOf('function n(', start);
|
||||
if (start === -1 || end === -1) throw new Error('Public generateFilename marker not found');
|
||||
publicJs = publicJs.slice(0, start) + publicReplacement + publicJs.slice(end);
|
||||
}
|
||||
fs.writeFileSync(publicFile, publicJs);
|
||||
|
||||
const ssrFile = '/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs';
|
||||
let ssr = fs.readFileSync(ssrFile, 'utf8');
|
||||
ssr = ssr.replace(
|
||||
/function generateFilename\(prefix, extension\) \{\n\s*return `\$\{slugify\(prefix\)\}\$\{extension \? `\.\$\{extension\}` : ""\}`;\n\}/,
|
||||
'function generateFilename(prefix, extension) {\n\tconst filename = (prefix || "resume").toString().trim() || "resume";\n\treturn `${filename}${extension ? `.${extension}` : ""}`;\n}'
|
||||
/\n\t\tname: "",\n\t\tdata: \{/,
|
||||
'\n\t\tname: resume.name,\n\t\tdata: {',
|
||||
);
|
||||
const ssrReplacement = `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 (!ssr.includes(ssrReplacement)) {
|
||||
const start = ssr.indexOf('function generateFilename(');
|
||||
const end = ssr.indexOf('\nfunction downloadWithAnchor(', start);
|
||||
if (start === -1 || end === -1) throw new Error('SSR generateFilename marker not found');
|
||||
ssr = ssr.slice(0, start) + ssrReplacement + ssr.slice(end);
|
||||
}
|
||||
fs.writeFileSync(ssrFile, ssr);
|
||||
|
||||
const importers = patchPublicImporters();
|
||||
let serverIndex = fs.readFileSync(serverIndexFile, 'utf8');
|
||||
serverIndex = patchStaticManifestEntry(serverIndex, '/assets/file-D5WsIgJH.js', publicFile);
|
||||
for (const file of importers) {
|
||||
const urlPath = `/assets/${file.split('/').pop()}`;
|
||||
serverIndex = patchStaticManifestEntry(serverIndex, urlPath, file);
|
||||
}
|
||||
fs.writeFileSync(serverIndexFile, serverIndex);
|
||||
NODE
|
||||
|
||||
node --check "$PUBLIC_FILE" >/dev/null
|
||||
node --check "$SSR_FILE" >/dev/null
|
||||
node --check "$SERVER_INDEX_FILE" >/dev/null
|
||||
SH
|
||||
|
||||
# Nitro loads the static asset manifest into memory at process startup. Restart
|
||||
# after patching so updated content-length/etag values are used immediately.
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER")"
|
||||
[ "$health" = "healthy" ] && exit 0
|
||||
sleep 2
|
||||
done
|
||||
|
||||
docker logs --tail 80 "$CONTAINER" >&2
|
||||
exit 1
|
||||
|
||||
@@ -9,7 +9,7 @@ set -eu
|
||||
SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"
|
||||
PUBLIC_FILE="/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js"
|
||||
SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs"
|
||||
CACHE_BUST="rr-glalie-layout-20260518"
|
||||
CACHE_BUST="rr-glalie-layout-20260519"
|
||||
|
||||
test -f "$SSR_FILE.bak-glalie-layout" || cp "$SSR_FILE" "$SSR_FILE.bak-glalie-layout" 2>/dev/null || true
|
||||
test -f "$PUBLIC_FILE.bak-glalie-layout" || cp "$PUBLIC_FILE" "$PUBLIC_FILE.bak-glalie-layout" 2>/dev/null || true
|
||||
@@ -22,7 +22,8 @@ const crypto = require("crypto");
|
||||
const ssrFile = "/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs";
|
||||
const publicFile = "/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js";
|
||||
const serverIndexFile = "/app/apps/web/.output/server/index.mjs";
|
||||
const cacheBust = "rr-glalie-layout-20260518";
|
||||
const cacheBust = "rr-glalie-layout-20260519";
|
||||
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
|
||||
|
||||
function replaceOnce(source, from, to, label) {
|
||||
if (source.includes(to)) return source;
|
||||
@@ -103,7 +104,20 @@ function patchSsr(source) {
|
||||
.replace(/metrics\.gapY\(2\.2\)/g, "metrics.gapY(3.0)");
|
||||
}
|
||||
|
||||
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("public PDF bundle import prelude not found");
|
||||
}
|
||||
|
||||
return source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt);
|
||||
}
|
||||
|
||||
function patchPublic(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}}")
|
||||
@@ -158,7 +172,7 @@ function patchImporters() {
|
||||
|
||||
for (const file of files) {
|
||||
let source = fs.readFileSync(file, "utf8");
|
||||
source = source.replace(/\.\/pdf-document-BplbXx-0\.js(?:\?v=rr-glalie-layout-20260518)?/g, `./pdf-document-BplbXx-0.js?v=${cacheBust}`);
|
||||
source = source.replace(/\.\/pdf-document-BplbXx-0\.js(?:\?v=rr-[^"'`]+)?/g, `./pdf-document-BplbXx-0.js?v=${cacheBust}`);
|
||||
fs.writeFileSync(file, source);
|
||||
}
|
||||
|
||||
@@ -207,3 +221,16 @@ node --check "$SSR_FILE" >/dev/null
|
||||
node --check "$PUBLIC_FILE" >/dev/null
|
||||
node --check "$SERVER_INDEX_FILE" >/dev/null
|
||||
SH
|
||||
|
||||
# Nitro loads the static asset manifest into memory at process startup. Restart
|
||||
# after patching so updated content-length/etag values are used immediately.
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER")"
|
||||
[ "$health" = "healthy" ] && exit 0
|
||||
sleep 2
|
||||
done
|
||||
|
||||
docker logs --tail 80 "$CONTAINER" >&2
|
||||
exit 1
|
||||
|
||||
45
工程分析/实现方案-2026-05-19-23-10-56.md
Normal file
45
工程分析/实现方案-2026-05-19-23-10-56.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 实现方案-2026-05-19-23-10-56
|
||||
|
||||
## 实现方案文档路径
|
||||
|
||||
`工程分析/实现方案-2026-05-19-23-10-56.md`
|
||||
|
||||
## 修改目标
|
||||
|
||||
将 Reactive Resume PDF 下载文件名从 `ZhiboWang-Resume.pdf` 改为 `王志博-医工智能外科-简历.pdf`。
|
||||
|
||||
## 涉及路径
|
||||
|
||||
- `scripts/patch-reactive-resume-filename.sh`
|
||||
- `工程分析/工程整体分析.md`
|
||||
- `工程分析/需求分析-2026-05-19-23-10-56.md`
|
||||
- `工程分析/实现方案-2026-05-19-23-10-56.md`
|
||||
- `工程分析/测试方案-2026-05-19-23-10-56.md`
|
||||
- `工程分析/经验记录.md`
|
||||
|
||||
## 技术路线
|
||||
|
||||
1. 修改补丁脚本中的 shell 变量 `PDF_FILENAME`。
|
||||
2. 修改脚本中 Node 补丁逻辑的 `pdfFilename` 常量。
|
||||
3. 更新 `CACHE_BUST`,并让静态资源 import 替换逻辑兼容旧 cache bust。
|
||||
4. 用 `sh -n` 检查脚本语法。
|
||||
5. 执行 `scripts/patch-reactive-resume-filename.sh reactive-resume-reactive-resume-1`。
|
||||
6. 验证容器健康、文件内新文件名、HTTP 静态资源新文件名。
|
||||
7. 提交本次脚本和文档变更。
|
||||
|
||||
## 兼容性与回滚方案
|
||||
|
||||
- 回滚时可把 `PDF_FILENAME`、`pdfFilename` 和 `CACHE_BUST` 改回旧值,再重新执行脚本。
|
||||
- 若补丁失败,容器内已有 `.bak-filename` 备份可用于人工恢复。
|
||||
- 本次不改数据库、简历 JSON 和 Docker Compose 配置。
|
||||
|
||||
## 预计文件变更
|
||||
|
||||
- 更新:`scripts/patch-reactive-resume-filename.sh`
|
||||
- 新增/更新:`工程分析/` 本次文档与经验记录。
|
||||
|
||||
## 提交与部署策略
|
||||
|
||||
- 显式暂存本次相关文件,避免混入无关变更。
|
||||
- commit message 使用:`2026-05-19-23-10-56 修改简历PDF下载文件名`
|
||||
- 部署通过运行补丁脚本完成,脚本内部会重启 Reactive Resume 容器。
|
||||
44
工程分析/实现方案-2026-05-19-23-23-50.md
Normal file
44
工程分析/实现方案-2026-05-19-23-23-50.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 实现方案-2026-05-19-23-23-50
|
||||
|
||||
## 实现方案文档路径
|
||||
|
||||
`工程分析/实现方案-2026-05-19-23-23-50.md`
|
||||
|
||||
## 修改目标
|
||||
|
||||
回滚上一条错误指令,将 PDF 下载文件名恢复为 `ZhiboWang-Resume.pdf`。
|
||||
|
||||
## 涉及路径
|
||||
|
||||
- `scripts/patch-reactive-resume-filename.sh`
|
||||
- `工程分析/需求分析-2026-05-19-23-23-50.md`
|
||||
- `工程分析/实现方案-2026-05-19-23-23-50.md`
|
||||
- `工程分析/测试方案-2026-05-19-23-23-50.md`
|
||||
- `工程分析/经验记录.md`
|
||||
|
||||
## 技术路线
|
||||
|
||||
1. 将脚本中的 `PDF_FILENAME` 恢复为 `ZhiboWang-Resume.pdf`。
|
||||
2. 将 Node 补丁逻辑中的 `pdfFilename` 恢复为 `ZhiboWang-Resume.pdf`。
|
||||
3. 将 `CACHE_BUST` 与 `cacheBust` 恢复为 `rr-filename-20260519`。
|
||||
4. 执行 `sh -n` 做脚本语法检查。
|
||||
5. 重新执行补丁脚本,更新运行中容器并等待 healthy。
|
||||
6. 验证 HTTP 静态资源中包含旧文件名且不再包含中文文件名。
|
||||
7. 创建回滚 commit,并尝试推送 Gitea。
|
||||
|
||||
## 兼容性与回滚方案
|
||||
|
||||
- 本次不改数据库、简历 JSON、Compose 或上传文件。
|
||||
- 如用户后续给出新的正确文件名,可再次改脚本变量并执行补丁脚本。
|
||||
- 当前选择新增回滚提交,保留历史可追溯性。
|
||||
|
||||
## 预计文件变更
|
||||
|
||||
- 更新:`scripts/patch-reactive-resume-filename.sh`
|
||||
- 新增:本次 `需求分析`、`实现方案`、`测试方案`
|
||||
- 更新:`工程分析/经验记录.md`
|
||||
|
||||
## 提交与部署策略
|
||||
|
||||
- commit message 使用:`2026-05-19-23-23-50 回滚简历PDF下载文件名`
|
||||
- 部署通过执行 `scripts/patch-reactive-resume-filename.sh reactive-resume-reactive-resume-1` 完成。
|
||||
32
工程分析/工程整体分析.md
Normal file
32
工程分析/工程整体分析.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 工程整体分析
|
||||
|
||||
更新时间:2026-05-19-23-10-56
|
||||
|
||||
## 项目定位
|
||||
|
||||
本项目用于部署和维护个人 Reactive Resume 服务,服务公网访问地址为 `https://me.huijutec.cn`,公开简历入口为 `/audience/resume`。
|
||||
|
||||
## 当前结构
|
||||
|
||||
- `README.md`:部署、访问、安装包说明。
|
||||
- `compose.yml`:本机 Docker Compose 部署,包含 Postgres、Reactive Resume、frpc。
|
||||
- `compose-Nas.yml`:QNAP/NAS 部署版本。
|
||||
- `scripts/patch-reactive-resume-filename.sh`:对运行中 Reactive Resume 容器打补丁,控制 PDF 下载文件名,并修正静态资源 cache bust 与 Nitro manifest。
|
||||
- `scripts/patch-reactive-resume-glalie-layout.sh`:简历布局补丁。
|
||||
- `scripts/patch-reactive-resume-service-worker-cache.sh`:Service Worker 缓存补丁。
|
||||
- `packages/`:安装包目录。
|
||||
- `dist/`:发布归档。
|
||||
- `生成简历/`:简历源数据、备份、生成 PDF 和展示素材。
|
||||
|
||||
## 当前运行环境
|
||||
|
||||
- Docker 容器 `reactive-resume-reactive-resume-1` 运行 Reactive Resume,映射 `127.0.0.1:3003 -> 3000`。
|
||||
- 容器 `reactive-resume-frpc-1` 负责公网映射。
|
||||
- `origin` 远端为 Gitea HTTP 地址:`http://192.168.31.5:5002/admin/Reactive_Resume.git`。
|
||||
|
||||
## 维护注意事项
|
||||
|
||||
- 下载文件名由容器内打补丁脚本控制,不是普通业务源码直接控制。
|
||||
- 修改 PDF 文件名后需要更新 cache bust,防止浏览器继续使用旧静态资源。
|
||||
- 补丁脚本会修改容器内 `/app/apps/web/.output` 文件并重启容器,执行后必须等待健康检查通过。
|
||||
- Gitea HTTP 远端可能缺少交互式凭据,推送失败时应保留本地 commit,不要在命令行写入账号密码。
|
||||
44
工程分析/测试方案-2026-05-19-23-10-56.md
Normal file
44
工程分析/测试方案-2026-05-19-23-10-56.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 测试方案-2026-05-19-23-10-56
|
||||
|
||||
## 测试方案文档路径
|
||||
|
||||
`工程分析/测试方案-2026-05-19-23-10-56.md`
|
||||
|
||||
## 静态检查
|
||||
|
||||
- 执行 `sh -n scripts/patch-reactive-resume-filename.sh`。
|
||||
|
||||
## 部署验证
|
||||
|
||||
- 执行 `scripts/patch-reactive-resume-filename.sh reactive-resume-reactive-resume-1`。
|
||||
- 确认容器 `reactive-resume-reactive-resume-1` 恢复 healthy。
|
||||
|
||||
## 业务验证
|
||||
|
||||
- 在容器内搜索 `王志博-医工智能外科-简历.pdf`。
|
||||
- 通过 `curl http://127.0.0.1:3003/assets/file-D5WsIgJH.js` 验证静态资源已包含新文件名。
|
||||
- 通过 `curl http://127.0.0.1:3003/api/health` 验证服务健康。
|
||||
|
||||
## Git/Gitea 备份验证
|
||||
|
||||
- 本地创建包含时间戳和简要描述的 commit。
|
||||
- 尝试 `git push origin main`。
|
||||
- 若 Gitea HTTP 凭据不可用,记录失败原因并保留本地 commit。
|
||||
|
||||
## 风险与回归关注点
|
||||
|
||||
- 浏览器缓存可能保留旧静态资源,因此本次必须更新 cache bust。
|
||||
- Reactive Resume 镜像升级后构建文件哈希可能变化,脚本内固定文件路径需要重新定位。
|
||||
|
||||
## 实际执行结果
|
||||
|
||||
- `sh -n scripts/patch-reactive-resume-filename.sh`:通过。
|
||||
- `./scripts/patch-reactive-resume-filename.sh reactive-resume-reactive-resume-1`:通过。
|
||||
- 容器健康状态:`reactive-resume-reactive-resume-1` 为 `healthy`。
|
||||
- 容器内验证:`/app/apps/web/.output/public/assets/file-D5WsIgJH.js` 和 `/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs` 均包含 `王志博-医工智能外科-简历.pdf`。
|
||||
- HTTP 静态资源验证:`curl http://127.0.0.1:3003/assets/file-D5WsIgJH.js` 可检索到 `王志博-医工智能外科-简历.pdf`。
|
||||
- Cache bust 验证:业务页面 import 已指向 `file-D5WsIgJH.js?v=rr-filename-20260519-cn`。
|
||||
- 服务健康验证:`curl http://127.0.0.1:3003/api/health` 返回 `status: healthy`。
|
||||
- 公开简历页验证:`curl -I http://127.0.0.1:3003/audience/resume` 返回 `HTTP/1.1 200 OK`。
|
||||
- Git 本地备份 commit:已创建,提交信息为 `2026-05-19-23-10-56 修改简历PDF下载文件名`。
|
||||
- Gitea 远端推送:执行 `git push origin main` 时失败,原因是 HTTP 远端 `http://192.168.31.5:5002` 无法读取用户名;未在命令行写入账号密码。
|
||||
48
工程分析/测试方案-2026-05-19-23-23-50.md
Normal file
48
工程分析/测试方案-2026-05-19-23-23-50.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 测试方案-2026-05-19-23-23-50
|
||||
|
||||
## 测试方案文档路径
|
||||
|
||||
`工程分析/测试方案-2026-05-19-23-23-50.md`
|
||||
|
||||
## 静态检查
|
||||
|
||||
- 执行 `sh -n scripts/patch-reactive-resume-filename.sh`。
|
||||
- 搜索脚本确认包含 `ZhiboWang-Resume.pdf`。
|
||||
|
||||
## 部署验证
|
||||
|
||||
- 执行 `./scripts/patch-reactive-resume-filename.sh reactive-resume-reactive-resume-1`。
|
||||
- 确认容器 `reactive-resume-reactive-resume-1` 为 `healthy`。
|
||||
|
||||
## 业务验证
|
||||
|
||||
- 验证容器内 public JS 与 SSR 文件包含 `ZhiboWang-Resume.pdf`。
|
||||
- 验证 HTTP 静态资源 `/assets/file-D5WsIgJH.js` 包含 `ZhiboWang-Resume.pdf`。
|
||||
- 验证 HTTP 静态资源不再包含 `王志博-医工智能外科简历.pdf` 或 `王志博-医工智能外科-简历.pdf`。
|
||||
- 验证 `http://127.0.0.1:3003/api/health` 返回 healthy。
|
||||
- 验证 `http://127.0.0.1:3003/audience/resume` 返回 200。
|
||||
|
||||
## Git/Gitea 备份验证
|
||||
|
||||
- 创建本地回滚 commit。
|
||||
- 尝试 `git push origin main`。
|
||||
- 若 HTTP 凭据不可用,记录失败原因。
|
||||
|
||||
## 风险与回归关注点
|
||||
|
||||
- 浏览器缓存可能短暂保留旧中文 cache bust;脚本恢复 `rr-filename-20260519` 后应重新更新 importer 与 manifest。
|
||||
|
||||
## 实际执行结果
|
||||
|
||||
- `sh -n scripts/patch-reactive-resume-filename.sh`:通过。
|
||||
- 脚本搜索:`PDF_FILENAME` 与 `pdfFilename` 均恢复为 `ZhiboWang-Resume.pdf`,`CACHE_BUST` 与 `cacheBust` 均恢复为 `rr-filename-20260519`。
|
||||
- `./scripts/patch-reactive-resume-filename.sh reactive-resume-reactive-resume-1`:通过。
|
||||
- 容器状态:`reactive-resume-reactive-resume-1` 为 `healthy`。
|
||||
- 容器内验证:public JS 与 SSR 文件均包含 `ZhiboWang-Resume.pdf`。
|
||||
- 容器内中文残留验证:public JS 与 SSR 文件未检出 `王志博-医工智能外科`。
|
||||
- HTTP 静态资源验证:`/assets/file-D5WsIgJH.js` 返回内容包含 `ZhiboWang-Resume.pdf`。
|
||||
- Cache bust 验证:业务页面 import 已恢复为 `file-D5WsIgJH.js?v=rr-filename-20260519`。
|
||||
- 服务健康验证:`curl http://127.0.0.1:3003/api/health` 返回 `status: healthy`。
|
||||
- 公开简历页验证:`curl -I http://127.0.0.1:3003/audience/resume` 返回 `HTTP/1.1 200 OK`。
|
||||
- Git 本地备份 commit:已创建,提交信息为 `2026-05-19-23-23-50 回滚简历PDF下载文件名`。
|
||||
- Gitea 远端推送:执行 `git push origin main` 时失败,原因是 HTTP 远端 `http://192.168.31.5:5002` 无法读取用户名;未在命令行写入账号密码。
|
||||
57
工程分析/经验记录.md
Normal file
57
工程分析/经验记录.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 经验记录
|
||||
|
||||
本文件用于记录个人简历构建项目修改过程中的关键问题与解决方案。每条经验使用四段式结构。
|
||||
|
||||
## 2026-05-19-23-10-56 Reactive Resume PDF 下载文件名修改
|
||||
|
||||
A. 具体问题
|
||||
|
||||
用户希望浏览器下载 PDF 时的文件名从 `ZhiboWang-Resume.pdf` 改为 `王志博-医工智能外科-简历.pdf`,当前 DICOM 项目中没有该文件名,需要定位真实项目。
|
||||
|
||||
B. 产生问题原因
|
||||
|
||||
该下载文件名不是 DICOM 项目功能,而是个人简历构建项目中的 Reactive Resume 容器补丁脚本硬编码。Reactive Resume 的前端 public JS 和 SSR 文件都需要同步修改,且浏览器可能缓存旧静态资源。
|
||||
|
||||
C. 解决问题方案
|
||||
|
||||
在 `/home/wkmgc/Desktop/个人材料编写/个人简历构建` 中定位到 `scripts/patch-reactive-resume-filename.sh`,将 shell 与 Node 补丁逻辑中的 PDF 文件名统一改为中文文件名;同时把 cache bust 更新为 `rr-filename-20260519-cn`,并让 import 替换逻辑兼容旧 cache bust。执行脚本后容器重启并恢复 healthy。
|
||||
|
||||
D. 后续如何避免问题
|
||||
|
||||
涉及线上下载文件名时,应先全局搜索目标文件名并确认真实项目;修改容器补丁脚本后必须立即运行脚本、验证容器内 public/SSR 文件、验证 HTTP 静态资源和服务健康,避免只改脚本未生效。
|
||||
|
||||
## 2026-05-19-23-10-56 Reactive Resume Gitea 推送凭据缺失
|
||||
|
||||
A. 具体问题
|
||||
|
||||
本次本地备份 commit 已创建,但执行 `git push origin main` 时失败,Git 提示无法读取 `http://192.168.31.5:5002` 的用户名。
|
||||
|
||||
B. 产生问题原因
|
||||
|
||||
当前 `origin` 使用 HTTP Gitea 地址,执行环境没有交互式凭据输入,也没有已配置的凭据助手。
|
||||
|
||||
C. 解决问题方案
|
||||
|
||||
保留本地 commit,不把账号密码写入命令、文档或 Git remote。将推送失败写入测试方案和经验记录,后续由用户配置安全凭据或改为 SSH remote 后再推送。
|
||||
|
||||
D. 后续如何避免问题
|
||||
|
||||
Gitea 推送前先检查认证方式。优先使用 SSH 或安全凭据助手;如果 HTTP remote 需要 token,应通过安全环境变量或凭据管理器提供,不要直接拼进 URL。
|
||||
|
||||
## 2026-05-19-23-23-50 回滚错误的 PDF 下载文件名指令
|
||||
|
||||
A. 具体问题
|
||||
|
||||
用户说明上一条修改 PDF 下载文件名的指令写错了,需要撤销中文文件名变更。
|
||||
|
||||
B. 产生问题原因
|
||||
|
||||
上一条需求将 Reactive Resume 的 PDF 下载文件名从 `ZhiboWang-Resume.pdf` 改为中文文件名;之后最新提交又把中文文件名调整为不带短横线版本。只回滚最新提交会仍然保留中文文件名,不能真正回到原始状态。
|
||||
|
||||
C. 解决问题方案
|
||||
|
||||
以原始文件名 `ZhiboWang-Resume.pdf` 为目标,直接恢复 `scripts/patch-reactive-resume-filename.sh` 中 shell 与 Node 两处文件名常量,并恢复 cache bust 为 `rr-filename-20260519`。随后重新执行补丁脚本,让运行中容器 public JS 与 SSR 文件同步恢复。
|
||||
|
||||
D. 后续如何避免问题
|
||||
|
||||
用户要求回滚时,先确认要回到哪一个历史状态,而不是只撤销最近一个 commit。涉及线上容器补丁时,Git 文件回滚后必须重新执行补丁脚本并验证 HTTP 静态资源。
|
||||
42
工程分析/需求分析-2026-05-19-23-10-56.md
Normal file
42
工程分析/需求分析-2026-05-19-23-10-56.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 需求分析-2026-05-19-23-10-56
|
||||
|
||||
## 开始时间
|
||||
|
||||
2026-05-19-23-10-56
|
||||
|
||||
## 原始需求摘要
|
||||
|
||||
用户要求将下载的 PDF 文件名从 `ZhiboWang-Resume.pdf` 改为 `王志博-医工智能外科-简历.pdf`。
|
||||
|
||||
## 业务目标
|
||||
|
||||
- 让用户从 Reactive Resume 网页下载 PDF 时,浏览器保存文件名显示为中文简历名称。
|
||||
- 保持现有简历内容、部署地址和其他导出格式不变。
|
||||
|
||||
## 输入与输出
|
||||
|
||||
- 输入:当前运行的 Reactive Resume 服务和既有补丁脚本。
|
||||
- 输出:更新后的补丁脚本、运行中容器内已生效的新 PDF 下载文件名。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `scripts/patch-reactive-resume-filename.sh`
|
||||
- 运行中 Docker 容器 `reactive-resume-reactive-resume-1` 的静态资源与 SSR 文件。
|
||||
- 新增本次工程分析文档与经验记录。
|
||||
|
||||
## 关键约束
|
||||
|
||||
- 当前 DICOM 项目内没有该下载文件名,真实目标位于 `/home/wkmgc/Desktop/个人材料编写/个人简历构建`。
|
||||
- 修改后需要重新执行补丁脚本并等待容器健康。
|
||||
- 不能把 Gitea 凭据写入脚本、remote URL 或命令历史。
|
||||
|
||||
## 风险点
|
||||
|
||||
- 若只改脚本不执行,线上下载文件名不会立即变化。
|
||||
- 若不更新 cache bust,浏览器可能继续使用旧 JS 静态资源。
|
||||
- 若补丁脚本匹配不到新的构建文件名,容器补丁可能失败。
|
||||
|
||||
## 默认假设
|
||||
|
||||
- 用户说的“下载的 PDF”指当前 Reactive Resume 公开简历的 PDF 下载文件名。
|
||||
- 只改 PDF 文件名,不改简历标题、数据库内容或其他导出格式。
|
||||
41
工程分析/需求分析-2026-05-19-23-23-50.md
Normal file
41
工程分析/需求分析-2026-05-19-23-23-50.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 需求分析-2026-05-19-23-23-50
|
||||
|
||||
## 开始时间
|
||||
|
||||
2026-05-19-23-23-50
|
||||
|
||||
## 原始需求摘要
|
||||
|
||||
用户说明上一条“修改 PDF 下载文件名”的指令写错了,要求回滚。
|
||||
|
||||
## 业务目标
|
||||
|
||||
- 将 Reactive Resume PDF 下载文件名恢复到改名前的 `ZhiboWang-Resume.pdf`。
|
||||
- 同步恢复运行中容器的 public JS 与 SSR 下载文件名逻辑。
|
||||
- 保留本次回滚记录,避免后续误以为中文文件名仍是目标状态。
|
||||
|
||||
## 输入与输出
|
||||
|
||||
- 输入:当前最新脚本、当前 Docker 容器、相关 commit `d2edebe` 与 `d77954b`。
|
||||
- 输出:脚本恢复旧文件名、运行中服务恢复旧下载名、回滚分析与经验记录。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `scripts/patch-reactive-resume-filename.sh`
|
||||
- 运行中容器 `reactive-resume-reactive-resume-1`
|
||||
- `工程分析/` 本次回滚文档与经验记录
|
||||
|
||||
## 关键约束
|
||||
|
||||
- 使用非破坏性新增提交完成回滚,不使用 `git reset --hard`。
|
||||
- 不删除历史审计文档,新增回滚文档说明原因。
|
||||
- Gitea HTTP 远端可能仍缺少凭据,推送失败需要记录。
|
||||
|
||||
## 风险点
|
||||
|
||||
- 如果只改 Git 脚本,不重新执行脚本,线上容器仍会保持中文文件名。
|
||||
- 如果只回滚最新 `d77954b`,文件名会退到 `王志博-医工智能外科-简历.pdf`,仍然不是原始文件名。
|
||||
|
||||
## 默认假设
|
||||
|
||||
- “回滚一下”指回到用户提出中文文件名前的状态,即 `ZhiboWang-Resume.pdf`。
|
||||
Reference in New Issue
Block a user