把只有資深員工知道的報價經驗,變成可搜尋、可審核、可執行的工作流程。
第一個流程只解一個問題:製造業/貿易商從收到詢價到送出正確報價的時間,同時保留人工判斷與完整操作紀錄。
客戶詢價 → AI 擷取結構化需求 → 缺件檢查/追問草稿 → 產品·庫存·歷史·SOP 比對
→ 規則引擎算價與風險 → 人工核准 → 寄送 → 追蹤任務 + 稽核紀錄
截圖皆來自 e2e Demo 的虛構資料(seed 建立的示範組織「盛華」)。
![]() 儀表板——案件狀態、待辦與風險一眼看完 |
![]() AI 擷取——每個欄位帶信心分數與原文證據 |
![]() 報價草稿——規則引擎算價,逐項列出引用來源 |
![]() 詢價案件——從收信到寄出的完整時間軸 |
![]() 成效報表——兩邊碼表實測的節省率,不灌水 |
需求:Node.js ≥ 20.11、pnpm。不需要 Docker,也不需要任何 API 金鑰。
pnpm installcp .env.example .envpnpm setup && pnpm devpnpm setup = prisma generate + prisma migrate deploy + seed(可重複執行)。
開啟 http://localhost:3210,用下方任一 Demo 帳號登入。
Windows PowerShell 的複製指令是 Copy-Item .env.example .env。
| 帳號 | 角色 | 可以做什麼 |
|---|---|---|
admin@demo.local |
admin | 全部權限,含組織設定與風險門檻 |
manager@demo.local |
manager | 核准報價、管理知識與 SOP、看稽核紀錄 |
operator@demo.local |
operator | 建立案件、產生報價草稿、寄送已核准報價 |
viewer@demo.local |
viewer | 唯讀 |
other@demo.local |
另一個組織的 admin | 用來驗證組織隔離 |
DEMO_PASSWORD只在本機 Demo 使用。正式環境請改掉SESSION_SECRET,並停用 Demo 帳號。
| 指令 | 用途 |
|---|---|
pnpm dev |
開發伺服器(port 3210) |
pnpm setup |
migration + seed(可重複執行) |
pnpm db:reset |
砍掉重練資料庫並重新 seed |
pnpm test |
unit + integration(Vitest,會自建獨立 test.db) |
pnpm test:e2e |
build + 重建 e2e.db + Playwright;強制使用 mock adapter,即使 .env 設成 anthropic |
pnpm verify |
lint → typecheck → test → production build |
pnpm schema:postgres |
由 SQLite schema 產生 PostgreSQL 版本 |
pnpm preflight:llm |
接上真實模型後的 smoke test:跑一封虛構詢價並驗證 ai_runs 是 anthropic/正確模型/succeeded |
- 用
operator@demo.local登入 →〈建立詢價案件〉。 - 右側點「情境 A:資料完整的詢價」→〈載入這個範例〉→〈建立並執行 AI 擷取〉。
- 案件頁會看到:原始詢價、結構化需求(每個欄位都有信心分數與原文證據)、兩個已對應 SKU 的品項。
- 按〈產生報價建議〉。系統比對產品主檔、價格版本、庫存快照、同客戶歷史核准折扣與已發布 SOP,算出單價與總額,並列出 14 筆引用來源。
- 〈送出核准〉→ 用
manager@demo.local登入 →〈核准中心〉→ 看差異與依據 →〈核准〉。 - 回到 operator →〈寄送報價〉→ 到〈寄件匣〉看模擬寄出的內容;案件變成「已寄出」並自動建立 3 個工作天後的追蹤任務。
- 載入「情境 B」範例(英文詢價)。
- 系統標示缺少「各品項數量」,〈產生報價建議〉按鈕被停用並說明原因。
- 〈產生追問草稿〉→ AI 依缺少欄位寫出英文信件草稿 → 人工修改 →〈確認後寄出〉。
- 案件時間軸同時保留 AI 草稿與人工修改前後的內容。
- 載入「情境 C」範例(日文詢價、JPY 報價)。
- 產生報價建議後出現三個警告:折扣超過 10% 門檻(高風險)、庫存不足、交期衝突;版本被標記為「需主管核准」。
- operator 送審後在核准中心只會看到「你不能核准這筆報價」——送審者不得核准自己的報價。
- manager 看得到與前一版的差異、風險理由與所有引用來源,可核准/退回/拒絕;退回與拒絕必須填原因。
這是拿來決定「要不要繼續投資」的量測層,不是產品功能。每個案件量四件事:
| 指標 | 回答的問題 |
|---|---|
| 純文字/附件覆蓋率 | 目前 MVP 能服務多少真實案件? |
| AI 擷取修改率 | AI 理解是否可靠?(分母只算 AI 有給值的欄位;AI 漏抓、人工補上的另外列) |
| 報價修改率 | 建議能不能直接用?(分母 = 品項數 × 2 + 6 個文件欄位) |
| 實測節省率 | 產品有沒有真的省時間? |
量尺規則(重要):節省率一律用兩邊都碼表實測的數字計算:
實測節省率 = (原流程實測分鐘 − AI 輔助實測分鐘) / 原流程實測分鐘
兩個數字都必須用同一支碼表、同一套開始/暫停/結束規則。系統不會拿碼表實測的人工時間去減 audit 推估的活躍時間——推估看不到閱讀與思考時間,那樣減會系統性高估節省率。只填了一邊時,報表顯示「尚未完成兩邊實測,無法下結論」。
計時規則(UI 上也會提示):
- 開始:打開這封詢價、準備動手的那一刻
- 暫停:離開去做別的事就停錶,回來再繼續
- 結束:報價可以送出核准的那一刻(不含等主管核准的時間)
- 兩種流程用同一支碼表、同一個人計時
「推估活躍操作時間」只作診斷(這個名稱是刻意的):
- 由稽核事件的時間戳推算,相鄰事件間隔超過門檻(預設 5 分鐘,可在〈設定〉調整)就視為離開,該空檔不累加。
- 它看不到閱讀與思考時間——第一個動作之前、最後一個動作之後、每個區段開頭的判斷時間都沒算進去,所以是下限估計,不能當成精確工時。
- 活躍時間不到 0.5 分鐘的案件(腳本、API 直接呼叫、連點完成)會被標成「不可信」並排除在推估平均值之外。寧可少一個數字,也不要一個 100% 節省的假數字。
- 完整牆鐘時間一律並列顯示。
- 報表另外算「推估落差」= AI 輔助碼表實測 − 推估活躍時間。這個差值就是事件時間戳看不到的閱讀與思考時間;累積幾件之後可以用它校準推估值到底低估多少。
要得到能對外講的數字,必須在案件頁的〈成效量測〉填入兩邊的碼表實測分鐘數,並建議:
- 5–10 封去識別化的真實詢價,不要只測一封(n=1 會被那封信的格式運氣主導)。第一封視為校準,至少累積 5 封可比較案件再做第一次 go/no-go。 報表首頁的「可比較案件」直接顯示
n/5進度。 - 交錯順序:一半「人工先、系統後」,一半「系統先、人工後」。否則同一封信做第二次時業務已經知道答案,會讓第二種方法看起來不公平地更快。
- 測試前先標記每封的輸入類型(純文字/附件/混合),覆蓋率才算得出來。
- 報表右上角可以匯出 CSV(實測欄位、推估欄位、推估落差、可信度標記分開列)拿去自己算。
真實客戶用自己的料號詢價是常態。料號是「客戶與產品之間的關係」,所以獨立成 customer_product_aliases 表,不是產品的欄位——這樣不同客戶可以用相同料號,舊料號也能保留歷史。
比對順序(任何一步不是唯一結果就往下走,系統絕不自行決定):
- 客戶 + 客戶料號精確對照
- 我方 SKU 精確匹配
- 名稱/規格候選比對 → 只列候選,不自動選定
- 沒有唯一結果 → 交由人工指定
標準化只做大小寫統一、前後空白移除、連續空白整理。刻意不移除 -、/、. 等符號——ABC-M8-001 與 ABCM8001 在真實世界可能是兩個料號,自動合併會造成無法追溯的錯誤對照。
唯一性:同一組織+同一客戶+同一標準化料號,同時只能有一筆 active 對照。由 DB trigger 強制(繞過應用層直接寫入也會被拒),要改指向就先停用舊的。不同客戶可以使用相同料號。
CSV 匯入(獨立檔案,不塞進產品 CSV):
customerCode,customerPartNumber,internalSku,description
C-1001,ABC-M8-001,HB-M8X30-SS,客戶舊料號匯入流程是「先預覽、再確認」,逐列回報狀態:可建立/略過(已存在相同對照)/衝突(既有對照指向別的 SKU,不會自動覆寫)/錯誤(客戶或 SKU 不存在、欄位缺漏),並附成功·略過·衝突·失敗統計與 audit log。同一批 CSV 內部的自我衝突也會被抓出來。
指標歸因:AI 抓到料號但查無對照時,不算擷取錯誤——那是主檔資料不足。報表因此把三件事分開:
| 指標 | 意義 |
|---|---|
| 客戶料號擷取修改率 | AI 有沒有抓錯料號(AI 的問題) |
| 料號對照命中率 | 主檔有沒有這筆對照(資料的問題) |
| 未對照料號數量 | 還缺多少對照要建 |
- D — 擋住未核准寄送:對未核准的報價直接呼叫
POST /api/quotes/:id/send會拿到 403,Outbox 不會產生任何紀錄,稽核紀錄會留下quote.send_blocked。 - E — 沒有依據就不亂答:到〈知識庫〉問「公司對員工寵物保險的補助政策是什麼?」,系統回答「現有企業資料中找不到足夠依據回答這個問題。」並提供「新增知識來源」入口。
- F — 組織隔離:
other@demo.local屬於第二個組織,用 URL 或 API 都拿不到盛華的案件、報價、知識與稽核紀錄。
Next.js App Router(Server Components 讀資料 / Client Components 做互動)
│
├── Route Handlers(src/app/api/**)── 驗證 session → 檢查角色 → Zod 驗證輸入 → 呼叫 service
│
├── Services(src/lib/services/**)── 所有商業邏輯與 organization_id 隔離
│ ├── inquiry:建立、AI 擷取、缺件判斷、追問草稿
│ ├── quote:比價、算價、版本、風險旗標、寄送
│ ├── approval:核准/退回/拒絕、版本差異
│ ├── knowledge:解析、分段、索引、混合檢索、有依據才回答
│ └── sop / product / customer / workspace
│
├── Domain(src/lib/domain/**)── 純函式:金額計算、狀態機、風險規則、SOP 結構
├── Adapters(src/lib/adapters/**)── LLM / Email / Embedding,可替換、預設離線
└── Prisma(SQLite 本機|PostgreSQL 可移轉)+ append-only 稽核表
| 檔案 | 內容 |
|---|---|
prisma/schema.prisma |
28 張表:組織、知識、SOP、業務資料(含客戶料號對照)、工作流程、稽核 |
src/lib/domain/part-number.ts |
料號標準化規則與比對來源 |
src/lib/services/alias.ts |
客戶料號對照:查詢、建立、停用、CSV 預覽與匯入 |
prisma/seed.ts |
Demo 資料(12 產品/3 客戶/4 筆客戶料號對照/6 筆歷史報價/3 SOP/5 知識來源/第二組織) |
src/lib/money.ts |
金額一律 decimal 字串,四捨五入與幣別小數位規則 |
src/lib/domain/quote-calc.ts |
單價、折扣、小計、稅、總額(純函式,不經 LLM) |
src/lib/domain/risk.ts |
風險規則與「是否強制主管核准」 |
src/lib/domain/status.ts |
案件與報價版本的狀態機 |
src/lib/auth/rbac.ts |
權限矩陣與「不得核准自己報價」 |
src/lib/adapters/llm/mock.ts |
離線、可重現的規則式擷取器(Demo 預設) |
src/lib/adapters/llm/anthropic.ts |
真實 LLM adapter(tool-use 結構化輸出+Zod 驗證+重試一次) |
src/lib/search/rank.ts |
BM25 + 向量的 RRF 混合排序 |
src/lib/audit.ts |
稽核事件寫入(資料表由 DB trigger 保護) |
src/lib/domain/effort.ts |
成效量測純函式:推估活躍時間、修改率、可信度判定 |
src/lib/services/analytics.ts |
由稽核紀錄組出每個案件的成效報表與 CSV |
- 金額只由程式算。價格、成本、庫存、稅率、匯率全部來自資料庫或使用者明確輸入;LLM 只負責擷取文字與寫說明,不參與任何計算。
- AI 產出一定有出處。每個欄位帶信心分數與原文證據;每份報價帶引用來源(產品主檔/價格版本/庫存快照/歷史報價/SOP/知識段落/匯率設定)。找不到依據時明講「資料不足」。
- 有外部影響的動作要人核准。寄送在後端重新驗證核准狀態,未核准一律 403,並寫入稽核紀錄。
UI 全站使用同一組標籤,讓使用者一眼看出數字從哪來:
- ■ 原始資料:資料庫或客戶原文
- = 規則計算:程式與規則引擎算出來的
- ◇ AI 建議:AI adapter 產生,需人工確認
- ✎ 人工輸入:使用者填寫或修改過
沒有金鑰時全部自動退回可離線執行的 mock,Demo 流程完全不受影響。
| 變數 | 預設 | 說明 |
|---|---|---|
DATABASE_URL |
file:./dev.db |
SQLite 路徑相對於 prisma/ |
SESSION_SECRET |
本機字串 | Session token 的 HMAC 金鑰,正式環境必換 |
LLM_PROVIDER |
mock |
mock(規則式、可重現)或 anthropic |
ANTHROPIC_API_KEY |
空 | 有值且 LLM_PROVIDER=anthropic 才會使用真實模型 |
ANTHROPIC_MODEL |
claude-sonnet-5 |
真實 adapter 使用的模型 |
LLM_INPUT_RATE_USD_PER_MTOK |
空 | 估算成本用的輸入費率(美元/百萬 token)。留空=只存 token 不估成本 |
LLM_OUTPUT_RATE_USD_PER_MTOK |
空 | 同上,輸出費率。兩個都要設才會估算 |
EMAIL_PROVIDER |
mock |
mock(寫入 Outbox 標記 simulated)或 resend |
RESEND_API_KEY |
空 | 啟用真實寄信 |
EMAIL_FROM |
quotes@demo.local |
寄件者 |
EMBEDDING_PROVIDER |
local |
離線雜湊向量(256 維),與 BM25 混合排序 |
INBOUND_WEBHOOK_SECRET |
本機字串 | POST /api/connectors/email/inbound 的驗證 |
UPLOAD_DIR / MAX_UPLOAD_BYTES |
./var/uploads / 20 MB |
知識文件儲存位置與大小上限 |
啟用真實 LLM:把 LLM_PROVIDER=anthropic 與 ANTHROPIC_API_KEY=... 寫進 .env 後重啟。金鑰只存在伺服器端,不會進 client bundle;〈設定〉頁會顯示目前實際使用的 adapter 與退回原因。
所有 mutation 都會:驗證 session → 檢查角色 → Zod 驗證輸入 → 檢查目前狀態 → 寫稽核紀錄。錯誤格式一致:
{ "ok": false, "requestId": "req_...", "error": { "code": "FORBIDDEN", "message": "報價尚未核准,不可寄送。", "details": null } }| Method | Path |
|---|---|
| POST | /api/auth/login、/api/auth/logout |
| GET/POST | /api/inquiries |
| GET/PATCH | /api/inquiries/:id |
| POST | /api/inquiries/:id/extract、/clarification-draft、/clarification-send、/quote-draft、/items |
| PATCH/DELETE | /api/inquiry-items/:itemId |
| GET/PATCH | /api/quotes/:id |
| POST | /api/quotes/:id/submit、/api/quotes/:id/send |
| POST | /api/approvals/:id/approve|return|reject |
| GET/POST | /api/knowledge、/api/knowledge/upload |
| POST | /api/knowledge/:id/process、/api/knowledge/:id/expire |
| GET | /api/knowledge/search?q=、/api/knowledge/:id/download |
| POST | /api/knowledge/answer |
| GET/POST | /api/products、/api/customers、/api/sops |
| PATCH | /api/products/:id、/api/customers/:id、/api/sop-versions/:id |
| POST | /api/products/import、/api/customers/import、/api/sops/:id/draft、/api/sop-versions/:id/submit|publish |
| POST | /api/tasks/:id/complete |
| GET/PATCH | /api/inquiries/:id/measurement(兩邊碼表實測分鐘、輸入類型、量測備註) |
| GET | /api/analytics/cases?format=json|csv&measuredOnly=true |
| GET/POST | /api/customer-product-aliases |
| POST | /api/customer-product-aliases/import(preview: true 只檢查不寫入) |
| POST | /api/customer-product-aliases/:id/deactivate |
| GET/PATCH | /api/settings |
| POST | /api/connectors/email/inbound(需 x-webhook-secret) |
| GET | /api/health |
pnpm test # 21 個檔案 / 199 個測試(112 unit + 87 integration)
pnpm test:e2e # 10 個 Playwright 測試(情境 A/B/C/D/E + 權限 + 375px + adapter 鎖定)
pnpm verify # lint + typecheck + test + production build- Unit:金額與幣別計算、風險規則、狀態機、權限矩陣、AI structured output schema、檔案解析(含掃描 PDF 的失敗路徑)、BM25/向量排序、CSV、成效量測(活躍時間推估、修改率、可信度判定)。
- Integration:情境 A(建案→擷取→報價→送審→核准→寄送→追蹤任務→稽核)、情境 B(缺件擋住報價+追問草稿)、情境 C(高風險與核准權限)、情境 D(未核准寄送被擋、Outbox 無紀錄)、情境 E(沒依據不亂答)、情境 F(組織隔離)、API 邊界(401/403/422、webhook 驗證)、成效報表(修改率、基準線、CSV、組織隔離、不影響報價流程)、客戶料號對照(衝突拒絕含 DB trigger、不同客戶同料號、有效期間、組織隔離、CSV 預覽與部分成功、指標歸因)。
- E2E:以真實瀏覽器跑完 A/B/C 三個情境,加上未登入導向、唯讀角色限制、375px 無水平捲軸。
測試一律使用 mock adapter,不依賴任何外部 API。Vitest 會自建 prisma/test.db,Playwright 會自建 prisma/e2e.db,都不會動到 dev.db。
E2E 用 mock 是機械保證,不是靠人工記得切 .env。 E2E 會觸發擷取、報價說明與知識問答;只要 .env 是 anthropic,跑一次測試就等於拿真實額度去跑。因此有三層鎖定:
scripts/e2e-serve.mjs把ANTHROPIC_API_KEY/RESEND_API_KEY從子行程移除,並覆寫LLM_PROVIDER=mock、EMAIL_PROVIDER=mock、E2E=1。src/lib/env.ts的 E2E 鎖定:即使 Next.js 載入.env時又把金鑰塞回process.env,應用層仍會清掉它;provider 不是 mock 就直接拋錯讓伺服器起不來。這一層不依賴任何載入順序。tests/e2e/global-setup.ts在跑第一個測試之前先問/api/health:adapter 不是 mock 就中止整輪。
Vitest 那側由 vitest.config.ts 寫死 LLM_PROVIDER: 'mock'。對應測試:tests/unit/env-e2e-lockdown.test.ts 與 E2E 的〈E2E 一律使用 mock adapter〉。
- 組織、Email/密碼登入、session、四種角色與權限矩陣,所有查詢都以
organization_id隔離。 - 知識來源:上傳(pdf/docx/xlsx/csv/txt/md)、解析、分段、索引、混合檢索、失效標記、原始檔下載、重新解析。
- SOP:結構化步驟編輯器、
draft → in_review → published → archived、版本不可覆寫、發布後自動同步進知識索引供報價引用。 - 詢價:貼上/手動/Demo 範例/webhook 建立、AI 擷取(欄位信心+原文證據)、人工修改、缺件判斷、追問草稿與模擬寄送。
- 報價:產品比對、價格版本快照、庫存與交期、歷史折扣中位數、匯率換算(用組織設定的匯率,不抓即時匯率)、稅與運費規則、風險旗標、版本管理、列印版報價單。
- 核准中心:待辦佇列、與前一版差異、風險與來源、核准/退回/拒絕、送審者不得自審。
- 寄送:MockEmailAdapter(預設)/ResendEmailAdapter、Outbox、重複寄送保護、3 個工作天後的追蹤任務。
- 稽核:所有核心動作寫入 append-only 事件表(DB trigger 阻擋 UPDATE/DELETE),含變更前後、request ID 與 AI adapter 資訊。
- 全站 zh-TW 介面、桌面優先、手機可核准(375px 無水平捲軸)。
- PDF 匯出:目前提供穩定的列印版 HTML(
/quotes/:id/print,瀏覽器列印即可存 PDF)。伺服器端產 PDF 需要 headless 瀏覽器,成本會影響核心流程,排在下一階段。 - 掃描版 PDF/OCR:文字型 PDF 可解析;掃描檔會明確標示「目前無法可靠解析」,不會產生假內容。
- 真正的向量資料庫:本機用可重現的雜湊向量 + BM25 混合排序;
pnpm schema:postgres會產生 PostgreSQL 版本並列出改用 pgvector 的步驟。 - PostgreSQL 佈署:資料模型已可移轉(附
docker-compose.yml與 schema 產生器),但金額欄位轉成NUMERIC後,讀取路徑需要把Prisma.Decimal轉字串再交給money.ts。這一步還沒做。 - 深色模式:使用場景是白天辦公室的資料密集介面,這版只做淺色,不做半套的深色。
- 使用者邀請/角色編輯 UI(目前由 seed 建立)、LINE/Gmail connector、ERP 整合、多語報價單、成交分析。
- 檢索在應用層計算(Demo 資料量下毫秒等級);資料量放大後應改用資料庫層的全文與向量索引。
- Mock LLM 是規則式擷取器:對格式清楚的詢價很準,對高度非結構化的長信會漏欄位——這正是要換上真實 adapter 的時機,介面完全相同。
- 匯率是組織設定的固定值,不是即時匯率(刻意)。
- 資料最小化:送進模型的 payload 不含標準成本、毛利與成本相關的風險細節。擷取用的產品目錄只有 SKU/品名/規格/單位/標籤(比對 SKU 必需,且不含價格);報價說明只拿到已算好的定價、單價、折扣與交期。由
tests/integration/llm-payload.test.ts守住。 - Fail-closed,不會無聲退回 mock:
LLM_PROVIDER=anthropic時,API 失敗會明確拋錯並在ai_runs記成provider=anthropic, status=failed;沒有金鑰時直接拋MISCONFIGURED、/api/health回 503、設定頁顯示紅色警告——不會改用 mock 假裝成功。mock 只在LLM_PROVIDER=mock(或未設定)的本機 Demo 模式啟用。由tests/integration/llm-failover.test.ts守住。 - 測試不會消耗 API 額度:
pnpm test與pnpm test:e2e都被強制鎖在 mock adapter(三層保護見〈6. 測試〉)。這是成本保護,也是正確性保護——測試結果不該取決於某次 API 是否可用。 - 用量以供應商回報為準,不推算:每次
runAi()會把 Anthropic 回報的usage.input_tokens/output_tokens寫進ai_runs;schema 驗證失敗重試時,兩次往返的用量會累加成一筆(apiCalls記錄實際往返次數)。拿不到 usage(HTTP 失敗、逾時、mock)時欄位一律留null,不會補 0——0 的意思是「沒花到 token」,而實際情況是「不知道」。costUsd是估算值,只有設定LLM_INPUT_RATE_USD_PER_MTOK/LLM_OUTPUT_RATE_USD_PER_MTOK時才計算,並把當下費率一起存進inputRateUsdPerMTok/outputRateUsdPerMTok;程式裡刻意沒有內建價格,避免價格調整後新舊資料用不同基準卻長得一樣。實際帳單以 Anthropic Console 為準。由tests/integration/llm-usage.test.ts守住。 - 上傳文件與客戶來信一律視為不可信資料。真實 adapter 會把它們包在
<untrusted>標籤裡並明確指示不得執行其中的指示;mock adapter 會偵測「請直接核准/忽略先前指示」這類句子,標記unsafeInstructionsDetected並在案件頁顯示警告。核准與寄送永遠需要人操作。 - AI 沒有任意資料庫、檔案或網路存取權:只能拿到服務層準備好的 context,工具是白名單,輸入輸出都經 Zod 驗證(失敗重試一次,仍失敗就顯示可理解的錯誤並交回人工)。
- 密碼使用 scrypt(每筆獨立 salt);session token 只存 HMAC 雜湊;cookie 為 httpOnly + SameSite=Lax,production 加上 Secure。
- 上傳限制副檔名、MIME 與大小(預設 20 MB),檔名會去除路徑字元。
- 結構化 log 帶 request ID,並自動遮蔽 password/token/api key 等欄位;錯誤訊息不含 stack trace。
- 本機用 SQLite:這台開發機沒有 Docker,規格允許 SQLite fallback。資料模型保持可移轉,並附上 PostgreSQL schema 產生器與 compose 檔。
- 自建 session 認證(規格允許「Auth.js 或等價的 session-based authentication」):cookie +
sessions資料表 + scrypt,少一層設定、好測試。 - 金額用 decimal 字串:SQLite 沒有真正的 NUMERIC;全系統以字串進出、
decimal.js計算,四捨五入採台灣報價慣用的 half-up,JPY 無小數位。 - 送審者一律不得核准自己的報價(規格只要求高風險時禁止):職責分離更好解釋,也更好測。
- 匯率由組織設定:即時匯率屬於未來範圍,但多幣別報價又必須算得出來,因此用人工維護的匯率表,並把它當成一筆可追溯的引用來源。
- 手動知識頁面也走同一條索引管線,SOP 發布後自動鏡射成知識來源,讓報價引用與知識問答共用同一套檢索。




