diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..46cda80
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# deploy.sh — 本地运行,构建并上传到 VPS
+set -euo pipefail
+
+VPS_USER="root"
+VPS_HOST="your.vps.ip" # ← 替换为你的 VPS IP 或域名
+VPS_DIR="/opt/mikivl"
+
+info() { printf "\033[36m[INFO]\033[0m %s\n" "$*"; }
+ok() { printf "\033[32m[OK]\033[0m %s\n" "$*"; }
+die() { printf "\033[31m[ERROR]\033[0m %s\n" "$*" >&2; exit 1; }
+
+# ── Step 1: 本地构建 ──────────────────────────────────────────────────────────
+info "Building Vite app (base=/app/) ..."
+npm run build
+ok "Build complete → dist/"
+
+# ── Step 2: 上传文件 ──────────────────────────────────────────────────────────
+info "Uploading to ${VPS_USER}@${VPS_HOST}:${VPS_DIR} ..."
+
+ssh "${VPS_USER}@${VPS_HOST}" "mkdir -p ${VPS_DIR}/{homepage,app,nginx,program1/server,program1/node_modules}"
+
+# 个人主页
+scp homepage/index.html "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/homepage/"
+
+# App 构建产物
+rsync -az --delete dist/ "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/app/"
+
+# 后端源码
+rsync -az server/ "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/program1/server/"
+scp package.json package-lock.json "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/program1/"
+
+# Nginx 配置 + Docker Compose
+scp nginx/default.conf "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/nginx/"
+scp docker-compose.yml "${VPS_USER}@${VPS_HOST}:${VPS_DIR}/"
+
+ok "Upload complete."
+
+# ── Step 3: VPS 上安装依赖并重启服务 ─────────────────────────────────────────
+info "Restarting services on VPS ..."
+
+ssh "${VPS_USER}@${VPS_HOST}" bash <<'REMOTE'
+set -euo pipefail
+cd /opt/mikivl/program1
+npm ci --omit=dev
+cd /opt/mikivl
+docker compose up -d --remove-orphans
+docker compose ps
+REMOTE
+
+ok "Deployment complete! → https://www.mikivl.online"
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..7baa322
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,38 @@
+services:
+ hono-server:
+ image: node:22-alpine
+ working_dir: /app
+ command: npx tsx server/index.ts
+ environment:
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
+ - MODELS_FILE=/app/models_data/models.json
+ - NODE_ENV=production
+ volumes:
+ - ./program1:/app:ro
+ - models_data:/app/models_data
+ expose:
+ - "3001"
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/models"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ nginx:
+ image: nginx:1.27-alpine
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
+ - ./homepage:/usr/share/nginx/html/homepage:ro
+ - ./app:/usr/share/nginx/html/app:ro
+ - /etc/letsencrypt:/etc/letsencrypt:ro
+ depends_on:
+ hono-server:
+ condition: service_healthy
+ restart: unless-stopped
+
+volumes:
+ models_data:
diff --git a/homepage/index.html b/homepage/index.html
new file mode 100644
index 0000000..a94274e
--- /dev/null
+++ b/homepage/index.html
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+ mikivl — 开发项目
+
+
+
+
+
+
+
+
+
+
+ // hello, world
+ mikivl.
+
+ 独立开发者,专注于构建 AI 驱动的生产力工具。
+ 喜欢探索 AI 与实用工具的交叉地带,用代码解决真实问题。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // skills
+ 技术栈
+
+
+
Frontend
+
+ - React / TypeScript
+ - Vite / TailwindCSS
+ - TipTap / ProseMirror
+ - Framer Motion
+
+
+
+
Backend
+
+ - Node.js / Hono
+ - REST / SSE
+ - Docker / Nginx
+ - Linux / VPS
+
+
+
+
AI
+
+ - Anthropic Claude API
+ - Prompt Engineering
+ - Streaming (SSE)
+ - RAG / Embeddings
+
+
+
+
Tooling
+
+ - Git / GitHub
+ - Vitest / ESLint
+ - Claude Code
+ - SSH / rsync
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nginx/default.conf b/nginx/default.conf
new file mode 100644
index 0000000..4c6cfa3
--- /dev/null
+++ b/nginx/default.conf
@@ -0,0 +1,55 @@
+server {
+ listen 80;
+ server_name www.mikivl.online mikivl.online;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ server_name www.mikivl.online mikivl.online;
+
+ ssl_certificate /etc/letsencrypt/live/www.mikivl.online/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/www.mikivl.online/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_session_cache shared:SSL:10m;
+
+ add_header X-Frame-Options SAMEORIGIN;
+ add_header X-Content-Type-Options nosniff;
+ add_header Referrer-Policy strict-origin-when-cross-origin;
+
+ # ── 后端 API(反向代理到 Hono,/api/ 优先匹配)──
+ location /api/ {
+ proxy_pass http://hono-server:3001;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # SSE 流式响应必须关闭缓冲
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_read_timeout 300s;
+ chunked_transfer_encoding on;
+ }
+
+ # ── 读书笔记 App(/app/ 子路径,SPA)──
+ location /app/ {
+ alias /usr/share/nginx/html/app/;
+ index index.html;
+ try_files $uri $uri/ /app/index.html;
+ }
+
+ # ── 个人主页(根路径)──
+ location / {
+ root /usr/share/nginx/html/homepage;
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ # 静态资源长缓存
+ location ~* \.(js|css|svg|ico|woff2?|png|jpg|webp)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+}
diff --git a/server/index.ts b/server/index.ts
index f53048d..b22dfa3 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -10,7 +10,7 @@ const app = new Hono()
app.use('*', cors())
// ── Model config persistence ──────────────────────────────────────────────────
-const MODELS_FILE = path.resolve('models.json')
+const MODELS_FILE = process.env.MODELS_FILE ?? path.resolve('models.json')
type ModelConfig = {
id: string
diff --git a/src/lib/export.ts b/src/lib/export.ts
index 51bfd22..9d93c0d 100644
--- a/src/lib/export.ts
+++ b/src/lib/export.ts
@@ -169,7 +169,7 @@ function nodesToDocxParagraphs(nodes: TipTapNode[]): Paragraph[] {
})
case 'codeBlock': {
const code = (node.content ?? []).map((n: TipTapNode) => n.text ?? '').join('')
- return code.split('\n').map(line => new Paragraph({
+ return code.split('\n').map((line: string) => new Paragraph({
children: [new TextRun({ text: line, font: 'Courier New' })],
}))
}
diff --git a/vite.config.ts b/vite.config.ts
index ee7de76..14c19dd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
+ base: '/app/',
plugins: [react(), tailwindcss()],
server: {
proxy: {