From jurislm-claude-plugins-lessons-learned
This skill should be used when encountering bugs, debugging issues, writing tests, setting up infrastructure, reviewing PRs, or starting new projects. Activate when the user faces errors, test failures, deployment problems, or asks about best practices. Also activate when patterns like "why is this failing", "how to debug", "test not passing", "deployment issue", or "PR review" appear.
npx claudepluginhub terry90918/jurislm-claude-pluginsThis skill uses the workspace's default tool permissions.
從實際踩坑中提煉的 68 個關鍵教訓與改進方案,按主題分類。每個模式包含問題描述、根因分析、正確解法。
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Share bugs, ideas, or general feedback.
從實際踩坑中提煉的 68 個關鍵教訓與改進方案,按主題分類。每個模式包含問題描述、根因分析、正確解法。
原則:先解決環境問題,再考慮改代碼。
| 症狀 | 正確做法 | 錯誤做法 |
|---|---|---|
| Docker 沒運行 | 請用戶啟動 Docker | 改測試配置繞過 |
| 工具找不到 | 檢查代碼用什麼(unrar vs unar) | 猜測工具名稱 |
Cannot find module | 先 bun install | 推給其他分支 |
來源:多個專案的反覆踩坑
警訊:修改 3+ 個配置檔來「繞過」問題。
正確做法:停下問「有沒有更直接的解法?」
來源:JurisLM 基礎設施配置
適用:複雜基礎設施問題。
流程:建立檢查清單 → 交叉驗證 → Context7 查文檔 → 理解架構再下結論。
來源:Coolify + Hetzner 部署問題
來源:多次用戶互動經驗
流程:看錯誤 → grep package.json → ls node_modules → bun install → 再次 typecheck。
關鍵:不要直接改型別定義來消除錯誤,先確認依賴安裝正確。
來源:多個 Next.js 專案
做法:Context7 查兩邊文檔,找根本原因。
案例:ESLint 報 useEffect 依賴問題 → 官方推薦 useSyncExternalStore 替代 effect。
來源:stock 專案 settings page
原則:修改類型定義後,測試的 mock 和預期值必須反映新邏輯。
來源:JurisLM agent 型別重構
流程:先 git log + git status 確認狀態 → 比對 commit → 驗證建議適用性。
原則:不盲目採納所有建議,驗證每條建議是否適用於當前代碼。
來源:stock PR #12 Copilot Review
原則:提案前先問「這真的在驗證業務邏輯,還是只在檢查表層風格?」
來源:多個專案 code review
| 陷阱 | 正確 | 錯誤 |
|---|---|---|
| 執行測試 | bun run test(vitest) | bun test(Bun 內建) |
| 清除 mock | vi.resetModules()(重置模組緩存) | vi.clearAllMocks()(只清記錄) |
何時用 resetModules:測試環境變數時,需要完全重置模組緩存。
來源:JurisLM + stock 專案
聲稱「完成」之前必須全通過:
bun run test:run # 全部通過
bun run typecheck # 無錯
bun run lint # 無錯
來源:核心工作原則
問題 1:Playwright use(page) 被 react-hooks/rules-of-hooks 誤判。
eslint-plugin-playwright + 關閉 react-hooks/rules-of-hooksglobalIgnores 整個目錄問題 2:Vitest 載入 Playwright spec 導致錯誤。
vitest.config.ts 必須 exclude:[...configDefaults.exclude, 'tests/e2e/**']node_modules 等排除項)來源:stock PR #12
| 陷阱 | 解法 |
|---|---|
| Conv auto-trigger 不觸發 | 需 ?trigger=1 + hasUserMessage + !hasAssistantMessage |
| Mobile webkit 點擊失敗 | locator.evaluate((el: HTMLElement) => el.click()) 或 test.skip |
| Mock assistant message 停用 auto-trigger | 依賴 DB seed data |
getByText strict mode 多個匹配 | { exact: true } 精確匹配 |
contentEditable toBeFocused 不可靠 | 測試功能性(click + type + verify) |
來源:JurisLM E2E 測試
問題:完全 ignore 一個目錄 = 失去所有 lint 保護(formatting、type safety、best practices 全部消失)。
正確做法:為目標檔案設定專屬 ESLint override,只關閉真正不適用的規則。
範例:E2E 測試檔案
{
files: ['tests/e2e/**/*.ts'],
plugins: { playwright },
rules: {
...playwright.configs['flat/recommended'].rules,
'react-hooks/rules-of-hooks': 'off', // Playwright use() 不是 React hook
'@next/next/no-html-link-for-pages': 'off', // E2E 不需要 Next.js 規則
},
}
來源:stock PR #12 — Copilot 指出
tests/e2e/**不該全域忽略
問題:getSnapshot 每次返回新物件 → Object.is 永遠 false → 無限 re-render → Maximum update depth exceeded。
根因:每次 JSON.parse 產生新物件引用。
解法:cache raw string + parsed result,只在 raw string 改變時才創建新物件。
let cachedRaw = '';
let cachedResult = defaultSettings;
function readSettings(): Settings {
const raw = localStorage.getItem('settings') || '';
if (raw !== cachedRaw) {
cachedRaw = raw;
cachedResult = raw ? JSON.parse(raw) : defaultSettings;
}
return cachedResult; // 穩定引用
}
來源:stock 專案 settings page
問題:mock 回傳格式與 API 不同 → undefined.name → runtime crash。
案例:/api/market/indices 回傳 { taiex: {...}, otc: {...} }(物件),mock 回傳 [{...}](陣列)。
最佳實踐:寫 mock 前先讀 API route handler 的回傳型別 → 從 TypeScript 型別定義反推 mock 結構。
來源:stock E2E 測試
問題:聲稱「全部修完」但漏了 1 項 = 信任崩壞。
做法:修完 N 項後必須逐一 Read 對應檔案行號確認實際變更。
追蹤表格:
| # | 檔案:行號 | 建議 | 狀態 | 驗證 |
|---|-----------|------|------|------|
| 1 | test-base.ts:15 | 改用 **/api/** | ✅ 已修 | ✅ 已讀 |
來源:stock PR #12 review fixes
| 陷阱 | 問題 | 正確做法 |
|---|---|---|
.catch(() => {}) | 吞掉斷言失敗,測試永遠不會 fail | 移除 .catch() |
not.toBeVisible() | 語義不清 | 用 toBeHidden() 更語義化 |
| 缺少 assertion | 測試無斷言 = 永遠通過 | 每個 test 至少一個 assertion |
忘記 await | async matcher 未等待 | toHaveClass() 等必須 await |
ESLint 規則:
playwright/expect-expect — 每個 test 至少一個 assertionplaywright/missing-playwright-await — async matcher 必須 awaitplaywright/no-useless-not — 用 toBeHidden() 替代 not.toBeVisible()來源:stock PR #12 eslint-plugin-playwright
問題:merge commit 包含非 merge 的修改 → git history 混亂。
策略:
git stash review fixesgit stash pop來源:stock PR #12 merge + review 流程
問題:先 prisma.model.count() 檢查數量限制,再 prisma.model.create() 新增,兩步之間有 race condition。並發請求可能同時通過 count 檢查,超過限制。
案例:觀察清單上限 50 筆,兩個請求同時讀到 49 筆 → 都通過 → 變成 51 筆。
正確做法:用 prisma.$transaction() 包裝 count + create:
const result = await prisma.$transaction(async (tx) => {
const count = await tx.holding.count({ where: { portfolioId } });
if (count >= WATCHLIST_LIMIT) {
throw new Error(`觀察清單已達上限 ${WATCHLIST_LIMIT} 筆`);
}
return tx.holding.create({ data: { ... } });
});
教訓:
$transaction 在 PostgreSQL 中使用 serializable isolation 確保原子性來源:stock PR review — watchlist/holding 新增的 race condition(2026-02-10)
問題:將 raw prisma.xxx.findMany() 重構為 hook method 呼叫(如 usePortfolio 的方法),但測試仍在 mock 舊的 Prisma 方法 → 測試通過但沒測到真正的邏輯。
案例:API route 從直接呼叫 prisma.holding.findMany() 改為呼叫 hook 的 addToWatchlist(),但測試仍 mock prisma.holding.findMany → 測試通過但實際上沒走過新的 hook 邏輯。
正確做法:
vi.spyOn 確認新方法確實被呼叫來源:stock PR review — API route 重構後測試 mock 不一致(2026-02-10)
問題:測試需要 mock prisma.$transaction(),但 $transaction 接受 callback 並注入 tx 物件。
正確做法:mock $transaction 時執行 callback 並注入 mock tx:
const mockTx = {
holding: { count: vi.fn(), create: vi.fn(), findMany: vi.fn() },
watchlistGroup: { findUnique: vi.fn() },
};
vi.mocked(prisma.$transaction).mockImplementation(async (cb) => {
return cb(mockTx as unknown as PrismaClient);
});
教訓:
$transaction 的 callback 參數是一個與 prisma 同介面的 tx 物件mockResolvedValue)mockTx 上設定各 model 的回傳值來控制 transaction 內行為來源:stock PR review — $transaction 測試模式(2026-02-10)
問題:同一服務有獨立安裝和 Plugin 兩套工具。
診斷:env | grep TOKEN + ls ~/.claude/plugins/
解法:移除獨立安裝(.claude.json 中的 mcpServers),統一使用 Plugin。
來源:jurislm-plugins MCP 遷移
做法:Context7 查 API 文檔確認參數。
備案:用 curl 直接呼叫 API 繞過 MCP。
來源:Coolify MCP Server
原則:建立前先列出(DNS records、applications),注意 wildcard。
案例:建 subdomain 前先檢查 DNS wildcard 是否已涵蓋。
來源:Cloudflare + Coolify 部署
| 類型 | 更新 FQDN | 可更新欄位 |
|---|---|---|
| Application | 直接更新 | FQDN + 其他設定 |
| Service | 修改 docker_compose_raw 的 Traefik labels | name, description, docker_compose_raw |
來源:Coolify 部署管理
容量估算:Embedding ≈ 維度 × 4B × 記錄數。
PostgreSQL 遷移流程:停容器 → 複製資料(chown 999:999)→ symlink → 啟動。
來源:jurislm-shared-db Volume 遷移
UNNEST + ON CONFLICT 批次 10K 筆來源:JurisLM 資料同步
版本規則:
feat: → MINOR(0.1.0 → 0.2.0)fix: → PATCH(0.1.0 → 0.1.1)feat!: / BREAKING CHANGE: → MAJOR前置需求:GitHub Repo Settings → Actions → 允許 GitHub Actions 建立 PR。
來源:stock + lawyer-app Release Please 配置
流程:備份欄位 → 備份資料 → 刪舊 constraint → 轉換 → 驗證(0 筆異常)→ 新 constraint。
來源:JurisLM 資料庫遷移
問題:在 Coolify/CI 中設定 ENV_VAR=placeholder 作為佔位提醒,但代碼用 if (process.env.ENV_VAR) 做條件判斷,truthy 值觸發功能載入,導致不可預期的行為。
案例:Payload CMS 的 S3 plugin 使用 process.env.S3_ACCESS_KEY_ID 條件載入。設定 S3_ACCESS_KEY_ID=placeholder 導致 S3 plugin 載入 → 註冊客戶端元件 → importMap.js 中找不到 → Admin UI 白屏。
教訓:
來源:lawyer-app Payload Admin 白屏事件(2026-02-09)
問題:repo 中同時存在 package-lock.json 和 bun.lock,Nixpacks 偵測到 package-lock.json 自動使用 npm ci,導致 build 失敗。
解法:
.gitignorenixpacks.toml 明確指定 package manager(不要用 NIXPACKS_*_CMD env vars,會導致 CLI 解析錯誤)來源:lawyer-app Coolify 部署(2026-02-09)
問題:Next.js + Payload CMS 在 next build 時生成 importMap.js,此檔案映射所有 plugin 的客戶端元件。如果 build 環境的 env vars 與 runtime 不同,importMap 可能缺少(或多出)某些元件映射。
影響:
教訓:
getFromImportMap: PayloadComponent not found 警告來源:lawyer-app Payload Admin 白屏事件(2026-02-09)
問題:在 Cloudflare WAF 建立自訂規則,使用 cf.bot_management.verified_bot 欄位,「部署」按鈕無反應,無錯誤提示。
根因:cf.bot_management.verified_bot 需要 Bot Management 付費方案(Enterprise add-on),Free plan 無法使用。Cloudflare dashboard 不顯示明確錯誤訊息,按鈕靜默失敗。
Free plan 可用欄位:http.host、ip.src、http.user_agent、http.request.uri.path 等基本欄位。
替代方案(不需 WAF):
middleware.ts — Next.js HTTP Basic Auth(401 直接擋爬蟲)robots.ts — Disallow: /(advisory 信號)next.config.ts — X-Robots-Tag: noindex, nofollow, noarchive header教訓:Cloudflare dashboard UI 不一定會對付費功能的表達式報錯 — 按鈕無反應時先懷疑欄位可用性。
來源:lawyer-app staging 爬蟲封鎖(2026-02-09)
問題:finally { await cleanup1(); await cleanup2(); controller.close(); } → cleanup1 拋錯會跳過後續。
正確做法:每個 async 操作獨立 try-catch,確保 controller.close() 一定執行。
finally {
try { await cleanup1(); } catch (e) { /* log */ }
try { await cleanup2(); } catch (e) { /* log */ }
controller.close(); // 一定執行
}
來源:JurisLM PR #134 — SSE stream memory leak
Promise.race + setTimeout 包裝所有外部工具呼叫來源:JurisLM agent tool execution
問題:全域 rate limit 不夠 → 單一資源被 rapid-fire 攻擊。
解法:需要 per-conversation / per-resource 限制。
來源:JurisLM API 安全
[REDACTED](而非靜默移除)[^\S\n]+ 而非 \s+)來源:JurisLM content sanitization
問題:NODE_ENV !== "development" 判斷會在 test 環境也隱藏錯誤,導致測試無法驗證錯誤訊息。
正確做法:NODE_ENV === "production" — 只在 production 隱藏。
前端配套:toUserFriendlyError() 函數映射內部錯誤為用戶友善訊息(中文)。
來源:JurisLM PR #134 review(2026-02-09)
問題:所有 API 錯誤統一處理,用戶看到「Internal Server Error」。
正確做法:區分 429(rate limit)和 500+(server error),提供不同用戶訊息和重試策略:
if (apiError.status === 429) throw new Error("AI 服務目前繁忙,請稍後再試");
if (apiError.status >= 500) throw new Error("AI 服務暫時無法使用,請稍後再試");
來源:JurisLM PR #134 Anthropic API error handling(2026-02-09)
問題:帶 g flag 的 regex 呼叫 test() 後 lastIndex 推進,下次 test() 從中間開始→漏匹配或行為異常。
正確做法:每次使用前重置 pattern.lastIndex = 0。
for (const pattern of DANGEROUS_PATTERNS) {
pattern.lastIndex = 0; // ← 必須重置
if (pattern.test(sanitized)) { ... }
}
來源:JurisLM PR #134 sanitize.ts regex fix(2026-02-09)
問題:String.slice(0, N) 按 UTF-16 code unit 截斷,可能切斷 emoji 或 CJK 擴展字元。
正確做法:Array.from(str).slice(0, N).join("") 按 code point 截斷。
來源:JurisLM PR #134 附件截斷修復(2026-02-09)
問題:排序 API 接受 orderedIds: string[],直接用 index 更新 sortOrder,未驗證陣列內容。攻擊者可傳入重複 ID、不存在 ID、或遺漏部分 ID。
正確做法:三重驗證:
// 1. 無重複
const unique = new Set(orderedIds);
if (unique.size !== orderedIds.length) throw new Error('ID 不可重複');
// 2. 全部存在
const existing = await tx.model.findMany({ where: { id: { in: orderedIds } } });
if (existing.length !== orderedIds.length) throw new Error('包含不存在的 ID');
// 3. 完整集合(不可遺漏)
const allItems = await tx.model.findMany({ where: { parentId } });
if (allItems.length !== orderedIds.length) throw new Error('必須包含所有項目');
教訓:
$transaction 內一起做Set 檢查重複最高效來源:stock PR review — watchlist groups reorder API 驗證(2026-02-10)
問題:.env.example 是 committed 到 repo 的範本檔案,如果包含真實 DATABASE_URL(含密碼),密碼會被公開。
正確做法:
# .env.example(只放格式範例)
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
教訓:
.env.example 是給新開發者看格式的,不是放真實值的.env* 檔案的變更來源:stock PR review — .env.example 包含真實連線字串(2026-02-10)
問題:switch 隨 case 增長難以維護。
解法:Record<string, Type> map + fallback 比 switch 更易擴展。
案例:TOOL_TO_AGENT_TYPE map 取代 detectAgentType switch。
來源:JurisLM agent routing
來源:JurisLM agent conversation
問題:content.length / 4 對中文法律文本嚴重低估 token 數(中文 1 字 ≈ 0.67 token,不是 0.25)。
正確做法:分別計算 CJK 和 ASCII 字元:
const CJK_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g;
export function estimateTokens(text: string): number {
const cjkCount = (text.match(CJK_REGEX) ?? []).length;
const asciiCount = text.length - cjkCount;
return Math.ceil(cjkCount / 1.5 + asciiCount / 4);
}
來源:JurisLM PR #134 agent.ts token estimation(2026-02-09)
問題:所有 tool 共用 30s timeout,LLM 呼叫類 tool 經常超時。
正確做法:TOOL_TIMEOUTS map 差異化設定:
來源:JurisLM PR #134 tool-registry.ts(2026-02-09)
問題:SSE event 和 data 行可能分在不同 TCP chunk。Line lookahead lines[i+1] 在 chunk 邊界找不到 data 行。
正確做法:State machine 模式 — 用 pendingEventType 追蹤狀態:
let pendingEventType: string | null = null;
for (const line of lines) {
if (line.startsWith("event: ")) pendingEventType = line.slice(7).trim();
else if (line.startsWith("data: ") && pendingEventType) {
handleEvent({ type: pendingEventType, data: JSON.parse(line.slice(6)) });
pendingEventType = null;
} else if (line === "") pendingEventType = null;
}
來源:JurisLM PR #134 use-chat-stream.ts SSE fix(2026-02-09)
見模式 33。
Read 對應檔案行號驗證來源:stock PR #12
核心禁止:絕對不可使用 --no-verify 跳過 pre-commit hook(違反 CLAUDE.md 核心禁止行為)。
問題:merge develop 後 typecheck 失敗常因新增 devDependencies 未安裝。
正確流程:
git merge develop(或其他分支)bun install(安裝新依賴)git commit(讓 pre-commit hook 驗證)若誤用 --no-verify:
git reset --hard HEAD~1 # 撤銷錯誤 commit
bun install # 安裝依賴
git merge develop # 重新 merge
git commit # 正確執行(不跳 hook)
覆蓋遠端錯誤 commit:
git push --force-with-lease # 安全的 force push
教訓:
bun install),不是跳過驗證--force-with-lease 比 --force 更安全(保護遠端變更)來源:jurislm plan-b merge develop 經驗(2026-02-09)
4 個 artifacts:proposal → design → specs(GIVEN/WHEN/THEN)→ tasks。
用途:大型功能開發的結構化流程。
來源:stock + JurisLM 功能開發
做法:用完整名稱(如 code-simplifier:code-simplifier),查 Available agents 列表。
來源:Claude Code 使用經驗
流程:sanitizeContent() → 結構化 JSON → Zod 驗證 → 不執行 LLM 回應的系統操作。
來源:JurisLM security review
問題:join(__dirname, "../../..", ".env.shared") 路徑錯誤(多了一層 ..),但因為 existsSync 檢查 + 靜默跳過,.env.shared 從未被載入。所有 env vars 都用了 fallback 值,測試表面上能通過。
根因:__dirname 在 jurislm_app/tests/ 下,往上 3 層到了 Documents/Github/(而非專案根目錄)。
為什麼危險:
if (existsSync) 靜默跳過)教訓:
__dirname 相對路徑後,用 console.log(envPath) 驗證實際路徑來源:JurisLM 雲端 DB 遷移(2026-02-09)
問題:Git worktree 各自有獨立的 .env.shared(在 .gitignore 中),credentials 不會自動同步。新 worktree 或長期未使用的 worktree 可能保留 placeholder 值。
症狀:
ANTHROPIC_API_KEY=your-anthropic-api-key-here → Zod 驗證失敗TOKEN_ENCRYPTION_KEY=CHANGE_ME_USE_OPENSSL_RAND_HEX_32 → 非 64 位 hexGOOGLE_CLIENT_ID=your-google-client-id-here → OAuth 登入失敗教訓:
diff 比對 .env.shared 與主 worktreeyour-xxx-here),用格式正確但無效的值(如 sk-ant-test-000...)scripts/sync-env.sh 自動從主 worktree 複製 credentials# 快速比對
diff <(grep -E '^[A-Z]' /path/to/main/.env.shared) <(grep -E '^[A-Z]' .env.shared)
來源:JurisLM plan-b worktree 驗收(2026-02-09)
問題:.env.shared 設定 OLLAMA_MODEL=ministral-3:latest,但 Ollama 安裝的是 ministral-3:8b。:latest tag 不一定存在。
症狀:頁面頂部顯示「Ollama 不可用:找不到模型 ministral-3:latest」。
驗證方式:
ollama list | grep ministral
# 輸出:ministral-3:8b abc123 4.1 GB
教訓:
ollama list 完全一致(含 tag):latest 不是萬用 tag — 取決於模型發布者是否有設定curl http://localhost:11434/api/tags | jq '.models[].name'來源:JurisLM 本地開發驗收(2026-02-09)
問題:Google OAuth Console 只註冊了 http://localhost:3000/api/auth/callback/google,但 dev server 跑在 port 3002 → redirect_uri_mismatch。
三個必須一致的值:
NEXTAUTH_URL 環境變數(如 http://localhost:3000)next dev --port 3000)教訓:
NEXTAUTH_URL 改了 port,OAuth callback URL 也會跟著變來源:JurisLM plan-b port 3002 驗收失敗(2026-02-09)
策略:當多個消費者都透過同一個 getter(如 worktreeConfig.syncDatabaseUrl)取得設定值時,修改 getter 讓它優先讀取環境變數,所有消費者自動切換。
案例:11 個檔案都使用 worktreeConfig.syncDatabaseUrl,只改 getter 一處:
get syncDatabaseUrl(): string {
const envUrl = process.env.DATABASE_URL;
if (envUrl) {
return envUrl.replace(/^postgresql\+\w+:\/\//, "postgresql://");
}
return `postgresql://...@localhost:${this._ports.postgres}/jurislm_db`;
}
適用條件:
來源:JurisLM Docker DB → 雲端遷移(2026-02-09)
遷移步驟(已驗證):
.env.shared:指向雲端 DB URLdb reset:psql-based(DROP SCHEMA CASCADE + migrations)取代 Docker composedocker-compose.yml、reset.sh 等CLAUDE.md Quick StartMigration 對全新 DB 跑不過的解法:
users.id 從 INTEGER 改 TEXT)pg_dump --schema-only 從既有 DB 匯入 + 記錄所有 migration 為已套用驗證清單:
bun run typecheck # 型別檢查
bun run lint # Lint
bun run test # 測試(含 E2E 連雲端 DB)
# 瀏覽器驗收:登入、對話、RAG 檢索、側邊欄
來源:JurisLM 9-phase 雲端遷移計畫(2026-02-09)
push: true 是 dev-only問題:Payload CMS 新 production DB 啟動後所有 table 不存在,runtime 報 relation "users" does not exist。盲目加 push: true 到 postgres adapter 無效。
根本原因:
push: true 只在 next dev(development mode)有效,next start(production)完全不觸發prodMigrations 讓 Payload 啟動時自動執行 migration正確做法:
payload CLI 在 Node.js 24 + bun 下壞掉,用程式化 API):bun -e "
import { getPayload } from 'payload';
import config from './payload.config.ts';
const payload = await getPayload({ config });
await payload.db.createMigration({ payload, migrationName: 'initial' });
process.exit(0);
"
import { migrations } from './migrations' + prodMigrations: migrationsnext build prerender 不會炸掉教訓:
payload, req 參數 → 改為 { db } 避免 lint 錯誤來源:Lawyer App Ghost → Payload CMS 遷移 Production 部署(2026-02-09)
問題:安裝 Tailwind CSS v4 後,CSS 檔案載入正常但 utility classes 不生效(所有元素無樣式)。
根因:Tailwind CSS v4 重構了建置流程,不再使用 PostCSS 插件。Vite 專案必須使用 @tailwindcss/vite 插件來掃描模板檔案並生成 utility classes。
正確做法:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});
CSS 引入:
/* globals.css */
@import "tailwindcss";
教訓:
tailwind.config.ts / postcss.config.ts@theme directive 自訂 design tokens來源:JurisLM Dashboard Vite 7 + Tailwind CSS v4 整合(2026-02-09)
問題:COUNT(*) 在 21M+ 行的表上耗時數十秒,不適合 Dashboard 即時顯示。
解法:使用 PostgreSQL 統計表 pg_class.reltuples 取得估算值:
SELECT relname, reltuples::bigint AS estimated_rows
FROM pg_class
WHERE relname = 'documents_051';
注意事項:
-1 表示從未執行 ANALYZE(新表或匯入後未分析)來源:JurisLM Dashboard 共用 DB 統計頁面(2026-02-09)
問題:Dashboard API 查詢遠端 DB(跨機房)timeout,但相同查詢在本地 psql 正常。
根因:Bun.serve() 預設 idleTimeout 為 10 秒,遠端 DB 查詢(尤其是大表 COUNT 或 JOIN)可能超過此限制。
正確做法:
Bun.serve({
port: 3001,
idleTimeout: 120, // 秒,預設 10
fetch: app.fetch,
});
教訓:
idleTimeout 是連線閒置超時,不是請求超時connect_timeout 和 idle_timeout來源:JurisLM Dashboard 遠端 DB 查詢超時(2026-02-09)
問題:turbo prune <workspace> --docker 建立最小化 Docker context 時遇到多個問題。
| 問題 | 原因 | 修正 |
|---|---|---|
turbo prune jurislm_dashboard not found | package.json name 用 hyphen(jurislm-dashboard) | 用 package.json 中的 name 值 |
bun install --frozen-lockfile 失敗 | turbo prune 不修改根 package.json workspaces | 加過濾腳本 + 移除 --frozen-lockfile |
extends "../tsconfig.json" 找不到 | turbo prune 不包含根 tsconfig.json | COPY --from=pruner /app/tsconfig.json . |
| Next.js build 缺環境變數 | SSG page collection 需要 env vars | 建立 dummy .env.shared |
教訓:
turbo prune 的 workspace 名稱是 package.json 中的 name,不是目錄名json/ 目錄缺少根 tsconfig → 手動 COPYturbo prune --dry 確認會包含哪些檔案來源:JurisLM Dashboard Dockerfile 建置(2026-02-09)
問題:本地 Docker build 成功,但 Coolify 部署失敗或行為異常。
根因:Coolify 自動注入 ARG NODE_ENV=production 到每個 build stage,影響 bun install 行為(production 模式不安裝 devDependencies)。
正確做法:
# Builder stage - 明確覆蓋 NODE_ENV
RUN NODE_ENV=development bun install
# 或在 stage 開頭重設
ENV NODE_ENV=development
RUN bun install
Coolify 也注入環境變數 ARG:SHARED_DATABASE_URL 等 → runtime env vars 自動可用,不需 --env-file。
來源:JurisLM Dashboard + App Coolify 部署(2026-02-09)
問題:從 builder 階段 COPY node_modules 到 runner,workspace symlinks 壞掉 → Cannot find package runtime error。
根因:Bun workspace 在 node_modules 中建立 symlinks 指向 workspace packages,COPY 會破壞這些 symlinks。
正確做法:Runner 階段重新安裝 production dependencies:
# Runner stage
COPY --from=builder /app/package.json ./
COPY --from=builder /app/bun.lock ./
COPY --from=builder /app/<workspace>/package.json ./<workspace>/
RUN bun install --production
教訓:
--production 只安裝 runtime dependencies,減少 image 大小來源:JurisLM Dashboard runner 階段
Cannot find package 'hono'(2026-02-09)
問題:Hono serveStatic({ root: "./dist" }) 返回 404,但檔案確實存在。
根因:serveStatic 的 root 是相對於 process.cwd()(即 Docker WORKDIR),不是相對於程式碼檔案位置。
正確做法:確保 WORKDIR 設定正確:
# 如果 dist 在 /app/jurislm_dashboard/dist
WORKDIR /app/jurislm_dashboard
# 則 serveStatic({ root: "./dist" }) 解析為 /app/jurislm_dashboard/dist
或使用絕對路徑:
serveStatic({ root: "/app/jurislm_dashboard/dist" })
來源:JurisLM Dashboard 靜態檔案 404(2026-02-09)
問題:手動 CREATE TABLE 建立了資料表,事後才發現有 migration 系統。往 migrations 表補登記錄時,用了 md5() 算 checksum(32 字元),但系統用 SHA256(64 字元);sql_content 用佔位符(-- 已手動建立)而非完整 SQL。
後果:
db status 報 checksum mismatch(長度不同、演算法不同)db migrate 時,migration 記錄與本地檔案不一致正確做法:
db migrate → 系統自動記錄正確 checksum + sql_contentcalculateChecksum(filePath) 同樣的 SHA256 算法# 驗證 checksum 一致
node -e "
const { createHash } = require('crypto');
const content = require('fs').readFileSync('path/to/migration.sql');
console.log(createHash('sha256').update(content).digest('hex'));
"
教訓:
db status 驗證無 mismatch來源:jurislm plan-b deleted_judgments 表建立 — 用 md5 補登導致 checksum mismatch(2026-02-10)
問題:用 regex 解析司法院 Delete-Infor.csv,匹配率 0%。
根因:
\r\n,regex $ 前殘留 \rTPH,106,重上,41,20180427)和 6 段(TPSV,106,台抗,1132,,20171026),尾逗號正確做法:
// 1. 先清理 CRLF
const clean = text.replace(/\r/g, "");
// 2. 用 position-based parsing 取代 regex
const lines = clean.split("\n").filter(Boolean);
for (const line of lines) {
const firstQuote = line.indexOf('"');
const lastQuote = line.lastIndexOf('"');
const jid = line.substring(firstQuote + 1, lastQuote).replace(/,+$/, ""); // 移除尾逗號
const meta = line.substring(0, firstQuote - 1); // 引號前的部分
const [deletedDate, yearMonth, courtName] = meta.split(",");
}
教訓:
來源:jurislm plan-b Delete-Infor.csv 匯入 — regex 0% 匹配率(2026-02-10)
問題:8 位日期字串 20210231 通過 regex /^\d{8}$/ 驗證,但 2 月 31 日不存在。
嚴重後果:PostgreSQL UNNEST batch INSERT 是 all-or-nothing — 一筆壞日期導致整個 batch(可能 1000 筆)全部 rollback。
正確做法:
function isValidDate(dateStr: string): boolean {
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6));
const day = parseInt(dateStr.substring(6, 8));
if (month < 1 || month > 12 || day < 1) return false;
const maxDays = new Date(year, month, 0).getDate(); // 該月最大天數
return day <= maxDays; // 自動處理閏年
}
防禦性批次 INSERT:
const valid = records.filter(r => isValidDate(r.date));
const skipped = records.length - valid.length;
if (skipped > 0) logger.warn(`跳過 ${skipped} 筆不合法日期`);
// 只 INSERT 驗證通過的記錄
await sql`INSERT INTO ... SELECT * FROM UNNEST(...) ON CONFLICT DO NOTHING`;
教訓:
來源:jurislm plan-b Delete-Infor.csv 日期驗證 — 2/31 導致 batch INSERT 失敗(2026-02-10)
問題:新服務(jurislm_dashboard)需要限制公開存取,直接實作 Basic Auth 但漏掉搜尋引擎防護層。用戶指出 lawyer 專案已有更完整的三層防護方案。
Lawyer 專案三層防護(Next.js 版本):
middleware.ts 檢查 STAGING=true → 要求認證(跳過 /api/*)STAGING=true 時回傳 Disallow: /next.config.ts 加 noindex, nofollow, noarchive headerHono 版本(jurislm_dashboard):
if (config.STAGING === "true") {
app.get("/robots.txt", (c) => c.text("User-agent: *\nDisallow: /\n"));
app.use("*", async (c, next) => {
await next();
c.header("X-Robots-Tag", "noindex, nofollow, noarchive");
});
app.use("*", async (c, next) => {
if (c.req.path.startsWith("/api/")) return next();
return auth(c, next);
});
}
關鍵設計決策:
STAGING=true 環境變數控制開關(非寫死)/api/* 路由豁免認證(webhook/健康檢查需要)/api/health 在 auth middleware 之前註冊STAGING_USER / STAGING_PASSWORD(預設 admin/staging2026)教訓:
grep -r "STAGING\|basicAuth\|Basic Auth" 搜尋現有專案做法來源:jurislm dashboard staging 防護 — 對齊 lawyer 三層防護模式(2026-02-10)