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 領域具教學價值的設計模式:

  1. Prompt-as-Agent 架構:不使用獨立模型或工具鏈,而是透過精心設計的 system prompt + structured output schema 來模擬多 Agent 行為,大幅降低系統複雜度
  2. 結構化輸出 (Structured Output):利用 Gemini 的 responseMimeType: "application/json" + responseSchema 強制 LLM 輸出符合預定義 TypeScript 型別的 JSON,消除下游解析不確定性
  3. 多模態整合:單一系統同時處理文字報告、醫學影像圖片、檢查影片、DICOM 格式,展示完整的檔案處理管線
  4. 醫療 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 實例互相通訊,而是:

  1. 單一 Gemini API 呼叫 完成所有分析
  2. 透過 system prompt 指示 LLM「模擬」多個 Agent 的行為
  3. 透過 responseSchema 強制輸出結構包含各 Agent 的分析區段
  4. 透過 前端進度動畫 模擬 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 的完整結構,共四大區塊:

區塊欄位數設計要點
patientInfo3姓名/年齡/性別 — 回傳驗證用
executiveSummary3mainFindings / coreRisks / primaryRecommendations — 陣列型,給管理層看
detailedAnalysis8imagingFindings, videoFindings, labAbnormalities (含 patientFriendly), possibleDiagnoses (含 probability enum), differentialDiagnosis, imagingReportSummary, clinicalReasoning, riskFactors
riskAssessment3overallAssessment, diagnosticConclusion, actionableRecommendations (followUp / specialistConsultation / lifestyleAdjustments)

注意 schema 中的 maxItemsmaxLength 限制,這是控制 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 圖片分析流程

圖片處理是最完整的管線,分三步:

  1. Sharp 預處理:提取 metadata(寬高、格式、密度),生成 300x300 縮圖
  2. Base64 編碼:讀取圖片 bytes → base64 字串
  3. Gemini Vision 分析:送入 gemini-2.5-flash 搭配專門的醫學圖像分析 prompt

分析 prompt 要求 LLM 識別:報告類型(血常規/生化/影像學)、患者資訊、檢查項目與結果、異常指標、參考範圍、醫師建議。

4.3 影片分析流程

  1. ffmpeg 探測:讀取影片 metadata(時長、bitrate、串流資訊)
  2. 截圖生成:在 10% 時間點截取 300x300 縮圖
  3. 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 的設計亮點:

  1. 歷史資料上下文注入:自動載入使用者最近 3 份報告作為對話上下文
  2. 醫學術語即時高亮highlightMedicalTerms() 函式對 AI 回覆進行 regex 處理
  3. Markdown 清理:將 AI 回覆中的 **text** 轉為 【text】,移除 # 標題符號
  4. 新/舊使用者區分:有歷史報告時提供個性化分析;無歷史資料時誠實告知並引導上傳

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 對比分析:

  1. 按時間排序使用者選擇的報告(最多 3 份)
  2. 組裝對比 prompt,要求 Gemini 生成 trends / riskFactorComparison / keyFindings / recommendations / chartData
  3. 設置 60 秒 timeout + Promise.race 防止長時間等待
  4. 若 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 安全問題清單

問題嚴重度位置說明
密碼明文存儲CRITICALroutes.ts L49, L97password: password 直接存入 DB,比對時 user.password !== password
無 CSRF 保護HIGHserver/index.ts未使用 CSRF token,依賴 SameSite cookie
無速率限制HIGHserver/routes.ts所有 API 端點無 rate limiting
使用者輸入未消毒MEDIUMReportDisplay.tsxdangerouslySetInnerHTML 處理 AI 回覆,若 AI 被注入可能產生 XSS
Session Secret 硬編碼風險MEDIUMserver/index.ts依賴 env var,但 fallback 可能不安全
檔案上傳無病毒掃描MEDIUMfileProcessor.ts100MB 限制但無 malware 檢查
localStorage 儲存認證狀態LOWDashboard.tsxlocalStorage.setItem("isLoggedIn", "true") 可被篡改
刪除報告無雙重認證LOWroutes.ts L406report.userId !== req.session.userId 只做 owner check
console.log 洩漏敏感資訊LOWroutes.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:

  1. lint-and-test:npm ci → lint → type check → test → coverage
  2. build:npm run build(僅 main 分支觸發)
  3. docker:Build + push 到 ghcr.io(僅 main 分支觸發)
  4. 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 checkTypeScript 型別檢查
npm run db:pushDrizzle 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 改進建議

安全面(必要)

  1. 密碼改用 bcrypt/argon2 雜湊
  2. 加入 CSRF 保護(csurf 或 helmet)
  3. API 加入速率限制(express-rate-limit)
  4. 移除 dangerouslySetInnerHTML,改用 React 元件化渲染或 DOMPurify
  5. console.log 移除敏感資訊或改用 structured logging

架構面(建議)

  1. 將 Agent 模擬改為真正的並行執行(Promise.all 呼叫多次 Gemini API with 不同 prompt),提高分析品質
  2. 加入 WebSocket 推送真實的分析進度(目前前端用 setTimeout 模擬)
  3. PDF 解析改用 pdf-parse 或 docling 提取文字(目前只提示「請手動輸入」)
  4. 加入 RBAC(醫師 / 患者 / 管理員角色區分)

功能面(增值)

  1. 報告 PDF 匯出(目前只有 window.print)
  2. 多語言支援(目前硬編碼簡體中文)
  3. 報告共享功能(醫師分享給患者)
  4. 歷史趨勢圖表(已有 Recharts 依賴但對比分析的圖表資料來自 AI 生成,非真實計算)

S11 資安掃描報告

11.1 掃描範圍

對整個 repo(185 個檔案,約 12,000 行 TypeScript 程式碼)進行靜態資安掃描。

11.2 發現摘要

等級數量類別
CRITICAL1密碼明文存儲
HIGH3無 CSRF / 無 rate limit / XSS via dangerouslySetInnerHTML
MEDIUM4Session 設定、檔案上傳無掃描、console.log 敏感資訊、API key fallback
LOW3localStorage 認證、刪除操作無確認 API、cookies.txt 存在 repo
INFO2AGPL-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 而非直接使用者輸入,但仍存在間接注入風險:

  1. 使用者上傳含惡意文字的報告 → Gemini 可能在分析結果中保留該文字 → 前端渲染為 HTML
  2. 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