Tutorial: 2023Anita/MedicalAI-Platform — 多智慧體醫療分析平台
S1 專案定位與背景
1.1 這是什麼
MedicalAI-Platform(Med Agentic-AI)是一個基於多智慧體協作(Multi-Agent Collaboration)架構的智慧醫療分析平台,由江陰市人民醫院「睡眠魔法師 Team」開發。平台以 Google Gemini 2.5 Flash/Pro 為底層 LLM,透過四個專業 AI 代理的分工協作,對體檢報告進行全面分析並生成結構化的中文醫療評估報告。
核心定位:將「一個 LLM 做所有事」的模式,拆解為「編排器 + 專業代理」的 Agentic 架構,讓每個代理專注處理特定類型的醫療資料(影像、化驗、病史),最後由綜合分析模組整合結果。
1.2 為什麼重要
這個專案展示了幾個在醫療 AI 領域具教學價值的設計模式:
- Prompt-as-Agent 架構:不使用獨立模型或工具鏈,而是透過精心設計的 system prompt + structured output schema 來模擬多 Agent 行為,大幅降低系統複雜度
- 結構化輸出 (Structured Output):利用 Gemini 的
responseMimeType: "application/json"+responseSchema強制 LLM 輸出符合預定義 TypeScript 型別的 JSON,消除下游解析不確定性 - 多模態整合:單一系統同時處理文字報告、醫學影像圖片、檢查影片、DICOM 格式,展示完整的檔案處理管線
- 醫療 UI 設計模式:報告展示元件 (ReportDisplay.tsx, 664 行) 展示了如何用 regex 高亮醫學術語、color-coded 異常值、患者友善解釋等醫療領域特有的 UI 需求
1.3 適用場景與限制
適合學習的面向:
- 如何用單一 LLM 模擬 multi-agent 行為
- 醫療資料的結構化報告設計
- 多模態檔案處理管線(圖片 OCR + 影片分析 + DICOM 解析)
- 醫療級 UI 設計(術語高亮、風險分級 color-coding、雙語顯示)
已知限制:
- 密碼以明文存儲(routes.ts L97:
user.password !== password),不適用於生產環境 - Agent 並非真正獨立運行,而是透過
delay()模擬進度、最終合併為單一 Gemini API 呼叫 - 無 RBAC(Role-Based Access Control),所有使用者權限相同
- 無速率限制(Rate Limiting),API 端點缺乏保護
- AGPL-3.0 授權要求衍生作品必須開源
S2 系統架構總覽
2.1 四層架構
1使用者介面層 (React 18 + Radix UI + Tailwind CSS)
2 |
3API 服務層 (Express.js + Session 認證)
4 |
5業務邏輯層 (MedicalAnalysisService + FileProcessorService + ComparisonAnalysis)
6 |
7AI 服務層 (Google Gemini 2.5 Flash/Pro + Structured Output)
8 |
9資料儲存層 (PostgreSQL + Drizzle ORM + File System)
2.2 核心架構圖
graph TB
subgraph "使用者介面層"
UI_Input["ReportInput
多模態資料輸入"]
UI_Progress["AnalysisProgress
Agent 進度可視化"]
UI_Report["ReportDisplay
結構化報告展示"]
UI_Chat["AIChat
智慧問答助手"]
UI_Compare["ReportComparison
多報告 AI 對比"]
end
subgraph "API 服務層"
API_Auth["/api/auth/*
Session 認證"]
API_Analyze["/api/analyze
多模態分析"]
API_Chat_EP["/api/chat
AI 聊天"]
API_Compare_EP["/api/reports/compare
對比分析"]
end
subgraph "業務邏輯層"
SVC_Medical["MedicalAnalysisService
核心分析服務"]
SVC_File["FileProcessorService
檔案處理管線"]
SVC_Compare["ComparisonAnalysis
對比分析服務"]
end
subgraph "AI Agent 系統"
Agent_Orch["Orchestrator Agent
編排代理"]
Agent_Img["Imaging Agent
影像分析代理"]
Agent_Lab["Lab Agent
化驗分析代理"]
Agent_Hist["Medical History Agent
病史分析代理"]
Agent_Comp["Comprehensive Analysis
綜合推理"]
end
subgraph "資料層"
DB[(PostgreSQL)]
FS[("File System
uploads/")]
Gemini["Google Gemini 2.5"]
end
UI_Input --> API_Analyze
UI_Chat --> API_Chat_EP
UI_Compare --> API_Compare_EP
API_Analyze --> SVC_Medical
API_Analyze --> SVC_File
API_Chat_EP --> Gemini
API_Compare_EP --> SVC_Compare
SVC_Medical --> Agent_Orch
Agent_Orch --> Agent_Img
Agent_Orch --> Agent_Lab
Agent_Orch --> Agent_Hist
Agent_Img --> Agent_Comp
Agent_Lab --> Agent_Comp
Agent_Hist --> Agent_Comp
Agent_Comp --> Gemini
SVC_File --> Gemini
SVC_Compare --> Gemini
SVC_Medical --> DB
SVC_File --> FS
2.3 資料流(一次完整分析)
sequenceDiagram
participant U as 使用者
participant FE as React 前端
participant API as Express API
participant FP as FileProcessor
participant MA as MedicalAnalysis
participant G as Gemini 2.5
participant DB as PostgreSQL
U->>FE: 填寫患者資訊 + 上傳檔案
FE->>API: POST /api/analyze (FormData)
API->>FP: processFiles(files)
FP->>G: analyzeImageWithGemini(圖片)
G-->>FP: OCR + 醫學影像描述
FP->>G: analyzeVideoWithGemini(影片)
G-->>FP: 影片分析結果
FP-->>API: processedFiles + extractedText
API->>MA: analyzeReport(combinedData)
Note over MA: Step 1: Orchestrator → completed
MA-->>FE: progress(orchestrator: completed)
Note over MA: Step 2: Imaging Agent → completed
MA-->>FE: progress(imagingAgent: completed)
Note over MA: Step 3: Lab Agent → completed
MA-->>FE: progress(labAgent: completed)
Note over MA: Step 4: History Agent → completed
MA-->>FE: progress(historyAgent: completed)
Note over MA: Step 5: Comprehensive Analysis
MA->>G: generateContent(prompt + schema)
G-->>MA: HealthAssessmentReport JSON
MA-->>API: analysisResult
API->>DB: createMedicalReport(report)
API-->>FE: { success, analysis }
FE->>U: 顯示結構化報告
S3 多智慧體協作架構深度解析
3.1 架構模式:Prompt-as-Agent
這個專案採用的 Multi-Agent 模式與 AutoGen、CrewAI 等框架有本質差異。它不是讓多個獨立的 LLM 實例互相通訊,而是:
- 單一 Gemini API 呼叫 完成所有分析
- 透過 system prompt 指示 LLM「模擬」多個 Agent 的行為
- 透過 responseSchema 強制輸出結構包含各 Agent 的分析區段
- 透過 前端進度動畫 模擬 Agent 依序執行的視覺效果
關鍵程式碼位於 server/services/medicalAnalysis.ts:
1// 進度模擬(非真正的 Agent 獨立執行)
2// Step 1: Orchestrator - 300ms delay
3this.updateProgress(analysisId, { orchestrator: 'completed', imagingAgent: 'processing', ... });
4await this.delay(300);
5
6// Step 2-4: 各 Agent 依序「完成」(每個 300-500ms delay)
7// ...
8
9// Step 5: 實際的 Gemini API 呼叫(真正的分析在這裡發生)
10const response = await ai.models.generateContent({
11 model: "gemini-2.5-flash",
12 config: {
13 systemInstruction: MEDICAL_ANALYSIS_PROMPT, // system prompt 定義 Agent 行為
14 responseMimeType: "application/json",
15 responseSchema: { /* HealthAssessmentReport 完整 schema */ },
16 maxOutputTokens: 1000000,
17 temperature: 0.1,
18 },
19 contents: analysisPrompt,
20});
3.2 Agent 定義方式
Agent 的行為完全由 system prompt 定義(MEDICAL_ANALYSIS_PROMPT),核心要求:
1你是專業醫學分析 AI,必須嚴格使用中文生成所有報告內容,
2專業醫學術語採用中英文對照格式(如:高血壓 (Hypertension))。
3
4詳細分析要求:
51. 影像發現:提取關鍵異常,標注數據來源
62. 視頻發現:詳細解讀檢查結果,包含 finding / medicalTerms / patientExplanation / significance
73. 化驗異常:完整解讀,包含 patientFriendly 欄位
84. 可能診斷:提供多個可能性,標明概率和患者解釋
95. 鑑別診斷:列出需排除的疾病及區別要點
106. 影像學報告摘要:technicalFindings + clinicalCorrelation + patientSummary + nextSteps
3.3 Structured Output Schema 設計
這是本專案最有教學價值的部分之一。responseSchema 定義了 HealthAssessmentReport 的完整結構,共四大區塊:
| 區塊 | 欄位數 | 設計要點 |
|---|---|---|
patientInfo | 3 | 姓名/年齡/性別 — 回傳驗證用 |
executiveSummary | 3 | mainFindings / coreRisks / primaryRecommendations — 陣列型,給管理層看 |
detailedAnalysis | 8 | imagingFindings, videoFindings, labAbnormalities (含 patientFriendly), possibleDiagnoses (含 probability enum), differentialDiagnosis, imagingReportSummary, clinicalReasoning, riskFactors |
riskAssessment | 3 | overallAssessment, diagnosticConclusion, actionableRecommendations (followUp / specialistConsultation / lifestyleAdjustments) |
注意 schema 中的 maxItems 和 maxLength 限制,這是控制 token 消耗和回應截斷的實用技巧:
1labAbnormalities: {
2 type: "array",
3 maxItems: 8, // 最多 8 項化驗異常
4 items: {
5 properties: {
6 indicator: { type: "string", maxLength: 50 },
7 value: { type: "string", maxLength: 30 },
8 status: { type: "string", enum: ["high", "low", "normal"] },
9 interpretation: { type: "string", maxLength: 80 },
10 patientFriendly: { type: "string", maxLength: 120 }
11 }
12 }
13}
3.4 與真正 Multi-Agent 架構的比較
| 面向 | 本專案 (Prompt-as-Agent) | 真正 Multi-Agent (AutoGen/CrewAI) |
|---|---|---|
| Agent 實體 | 1 個 LLM 呼叫模擬多個 | 多個獨立 LLM 實例 |
| Agent 間通訊 | 無(全在同一 prompt 內) | 訊息傳遞 / 共享記憶體 |
| 錯誤隔離 | 一個失敗全部失敗 | 個別 Agent 可重試 |
| Token 成本 | 低(1 次呼叫) | 高(N 次呼叫 + 協調 overhead) |
| 延遲 | 低(單次 API call) | 高(多輪通訊) |
| 可擴展性 | 受限於單一 prompt 長度 | 可水平擴展 |
| 複雜度 | 極低 | 中高 |
| 適用場景 | 報告生成、結構化輸出 | 複雜推理、多步驟任務、需要迭代的分析 |
設計取捨分析:對於「給定輸入 → 生成結構化報告」這類任務,Prompt-as-Agent 是務實的選擇。真正需要 Multi-Agent 的場景通常是:Agent 之間需要多輪對話來收斂結論、或不同 Agent 需要存取不同的工具/資料庫。
S4 多模態檔案處理管線
4.1 管線架構
server/services/fileProcessor.ts 實作了一個多格式檔案處理管線:
graph LR
Upload["Multer 上傳
100MB 限制"]
subgraph "格式路由"
PDF["PDF → 提示手動輸入"]
Word["DOCX → mammoth 提取文字"]
Image["PNG/JPEG → Sharp metadata
+ Gemini Vision OCR"]
Video["MP4 → ffmpeg 抽幀
+ Gemini Video 分析"]
DICOM["DCM → dicom-parser
提取 DICOM metadata"]
end
Upload --> PDF
Upload --> Word
Upload --> Image
Upload --> Video
Upload --> DICOM
Image --> Combine["合併 extractedText"]
Video --> Combine
Word --> Combine
DICOM --> Combine
Combine --> Analysis["送入 MedicalAnalysisService"]
4.2 圖片分析流程
圖片處理是最完整的管線,分三步:
- Sharp 預處理:提取 metadata(寬高、格式、密度),生成 300x300 縮圖
- Base64 編碼:讀取圖片 bytes → base64 字串
- Gemini Vision 分析:送入
gemini-2.5-flash搭配專門的醫學圖像分析 prompt
分析 prompt 要求 LLM 識別:報告類型(血常規/生化/影像學)、患者資訊、檢查項目與結果、異常指標、參考範圍、醫師建議。
4.3 影片分析流程
- ffmpeg 探測:讀取影片 metadata(時長、bitrate、串流資訊)
- 截圖生成:在 10% 時間點截取 300x300 縮圖
- Gemini Video 分析:整個影片 base64 編碼後送入 Gemini,prompt 要求識別檢查類型(超聲/內鏡/X光透視)、檢查部位、異常表現、測量數據
4.4 DICOM 解析
使用 dicom-parser 讀取 DICOM 標準標籤:
| Tag | 說明 | DICOM ID |
|---|---|---|
| patientName | 患者姓名 | x00100010 |
| studyDate | 檢查日期 | x00080020 |
| modality | 檢查方式 (CT/MR/US/CR) | x00080060 |
| studyDescription | 檢查描述 | x00081030 |
| seriesDescription | 序列描述 | x0008103e |
| transferSyntax | 傳輸語法 | x00020010 |
S5 前端 UI 設計模式
5.1 醫療級報告展示
ReportDisplay.tsx(664 行)是本專案 UI 設計最精華的元件,展示了多項醫療領域特有的 UI 模式:
5.1.1 動態段落編號系統
根據報告中是否有影片資料、臨床推理等可選區段,動態調整章節編號:
1let sectionCounter = 1;
2// 後續每個區段使用 2.{sectionCounter++} 自動編號
3// 條件渲染的區段(如 videoFindings)只在資料存在時才佔編號
5.1.2 Regex 醫學術語高亮
透過 dangerouslySetInnerHTML + regex 替換,實現不同類別醫學術語的 color-coded 高亮:
1finding
2 .replace(/(結節|佔位|腫塊|病變|異常|陰影|鈣化|積液)/g,
3 '<span class="text-red-700 font-bold">$1</span>') // 異常:紅色
4 .replace(/(正常|穩定|良好|清晰|對稱)/g,
5 '<span class="text-green-700 font-bold">$1</span>') // 正常:綠色
6 .replace(/(\d+%?|mm|cm|密度|增強|對比劑)/g,
7 '<span class="text-blue-800 font-bold">$1</span>') // 數值:藍色
8 .replace(/(輕微|中度|重度|嚴重)/g,
9 '<span class="text-orange-700 font-bold">$1</span>') // 程度:橘色
5.1.3 化驗異常分級 Color Coding
根據 status 欄位(high/low/normal)套用不同的樣式:
| 狀態 | 顯示 | 樣式 |
|---|---|---|
| high | ↑ 偏高 (Above Normal Range) | 紅色底 + 紅字 |
| low | ↓ 偏低 (Below Normal Range) | 藍色底 + 藍字 |
| normal | ✓ 正常 (Within Normal Range) | 綠色底 + 綠字 |
5.1.4 診斷概率標籤
1<span className={`px-4 py-2 rounded-full text-sm font-bold ${
2 diagnosis.probability === 'high' ? 'bg-red-200 text-red-800' :
3 diagnosis.probability === 'moderate' ? 'bg-yellow-200 text-yellow-800' :
4 'bg-green-200 text-green-800'
5}`}>
6 {probability === 'high' ? '高可能性' : probability === 'moderate' ? '中等可能性' : '低可能性'}
7</span>
5.2 分析進度可視化
AnalysisProgress.tsx 以五步驟進度條呈現 Agent 執行狀態:
1[✓ 完成] 編排器 (Orchestrator)
2[⟳ 分析中...] 影像分析 Agent
3[○ 等待中] 化驗單解讀 Agent
4[○ 等待中] 病史數據 Agent
5[○ 等待中] 綜合推理分析
三態設計:pending(灰色)→ processing(旋轉動畫)→ completed(綠色勾選)
5.3 AI 聊天介面設計
AIChat.tsx 的設計亮點:
- 歷史資料上下文注入:自動載入使用者最近 3 份報告作為對話上下文
- 醫學術語即時高亮:
highlightMedicalTerms()函式對 AI 回覆進行 regex 處理 - Markdown 清理:將 AI 回覆中的
**text**轉為【text】,移除#標題符號 - 新/舊使用者區分:有歷史報告時提供個性化分析;無歷史資料時誠實告知並引導上傳
S6 後端服務設計
6.1 Storage 雙實作模式
server/storage.ts 定義了 IStorage 介面,提供兩種實作:
| 實作 | 用途 | 持久化 |
|---|---|---|
DatabaseStorage | 生產環境 | PostgreSQL + Drizzle ORM |
MemStorage | 開發/測試 | 記憶體 Map |
兩者共享相同介面,切換時無需修改業務邏輯。這是典型的 Repository Pattern。
6.2 資料庫 Schema
使用 Drizzle ORM 定義,共兩張表:
1users: {
2 id: serial PK,
3 username: text UNIQUE NOT NULL, // 實際存儲 email
4 password: text NOT NULL // ⚠️ 明文存儲
5}
6
7medicalReports: {
8 id: serial PK,
9 userId: integer FK → users.id,
10 patientName: text NOT NULL,
11 patientAge: text NOT NULL,
12 patientGender: text,
13 examDate: timestamp NOT NULL,
14 reportData: text NOT NULL, // 原始報告文字
15 uploadedFiles: jsonb, // 上傳檔案 metadata
16 analysisResult: jsonb, // HealthAssessmentReport 完整 JSON
17 createdAt: timestamp DEFAULT NOW()
18}
6.3 JSON 修復機制
medicalAnalysis.ts 包含一個 truncated JSON 修復機制,因為 LLM 長回覆可能被截斷:
1// 計算缺失的大括號數量
2const openBraces = (trimmedText.match(/{/g) || []).length;
3const closeBraces = (trimmedText.match(/}/g) || []).length;
4const missingBraces = openBraces - closeBraces;
5
6// 找到最後一個完整欄位,補齊缺失的 }
7if (missingBraces > 0) {
8 let repairText = trimmedText.substring(0, lastQuoteIndex + 1);
9 for (let i = 0; i < missingBraces; i++) {
10 repairText += '}';
11 }
12}
並有 fallback 機制:若 JSON 解析完全失敗,回傳預設的錯誤報告結構。
6.4 對比分析服務
comparisonAnalysis.ts 實作了多報告 AI 對比分析:
- 按時間排序使用者選擇的報告(最多 3 份)
- 組裝對比 prompt,要求 Gemini 生成 trends / riskFactorComparison / keyFindings / recommendations / chartData
- 設置 60 秒 timeout +
Promise.race防止長時間等待 - 若 API 失敗或超時,使用
generateFallbackComparison()生成基於統計的降級結果
S7 認證與 Session 管理
7.1 認證流程
sequenceDiagram
participant U as 使用者
participant FE as React 前端
participant API as Express
participant DB as PostgreSQL
U->>FE: 輸入 email + password
FE->>API: POST /api/auth/register
API->>DB: 查詢 email 是否存在
DB-->>API: 不存在
API->>DB: INSERT users (明文 password)
DB-->>API: user record
API->>API: req.session.userId = user.id
API->>API: req.session.save()
API-->>FE: { success, user }
FE->>FE: localStorage.setItem("isLoggedIn", "true")
Note over FE,API: 後續請求自動帶 session cookie
FE->>API: GET /api/auth/status
API->>API: 檢查 session.userId
API-->>FE: { authenticated: true }
7.2 安全問題清單
| 問題 | 嚴重度 | 位置 | 說明 |
|---|---|---|---|
| 密碼明文存儲 | CRITICAL | routes.ts L49, L97 | password: password 直接存入 DB,比對時 user.password !== password |
| 無 CSRF 保護 | HIGH | server/index.ts | 未使用 CSRF token,依賴 SameSite cookie |
| 無速率限制 | HIGH | server/routes.ts | 所有 API 端點無 rate limiting |
| 使用者輸入未消毒 | MEDIUM | ReportDisplay.tsx | dangerouslySetInnerHTML 處理 AI 回覆,若 AI 被注入可能產生 XSS |
| Session Secret 硬編碼風險 | MEDIUM | server/index.ts | 依賴 env var,但 fallback 可能不安全 |
| 檔案上傳無病毒掃描 | MEDIUM | fileProcessor.ts | 100MB 限制但無 malware 檢查 |
| localStorage 儲存認證狀態 | LOW | Dashboard.tsx | localStorage.setItem("isLoggedIn", "true") 可被篡改 |
| 刪除報告無雙重認證 | LOW | routes.ts L406 | report.userId !== req.session.userId 只做 owner check |
| console.log 洩漏敏感資訊 | LOW | routes.ts 多處 | Session ID、user email 被 log 到 stdout |
S8 部署與容器化
8.1 Docker 設定
1FROM node:18-alpine
2WORKDIR /app
3COPY package*.json ./
4RUN npm ci --only=production
5COPY . .
6RUN npm run build
7EXPOSE 5000
8CMD ["npm", "start"]
8.2 Docker Compose
1services:
2 app:
3 build: .
4 ports: ["5000:5000"]
5 environment:
6 - NODE_ENV=production
7 - DATABASE_URL=postgresql://postgres:password@postgres:5432/med_agentic_ai
8 - GEMINI_API_KEY=${GEMINI_API_KEY}
9 - SESSION_SECRET=${SESSION_SECRET}
10 depends_on: [postgres]
11
12 postgres:
13 image: postgres:14
14 environment:
15 POSTGRES_DB: med_agentic_ai
16 POSTGRES_USER: postgres
17 POSTGRES_PASSWORD: password
18 volumes:
19 - postgres_data:/var/lib/postgresql/data
8.3 CI/CD Pipeline
GitHub Actions 定義了四個 job:
- lint-and-test:npm ci → lint → type check → test → coverage
- build:npm run build(僅 main 分支觸發)
- docker:Build + push 到 ghcr.io(僅 main 分支觸發)
- deploy:自動部署(需自行設定)
S9 開發環境設定
9.1 必要環境
1# Node.js 18+
2nvm install 18
3nvm use 18
4
5# PostgreSQL 14+(可用 Docker)
6docker run -d --name medai-pg \
7 -e POSTGRES_DB=med_agentic_ai \
8 -e POSTGRES_PASSWORD=password \
9 -p 5432:5432 postgres:14
10
11# ffmpeg(影片處理必要)
12# macOS
13brew install ffmpeg
14# Ubuntu
15sudo apt-get install ffmpeg
9.2 環境變數
1# .env
2DATABASE_URL="postgresql://postgres:password@localhost:5432/med_agentic_ai"
3GEMINI_API_KEY="your_gemini_api_key" # Google AI Studio 取得
4GOOGLE_AI_API_KEY="your_google_ai_api_key" # 備用 key
5SESSION_SECRET="your_session_secret_32chars"
6NODE_ENV="development"
7PORT=5000
9.3 啟動流程
1git clone https://github.com/2023Anita/MedicalAI-Platform.git
2cd MedicalAI-Platform
3npm install
4npm run db:push # 建立資料庫 schema
5npm run dev # Vite dev server + Express → http://localhost:5000
9.4 專案腳本
| 命令 | 功能 |
|---|---|
npm run dev | 開發模式(tsx + Vite HMR) |
npm run build | 前端 Vite build + 後端 esbuild bundle |
npm start | 生產模式啟動 |
npm run check | TypeScript 型別檢查 |
npm run db:push | Drizzle schema 推送到 DB |
S10 可借鑒的設計模式與改進建議
10.1 值得借鑒的模式
1. Structured Output + Schema Enforcement
使用 Gemini 的 responseSchema 強制 LLM 回傳符合 TypeScript 型別的 JSON,這是目前控制 LLM 輸出格式最可靠的方法之一。比 prompt engineering 或 regex 後處理更穩定。
2. 降級策略(Graceful Degradation)
三層降級:
- L1: JSON 修復(補齊截斷的大括號)
- L2: Fallback 結構(解析失敗時回傳預設報告)
- L3: 對比分析超時時使用統計降級
3. 雙語醫學報告格式
中文術語 (English Term) 的格式在醫療 AI 系統中是最佳實踐,兼顧醫護人員的中文閱讀習慣和國際醫學術語標準化。
4. 患者友善層 (Patient-Friendly Layer)
labAbnormalities 中的 patientFriendly 欄位、possibleDiagnoses 中的 patientExplanation 欄位,展示了如何在同一報告中同時服務醫護人員和患者兩種讀者。
10.2 改進建議
安全面(必要):
- 密碼改用 bcrypt/argon2 雜湊
- 加入 CSRF 保護(csurf 或 helmet)
- API 加入速率限制(express-rate-limit)
- 移除
dangerouslySetInnerHTML,改用 React 元件化渲染或 DOMPurify - console.log 移除敏感資訊或改用 structured logging
架構面(建議):
- 將 Agent 模擬改為真正的並行執行(Promise.all 呼叫多次 Gemini API with 不同 prompt),提高分析品質
- 加入 WebSocket 推送真實的分析進度(目前前端用 setTimeout 模擬)
- PDF 解析改用 pdf-parse 或 docling 提取文字(目前只提示「請手動輸入」)
- 加入 RBAC(醫師 / 患者 / 管理員角色區分)
功能面(增值):
- 報告 PDF 匯出(目前只有 window.print)
- 多語言支援(目前硬編碼簡體中文)
- 報告共享功能(醫師分享給患者)
- 歷史趨勢圖表(已有 Recharts 依賴但對比分析的圖表資料來自 AI 生成,非真實計算)
S11 資安掃描報告
11.1 掃描範圍
對整個 repo(185 個檔案,約 12,000 行 TypeScript 程式碼)進行靜態資安掃描。
11.2 發現摘要
| 等級 | 數量 | 類別 |
|---|---|---|
| CRITICAL | 1 | 密碼明文存儲 |
| HIGH | 3 | 無 CSRF / 無 rate limit / XSS via dangerouslySetInnerHTML |
| MEDIUM | 4 | Session 設定、檔案上傳無掃描、console.log 敏感資訊、API key fallback |
| LOW | 3 | localStorage 認證、刪除操作無確認 API、cookies.txt 存在 repo |
| INFO | 2 | AGPL-3.0 授權要求、Replit 開發環境殘留 |
11.3 CRITICAL: 密碼明文存儲
位置:server/routes.ts L49 (註冊) + L97 (登入)
1// 註冊 — 明文存入
2const newUser = await storage.createUser({
3 username: email,
4 password: password // ⚠️ 無雜湊
5});
6
7// 登入 — 明文比對
8if (!user || user.password !== password) { // ⚠️ 直接比對
修復建議:
1import bcrypt from 'bcrypt';
2
3// 註冊
4const hashedPassword = await bcrypt.hash(password, 12);
5await storage.createUser({ username: email, password: hashedPassword });
6
7// 登入
8const isValid = await bcrypt.compare(password, user.password);
11.4 HIGH: XSS 風險
位置:client/src/components/ReportDisplay.tsx 多處、AIChat.tsx L298
使用 dangerouslySetInnerHTML 渲染 AI 回覆和報告內容。雖然資料來源是 Gemini API 而非直接使用者輸入,但仍存在間接注入風險:
- 使用者上傳含惡意文字的報告 → Gemini 可能在分析結果中保留該文字 → 前端渲染為 HTML
- Prompt injection 導致 Gemini 回傳包含
<script>的內容
修復建議:使用 DOMPurify 對所有 dangerouslySetInnerHTML 的內容進行消毒。
11.5 HIGH: 無速率限制
位置:server/routes.ts 所有端點
POST /api/analyze 端點會觸發 Gemini API 呼叫(maxOutputTokens: 1,000,000),無任何速率限制。攻擊者可大量發送請求消耗 API quota 和伺服器資源。
修復建議:
1import rateLimit from 'express-rate-limit';
2
3const analyzeLimit = rateLimit({
4 windowMs: 15 * 60 * 1000, // 15 分鐘
5 max: 10, // 每 IP 最多 10 次
6 message: { success: false, error: '請求過於頻繁,請稍後重試' }
7});
8
9app.post("/api/analyze", analyzeLimit, upload.array('files', 10), ...);
11.6 MEDIUM: cookies.txt 存在 repo
位置:cookies.txt + test_cookies.txt(repo 根目錄)
兩個 cookie 檔案被 commit 到 repo。雖然內容為測試用 cookie,但存在洩漏真實 session cookie 的風險。
修復建議:加入 .gitignore 並從 history 中移除。
11.7 資安建議優先順序
1P0 (立即):bcrypt 密碼雜湊、移除 cookies.txt
2P1 (上線前):DOMPurify、express-rate-limit、CSRF 保護
3P2 (建議):structured logging、檔案上傳掃描、RBAC
Comments