分類: 開發集成

  • iDempiere Agent SDK 企業級設計:本地模型 + 脫敏恢復架構

    文檔版本: v2.0 Enterprise | 發布日期: 2026-03-20 | 針對: 企業隱私、資料安全、本地部署

    📑 目錄


    🎯 核心挑戰 — 三層矛盾

    企業要用 AI 分析敏感資料,但面臨三個互相衝突的需求:

    需求 挑戰 風險
    用 AI 智能分析資料 LLM 需要看到足夠的上下文才能分析準確 如果資料不完整,分析結果錯誤
    資料不離開本地伺服器 敏感資料(客戶名、電話、身份證)不能上雲 違反 GDPR、個資法、企業隱私政策
    最後還是需要看原始敏感資訊 脫敏後的資料需要恢復成原始值供人工審核 如果無法準確恢復,所有分析都沒用

    解決方案: 四層脫敏-分析-恢復架構,確保 masked values 在整個流程中被保留和追蹤。


    🏗️ 四層架構設計

    架構圖

    ┌─────────────────────────────────────────────────────────────────┐
    │ 第 1 層:原始資料(iDempiere 資料庫)                              │
    │ ─────────────────────────────────────────────────────────────    │
    │ Customer: ABC Corp                                              │
    │ Amount: $50,000                                                 │
    │ Phone: +886-2-1234-5678                                         │
    │ Email: [email protected]                                     │
    └─────────────────────────────┬─────────────────────────────────────┘
                                  │
                          🔐 脫敏層 (MASK)
                                  │
    ┌─────────────────────────────▼─────────────────────────────────────┐
    │ 第 2 層:脫敏層(本地加密 + 映射表)                               │
    │ ─────────────────────────────────────────────────────────────    │
    │ Customer: CUST_A1B2C3D4 ← 記錄映射 (AES-256 加密)                │
    │ Amount: $50,000         ← 金額不脫敏(需要分析)                   │
    │ Phone: +886-***-****-78 ← 電話遮罩                               │
    │ Email: j***@abccorp.com ← 郵箱遮罩                               │
    │                                                                 │
    │ 映射表(本地密鑰管理):                                         │
    │ CUST_A1B2C3D4 ←→ "ABC Corp" (AES-256 加密)                      │
    └─────────────────────────────┬─────────────────────────────────────┘
                                  │
                       📤 發送給 LLM 分析
                       (脫敏資料保留 masked values)
                                  │
    ┌─────────────────────────────▼─────────────────────────────────────┐
    │ 第 3 層:LLM 分析(本地 Ollama 或雲端 Claude)                    │
    │ ─────────────────────────────────────────────────────────────    │
    │ 輸入:「Customer CUST_A1B2C3D4 有 $50,000 訂單...」              │
    │ 分析邏輯:趨勢、風險、建議                                       │
    │ 輸出:「Customer CUST_A1B2C3D4 是最大客戶,風險等級...」         │
    │                                                                 │
    │ ⭐ 關鍵:LLM 輸出中仍然包含 CUST_A1B2C3D4                         │
    └─────────────────────────────┬─────────────────────────────────────┘
                                  │
                          🔓 恢復層 (REVERSE)
                       (根據用戶權限查詢映射表)
                                  │
    ┌─────────────────────────────▼─────────────────────────────────────┐
    │ 第 4 層:權限控制恢復 + 審計日誌                                   │
    │ ─────────────────────────────────────────────────────────────    │
    │ Admin:「Customer ABC Corp 是最大客戶...」 ✅ 完全恢復           │
    │ Manager:「Customer ABC Corp 是最大客戶...」 ✅ 有限恢復          │
    │ Analyst:「Customer CUST_A1B2C3D4 是最大客戶...」 ❌ 不可恢復     │
    │ Viewer:「Customer *** 是最大客戶...」 ❌ 完全遮罩                │
    │                                                                 │
    │ 審計日誌:                                                      │
    │ [2026-03-20 14:30:45] ADMIN 試圖恢復 CUST_A1B2C3D4 ✅ 成功       │
    │ [2026-03-20 14:31:12] ANALYST 試圖恢復 CUST_A1B2C3D4 ❌ 拒絕     │
    └─────────────────────────────────────────────────────────────────────┘

    四層的角色

    位置 功能 安全機制
    第 1 層
    原始資料
    iDempiere DB
    (本地)
    存儲完整的敏感資料 DB 加密、訪問控制
    第 2 層
    脫敏層
    Python 應用
    (本地記憶體)
    脫敏資料、管理映射表、產生脫敏版本 AES-256 加密、本地密鑰
    第 3 層
    LLM 分析
    Ollama 或
    Claude API
    只看脫敏資料,不知道原始值 LLM 無法反向推測原始值
    第 4 層
    恢復層
    Python 應用
    (本地)
    識別 LLM 輸出中的 masked values,根據權限恢復 權限檢查、審計日誌

    ⚖️ 本地 LLM vs 雲端模型

    方面 本地 LLM(Ollama) 雲端模型(Claude API) 建議方案
    隱私 ✅ 資料完全本地
    ✅ 沒有外洩風險
    ⚠️ 脫敏後上雲
    ✅ 有企業協議保護
    偏好本地
    分析質量 ⚠️ Llama 2/Mistral
    ❌ 複雜邏輯可能不夠準確
    ✅ Claude 很強
    ✅ 複雜推理更準確
    偏好 Claude
    成本 ✅ 一次投資
    ✅ GPU: $5-10k
    ✅ 無持續費用
    ❌ 按使用量付費
    ❌ $0.003/1K tokens
    ❌ 高量時昂貴
    看量級
    維護 ❌ 需要 GPU、記憶體
    ❌ 模型更新自己管理
    ✅ 完全託管
    ✅ 模型自動更新
    看資源

    混合方案(推薦)

    70% 簡單查詢用本地 Ollama,30% 複雜分析用 Claude。成本和質量的最佳平衡。


    💰 5 年成本分析

    方案 初期投資 年度運營 5 年總成本
    ✅ 本地 Ollama(100%) $8,000 $1,000 $13,000
    ❌ Claude API(100%) $0 $4,920 $24,600
    ⭐ 混合(70% 本地 + 30% Claude) $8,000 $1,876 $17,380

    💻 代碼實現 — MASK 和 REVERSE 的完整流程

    ⭐ 核心要點: Masked values(如 CUST_A1B2C3D4)必須在整個流程中被保留,這樣才能在 LLM 輸出中追蹤和恢復。

    Step 1: TOOL 輸出原始資料(未脫敏)

    # 從 iDempiere 資料庫查詢
    TOOL_OUTPUT = {
        "customer_name": "ABC Corp",
        "customer_phone": "+886-2-1234-5678",
        "customer_email": "[email protected]",
        "order_amount": 50000,
        "order_date": "2026-03-20"
    }

    Step 2: MASK 脫敏 — 建立映射表並記錄

    from cryptography.fernet import Fernet
    import hashlib
    
    class DataMaskingEngine:
        def __init__(self):
            # 本地密鑰(企業安全存儲)
            self.encryption_key = Fernet.generate_key()
            self.cipher = Fernet(self.encryption_key)
    
            # 映射表:masked_value → 加密的原始值
            self.mapping_table = {}
    
        def mask_customer_name(self, original_name):
            """脫敏客戶名:原始值 → CUST_HASH"""
            # 1. 生成 hash
            hash_value = hashlib.sha256(
                original_name.encode()
            ).hexdigest()[:8].upper()
    
            masked_value = f"CUST_{hash_value}"
    
            # 2. 加密原始值並存入映射表
            encrypted_original = self.cipher.encrypt(
                original_name.encode()
            )
            self.mapping_table[masked_value] = encrypted_original
    
            # 3. 返回 masked value
            return masked_value
    
        def mask_phone(self, original_phone):
            """脫敏電話:只保留最後 2 碼"""
            return f"{original_phone[:6]}***-****-{original_phone[-2:]}"
    
        def mask_email(self, original_email):
            """脫敏郵箱:只保留第一個字母和域名"""
            local, domain = original_email.split('@')
            masked_local = f"{local[0]}***"
            return f"{masked_local}@{domain}"
    
    
    # 執行脫敏
    masking_engine = DataMaskingEngine()
    
    masked_data = {
        "customer_name": masking_engine.mask_customer_name(
            TOOL_OUTPUT["customer_name"]  # "ABC Corp"
        ),  # 結果:CUST_A1B2C3D4
        "customer_phone": masking_engine.mask_phone(
            TOOL_OUTPUT["customer_phone"]  # "+886-2-1234-5678"
        ),  # 結果:+886-***-****-78
        "customer_email": masking_engine.mask_email(
            TOOL_OUTPUT["customer_email"]  # "[email protected]"
        ),  # 結果:j***@abccorp.com
        "order_amount": TOOL_OUTPUT["order_amount"],  # 50000(不脫敏)
        "order_date": TOOL_OUTPUT["order_date"]  # 2026-03-20(不脫敏)
    }
    
    print("✅ 脫敏後的資料:")
    print(masked_data)
    # {
    #   "customer_name": "CUST_A1B2C3D4",
    #   "customer_phone": "+886-***-****-78",
    #   "customer_email": "j***@abccorp.com",
    #   "order_amount": 50000,
    #   "order_date": "2026-03-20"
    # }
    
    print("\n🔐 映射表(本地加密):")
    for masked_val, encrypted_original in masking_engine.mapping_table.items():
        print(f"  {masked_val} ←→ {encrypted_original[:20]}...")
    # CUST_A1B2C3D4 ←→ gAAAAAB...(加密後)

    Step 3: 發送脫敏資料給 LLM —— 保留 Masked Values

    # 構造給 LLM 的提示
    llm_prompt = f"""
    分析以下客戶訂單資料:
    - 客戶:{masked_data['customer_name']}
    - 訂單金額:${masked_data['order_amount']}
    - 訂單日期:{masked_data['order_date']}
    - 聯絡方式:{masked_data['customer_email']}
    
    請提供:
    1. 這筆訂單的風險等級
    2. 建議的後續行動
    3. 客戶信用評分
    """
    
    print("📤 發送給 LLM 的提示(脫敏 + 保留 masked values):")
    print(llm_prompt)
    # 注意:CUST_A1B2C3D4 被保留在提示中!
    
    # 呼叫 LLM(本地 Ollama 或 Claude API)
    # 重點:LLM 看不到原始的 "ABC Corp",只看到 "CUST_A1B2C3D4"
    llm_response = """
    分析結果:
    - 客戶 CUST_A1B2C3D4 的風險等級:低
    - 建議:增加信用額度
    - 信用評分:900(優良)
    """
    
    print("\n📥 LLM 輸出(仍然包含 CUST_A1B2C3D4):")
    print(llm_response)

    Step 4: REVERSE 恢復 —— 識別 Masked Values 並恢復

    import re
    class DataRecoveryLayer:
        def __init__(self, masking_engine, user_role):
            self.masking_engine = masking_engine
            self.user_role = user_role  # admin, manager, analyst, viewer
            self.audit_log = []
            # 權限定義
            self.permissions = {
                "admin": ["customer_name", "phone", "email"],
                "manager": ["customer_name"],
                "analyst": [],  # 不能恢復任何敏感資訊
                "viewer": []
            }
        def can_restore(self, field_type):
            """檢查用戶是否有權限恢復該欄位"""
            return field_type in self.permissions.get(self.user_role, [])
        def unmask_value(self, masked_value):
            """恢復 masked value 為原始值"""
            if masked_value not in self.masking_engine.mapping_table:
                return masked_value  # 不是 masked value
            # 從映射表取出加密的原始值
            encrypted_original = self.masking_engine.mapping_table[masked_value]
            # 解密
            original_value = self.masking_engine.cipher.decrypt(
                encrypted_original
            ).decode()
            return original_value
        def restore_response(self, llm_output):
            """
            在 LLM 輸出中識別並恢復 masked values
            根據用戶權限決定是否恢復
            """
            restored_output = llm_output
            # 步驟 1: 在 LLM 輸出中找到所有 masked values
            # 模式:CUST_XXXXXXXX(8 位十六進制)
            masked_patterns = re.findall(r'CUST_[A-F0-9]{8}', llm_output)
            # 步驟 2: 對每個 masked value 進行恢復
            for masked_value in masked_patterns:
                # 記錄審計日誌
                self._log_access_attempt(masked_value)
                # 檢查權限
                if self.can_restore("customer_name"):
                    # 有權限:恢復為原始值
                    original_value = self.unmask_value(masked_value)
                    restored_output = restored_output.replace(
                        masked_value,
                        original_value
                    )
                    self._log_access_success(masked_value, original_value)
                else:
                    # 無權限:保留 masked value
                    self._log_access_denied(masked_value)
            return restored_output
        def _log_access_attempt(self, masked_value):
            """記錄訪問嘗試"""
            self.audit_log.append({
                "timestamp": "2026-03-20 14:30:45",
                "user_role": self.user_role,
                "action": "ATTEMPT_UNMASK",
                "masked_value": masked_value
            })
        def _log_access_success(self, masked_value, original_value):
            """記錄成功恢復"""
            self.audit_log.append({
                "timestamp": "2026-03-20 14:30:45",
                "user_role": self.user_role,
                "action": "UNMASK_SUCCESS",
                "masked_value": masked_value,
                "original_length": len(original_value)  # 不記錄原始值本身
            })
        def _log_access_denied(self, masked_value):
            """記錄被拒絕的訪問"""
            self.audit_log.append({
                "timestamp": "2026-03-20 14:30:45",
                "user_role": self.user_role,
                "action": "UNMASK_DENIED",
                "masked_value": masked_value,
                "reason": "INSUFFICIENT_PERMISSIONS"
            })
    # 執行恢復 —— 根據用戶角色
    print("\n=== REVERSE 過程:根據權限恢復 ===\n")
    llm_output = """
    分析結果:
    - 客戶 CUST_A1B2C3D4 的風險等級:低
    - 建議:增加信用額度
    - 信用評分:900(優良)
    """
    # 案例 1: Admin(有所有權限)
    print("【Admin 用戶】")
    recovery_admin = DataRecoveryLayer(masking_engine, "admin")
    restored_admin = recovery_admin.restore_response(llm_output)
    print(restored_admin)
    # 輸出:客戶 ABC Corp 的風險等級:低...
    # 案例 2: Manager(只能看客戶名)
    print("\n【Manager 用戶】")
    recovery_manager = DataRecoveryLayer(masking_engine, "manager")
    restored_manager = recovery_manager.restore_response(llm_output)
    print(restored_manager)
    # 輸出:客戶 ABC Corp 的風險等級:低...
    # 案例 3: Analyst(無法恢復敏感資訊)
    print("\n【Analyst 用戶】")
    recovery_analyst = DataRecoveryLayer(masking_engine, "analyst")
    restored_analyst = recovery_analyst.restore_response(llm_output)
    print(restored_analyst)
    # 輸出:客戶 CUST_A1B2C3D4 的風險等級:低... (沒有恢復)
    # 案例 4: Viewer(完全遮罩)
    print("\n【Viewer 用戶】")
    recovery_viewer = DataRecoveryLayer(masking_engine, "viewer")
    restored_viewer = recovery_viewer.restore_response(llm_output)
    print(restored_viewer)
    # 輸出:客戶 CUST_A1B2C3D4 的風險等級:低... (沒有恢復)
    # 審計日誌
    print("\n=== 審計日誌 ===")
    for admin_log in recovery_admin.audit_log:
        print(f"✅ {admin_log['timestamp']} | {admin_log['user_role']} | {admin_log['action']}")
    for analyst_log in recovery_analyst.audit_log:
        print(f"❌ {analyst_log['timestamp']} | {analyst_log['user_role']} | {analyst_log['action']} (拒絕)")
    

    Step 5: 完整流程圖

    原始資料(TOOL)
        ↓
    ABC Corp, +886-2-1234-5678, [email protected]
        ↓
        ↓ 🔐 MASK (脫敏層)
        ↓
        ├─ Customer: ABC Corp → CUST_A1B2C3D4 (記錄映射表)
        ├─ Phone: +886-2-1234-5678 → +886-***-****-78
        └─ Email: [email protected] → j***@abccorp.com
        ↓
    脫敏資料 + Masked Values
        ↓
        ├─ Customer: CUST_A1B2C3D4  ← 保留!
        ├─ Phone: +886-***-****-78
        └─ Email: j***@abccorp.com
        ↓
        ↓ 📤 發送給 LLM(本地 Ollama 或 Claude API)
        ↓
    LLM 分析脫敏資料
        ↓
        ├─ 輸入: "Customer CUST_A1B2C3D4 有 $50,000 訂單"
        ├─ 分析: 風險評估、信用評分
        └─ 輸出: "Customer CUST_A1B2C3D4 的風險等級:低"
        ↓
    LLM 輸出(仍然包含 CUST_A1B2C3D4)
        ↓
        ↓ 🔓 REVERSE (恢復層)
        ↓
    檢查用戶權限
        ↓
    ├─ Admin ✅ 可以恢復 → "Customer ABC Corp 的風險等級:低"
    ├─ Manager ✅ 可以恢復 → "Customer ABC Corp 的風險等級:低"
    ├─ Analyst ❌ 不可恢復 → "Customer CUST_A1B2C3D4 的風險等級:低"
    └─ Viewer ❌ 不可恢復 → "Customer CUST_A1B2C3D4 的風險等級:低"
        ↓
    📋 審計日誌
        ├─ [Admin] UNMASK_SUCCESS: CUST_A1B2C3D4
        ├─ [Manager] UNMASK_SUCCESS: CUST_A1B2C3D4
        ├─ [Analyst] UNMASK_DENIED: CUST_A1B2C3D4
        └─ [Viewer] UNMASK_DENIED: CUST_A1B2C3D4
    

    關鍵安全要點

    安全考慮 怎麼做 為什麼重要
    💾 映射表保存 本地加密(AES-256)+ 安全存儲(不在代碼中) 如果映射表洩露,攻擊者可以反推原始值
    🔑 密鑰管理 密鑰存在環境變數或 AWS KMS,不在代碼裡 代碼洩露時,沒有密鑰,攻擊者無法解密
    📊 Masked Values 保留 MASK 時使用確定性 HASH(同一值每次都產生相同 masked value) LLM 輸出中的 masked value 必須與 MASK 時的相同,才能查詢映射表
    🛡️ 權限檢查 REVERSE 前檢查用戶角色,然後才恢復 即使 LLM 輸出正確,無權限的用戶也看不到原始值
    📜 審計日誌 記錄所有訪問嘗試(成功和失敗) 符合 GDPR(日誌保留 7 年)、監測異常訪問、事件追蹤
    🚨 異常檢測 監測同一用戶短時間內多次失敗的恢復嘗試 可能是在試圖猜測或攻擊,觸發告警

    ✅ 企業部署清單

    安全檢查

    • ✅ 加密密鑰不在代碼庫中(存在 AWS KMS、Azure Key Vault 或環境變數)
    • ✅ 映射表存儲位置是加密的資料庫或本地檔案(不可讀權限)
    • ✅ 所有敏感操作都有日誌記錄
    • ✅ 定期備份和災難恢復計畫
    • ✅ 權限模型已定義並測試(admin/manager/analyst/viewer)
    • ✅ SQL injection、XSS 等防護已實施

    隱私合規

    • ✅ GDPR 合規:資料最小化、訪問控制、刪除權
    • ✅ 台灣個資法:敏感個資完全脫敏或刪除
    • ✅ 審計日誌保留 7 年
    • ✅ 用戶同意條款已更新(說明脫敏機制)
    • ✅ 第三方 LLM(Claude API)有資料處理協議(DPA)

    性能和可靠性

    • ✅ 脫敏和恢復的延遲在可接受範圍(< 100ms)
    • ✅ 在大量並發訪問下,映射表查詢仍然快速(考慮 Redis 緩存)
    • ✅ 本地 LLM(Ollama)的 GPU 記憶體充足
    • ✅ Claude API 配額充足,或有備用方案
    • ✅ 網路連線中斷時,本地功能仍可用

    運維檢查

    • ✅ 團隊了解脫敏-分析-恢復的整個流程
    • ✅ 有故障排查指南(映射表損壞、密鑰丟失)
    • ✅ 定期演練災難恢復(重新產生映射表、恢復備份)
    • ✅ 監控告警已設置(REVERSE 失敗、異常訪問)
    • ✅ 文檔完整(架構、API、權限、故障排查)

    範例

    https://github.com/tm731531/idempiere-agent-sdk-sample/tree/main


    🎯 總結

    通過四層架構和 Masked Values 追蹤機制,企業可以:

    • ✅ 用 AI 智能分析敏感資料
    • ✅ 確保資料不會洩露給 LLM
    • ✅ 根據用戶權限準確恢復原始值
    • ✅ 完整審計,符合法規要求
    • ✅ 成本可控(混合本地 + 雲端)

    關鍵要點: Masked values 必須被保留和追蹤,否則恢復層就無法工作。這個架構確保了脫敏和恢復的完整性。

  • 【整合與部署】2Pack 打包與 Plugin 開發:環境同步完全指南

    2Pack 打包與 Plugin 開發:環境同步完全指南

    2Pack 是 iDempiere 的設定匯出/匯入機制,可將 Table、Window、Menu 等定義打包成 XML,在不同環境間同步。本文將教你如何使用 2Pack 以及開發自己的 Plugin。

    (閱讀全文…)

  • 【開發實戰】Document Workflow 設計:簽核流程完全攻略

    Document Workflow 設計:簽核流程完全攻略

    iDempiere 內建完整的文件工作流引擎,支援多層簽核、自動審批、條件分支等功能。本文將帶你了解 Document Workflow 的核心概念,並實作一個訂單三層簽核流程。

    (閱讀全文…)

  • 【開發實戰】Callout 與 Model Validator:事件驅動開發實戰

    Callout 與 Model Validator:事件驅動開發實戰

    iDempiere 提供兩種事件驅動機制:Callout 處理 UI 層即時互動,Model Validator 處理資料層驗證與後處理。本文將詳解兩者差異,並提供完整的實作範例。

    (閱讀全文…)

  • 【開發實戰】建立第一個自訂 Window:從 Table 到 Menu 全流程實作

    建立第一個自訂 Window:從 Table 到 Menu 全流程實作

    想在 iDempiere 中建立自己的功能模組?本文以「圖書借閱系統」為例,帶你一步步完成從 Table 定義到 Menu 上線的完整流程。這是 iDempiere 開發的基礎功,學會後就能自由擴充系統功能。

    (閱讀全文…)