14 Commits

80 changed files with 14712 additions and 88 deletions

2
.env
View File

@@ -44,7 +44,7 @@ S3_ENDPOINT=
S3_BUCKET=
S3_FORCE_PATH_STYLE=false
REDIS_URL=
ENCRYPTION_SECRET=
ENCRYPTION_SECRET=10ddcad9814eaa5fa5bef8ba85d58a8122f3ede148bad11bc340899b5014af45
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
FLAG_DISABLE_SIGNUPS=false

View File

@@ -13,6 +13,14 @@
- `生成简历/backups/`: 每次线上 `resume` 被替换前导出的历史版本备份。
- `生成简历/visual-assets/`: 图文并茂版简历使用的轻量展示图。
- `reactive_resume_data`: Reactive Resume 本地上传文件持久化 Docker 命名卷。
- `packages/`: 三套可读可改的安装包目录。
- `dist/`: 三套安装包的 `.tar.gz` / `.zip` 发布归档和 `SHA256SUMS`
## 安装包
- `reactive-resume-clean-install-20260519`: 纯净迁移模板域名、端口、FRP、密钥均为待填写占位。
- `reactive-resume-personal-direct-20260519`: 服务器直接运行版,预置 `https://isiseg.huijutec.cn`、FRP `remotePort = 10004`,并内置当前简历与上传图片。
- `reactive-resume-personal-qnap-nas-20260519`: 威联通 QNAP NAS 直接部署版,预置 `/share/Container/Reactive_Resume_Personal`、本地端口 `3004`、FRP `10004`,并内置当前简历与上传图片。
## 启动

6
dist/SHA256SUMS vendored Normal file
View File

@@ -0,0 +1,6 @@
f9daa11eeb735e1920d822094a7caf3f7eebcccf8467755bb6f904c68a53bdbf reactive-resume-clean-install-20260519.tar.gz
18b247b33feaf9ccc4ce7516fb76ea3523c578e3099c77e7549bdfa2c68fd658 reactive-resume-personal-direct-20260519.tar.gz
d2718004a8a0592f38096f426ef307c9df99b2f090dac733fb6c0f412d2e4916 reactive-resume-personal-qnap-nas-20260519.tar.gz
54bf5114e8ca9f29ae1feb0510f738396f354174f691bf96e863b41058742a58 reactive-resume-clean-install-20260519.zip
b7cdb405f24ff7653aac9d21f3bd347884e377c8a993d8bb1b7d9d77295425d2 reactive-resume-personal-direct-20260519.zip
d847a76e3cac28f9e973e21ba7bcbac89ccc940345694b3a3483cde9eed01ee0 reactive-resume-personal-qnap-nas-20260519.zip

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,59 @@
# ===== Reactive Resume clean install environment =====
# Copy this file to .env, then replace every CHANGE_ME / YOUR_* value.
TZ=Asia/Shanghai
# Public URL behind your reverse proxy.
APP_URL=https://YOUR_DOMAIN.example.com
# Local debug binding on this Docker host.
# Use 127.0.0.1 if only frpc / local reverse proxy should reach the app.
LOCAL_BIND_IP=127.0.0.1
LOCAL_APP_PORT=CHANGE_ME_LOCAL_PORT
# PostgreSQL.
POSTGRES_DB=reactive_resume
POSTGRES_USER=reactive_resume
POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD
DATABASE_URL=postgresql://reactive_resume:CHANGE_ME_POSTGRES_PASSWORD@postgres:5432/reactive_resume
# Generate with: openssl rand -hex 32
AUTH_SECRET=CHANGE_ME_64_HEX_AUTH_SECRET
ENCRYPTION_SECRET=CHANGE_ME_64_HEX_ENCRYPTION_SECRET
# Optional email/auth/storage/AI settings can be enabled later.
BETTER_AUTH_API_KEY=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
OAUTH_PROVIDER_NAME=
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_DISCOVERY_URL=
OAUTH_AUTHORIZATION_URL=
OAUTH_TOKEN_URL=
OAUTH_USER_INFO_URL=
OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS=
OAUTH_SCOPES=
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=Reactive Resume <noreply@YOUR_DOMAIN.example.com>
SMTP_SECURE=false
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=us-east-1
S3_ENDPOINT=
S3_BUCKET=
S3_FORCE_PATH_STYLE=false
REDIS_URL=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
FLAG_DISABLE_SIGNUPS=false
FLAG_DISABLE_EMAIL_AUTH=false
FLAG_DISABLE_IMAGE_PROCESSING=false
FLAG_ALLOW_UNSAFE_AI_BASE_URL=false

View File

@@ -0,0 +1,38 @@
# Reactive Resume 纯净迁移安装包
这是一套不绑定具体域名、端口、FRP 服务器或密码的纯净安装包,适合迁移到新服务器或新 NAS 前作为模板使用。
## 文件说明
- `compose.yml`:通用 Linux / Docker Compose 部署版
- `compose-Nas.yml`:威联通 QNAP QTS / Container Station 部署模板
- `frpc.yaml`:通用服务器版 frpc 配置模板
- `.env.example`:通用服务器版环境变量模板
## 通用服务器部署
1. 复制环境变量文件:
```bash
cp .env.example .env
```
2. 编辑 `.env` 和 `frpc.yaml`,把所有 `CHANGE_ME` / `YOUR_*` 改成真实值。
3. 启动:
```bash
docker compose -f compose.yml up -d
```
## QNAP NAS 部署
1. 编辑 `compose-Nas.yml` 顶部注释中的路径、域名、端口、FRP 服务器等待填写项。
2. 在 QTS Container Station 中导入 `compose-Nas.yml`。
3. 启动后,确认 app 和 frpc 都为 healthy / running。
## 注意
- `AUTH_SECRET` 和 `ENCRYPTION_SECRET` 必须使用长期固定值,迁移后不要随意更换。
- `APP_URL` 必须与反向代理对外访问域名一致,例如 `https://resume.example.com`。
- FRP 的 `remotePort` 需要与公网服务器 Nginx Proxy Manager / 反向代理转发端口一致。

View File

