6 Commits

7 changed files with 324 additions and 12 deletions

View File

@@ -8,27 +8,110 @@ 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"
PDF_FILENAME="王志博-医工智能外科简历.pdf"
CACHE_BUST="rr-filename-20260519-cn"
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 pdfFilename = '王志博-医工智能外科简历.pdf';
const cacheBust = 'rr-filename-20260519-cn';
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;
}
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 = `function t(e,t){if(t==="pdf")return"${pdfFilename}";let n=(e||"resume").toString().trim()||"resume";return\`\${n}\${t?\`.\${t}\`:""}\`}`;
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}'
);
const ssrReplacement = `function generateFilename(prefix, extension) {\n\tif (extension === "pdf") return "${pdfFilename}";\n\tconst filename = (prefix || "resume").toString().trim() || "resume";\n\treturn \`\${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

View File

@@ -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

View 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 容器。

View 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不要在命令行写入账号密码。

View 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` 无法读取用户名;未在命令行写入账号密码。

View File

@@ -0,0 +1,39 @@
# 经验记录
本文件用于记录个人简历构建项目修改过程中的关键问题与解决方案。每条经验使用四段式结构。
## 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。

View 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 文件名,不改简历标题、数据库内容或其他导出格式。