管理後台 (Admin Console)¶
管理後台是一個 session 認證的操作介面,掛載於 /admin,供操作者上傳來源檔、執行與排程資料匯入、管理模型端點、設定與外部 FHIR 伺服器,並即時監控背景工作。預設啟用(ADMIN_ENABLED=true)——它是唯一能匯入資料的途徑;資料載入完成後可設為 false 以縮小公開部署的受攻擊面(MCP 工具、worker 與排程匯入照常運作)。
啟用與登入¶
於 .env 設定下列變數後重新啟動 app:
ADMIN_ENABLED=true
ADMIN_USERNAME=admin
# 二擇一:明文初始密碼(首次啟動時雜湊後寫入資料庫),或預先算好的雜湊
ADMIN_INITIAL_PASSWORD=
# 產生密碼雜湊(Node):
# node -e "console.log('sha256$' + require('crypto').createHash('sha256').update('change-me').digest('hex'))"
# ⚠️ 在 .env 中每個 $ 都要寫成 $$(Docker Compose 會把 $ 當變數展開,
# 雜湊值以字母開頭時會被靜默截斷)。Compose 會把 $$ 還原成單一 $。
ADMIN_PASSWORD_HASH=sha256$$...
ADMIN_SESSION_SECRET=<openssl rand -hex 32>
ADMIN_SESSION_TTL_MINUTES=240
ADMIN_MAX_UPLOAD_MB=512
# 外部 FHIR 伺服器 OAuth token / client secret 的對稱加密金鑰(pgcrypto)。
# 未設定時回退至 ADMIN_SESSION_SECRET。
FHIR_SERVER_SECRET_KEY=
密碼雜湊支援 sha256$<hex> 或 pbkdf2_sha256$<iterations>$<salt>$<hex>。ADMIN_INITIAL_PASSWORD 與 ADMIN_PASSWORD_HASH 並存時以前者為準,且兩者都只在首次啟動、資料庫尚無憑證列時使用。/admin 開放的條件是 ADMIN_ENABLED=true、ADMIN_USERNAME、ADMIN_SESSION_SECRET 皆有值,且 ADMIN_PASSWORD_HASH 與 ADMIN_INITIAL_PASSWORD 至少其一有值(404 = 後台關閉、503 = 已啟用但設定不齊、401 = 帳密錯誤)。
登入網址為 http://<host>:8080/admin(經由 nginx 前門;app 容器不對主機開埠)。
管理後台需要
admin-worker容器一起運作(背景工作執行器)。docker compose up -d會一併啟動。重要:
FHIR_SERVER_SECRET_KEY(或其回退值ADMIN_SESSION_SECRET)必須在app與admin-worker兩個容器上完全一致。worker 會以此金鑰pgp_sym_decrypt外部 FHIR 伺服器的 OAuth token 與 client secret;若 worker 的金鑰為空或不同,會在背景工作時拋出Illegal argument to function(金鑰為空)或Wrong key or corrupt data(金鑰不符)。
介面組成¶
管理介面是 web-v2/ 的 React SPA(Vite + React 19 + Mantine 8),建置後的靜態檔由獨立的 web-v2 容器(自帶 nginx)提供;根目錄 nginx/nginx.conf 把 /admin 頁面導向 web-v2,/admin/api/* 與 /admin/ws 則直接導向 app。存取控制由後端 API 的 session cookie(預設名稱 tw_health_admin_session,可用 ADMIN_SESSION_COOKIE 覆寫)負責,未登入時 SPA 會導向 /admin/login。
舊的 Next.js 前端(
web/+web/admin-app/)已被取代,僅保留在 compose profilelegacy-web供回滾使用,預設不啟動。
側邊欄(web-v2/src/layout/nav.ts)分為五區:
| 區塊 | 頁面(路徑相對於 /admin) |
用途 |
|---|---|---|
| 總覽 | 儀表板 /、背景工作 /jobs |
系統狀態一覽;匯入 / 嵌入 / 同步工作的佇列、步驟時間軸與即時日誌(見背景工作與排程)。 |
| 資料 | 醫療術語 /terminology(各模組 /terminology/<icd\|loinc\|snomed\|rxnorm>)、藥品 /drug(藥品瀏覽器 /drug/explorer)、健康食品 /data/health_supplements、食品營養 /data/food_nutrition |
各模組的來源檔、匯入、排程、資料預覽、嵌入、維護模式與清除;術語模組另有瀏覽器(見下文)。 |
| FHIR | IG 套件庫 /fhir/ig、FHIR 伺服器 /fhir-servers |
FHIR IG 匯入 / 相依套件 / 移除保護;外部 FHIR R4 伺服器登錄、認證與健康檢查。 |
| 整合 | 模型端點 /models、MCP 工具 /mcp、外部服務 /integrations |
Embedding 與分析 LLM 的 provider 帳號與 profile;MCP 端點、工具目錄與線上測試;OCR(MinerU / Indra)、物件儲存、TFDA、FHIR package registry、worker 設定。 |
| 系統 | 服務健康 /health、備份與設定 /system、帳號安全 /security |
基礎設施探測與 worker 心跳;系統備份與設定匯出 / 匯入;密碼與 passkey。 |
對應的後端模組見 node-server/src/admin/*.ts(adminApp.ts 為路由入口)。
登入方式除密碼外,另支援 passkey / WebAuthn(node-server/src/admin/webauthn.ts),於「帳號安全」頁管理(該頁變更密碼時,新密碼至少 12 個字元,伺服器端同樣檢查);passkey 僅能在 HTTPS 的 RP 網域(或本機 localhost)上使用,設定見環境變數配置。
MCP 工具頁(/admin/mcp)¶
由 node-server/src/admin/adminMcp.ts 提供 API(GET /admin/api/mcp/overview、GET /admin/api/mcp/tools、POST /admin/api/mcp/tools/{name}/invoke),分三個分頁:
- 總覽:對外端點(
/mcp、/openapi.json、/tools/{name}、JWKS、OAuth 回呼)、驗證模式、伺服器版本與用戶端設定範本。 - 工具目錄:
mcp.ts定義的全部 70 個工具,含是否已註冊;未註冊的工具會標示原因(模組資料表與門檻筆數);模組維護中的工具與群組會加上「維護中」徽章。 - 測試台:以預填範例直接執行任一已註冊工具,與
POST /tools/{name}走同一處理程序(有速率限制);模組維護中時「執行」按鈕停用並說明原因。
模型端點(/admin/models)¶
嵌入與分析 LLM 的端點完全不從環境變數讀取,存放於下列資料表(migration 20261016_llm_providers_and_routes.sql):
| 資料表 | 內容 |
|---|---|
admin.llm_providers |
Provider 帳號:某一 provider 類型的具名帳號(同類型可多個),含 URL、憑證、帳號層級限制。 |
admin.llm_routes |
Profile:一個策略(single / fallback / round_robin / load_balance)加上一組成員;kind 為 analysis 或 embedding。 |
admin.llm_route_members |
成員:某 provider 帳號上的一個模型(帶自己的參數),或另一個 profile(可巢狀,禁止循環)。 |
admin.llm_role_routes |
每個角色(analysis / embedding)使用哪個 profile。 |
舊的 admin.llm_profiles 已改名為 admin.llm_profiles_legacy,只供回滾。
- Provider adapter:
node-server/src/providers/registry.ts共註冊 24 個 adapter——OpenAI、Azure OpenAI、Anthropic、Gemini、Bedrock、Mistral、Cohere、Voyage、Jina、DeepSeek、xAI、Groq、OpenRouter、Together、Fireworks、SiliconFlow、DashScope、Moonshot、Zhipu、Volcengine、MiniMax、OpenAI 相容伺服器(vLLM、LM Studio、LiteLLM…)、Ollama,以及 ChatGPT。舊值vllm/google/azure/claude會自動對應到新 id(migration20261015_llm_provider_registry.sql一併改寫已存資料)。 - ChatGPT 登入:
chatgptprovider 使用 OpenAI 的「Sign in with ChatGPT」PKCE 流程(admin/chatgptOAuth.ts)。由於 redirect 只能是本機 loopback,操作者需把瀏覽器最後的網址貼回後台完成交換;token 存於admin.llm_providers.oauth,不會離開伺服器。 - Embedding 身分(fingerprint):
admin/embeddingIdentity.ts以一組固定探測文字的向量作為系統的嵌入身分(admin.embedding_identity/admin.embedding_fingerprints,定義在db/schema.sql與db/migrations/20261021_embedding_identity.sql;程式啟動時也會以CREATE TABLE IF NOT EXISTS補建)。任何新端點要加入 embedding profile,每個探測文字的 cosine 都必須 ≥ 0.95,才算「同一個模型」。已有向量時要換模型,必須走受保護的切換流程:停用嵌入角色 → 停止 embed 工作 → 清除向量(維度不同時在同一交易內調整所有*_embeddings向量欄位,例如 1024 → 2560)→ 採用新身分 → 重新排入各模組的 embed 工作。 - 還沒有身分列、但已有向量(舊版載入的資料):第一次儲存 Embedding profile、指定角色,或在「總覽」按「確認現有向量的模型並建立指紋」時,系統會從資料庫取幾筆既有向量與其原文(以
source_hash驗證原文),請角色內的端點、其他 Embedding profile 的端點、最後是這次要加入的端點逐一重新產生來比對,採用第一個對得上的;連不上或回 404 的端點會被略過,絕不會被採用。都對不上時回傳embedding_identity_unresolved(列出每個端點的失敗原因),不會丟出原始 HTTP 錯誤。 - 錯誤一律帶代碼:模型端點相關的錯誤(儲存 profile、指定角色、測試帳號/模型、比對指紋、切換、匯入設定)都回傳
code+params,後台翻成中文並附下一步;原始伺服器訊息只放在可展開的「技術細節」。主要代碼見下方〈更換 Embedding 模型〉。 - 角色層級參數(嵌入的 timeout / batch size / concurrency、分析的 max retries)存於
admin.app_settings的embedding/analysis群組。
更換 Embedding 模型(切換流程)¶
不能用編輯 Profile 更換 Embedding 模型。 把成員改成另一個模型(或另一個向量維度)時,儲存會被拒絕(HTTP 409,embedding_model_change_requires_switch),後台改為顯示說明對話框:目前模型與維度、要改成的模型、會重建的向量數與預估時間,並提供「改用切換模型流程」——切換對話框會帶入同一個帳號、模型與參數。指定一個不同模型的 profile 給 Embedding 角色也一樣。
切換步驟(模型端點 → 總覽 → Embedding → 「切換模型…」):
- 選帳號、模型與向量維度,按 預先檢查(
POST /admin/api/llm/embedding/precheck,不會變更任何東西)。它會實際呼叫新模型,並在刪除任何資料前回報:embedding_model_not_found:伺服器沒有這個模型,列出它實際提供的模型,可點選改用。embedding_dims_unsupported:回傳的向量不是設定的維度。沒有送出dimensions時,代表模型原生維度不同(例如 Qwen3-Embedding-4B 為 2560);有送出時,代表伺服器拒絕或忽略了它。兩種修法:改用模型原生維度,或(Matryoshka 模型)開啟「送出維度參數」——vLLM 須以--hf-overrides '{"is_matryoshka": true}'啟動,否則會回 HTTP 400。embedding_switch_same_model:新端點其實產生相同向量,不需要切換,直接加進 profile 當備援即可。- 通過時顯示回傳維度,以及向量欄位是否會改維度(例如 1024 → 2560)。
- 有向量時輸入
REINDEX確認後執行。系統關閉 Embedding 角色、停止 embed 工作、清除向量並調整欄位維度、採用新身分、建立並綁定新的 profile,再為每個有資料的模組排入重建工作。重建期間語意搜尋改為關鍵字比對;約 60 萬筆向量在共用 GPU 上約需 1–2 小時(0.6B),4B 模型約 3 小時。 若 GPU 同時服務正式環境的即時查詢,重建前先在「外部服務 → Embedding」把batch_size調小(例如 8)、concurrency保持 1:每批越小,單一查詢被一個大批次擋住的時間越短(實測 Qwen3-Embedding-4B:批次 16 時線上單筆查詢 p95 約 110–170 ms,批次 8 約 80 ms)。batch_size在每個工作開始時讀取,concurrency執行中即時生效;成員的rpm只限制平均速率,不會讓尖峰變小。
查詢指令(query instruction)。 模型成員的參數「查詢指令」決定搜尋查詢是否加上任務描述。預設「自動」:Qwen3-Embedding 系列(以及 gte-Qwen2-instruct)會依模組加上 Instruct: <任務>\nQuery: <查詢>(ICD、LOINC、SNOMED、IG 概念、食品、健康食品各有一句英文任務描述),其他模型不加;也可「關閉」或「自訂」一句所有模組共用的描述(params.query_instruction:省略 / off / 文字)。只有查詢向量會加指令,已存的向量與身分指紋的探測文字都不加,所以調整這個設定不需要重建索引。
「總覽」的 Embedding 卡片會實際探測角色內每個成員(快取 60 秒):成員失敗時顯示「Embedding 路由降級:成員 X 失敗(原因)——正由 Y 備援」與修正建議,服務健康頁的 Embedding 列也用同一份探測結果。
設定(Settings)與優先序¶
Bootstrap 變數(DB / Redis / MCP transport / ADMIN_* 認證)起始於 .env。首次啟動會把
ADMIN_INITIAL_PASSWORD(雜湊後)或 ADMIN_PASSWORD_HASH 寫入 admin.admin_credentials,之後登入只驗證資料庫中的 hash。
seed 使用 ON CONFLICT DO NOTHING,因此 credential row 建立後修改 .env 或重啟服務都不會
覆蓋有效密碼。密碼與 passkey 都在「帳號安全」(/admin/security)管理。
其餘外部系統設定(MinIO、TFDA base URL、FHIR package registry、worker 調校)為 seed-only:.env 僅在首次啟動、
對應鍵尚未存在時讀取一次以種子化 admin.app_settings;之後請在「外部服務」(/admin/integrations)管理與測試,
編輯 .env 對已種子化的資料庫無效。其中 TFDA 與 registry 可直接修改(熱套用);MinIO 與 worker 調校在後台為唯讀群組。
OCR 設定存於 admin.app_settings 的 ocr 群組,不從 .env 種子化,只能在「外部服務 → OCR」設定與測試:供應者 mineru(預設,外部 MinerU)或 indra(compose 內建的 CPU OCR 容器 ocr-indra,http://ocr-indra:8000),以及選用的備援供應者。各欄位見藥品服務 → OCR 供應者。
「備份與設定」(/admin/system)的設定匯出 / 匯入(格式版本 3)可在不同環境間搬移一份可用的設定:內含 admin.app_settings 各群組、provider 帳號 / profile / 角色綁定(含金鑰)、passkey 與其 RP ID。舊的 v1 / v2 匯出檔仍可匯入。匯出檔含明文密鑰,請視同憑證保管。
系統備份¶
「備份與設定」頁可逐項選擇設定與憑證、PostgreSQL database、MinIO object storage。
建立備份只會排入 system_backup 背景工作;worker 將 PostgreSQL custom dump 與 MinIO 物件
逐段串流進 ZIP,再以 multipart upload 寫回 MinIO 的 system-backups/ prefix,不會在單一
HTTP request 或 Node 記憶體中組裝整份備份。完成後可由備份歷史透過同源 API 串流下載。
備份包含 API keys、登入憑證與醫療資料,應視同 production secret 管理。資料庫 dump 可用
PostgreSQL 16 pg_restore 還原;object storage 檔案位於 ZIP 的 object-storage/ 目錄。
來源檔(Sources)¶
於各模組頁的來源檔區上傳來源檔(ICD-10-CM CMS order file zip、ICD-10-PCS、LOINC zip、SNOMED RF2、RxNorm zip、FHIR IG package.tgz 等),並可指定 source role(例如 IG 相依套件、選用的 WHO ICD-10 來源)。系統會以檔案指紋(SHA-256)阻擋重複上傳;檔案存於 MinIO,工作執行時再取回。
上傳當下就會檢查內容,錯的檔案立即被拒(附中文原因):檔案類型(magic bytes)、官方發布壓縮檔必須含 loader 需要的成員
(LOINC LoincTable/Loinc.csv、SNOMED sct2_Concept_*、RxNorm RXNCONSO.RRF、ICD-10-CM / PCS order 檔、UMLS MRCONSO.RRF;
zip64 等無法列目錄的檔案略過此檢查)、CSV 必須有必要欄位(藥品索引;LOINC 台灣對照 loinc_code,name_zh;參考值範圍
loinc_code,age_min,age_max,gender,range_low,range_high)。IG 上傳匯入前會先讀 package/package.json,不是 FHIR 套件就回 422,不會建立工作。
匯入工作綁定來源時,單一檔案的 source role 使用使用中(active)的那一版;只有該 role 沒有任何使用中版本時才退回最近一次上傳的檔案。
上傳需送出
Content-Type: application/octet-stream(SPA 已如此送出),否則會被 express 的 JSON parser 攔下並回 413。
術語瀏覽器¶
「醫療術語」各模組頁與 IG 套件庫內建唯讀瀏覽器,資料來自 GET /admin/api/terminology/*(node-server/src/admin/termViz/):
| 瀏覽器 | API 前綴 | 內容 |
|---|---|---|
| ICD-10-CM 樹 / WHO ICD-10 樹 | icd/cm/*、icd/who/* |
章節 → 類目 → 碼的階層、路徑與搜尋 |
| ICD-10-PCS 軸向 | icd/pcs/* |
依前三碼表格與第 4–7 軸篩選 |
| SNOMED CT 圖 | snomed/* |
概念鄰域(父 / 子 / 屬性關聯)與搜尋 |
| LOINC 軸向 | loinc/* |
六軸 facet 篩選、單一 term 詳情、LA / LP / LL 碼 |
| RxNorm | rxnorm/* |
摘要、依 TTY 搜尋、概念詳情 |
| IG CodeSystem 樹 | ig/codesystem* |
IG 套件內 CodeSystem 的概念階層與搜尋 |
維護模式(Maintenance Mode)¶
drug、icd、loinc、snomed、ig、rxnorm 模組可切換維護模式(POST /admin/api/module-maintenance,狀態存於 admin.app_settings 的 maintenance 群組),用來在重載或清除資料時暫停該模組的 MCP 工具:維護中的模組,其工具呼叫(/mcp、/tools/*、後台 playground)會立即回傳錯誤碼 MODULE_MAINTENANCE(中英文訊息),工具仍留在 tools/list 中(避免用戶端工具清單變動)。狀態快取 5 秒,切換後約 5 秒內生效。ICD-10 與 FHIR Condition 跟隨 icd、Drug 與 FHIR Medication 跟隨 drug、Lab 跟隨 loinc、FHIR IG 跟隨 ig;rxnorm 沒有自己的工具,旗標只作標示。模組頁會顯示工具「暫停(維護中)」,MCP 工具頁的工具卡也會標示「維護中」。錯誤格式見 API 參考。
外部 FHIR 伺服器¶
在「FHIR 伺服器」頁登錄外部 FHIR R4 伺服器,設定允許的資源型別 / 操作、OAuth 認證(含 private_key_jwt 金鑰產生與公開 JWKS 託管於 /fhir-client/<id>/jwks.json)、token 策略與健康檢查路徑。OAuth Authorization Code 回呼完成後會導回 /admin/fhir-servers。
MCP 端透過 list_fhir_servers / get_fhir_server_status / crud_fhir_server 使用,並由伺服器代為處理 token —— 呼叫端不經手任何密鑰。
crud_fhir_server是系統中唯一可能產生寫入的工具:寫入操作必須該伺服器的 allow-list 允許,且呼叫端帶入confirm_write=true。
錯誤訊息與下一步¶
後台 API 的錯誤回應(HTTP ≥ 400)除了原本的 error(英文)與 detail(原始技術訊息)外,一律附上:
| 欄位 | 內容 |
|---|---|
code |
穩定的機器代碼,例如 SOURCE_MISSING、DB_SCHEMA_OUTDATED、UPSTREAM_UNREACHABLE、JOB_ALREADY_ACTIVE |
message_zh |
發生了什麼(繁體中文) |
hint_zh |
下一步該怎麼做(到哪一頁、改哪個設定、跑哪個指令) |
由 node-server/src/admin/adminErrorCodes.ts 統一產生:先比對已知的英文訊息(來源檔缺漏、設定值超出範圍、OAuth 失敗…),
再從 detail 辨識技術原因(PostgreSQL 錯誤如缺資料表/欄位 → 提示套用 migration、重複鍵、外鍵;連線拒絕、DNS、逾時、TLS、
上游 HTTP 狀態;MinIO),都不符合時仍會說明「哪個操作失敗」並給通用的下一步。已帶 code 的回應(模型端點 API、IG 移除、
MODULE_MAINTENANCE)不會被改寫。介面上的提示訊息依序顯示說明、下一步,原始訊息只以小字「技術細節」呈現。
同樣的說明也套用在:
- 外部服務的「測試連線」(OCR / MinIO / TFDA):結果多了
message_zh/hint_zh。 - 服務健康的探測結果與儀表板的基礎設施狀態:
message_zh/detail_zh+hint_zh。 - 失敗的背景工作:
last_error_zh/last_error_hint_zh(工作詳情與模組頁「上次匯入失敗的原因」)。 - 閘道錯誤:app 沒有回應時(nginx 回 502 / 504 的 HTML),前端直接說明「後端服務沒有回應」與檢查方式。
DB 健康閘門¶
node-server/src/dbHealth.ts 為中央 DB 健康監測:當 PostgreSQL 無法連線時,會鎖定所有變動性操作並在 UI 顯示遮罩(overlay),避免在資料庫異常時進行匯入或修改。