@@ -0,0 +1,97 @@
# Reactive Resume / QNAP QTS 纯净模板。
# 使用前请填写:
# - NAS 数据目录,例如 /share/Container/Reactive_Resume
# - APP_URL例如 https://resume.example.com
# - 本地访问端口,例如 3004
# - PostgreSQL 密码、AUTH_SECRET、ENCRYPTION_SECRET
# - FRP serverAddr/serverPort/token/proxy name/remotePort
name: reactive-resume-nas
services:
reactive_resume_permissions:
image: alpine:3.20
restart: "no"
command: ["sh", "-c", "mkdir -p /app/data && chown -R 1000:1000 /app/data"]
volumes:
# 待填写:改成你的 NAS 数据目录。
- /share/Container/CHANGE_ME_REACTIVE_RESUME/data/uploads:/app/data
reactive_resume_db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: reactive_resume
POSTGRES_USER: reactive_resume
POSTGRES_PASSWORD: CHANGE_ME_POSTGRES_PASSWORD
volumes:
# 待填写:改成你的 NAS 数据目录。
- /share/Container/CHANGE_ME_REACTIVE_RESUME/data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U reactive_resume -d reactive_resume"]
interval: 10s
timeout: 5s
retries: 10
reactive_resume_app:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
depends_on:
reactive_resume_permissions:
condition: service_completed_successfully
reactive_resume_db:
condition: service_healthy
ports:
# 待填写NAS 本地访问端口。
- "CHANGE_ME_LOCAL_APP_PORT:3000"
volumes:
# 待填写:改成你的 NAS 数据目录。
- /share/Container/CHANGE_ME_REACTIVE_RESUME/data/uploads:/app/data
environment:
TZ: Asia/Shanghai
APP_URL: https://YOUR_DOMAIN.example.com
DATABASE_URL: postgresql://reactive_resume:CHANGE_ME_POSTGRES_PASSWORD@reactive_resume_db:5432/reactive_resume
AUTH_SECRET: CHANGE_ME_64_HEX_AUTH_SECRET
ENCRYPTION_SECRET: CHANGE_ME_64_HEX_ENCRYPTION_SECRET
SMTP_FROM: Reactive Resume <noreply@YOUR_DOMAIN.example.com>
SMTP_PORT: "587"
SMTP_SECURE: "false"
FLAG_DISABLE_SIGNUPS: "false"
FLAG_DISABLE_EMAIL_AUTH: "false"
FLAG_DISABLE_IMAGE_PROCESSING: "false"
FLAG_ALLOW_UNSAFE_AI_BASE_URL: "false"
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 30s
timeout: 10s
retries: 8
start_period: 30s
reactive_resume_frpc:
image: snowdreamtech/frpc:latest
restart: unless-stopped
entrypoint: ["/bin/sh"]
command:
- -c
- |
cat > /tmp/frpc.toml <<'EOF'
serverAddr = "YOUR_FRP_SERVER_IP_OR_DOMAIN"
serverPort = CHANGE_ME_FRP_SERVER_PORT
auth.method = "token"
auth.token = "CHANGE_ME_FRP_TOKEN"
transport.poolCount = 5
transport.heartbeatTimeout = -1
[[proxies]]
name = "CHANGE_ME_PROXY_NAME"
type = "tcp"
localIP = "reactive_resume_app"
localPort = 3000
remotePort = CHANGE_ME_REMOTE_PORT
EOF
exec frpc -c /tmp/frpc.toml
depends_on:
reactive_resume_app:
condition: service_healthy

View File

@@ -0,0 +1,72 @@
# Reactive Resume 通用服务器纯净模板。
# 使用前请先复制 .env.example 为 .env并填写域名、端口、数据库密码和密钥。
name: reactive-resume
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- resume_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
reactive-resume:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
env_file:
- .env
ports:
# 待填写LOCAL_BIND_IP / LOCAL_APP_PORT 在 .env 中配置。
# 例127.0.0.1:3004:3000仅允许本机反代或 frpc 访问。
- "${LOCAL_BIND_IP}:${LOCAL_APP_PORT}:3000"
volumes:
- reactive_resume_data:/app/data
networks:
- resume_net
depends_on:
postgres:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:3000/api/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1));",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
frpc:
image: fatedier/frpc:v0.68.0
restart: unless-stopped
command: ["-c", "/etc/frp/frpc.yaml"]
volumes:
# 待填写frpc.yaml 中配置 FRP 服务器、token、remotePort。
- ./frpc.yaml:/etc/frp/frpc.yaml:ro
networks:
- resume_net
depends_on:
reactive-resume:
condition: service_healthy
networks:
resume_net:
driver: bridge
volumes:
postgres_data:
reactive_resume_data:

View File

@@ -0,0 +1,22 @@
# Reactive Resume frpc 纯净模板。
# 使用前把所有 CHANGE_ME / YOUR_* 改成真实值。
serverAddr: "YOUR_FRP_SERVER_IP_OR_DOMAIN"
serverPort: CHANGE_ME_FRP_SERVER_PORT
auth:
method: "token"
token: "CHANGE_ME_FRP_TOKEN"
transport:
poolCount: 5
heartbeatTimeout: -1
proxies:
- name: "CHANGE_ME_PROXY_NAME"
type: "tcp"
# Docker Compose 内部服务名,通常不用改。
localIP: "reactive-resume"
localPort: 3000
# 待填写:公网 FRP 远程端口,需与反向代理 forward port 一致。
remotePort: CHANGE_ME_REMOTE_PORT

View File

@@ -0,0 +1,50 @@
TZ=Asia/Shanghai
APP_URL=https://isiseg.huijutec.cn
# Local debug access only: http://127.0.0.1:3004
LOCAL_BIND_IP=127.0.0.1
LOCAL_APP_PORT=3004
POSTGRES_DB=reactive_resume
POSTGRES_USER=reactive_resume
POSTGRES_PASSWORD=2ed1869944c609f070699bdf8c92194f
DATABASE_URL=postgresql://reactive_resume:2ed1869944c609f070699bdf8c92194f@postgres:5432/reactive_resume
AUTH_SECRET=9ef1720ee316f9316241bdc84f5dfad99b52f139b48880300942ee61d81b7cda
ENCRYPTION_SECRET=20851888c2a96b11f1f6fc21b4eeb70f1e8258f2d0d414be8bace65eaff289ae
BETTER_AUTH_API_KEY=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
OAUTH_PROVIDER_NAME=
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_DISCOVERY_URL=
OAUTH_AUTHORIZATION_URL=
OAUTH_TOKEN_URL=
OAUTH_USER_INFO_URL=
OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS=
OAUTH_SCOPES=
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=Reactive Resume <noreply@isiseg.huijutec.cn>
SMTP_SECURE=false
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=us-east-1
S3_ENDPOINT=
S3_BUCKET=
S3_FORCE_PATH_STYLE=false
REDIS_URL=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
FLAG_DISABLE_SIGNUPS=false
FLAG_DISABLE_EMAIL_AUTH=false
FLAG_DISABLE_IMAGE_PROCESSING=false
FLAG_ALLOW_UNSAFE_AI_BASE_URL=false

View File

@@ -0,0 +1,36 @@
# Reactive Resume 个人简历直接运行安装包
这套包已经按 `https://isiseg.huijutec.cn` 和 FRP `remotePort = 10004` 预置,可以在当前服务器上直接运行。包内已包含当前简历初始化数据、头像和作品集图片,首次启动后可直接访问公开简历。
## 启动
```bash
docker compose -f compose.yml up -d
```
启动后:
- 本机调试地址:`http://127.0.0.1:3004`
- 公网访问地址:`https://isiseg.huijutec.cn`
- 当前公开简历:`https://isiseg.huijutec.cn/audience/resume`
- FRP 映射:本地 `reactive-resume:3000` -> 公网服务器 `10004`
## 反向代理要求
公网服务器上的 Nginx Proxy Manager / 反向代理应配置:
- Domain Names`isiseg.huijutec.cn`
- Scheme`http`
- Forward Hostname / IP`82.157.255.195`
- Forward Port`10004`
- Websockets Support开启
- SSL按现有 huijutec.cn 域名策略配置
## 数据
Compose 会创建独立项目名 `reactive-resume-personal`,默认使用 Docker named volumes
- `reactive-resume-personal_postgres_data`
- `reactive-resume-personal_reactive_resume_data`
`seed/` 目录会在首次启动时导入当前用户、公开简历和上传图片。后续如需迁移数据,请备份这些 volumes。

View File

@@ -0,0 +1,94 @@
name: reactive-resume-personal
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- resume_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
reactive-resume:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
env_file:
- .env
ports:
- "${LOCAL_BIND_IP}:${LOCAL_APP_PORT}:3000"
volumes:
- reactive_resume_data:/app/data
networks:
- resume_net
depends_on:
postgres:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:3000/api/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1));",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
seed:
image: postgres:16-alpine
restart: "no"
user: "0:0"
entrypoint: ["/bin/sh"]
command:
- -c
- |
set -eu
mkdir -p /app/data/uploads
cp -a /seed/uploads/. /app/data/uploads/
chown -R 1000:1000 /app/data/uploads || true
psql -h postgres -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=1 -f /seed/seed.sql
env_file:
- .env
environment:
PGPASSWORD: ${POSTGRES_PASSWORD}
volumes:
- reactive_resume_data:/app/data
- ./seed:/seed:ro
networks:
- resume_net
depends_on:
postgres:
condition: service_healthy
reactive-resume:
condition: service_healthy
frpc:
image: fatedier/frpc:v0.68.0
restart: unless-stopped
command: ["-c", "/etc/frp/frpc.yaml"]
volumes:
- ./frpc.yaml:/etc/frp/frpc.yaml:ro
networks:
- resume_net
depends_on:
seed:
condition: service_completed_successfully
networks:
resume_net:
driver: bridge
volumes:
postgres_data:
reactive_resume_data:

View File

@@ -0,0 +1,17 @@
serverAddr: "82.157.255.195"
serverPort: 7000
auth:
method: "token"
token: "en.xjtu.edu.cn"
transport:
poolCount: 5
heartbeatTimeout: -1
proxies:
- name: "reactive-resume-personal"
type: "tcp"
localIP: "reactive-resume"
localPort: 3000
remotePort: 10004

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
# Reactive Resume 个人简历 / QNAP NAS 安装包
这套包面向威联通 QNAP QTS / Container Station已按以下参数预置
- NAS 数据目录:`/share/Container/Reactive_Resume_Personal`
- 本地端口:`3004:3000`
- 公网域名:`https://isiseg.huijutec.cn`
- 当前公开简历:`https://isiseg.huijutec.cn/audience/resume`
- FRP 服务器:`82.157.255.195:7000`
- FRP remotePort`10004`
包内已包含当前简历初始化数据、头像和作品集图片,首次启动后会由 `reactive_resume_seed` 自动导入。
## 部署
1. 将本安装包内容放到 `/share/Container/Reactive_Resume_Personal`
2. 打开 Container Station导入 `/share/Container/Reactive_Resume_Personal/compose-Nas.yml`
3. 启动项目
4. 访问 `https://isiseg.huijutec.cn/audience/resume`
发布归档中的顶层目录已经命名为 `Reactive_Resume_Personal/`,正常解压到 `/share/Container/` 下即可匹配上述路径。
## 反向代理要求
公网服务器上的 Nginx Proxy Manager / 反向代理应配置:
- Domain Names`isiseg.huijutec.cn`
- Scheme`http`
- Forward Hostname / IP`82.157.255.195`
- Forward Port`10004`
- Websockets Support开启
## 数据目录
- PostgreSQL`/share/Container/Reactive_Resume_Personal/data/postgres`
- 上传与本地存储:`/share/Container/Reactive_Resume_Personal/data/uploads`
- 初始化种子:`/share/Container/Reactive_Resume_Personal/seed`

View File

@@ -0,0 +1,145 @@
# Reactive Resume 个人简历 / QNAP QTS 直接部署版。
# 本文件已按 /share/Container/Reactive_Resume_Personal、
# https://isiseg.huijutec.cn、192.168.31.5:3004 本地访问、
# frpc 公网映射 82.157.255.195:10004 预置。
name: reactive-resume-personal-nas
services:
reactive_resume_permissions:
image: alpine:3.20
restart: "no"
command: ["sh", "-c", "mkdir -p /app/data && chown -R 1000:1000 /app/data"]
volumes:
- /share/Container/Reactive_Resume_Personal/data/uploads:/app/data
reactive_resume_db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: reactive_resume
POSTGRES_USER: reactive_resume
POSTGRES_PASSWORD: 5b341c0ca29fefd6d648661150c00fa4
volumes:
- /share/Container/Reactive_Resume_Personal/data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U reactive_resume -d reactive_resume"]
interval: 10s
timeout: 5s
retries: 10
reactive_resume_app:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
depends_on:
reactive_resume_permissions:
condition: service_completed_successfully
reactive_resume_db:
condition: service_healthy
ports:
- "3004:3000"
volumes:
- /share/Container/Reactive_Resume_Personal/data/uploads:/app/data
environment:
TZ: Asia/Shanghai
APP_URL: https://isiseg.huijutec.cn
DATABASE_URL: postgresql://reactive_resume:5b341c0ca29fefd6d648661150c00fa4@reactive_resume_db:5432/reactive_resume
AUTH_SECRET: c76b0eaf79f731e9ee95918dc69d41696aec9d1deffeabc122944898037bfab1
ENCRYPTION_SECRET: df3a460fa2f92f6e8765927a169322980e18f63a88fbcfedb090819b5afb2408
BETTER_AUTH_API_KEY: ""
GOOGLE_CLIENT_ID: ""
GOOGLE_CLIENT_SECRET: ""
GITHUB_CLIENT_ID: ""
GITHUB_CLIENT_SECRET: ""
LINKEDIN_CLIENT_ID: ""
LINKEDIN_CLIENT_SECRET: ""
OAUTH_PROVIDER_NAME: ""
OAUTH_CLIENT_ID: ""
OAUTH_CLIENT_SECRET: ""
OAUTH_DISCOVERY_URL: ""
OAUTH_AUTHORIZATION_URL: ""
OAUTH_TOKEN_URL: ""
OAUTH_USER_INFO_URL: ""
OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS: ""
OAUTH_SCOPES: ""
SMTP_HOST: ""
SMTP_PORT: "587"
SMTP_USER: ""
SMTP_PASS: ""
SMTP_FROM: "Reactive Resume <noreply@isiseg.huijutec.cn>"
SMTP_SECURE: "false"
S3_ACCESS_KEY_ID: ""
S3_SECRET_ACCESS_KEY: ""
S3_REGION: us-east-1
S3_ENDPOINT: ""
S3_BUCKET: ""
S3_FORCE_PATH_STYLE: "false"
REDIS_URL: ""
CLOUDFLARE_ACCOUNT_ID: ""
CLOUDFLARE_API_TOKEN: ""
FLAG_DISABLE_SIGNUPS: "false"
FLAG_DISABLE_EMAIL_AUTH: "false"
FLAG_DISABLE_IMAGE_PROCESSING: "false"
FLAG_ALLOW_UNSAFE_AI_BASE_URL: "false"
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 30s
timeout: 10s
retries: 8
start_period: 30s
reactive_resume_seed:
image: postgres:16-alpine
restart: "no"
user: "0:0"
entrypoint: ["/bin/sh"]
command:
- -c
- |
set -eu
mkdir -p /app/data/uploads
cp -a /seed/uploads/. /app/data/uploads/
chown -R 1000:1000 /app/data/uploads || true
psql -h reactive_resume_db -U reactive_resume -d reactive_resume -v ON_ERROR_STOP=1 -f /seed/seed.sql
environment:
PGPASSWORD: 5b341c0ca29fefd6d648661150c00fa4
volumes:
- /share/Container/Reactive_Resume_Personal/data/uploads:/app/data
- /share/Container/Reactive_Resume_Personal/seed:/seed:ro
depends_on:
reactive_resume_permissions:
condition: service_completed_successfully
reactive_resume_db:
condition: service_healthy
reactive_resume_app:
condition: service_healthy
reactive_resume_frpc:
image: snowdreamtech/frpc:latest
restart: unless-stopped
entrypoint: ["/bin/sh"]
command:
- -c
- |
cat > /tmp/frpc.toml <<'EOF'
serverAddr = "82.157.255.195"
serverPort = 7000
auth.method = "token"
auth.token = "en.xjtu.edu.cn"
transport.poolCount = 5
transport.heartbeatTimeout = -1
[[proxies]]
name = "Reactive_Resume_Personal_NAS"
type = "tcp"
localIP = "reactive_resume_app"
localPort = 3000
remotePort = 10004
EOF
exec frpc -c /tmp/frpc.toml
depends_on:
reactive_resume_seed:
condition: service_completed_successfully

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
CONTAINER="${1:-reactive-resume-reactive-resume-1}"
docker exec -u root -i "$CONTAINER" sh <<'SH'
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"
cp "$PUBLIC_FILE" "$PUBLIC_FILE.bak-filename" 2>/dev/null || true
cp "$SSR_FILE" "$SSR_FILE.bak-filename" 2>/dev/null || true
node - <<'NODE'
const fs = require('fs');
const publicFile = '/app/apps/web/.output/public/assets/file-D5WsIgJH.js';
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}`:""}`}'
);
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}'
);
fs.writeFileSync(ssrFile, ssr);
NODE
SH

View File

@@ -0,0 +1,209 @@
#!/bin/sh
set -eu
CONTAINER="${1:-reactive-resume-reactive-resume-1}"
docker exec -u root -i "$CONTAINER" sh <<'SH'
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"
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
test -f "$SERVER_INDEX_FILE.bak-glalie-layout" || cp "$SERVER_INDEX_FILE" "$SERVER_INDEX_FILE.bak-glalie-layout" 2>/dev/null || true
node - <<'NODE'
const fs = require("fs");
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";
function replaceOnce(source, from, to, label) {
if (source.includes(to)) return source;
if (!source.includes(from)) throw new Error(`Patch marker not found: ${label}`);
return source.replace(from, to);
}
function replaceRegexOnce(source, regex, to, label) {
if (source.includes(to)) return source;
const next = source.replace(regex, to);
if (next === source) throw new Error(`Patch marker not found: ${label}`);
return next;
}
function patchSsr(source) {
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)");
source = source
.replace(
/style: composeStyles\(styles\.sidebarContent, \{ rowGap: metrics\.sectionGap \}\),/g,
"style: composeStyles(styles.sidebarContent, { rowGap: metrics.gapY(2.2) }),",
)
.replace(
/style: composeStyles\(styles\.mainContent, \{ rowGap: metrics\.sectionGap \}\),/g,
"style: composeStyles(styles.mainContent, { rowGap: metrics.gapY(2.2) }),",
);
source = source.replace(
/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary(?:,\s*paddingBottom: 1(?:\.3)?)?\s*\},/,
`sectionHeading: {
\t\t\t\t\tborderBottomWidth: 1,
\t\t\t\t\tborderBottomColor: primary,
\t\t\t\t\tpaddingBottom: 1.3
\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: {
\t\t\t\t\tborderBottomWidth: 1,
\t\t\t\t\tborderBottomColor: primary,
\t\t\t\t\tpaddingBottom: 1.3
\t\t\t\t},
\t\t\t\tsectionItems: { paddingTop: metrics.gapY(.55) },
\t\t\t\titem: { rowGap: metrics.gapY(.2) },`,
"SSR Glalie section item spacing",
);
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: {
\t\t\t\t\tzIndex: 1,
\t\t\t\t\tbackgroundColor: primaryTint,
\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,
\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,
\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical,
\t\t\t\t\trowGap: metrics.gapY(3.0)
\t\t\t\t},`,
"SSR Glalie sidebar bottom padding",
);
source = replaceRegexOnce(
source,
/mainContent: \{\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical\s*)?\},/,
`mainContent: {
\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,
\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,
\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical
\t\t\t\t},`,
"SSR Glalie main bottom padding",
);
return 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)");
}
function patchPublic(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(/o\.gapY\(3\.5\)/g, "o.gapY(3.0)")
.replace(/c\.gapY\(3\.5\)/g, "c.gapY(3.0)")
.replace(/o\.gapY\(2\.6\)/g, "o.gapY(3.0)")
.replace(/c\.gapY\(2\.6\)/g, "c.gapY(3.0)")
.replace(/o\.gapY\(2\.2\)/g, "o.gapY(3.0)")
.replace(/c\.gapY\(2\.2\)/g, "c.gapY(3.0)");
source = source
.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)})");
source = source.replace(
/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a(?:,paddingBottom:1(?:\.3)?)?\}/,
"sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3}",
);
source = source.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)}",
"public Glalie sidebar bottom padding",
);
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}",
"public Glalie main bottom padding",
);
return 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(/o\.gapY\(3\.5\)/g, "o.gapY(3.0)")
.replace(/c\.gapY\(3\.5\)/g, "c.gapY(3.0)")
.replace(/o\.gapY\(2\.6\)/g, "o.gapY(3.0)")
.replace(/c\.gapY\(2\.6\)/g, "c.gapY(3.0)")
.replace(/o\.gapY\(2\.2\)/g, "o.gapY(3.0)")
.replace(/c\.gapY\(2\.2\)/g, "c.gapY(3.0)");
}
function patchImporters() {
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("pdf-document-BplbXx-0.js"));
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}`);
fs.writeFileSync(file, source);
}
return files;
}
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);
}
fs.writeFileSync(ssrFile, patchSsr(fs.readFileSync(ssrFile, "utf8")));
fs.writeFileSync(publicFile, patchPublic(fs.readFileSync(publicFile, "utf8")));
const importers = patchImporters();
let serverIndex = fs.readFileSync(serverIndexFile, "utf8");
serverIndex = patchStaticManifestEntry(serverIndex, "/assets/pdf-document-BplbXx-0.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 "$SSR_FILE" >/dev/null
node --check "$PUBLIC_FILE" >/dev/null
node --check "$SERVER_INDEX_FILE" >/dev/null
SH

View File

@@ -0,0 +1,161 @@
#!/bin/sh
set -eu
CONTAINER="${1:-reactive-resume-reactive-resume-1}"
docker exec -u root -i "$CONTAINER" sh <<'SH'
set -eu
SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"
SW_FILE="/app/apps/web/.output/public/sw.js"
SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs"
SSR_RENDERER_FILE="/app/apps/web/.output/server/_chunks/ssr-renderer.mjs"
test -f "$SSR_FILE.bak-sw-cache" || cp "$SSR_FILE" "$SSR_FILE.bak-sw-cache" 2>/dev/null || true
test -f "$SW_FILE.bak-sw-cache" || cp "$SW_FILE" "$SW_FILE.bak-sw-cache" 2>/dev/null || true
test -f "$SERVER_INDEX_FILE.bak-sw-cache" || cp "$SERVER_INDEX_FILE" "$SERVER_INDEX_FILE.bak-sw-cache" 2>/dev/null || true
test -f "$SSR_RENDERER_FILE.bak-sw-cache" || cp "$SSR_RENDERER_FILE" "$SSR_RENDERER_FILE.bak-sw-cache" 2>/dev/null || true
node - <<'NODE'
const fs = require("fs");
const crypto = require("crypto");
const ssrFile = "/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs";
const swFile = "/app/apps/web/.output/public/sw.js";
const serverIndexFile = "/app/apps/web/.output/server/index.mjs";
const ssrRendererFile = "/app/apps/web/.output/server/_chunks/ssr-renderer.mjs";
const registrationScript = `
\t(() => {
\t\tif (!("serviceWorker" in navigator)) return;
\t\twindow.addEventListener("load", () => {
\t\t\tconst clearReactiveResumeCaches = async () => {
\t\t\t\tif ("caches" in window) {
\t\t\t\t\tconst keys = await caches.keys();
\t\t\t\t\tawait Promise.all(keys.map((key) => caches.delete(key)));
\t\t\t\t}
\t\t\t\tif (navigator.serviceWorker.getRegistrations) {
\t\t\t\t\tconst registrations = await navigator.serviceWorker.getRegistrations();
\t\t\t\t\tawait Promise.all(registrations.map((registration) => registration.unregister()));
\t\t\t\t}
\t\t\t};
\t\t\tclearReactiveResumeCaches().catch(console.error);
\t\t});
\t})();
`;
let ssr = fs.readFileSync(ssrFile, "utf8");
const start = "var pwaServiceWorkerRegistrationScript = `";
const end = "`;\nvar src_default =";
const startIndex = ssr.indexOf(start);
if (startIndex === -1) {
throw new Error("Service worker registration script start marker not found");
}
const endIndex = ssr.indexOf(end, startIndex + start.length);
if (endIndex === -1) {
throw new Error("Service worker registration script end marker not found");
}
ssr =
ssr.slice(0, startIndex) +
start +
registrationScript +
ssr.slice(endIndex);
fs.writeFileSync(ssrFile, ssr);
const sw = `self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
await self.registration.unregister();
await self.clients.claim();
const clients = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
});
for (const client of clients) {
client.postMessage({ type: "RR_SW_CACHE_CLEARED" });
}
})());
});
self.addEventListener("fetch", () => {});
`;
fs.writeFileSync(swFile, sw);
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 end = source.indexOf("\n\t},", start);
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);
}
let serverIndex = fs.readFileSync(serverIndexFile, "utf8");
serverIndex = patchStaticManifestEntry(serverIndex, "/sw.js", swFile);
fs.writeFileSync(serverIndexFile, serverIndex);
let ssrRenderer = fs.readFileSync(ssrRendererFile, "utf8");
const ssrRendererOriginal = `function ssrRenderer({ req }) {
\treturn fetchViteEnv("ssr", req);
}`;
const ssrRendererPatched = `async function ssrRenderer(event) {
\tconst response = await fetchViteEnv("ssr", event.req);
\tconst headers = new Headers(response.headers);
\tconst accept = event.req.headers.get("accept") || "";
\tif (accept.includes("text/html")) {
\t\theaders.set("Cache-Control", "no-store, max-age=0");
\t\theaders.set("Pragma", "no-cache");
\t\theaders.set("Expires", "0");
\t}
\treturn new Response(response.body, {
\t\tstatus: response.status,
\t\tstatusText: response.statusText,
\t\theaders,
\t});
}`;
if (!ssrRenderer.includes(ssrRendererPatched)) {
if (!ssrRenderer.includes(ssrRendererOriginal)) {
throw new Error("SSR renderer marker not found");
}
ssrRenderer = ssrRenderer.replace(ssrRendererOriginal, ssrRendererPatched);
fs.writeFileSync(ssrRendererFile, ssrRenderer);
}
NODE
node --check "$SSR_FILE" >/dev/null
node --check "$SW_FILE" >/dev/null
node --check "$SERVER_INDEX_FILE" >/dev/null
node --check "$SSR_RENDERER_FILE" >/dev/null
SH

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
"email": "zub572701190@stu.xjtu.edu.cn",
"phone": "+86 139-4611-2059",
"website": {
"url": "",
"label": ""
"url": "https://me.huijutec.cn/audience/resume",
"label": "me.huijutec.cn/audience/resume"
},
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
"location": "陕西西安|西安交通大学",
@@ -50,15 +50,15 @@
},
"metadata": {
"page": {
"gapX": 4.4,
"gapY": 2.45,
"gapX": 3.8,
"gapY": 0.56,
"format": "a4",
"locale": "zh-CN",
"marginX": 8.2,
"marginY": 7.2,
"marginX": 8.4,
"marginY": 8.8,
"hideIcons": false
},
"notes": "Design direction: refined clinical-tech academic profile. Glalie two-column layout, wider A4 margins, CJK-first typography, navy-blue medical technology accent, and two-page hierarchy for personal achievements and translational impact.",
"notes": "Design direction: refined clinical-tech academic profile. Glalie two-column layout with CJK-safe line height, balanced list rhythm, moderate section spacing, a continued second-page sidebar, and split volunteer/social activity evidence.",
"design": {
"level": {
"icon": "star",
@@ -71,38 +71,46 @@
}
},
"layout": {
"sidebarWidth": 29.5,
"pages": [
{
"fullWidth": false,
"main": [
"summary",
"awards",
"education",
"projects",
"experience",
"1014b66d-3de1-4bf4-903c-93e7f07c8f81",
"54972f49-3c83-4912-b429-dc659a02eda9",
"36ba1eaf-0984-4863-9b4b-691374ee27aa",
"certifications",
"752ddba0-3400-4e33-8f54-49fc3a4b57b9",
"559335bd-99c4-44c2-97b6-420b011415f7"
"1014b66d-3de1-4bf4-903c-93e7f07c8f81"
],
"sidebar": [
"f4fa59b9-34cf-41b0-b324-f07f07934fc2",
"skills",
"languages",
"802bfd76-af47-4c5d-8b04-f89a38932dcd",
"volunteer"
]
"volunteer",
"4a9c2385-2f79-4d50-99cf-6d2aaef0b6c6"
],
"fullWidth": false
},
{
"main": [
"experience",
"54972f49-3c83-4912-b429-dc659a02eda9",
"752ddba0-3400-4e33-8f54-49fc3a4b57b9",
"certifications",
"36ba1eaf-0984-4863-9b4b-691374ee27aa",
"559335bd-99c4-44c2-97b6-420b011415f7"
],
"sidebar": [],
"fullWidth": false
}
]
],
"sidebarWidth": 29.2
},
"template": "glalie",
"typography": {
"body": {
"fontSize": 6.95,
"fontSize": 6.9,
"fontFamily": "Noto Sans SC",
"lineHeight": 1.22,
"lineHeight": 1.34,
"fontWeights": [
"400",
"500",
@@ -110,9 +118,9 @@
]
},
"heading": {
"fontSize": 9.25,
"fontSize": 9.15,
"fontFamily": "Noto Serif SC",
"lineHeight": 1.18,
"lineHeight": 1.28,
"fontWeights": [
"600",
"700"
@@ -367,7 +375,7 @@
"id": "17beb594-5513-4788-9db8-77cde270a476",
"name": "多设备兼容的术中影像记录分析系统",
"hidden": false,
"period": "2025",
"period": "2025 - 至今",
"website": {
"url": "",
"label": "",
@@ -379,7 +387,7 @@
"id": "64fe9ea6-7efc-47f8-bed0-2fa883211ef4",
"name": "微创化手术智能导航平台建设",
"hidden": false,
"period": "2024 - 2026",
"period": "2024 - 至今",
"website": {
"url": "",
"label": "",
@@ -420,7 +428,7 @@
"inlineLink": false
},
"location": "西安",
"description": "<ul><li>研究方向医工交叉、AI 腔镜外科导航、术中影像记录分析、多模态大模型与图文报告生成</li><li>现任医工学博士党支部宣传委员、未来技术学院 B2275 班学习委员</li></ul>"
"description": "<ul><li><u><strong>课题组:</strong>西安交通大学外科梦工场吕毅教授课题组;<strong>导师:</strong>吴荣谦教授</u></li><li>研究方向医工交叉、AI 腔镜外科导航、术中影像记录分析、多模态大模型与图文报告生成</li><li>现任医工学博士党支部宣传委员、未来技术学院 B2275 班学习委员</li></ul>"
},
{
"id": "17863b1a-cfbc-4990-b870-ff3485048b40",
@@ -428,7 +436,7 @@
"grade": "",
"degree": "本科",
"hidden": false,
"period": "2018.08 - 2022.07",
"period": "2018.09 - 2022.07",
"school": "西安交通大学人工智能学院",
"website": {
"url": "",
@@ -465,7 +473,7 @@
"id": "469cc8d5-b041-4f02-9ce5-83961cc00727",
"icon": "stethoscope",
"name": "医工交叉",
"hidden": false,
"hidden": true,
"keywords": [
"AI + 临床",
"智能外科"
@@ -476,7 +484,7 @@
"id": "43ab8051-a0e5-422e-b2b1-90c6a30311da",
"icon": "rocket",
"name": "成果转化",
"hidden": false,
"hidden": true,
"keywords": [
"创业资助",
"专利转化",
@@ -488,7 +496,7 @@
"id": "4721af36-ae2a-47f4-9852-4d71e7f5e44a",
"icon": "database",
"name": "数据要素",
"hidden": false,
"hidden": true,
"keywords": [
"术中影像",
"质控",
@@ -498,7 +506,7 @@
}
],
"title": "关键词",
"hidden": false,
"hidden": true,
"columns": 1
},
"languages": {
@@ -520,54 +528,54 @@
{
"id": "50c41f06-d048-48d6-a5b0-c67521104562",
"hidden": false,
"period": "2022 - 至今",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "西安交通大学",
"description": "<p>宣传委员</p>",
"location": "",
"description": "<p>2022 - 至今<br/>宣传委员</p>",
"organization": "未来技术学院医工学博士党支部"
},
{
"id": "bbc0e308-86db-4894-a136-24b80555d25c",
"hidden": false,
"period": "2024",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "西安交通大学",
"description": "<p>学习部负责人;第十五次研究生代表大会代表</p>",
"location": "",
"description": "<p>2024<br/>学习部负责人;第十五次研究生代表大会代表</p>",
"organization": "未来技术学院学生会 / 研究生会"
},
{
"id": "72a41ed7-2ce6-4c36-a524-aac20b6a7bef",
"hidden": false,
"period": "2026",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "西安交通大学",
"description": "<p>第一次党员代表大会代表</p>",
"location": "",
"description": "<p>2026<br/>第一次党员代表大会代表</p>",
"organization": "未来技术学院党员代表大会"
},
{
"id": "0e5bd75f-e635-44c7-8379-43fdc2dc45b8",
"hidden": false,
"period": "2022 - 至今",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "西安交通大学",
"description": "<p>学习委员</p>",
"organization": "未来技术学院 B2275 班"
"location": "",
"description": "<p>2022 - 至今<br/>学习委员</p>",
"organization": "未来技术学院 B2275 班班委"
}
],
"title": "校内任职",
@@ -589,7 +597,7 @@
},
"location": "",
"position": "主要工科参与人",
"description": "<p>基于多模态磁导航技术的困难气道插管系统的研究49 万元在研</p>"
"description": "<p>基于多模态磁导航技术的困难气道插管系统的研究49 万元在研</p>"
},
{
"id": "0b95f627-8395-4242-9f60-8fa0f1bb491c",
@@ -603,8 +611,8 @@
"inlineLink": true
},
"location": "",
"position": "在研",
"description": "<p>微创化手术智能导航平台建设</p>"
"position": "主要工科参与人(第三序位)",
"description": "<p>微创化手术智能导航平台建设70万元在研</p>"
},
{
"id": "47a5d869-535f-42e2-8ac7-bd927853b973",
@@ -619,7 +627,7 @@
},
"location": "",
"position": "主持",
"description": "<p>磁定位辅助多模态融合微创手术组织自动配准系统3 万元结题</p>"
"description": "<p>磁定位辅助多模态融合微创手术组织自动配准系统3 万元结题</p>"
},
{
"id": "affc3464-2cad-4ea4-8399-162bd2c2e2eb",
@@ -633,8 +641,8 @@
"inlineLink": false
},
"location": "",
"position": "结题",
"description": "<p>微创手术导航训练虚拟平台建设研究</p>"
"position": "主要工科参与人(第三序位)",
"description": "<p>微创手术导航训练虚拟平台建设研究20万元结题</p>"
},
{
"id": "eb837f99-e47f-4ec9-9c8b-0fb879302889",
@@ -648,8 +656,8 @@
"inlineLink": false
},
"location": "",
"position": "参与第二位,结题",
"description": "<p>微创化手术智能导航平台建设3 万元</p>"
"position": "主要工科参与人(第三序位)",
"description": "<p>微创化手术智能导航平台建设3 万元;结题</p>"
}
],
"title": "科研与获批项目",
@@ -1041,112 +1049,109 @@
{
"id": "802bfd76-af47-4c5d-8b04-f89a38932dcd",
"type": "volunteer",
"title": "学术组织任职",
"hidden": false,
"columns": 1,
"items": [
{
"id": "76390bf9-49d5-4fbc-98ab-6691477e982d",
"hidden": false,
"period": "2024 - 至今",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"organization": "中医药信息学会中西医外科智能诊疗分会",
"description": "<p>理事</p>"
"description": "<p>2024 - 至今<br/>理事</p>",
"organization": "中医药信息学会中西医外科智能诊疗分会"
},
{
"id": "c2291187-b7c3-48a2-820a-40e06836072b",
"hidden": false,
"period": "2024 - 至今",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"organization": "中国抗癌协会",
"description": "<p>青年理事</p>"
"description": "<p>2024 - 至今<br/>青年理事</p>",
"organization": "中国抗癌协会"
},
{
"id": "1f873e55-fde9-4b85-ab6f-d151d5b15d53",
"hidden": false,
"period": "2024 - 至今",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"organization": "中国医药教育协会数字医疗专业委员会",
"description": "<p>委员</p>"
"description": "<p>2024 - 至今<br/>数字医疗专业委员会委员</p>",
"organization": "中国医药教育协会"
},
{
"id": "e1bf3d6a-6018-4c3e-9330-3e7c3a2d89a1",
"hidden": false,
"period": "2024 - 至今",
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"organization": "中国生物医学工程学会",
"description": "<p>学生会员</p>"
"description": "<p>2024 - 至今<br/>学生会员</p>",
"organization": "中国生物医学工程学会"
}
]
],
"title": "学术组织任职",
"hidden": false,
"columns": 1
},
{
"id": "1014b66d-3de1-4bf4-903c-93e7f07c8f81",
"type": "summary",
"title": "论文与会议",
"hidden": false,
"columns": 1,
"items": [
{
"id": "c16accb2-c907-44bf-aa0c-89e0085c6a50",
"hidden": false,
"content": "<p><strong>期刊论文</strong></p><ul><li><strong>Frontiers in Oncology, 2025</strong> — Development of an AI-driven digital assistance system for real-time safety evaluation and quality control in laparoscopic liver surgery共同第一作者</li><li><strong>中华肝脏外科手术学电子杂志, 2023/2024</strong> — 智能化辅助图像实时去雾技术在腹腔镜胆囊切除术中的应用研究(共同一作作者)</li><li><strong>中华肝脏外科手术学电子杂志, 2023</strong> — 增强现实、虚拟现实与混合现实在腔镜肝脏外科中的应用进展(共同一作作者)</li></ul><p><strong>ACS Clinical Congress 2025</strong></p><ul><li><strong>Oral</strong> — Biomarker Risk Scoring Algorithm And Preoperative Stratification In Patients With Lung Cancer</li><li><strong>Oral</strong> — Differences In Perioperative Complications Of Laparoscopic Cholecystectomy Under Voice Risk Alert System (V-RAS)-assisted Monitoring: Results From 48 Hospitals In Western China</li><li><strong>Oral</strong> — Intraoperative Smart Molecular Imaging And Recognition To Enhance Surgeons' Ability To Identify Lymph Node Metastasis In Thyroid Surgery</li><li><strong>Oral</strong> — Comparison Of Robot-assisted And Conventional Laparoscopic Hepatectomy For Perioperative Outcomes: A Clinical Randomized Controlled Trial (RCT)</li><li><strong>ePoster</strong> — Cost-effectiveness Of Endoscopic Magnetic Traction Technology Compared To Laparoscopic Heller Myotomy For The Treatment Of Achalasia</li></ul><p><strong>ACS / CMAIC / FIS 2023</strong></p><ul><li><strong>ePoster</strong> — Application Of Computer Intelligent Surgical Confidential Assistant In Laparoscopic Liver Resection</li><li><strong>ePoster</strong> — Intelligent Surgical Confidential Assistant Helps Precise Magnetic Assisted Vascular Anastomosis</li><li><strong>Poster</strong> — Intelligent Surgery Enters the Blind Spot of Lumpectomy Liver Resection</li><li><strong>Poster</strong> — Intelligent digital fogging technology shows great potential in laparoscopic hepatectomy surgery</li><li><strong>Poster</strong> — Prospects for intelligent surgical machine assistants in precision liver segment resection</li><li><strong>Poster</strong> — Application of Orthogonal Decomposition in Surgical Image Segmentation - for Unsupervised Adaptability in Intraoperative Surgical Image Recognition Navigation</li><li><strong>Poster</strong> — Intraoperative Image Detection and Clearing System Based on Generative Adversarial Network</li></ul>"
"content": "<p><strong>期刊论文</strong></p><ul><li><p><strong>Frontiers in Oncology, 2025</strong> — Development of an AI-driven digital assistance system for real-time safety evaluation and quality control in laparoscopic liver surgery共同第一作者</p></li><li><p><strong>中华肝脏外科手术学电子杂志, 2023</strong> — 智能化辅助图像实时去雾技术在腹腔镜胆囊切除术中的应用研究(共同一作作者)</p></li><li><p><strong>中华肝脏外科手术学电子杂志, 2023</strong> — 增强现实、虚拟现实与混合现实在腔镜肝脏外科中的应用进展(共同一作作者)</p></li></ul><p><strong>ACS Clinical Congress 2025</strong></p><ul><li><p><strong>Oral</strong> — Biomarker Risk Scoring Algorithm And Preoperative Stratification In Patients With Lung Cancer</p></li><li><p><strong>Oral</strong> — Differences In Perioperative Complications Of Laparoscopic Cholecystectomy Under Voice Risk Alert System (V-RAS)-assisted Monitoring: Results From 48 Hospitals In Western China</p></li><li><p><strong>Oral</strong> — Intraoperative Smart Molecular Imaging And Recognition To Enhance Surgeons Ability To Identify Lymph Node Metastasis In Thyroid Surgery</p></li><li><p><strong>Oral</strong> — Comparison Of Robot-assisted And Conventional Laparoscopic Hepatectomy For Perioperative Outcomes: A Clinical Randomized Controlled Trial (RCT)</p></li><li><p><strong>ePoster</strong> — Cost-effectiveness Of Endoscopic Magnetic Traction Technology Compared To Laparoscopic Heller Myotomy For The Treatment Of Achalasia</p></li></ul><p></p><p><strong>ACS / CMAIC / FIS 2023</strong></p><ul><li><strong>ePoster</strong> — Application Of Computer Intelligent Surgical Confidential Assistant In Laparoscopic Liver Resection</li><li><strong>ePoster</strong> — Intelligent Surgical Confidential Assistant Helps Precise Magnetic Assisted Vascular Anastomosis</li><li><strong>Poster</strong> — Intelligent Surgery Enters the Blind Spot of Lumpectomy Liver Resection</li><li><strong>Poster</strong> — Intelligent digital fogging technology shows great potential in laparoscopic hepatectomy surgery</li><li><strong>Poster</strong> — Prospects for intelligent surgical machine assistants in precision liver segment resection</li><li><strong>Poster</strong> — Application of Orthogonal Decomposition in Surgical Image Segmentation - for Unsupervised Adaptability in Intraoperative Surgical Image Recognition Navigation</li><li><strong>Poster</strong> — Intraoperative Image Detection and Clearing System Based on Generative Adversarial Network</li></ul>"
}
]
],
"title": "论文与会议",
"hidden": false,
"columns": 1
},
{
"id": "54972f49-3c83-4912-b429-dc659a02eda9",
"type": "summary",
"title": "创新创业与成果转化",
"hidden": false,
"columns": 1,
"items": [
{
"id": "8ed9e356-fdbf-4c6b-bf15-4033f40b85f5",
"hidden": false,
"content": "<ul><li><strong>专利转化</strong>2026 年相关专利完成 <strong>50 万元</strong>技术许可 / 转化公示</li><li><strong>创业实践</strong>2023 年依托大学生创新创业实践创立公司,获 <strong>30 万元</strong>创业资助</li><li><strong>典型案例</strong>2025 年“多设备兼容的术中影像记录分析系统”入选陕西首批 <strong>30 个</strong>“数据要素×”典型案例</li></ul>"
"content": "<ul><li><p><strong>专利转化</strong>2026 年相关专利完成 <strong>50 万元</strong>技术许可 / 转化公示</p></li><li><p><strong>创业实践</strong>2023 年依托大学生创新创业实践创立公司,获 <strong>30 万元</strong>创业资助</p></li><li><p><strong>典型案例</strong>2025 年“多设备兼容的术中影像记录分析系统”入选陕西<strong>首批“数据要素×”典型案例</strong></p></li></ul><p></p>"
}
]
],
"title": "创新创业与成果转化",
"hidden": false,
"columns": 1
},
{
"id": "559335bd-99c4-44c2-97b6-420b011415f7",
"type": "summary",
"title": "媒体报道",
"hidden": false,
"columns": 1,
"items": [
{
"id": "83f3ac59-c07d-4270-b786-9234eaf1da92",
"hidden": false,
"content": "<ul><li><strong>新华社</strong>《西安交通大学:扎根西部传薪火 服务国家育英才》2026报道人与项目实践</li><li><strong>中国教育报</strong>《产学研“抱团”闯出创新路》2025关注创新港产学研协同实践</li><li><strong>陕西省科学技术厅</strong>转载陕西网《后端深度融合科学研究突破“围墙之困”》2025</li></ul>"
"content": "<ul><li><p><strong>新华社</strong>《西安交通大学:扎根西部传薪火 服务国家育英才》2026130周年校庆专栏其中报道人与项目实践</p></li><li><p><strong>中国教育报</strong>《产学研“抱团”闯出创新路》2025关注创新港产学研协同实践,其中报道本人与项目实践</p></li><li><p><strong>陕西省科学技术厅</strong>转载《后端深度融合科学研究突破“围墙之困”》2025关注复合型创新创业人才培养</p></li></ul><p></p>"
}
]
],
"title": "社会影响",
"hidden": false,
"columns": 1
},
{
"id": "752ddba0-3400-4e33-8f54-49fc3a4b57b9",
"type": "certifications",
"title": "编写著作",
"hidden": false,
"columns": 1,
"items": [
{
"id": "b71f628c-04ea-4d2b-9452-35f4dc0e0054",
@@ -1161,7 +1166,85 @@
},
"description": ""
}
]
],
"title": "编写著作",
"hidden": false,
"columns": 1
},
{
"id": "f4fa59b9-34cf-41b0-b324-f07f07934fc2",
"type": "summary",
"items": [
{
"id": "dff431c1-739e-48f9-a835-6c8b1965c5a3",
"hidden": false,
"content": "<p><strong>西安交通大学外科梦工场</strong><br/>吕毅教授课题组<br/><strong>导师:</strong>吴荣谦教授</p>"
}
],
"title": "所在课题组",
"hidden": false,
"columns": 1
},
{
"id": "4a9c2385-2f79-4d50-99cf-6d2aaef0b6c6",
"type": "volunteer",
"items": [
{
"id": "fc2f36e5-6737-4c77-a6e7-9d9a4f9f7250",
"hidden": false,
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"description": "<p>2024<br/>科技副校长聘任 / 中学宣讲与科普服务</p>",
"organization": "西安市高陵区第四中学教育集团"
},
{
"id": "ce1f0d77-245c-4145-9b30-0ff274b31834",
"hidden": false,
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"description": "<p>2022.12.30<br/>全血 400ml</p>",
"organization": "无偿献血"
},
{
"id": "a8b80562-00fc-42bb-ad5a-d8a0cd10bbbf",
"hidden": false,
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"description": "<p>2023.06.03<br/>成分血 2 治疗量</p>",
"organization": "无偿献血"
},
{
"id": "215ce4ab-7703-4d6e-9396-513c64877ead",
"hidden": false,
"period": "",
"website": {
"url": "",
"label": "",
"inlineLink": false
},
"location": "",
"description": "<p>2024.10.12<br/>全血 200ml</p>",
"organization": "无偿献血"
}
],
"title": "志愿及社会活动",
"hidden": false,
"columns": 1
}
]
}