# Client SDK Migration Plan: ZIP Packaging Support

## 概述

此文檔說明當 Gateway 更新為 ZIP 打包功能後，Client SDK 需要進行的修改。

**重要提示：** 在執行此計畫之前，請確保遠端 Gateway 已更新並部署。

---

## 背景

### Gateway API 變更

**舊版 Gateway (pre-2.0):**
```
GET /api/v1/tasks/{task_id}/results/trades     → 單一 Parquet 檔案
GET /api/v1/tasks/{task_id}/results/performance → 單一 JSON/Parquet 檔案
```

**新版 Gateway (2.0+):**
```
GET /api/v1/tasks/{task_id}/result → ZIP 壓縮包
  ├── trades.parquet
  └── performance.parquet (Parquet 格式，非 JSON)
```

### 關鍵變更

1. ✅ **Performance 資料現在是 Parquet 格式** (不再是 JSON)
2. ✅ **單一端點返回 ZIP** (包含所有結果)
3. ✅ **使用 Redis Pipeline 優化** (單次網路請求)

---

## 當前狀態分析

### ✅ 已實作但未使用

**檔案:** `packages/client/src/tradepose_client/resources/tasks.py`
- **Lines 73-153**: `download_backtest_results()` 方法
  - ✅ 已支援 ZIP 解壓縮
  - ✅ 已支援 `trades.parquet` 和 `performance.parquet`
  - ✅ 已有完整錯誤處理
  - ❌ **但 BatchTester 沒有使用此方法！**

### ❌ 需要更新

**檔案:** `packages/client/src/tradepose_client/batch/tester.py`
- **Lines 224-249**: `_async_download_results()` 方法
  - ❌ 仍使用已棄用的 `download_result_by_type()`
  - ❌ 呼叫兩次 API (trades + performance)
  - ❌ 效能較差

**檔案:** `packages/client/src/tradepose_client/batch/background.py`
- **Lines 148-214**: `_download_completed()` 方法
  - ❌ 仍使用已棄用的 `download_result_by_type()`
  - ❌ 對每個 task 呼叫兩次 API

---

## 修改計畫

### 階段 1: 更新核心下載邏輯 (優先級：HIGH)

#### 1.1 更新 `BatchTester._async_download_results()`

**檔案:** `packages/client/src/tradepose_client/batch/tester.py`
**修改位置:** Lines 224-249

**舊程式碼:**
```python
async def _async_download_results(
    self, task_id: str
) -> tuple[pl.DataFrame | None, pl.DataFrame | None]:
    """Download backtest results for completed task."""
    try:
        async with TradeposeClient(
            api_key=self._api_key, server_url=self._server_url
        ) as client:
            # Download trades and performance in parallel
            trades_task = client.tasks.download_result_by_type(task_id, "trades")
            perf_task = client.tasks.download_result_by_type(task_id, "performance")

            trades_df, perf_df = await asyncio.gather(
                trades_task, perf_task, return_exceptions=True
            )

            # Handle errors
            if isinstance(trades_df, Exception):
                logger.error(f"Failed to download trades: {trades_df}")
                trades_df = None
            if isinstance(perf_df, Exception):
                logger.error(f"Failed to download performance: {perf_df}")
                perf_df = None

            return (trades_df, perf_df)

    except Exception as e:
        logger.error(f"Failed to download results for task {task_id}: {e}")
        return (None, None)
```

**新程式碼:**
```python
async def _async_download_results(
    self, task_id: str
) -> tuple[pl.DataFrame | None, pl.DataFrame | None]:
    """
    Download backtest results using ZIP-aware method.

    This method now uses the optimized download_backtest_results() which:
    - Downloads a single ZIP archive (1 API call instead of 2)
    - Extracts both trades.parquet and performance.parquet
    - Performance data is now Parquet format (not JSON)
    """
    try:
        async with TradeposeClient(
            api_key=self._api_key, server_url=self._server_url
        ) as client:
            # Use new ZIP-aware download method (single API call)
            trades_df, perf_df = await client.tasks.download_backtest_results(
                task_id,
                as_dataframe=True
            )
            return (trades_df, perf_df)

    except Exception as e:
        logger.error(f"Failed to download results for task {task_id}: {e}")
        return (None, None)
```

**變更摘要:**
- ✅ 從 2 次 API 呼叫減少為 1 次
- ✅ 自動處理 ZIP 解壓縮
- ✅ Performance 資料自動轉為 DataFrame
- ✅ 更簡潔的錯誤處理

---

#### 1.2 更新 `BackgroundPoller._download_completed()`

**檔案:** `packages/client/src/tradepose_client/batch/background.py`
**修改位置:** Lines 148-214

**舊程式碼:**
```python
async def _download_completed(self) -> None:
    """Download results for newly completed tasks."""
    async with TradeposeClient(**self._client_config) as client:
        download_tasks = []
        task_ids = []

        for result in self._results._period_results.values():
            if result.status == TaskStatus.COMPLETED:
                if not self._results._cache or not self._results._cache.has(
                    result.task_id, "trades"
                ):
                    # Queue download tasks
                    download_tasks.append(
                        asyncio.gather(
                            client.tasks.download_result_by_type(
                                result.task_id, "trades"
                            ),
                            client.tasks.download_result_by_type(
                                result.task_id, "performance"
                            ),
                            return_exceptions=True,
                        )
                    )
                    task_ids.append(result.task_id)

        # ... rest of the method
```

**新程式碼:**
```python
async def _download_completed(self) -> None:
    """
    Download results for newly completed tasks using ZIP-aware method.

    Uses the optimized download_backtest_results() which downloads a single
    ZIP archive per task instead of 2 separate API calls.
    """
    async with TradeposeClient(**self._client_config) as client:
        download_tasks = []
        task_ids = []

        for result in self._results._period_results.values():
            if result.status == TaskStatus.COMPLETED:
                if not self._results._cache or not self._results._cache.has(
                    result.task_id, "trades"
                ):
                    # Use new ZIP-aware method (1 API call instead of 2)
                    download_tasks.append(
                        client.tasks.download_backtest_results(result.task_id)
                    )
                    task_ids.append(result.task_id)

        if not download_tasks:
            return

        # Download all results concurrently
        try:
            results = await asyncio.gather(*download_tasks, return_exceptions=True)
        except Exception as e:
            logger.error(f"Error gathering download results: {e}")
            return

        # Cache downloaded results
        for task_id, result_or_error in zip(task_ids, results):
            if isinstance(result_or_error, Exception):
                logger.error(
                    f"Error downloading result for task {task_id}: {result_or_error}"
                )
                continue

            # Unpack the tuple (trades_df, perf_df)
            trades_df, perf_df = result_or_error

            # Cache both DataFrames
            if trades_df is not None and self._results._cache:
                self._results._cache.set(task_id, "trades", trades_df)
            if perf_df is not None and self._results._cache:
                self._results._cache.set(task_id, "performance", perf_df)

            logger.debug(f"Downloaded and cached results for task {task_id}")
```

**變更摘要:**
- ✅ 每個 task 從 2 次 API 呼叫減少為 1 次
- ✅ 批次下載時總 API 呼叫數減半
- ✅ 更清晰的結果解包邏輯

---

### 階段 2: 向後相容處理 (優先級：MEDIUM)

#### 2.1 更新 `download_result_by_type()` 文檔

**檔案:** `packages/client/src/tradepose_client/resources/tasks.py`
**修改位置:** Lines 294-364 (docstring)

**更新 docstring:**
```python
async def download_result_by_type(
    self,
    task_id: str,
    result_type: str,
    as_dataframe: bool = True,
) -> pl.DataFrame | bytes | dict:
    """
    Download a specific result type from a completed backtest task.

    **⚠️ DEPRECATED for backtest_results export type.**
    **Use download_backtest_results() instead for better performance.**

    This method now internally delegates to download_backtest_results()
    for backtest tasks (trades/performance), providing backward compatibility
    while maintaining performance.

    Migration example:
        # Old (deprecated):
        trades = await client.tasks.download_result_by_type(task_id, "trades")
        perf = await client.tasks.download_result_by_type(task_id, "performance")

        # New (recommended):
        trades, perf = await client.tasks.download_backtest_results(task_id)

    Args:
        task_id: Task UUID string
        result_type: Type of result to download
            - "trades": Trade history DataFrame
            - "performance": Performance metrics DataFrame
            - "enhanced_ohlcv": Enhanced OHLCV data
            - "latest_trades": Latest trades snapshot
        as_dataframe: If True, return Polars DataFrame; if False, return raw bytes

    Returns:
        Polars DataFrame if as_dataframe=True, otherwise raw bytes

    Raises:
        ResourceNotFoundError: Task not found or result not available
        TaskNotCompletedError: Task not yet completed
        ValidationError: Invalid result type

    Note:
        For backtest tasks, this internally uses download_backtest_results()
        and extracts the requested result type, providing the same API
        while benefiting from ZIP packaging performance improvements.
    """
    # Implementation remains the same
    ...
```

**不需修改實作邏輯** - 只需更新文檔警告使用者遷移到新 API。

---

### 階段 3: 更新文檔 (優先級：MEDIUM)

#### 3.1 更新 ARCHITECTURE.md

**檔案:** `packages/client/ARCHITECTURE.md`
**新增章節:** 在現有的 "BatchTester API" 章節後

**新增內容:**
```markdown
## 批次測試性能優化 (v2.0+)

### ZIP 打包下載

從 v2.0 開始，BatchTester 使用 ZIP 打包下載所有回測結果，大幅提升性能：

**優化前 (v1.x):**
```python
# 每個 task 需要 2 次 API 呼叫
trades = await download_result_by_type(task_id, "trades")      # API call 1
perf = await download_result_by_type(task_id, "performance")  # API call 2
```

**優化後 (v2.0+):**
```python
# 每個 task 只需 1 次 API 呼叫
trades, perf = await download_backtest_results(task_id)  # API call 1 (ZIP)
```

**性能改善:**
- API 呼叫數減少 **50%**
- 網路往返減少 **50%**
- 下載時間減少約 **30-40%**
- Performance 資料現在是 **Parquet 格式** (更快、更小)

### 遷移指南

#### 遷移步驟

1. **確認 Gateway 版本**
   - 檢查遠端 Gateway 是否已更新至 v2.0+
   - 確認 `/api/v1/tasks/{task_id}/result` 端點返回 ZIP

2. **更新 Client SDK**
   ```bash
   uv sync --package tradepose-client
   ```

3. **程式碼遷移**

   **舊程式碼 (仍然可用，但較慢):**
   ```python
   from tradepose_client import TradeposeClient

   async with TradeposeClient(api_key="...") as client:
       # 2 次 API 呼叫
       trades = await client.tasks.download_result_by_type(task_id, "trades")
       perf = await client.tasks.download_result_by_type(task_id, "performance")
   ```

   **新程式碼 (推薦):**
   ```python
   from tradepose_client import TradeposeClient

   async with TradeposeClient(api_key="...") as client:
       # 1 次 API 呼叫 (ZIP)
       trades, perf = await client.tasks.download_backtest_results(task_id)
   ```

4. **BatchTester 自動遷移**
   - BatchTester 內部已自動使用新 API
   - 使用者程式碼無需修改
   - 性能自動提升

#### 驗證遷移

```python
import time
from tradepose_client import BatchTester
from tradepose_client.batch import Period

tester = BatchTester(api_key="...")

# 提交批次測試
batch = tester.submit(
    strategies=[strategy],
    periods=[Period.Q1(2024), Period.Q2(2024)]
)

# 測量下載時間
start = time.time()
batch.wait()  # 內部使用新 ZIP API
elapsed = time.time() - start

print(f"Download time: {elapsed:.2f}s (應該比舊版快 30-40%)")
```

### 格式變更

#### Performance 資料格式

**重要變更:** Performance 資料現在是 **Parquet DataFrame**，不再是 JSON。

**舊版 (JSON):**
```python
{
    "total_return": 0.15,
    "sharpe_ratio": 1.5,
    "max_drawdown": -0.08,
    ...
}
```

**新版 (Parquet DataFrame):**
```python
import polars as pl

# Performance DataFrame (1 row)
shape: (1, 7)
┌──────────────┬──────────────┬──────────────┬──────────┬──────────────┬─────────────────┬────────────────┐
│ total_return ┆ sharpe_ratio ┆ max_drawdown ┆ win_rate ┆ total_trades ┆ winning_trades  ┆ losing_trades  │
│ ---          ┆ ---          ┆ ---          ┆ ---      ┆ ---          ┆ ---             ┆ ---            │
│ f64          ┆ f64          ┆ f64          ┆ f64      ┆ i64          ┆ i64             ┆ i64            │
╞══════════════╪══════════════╪══════════════╪══════════╪══════════════╪═════════════════╪════════════════╡
│ 0.15         ┆ 1.5          ┆ -0.08        ┆ 0.65     ┆ 100          ┆ 65              ┆ 35             │
└──────────────┴──────────────┴──────────────┴──────────┴──────────────┴─────────────────┴────────────────┘
```

**優勢:**
- ✅ 型別安全 (Polars DataFrame)
- ✅ 更快的解析速度
- ✅ 更小的檔案大小
- ✅ 與 trades 資料一致的格式

#### 存取方式

```python
# 取得 performance metrics
trades, perf = await client.tasks.download_backtest_results(task_id)

# Parquet DataFrame - 使用列索引存取
total_return = perf["total_return"][0]  # 0.15
sharpe_ratio = perf["sharpe_ratio"][0]  # 1.5

# 或轉為字典
metrics = perf.to_dicts()[0]
# {'total_return': 0.15, 'sharpe_ratio': 1.5, ...}
```
```

---

#### 3.2 更新 README.md

**檔案:** `packages/client/README.md`
**位置:** Quick Start 章節

**新增效能提示:**
```markdown
## 快速開始

### 批次回測 (推薦方式)

```python
from tradepose_client import BatchTester
from tradepose_client.batch import Period

# 建立 tester
tester = BatchTester(api_key="your_api_key")

# 提交批次測試
batch = tester.submit(
    strategies=[strategy1, strategy2],
    periods=[Period.Q1(2024), Period.Q2(2024)]
)

# 等待完成並自動下載結果 (使用 ZIP 優化)
batch.wait()  # ⚡ v2.0+ 速度提升 30-40%

# 存取結果
for period_key, result in batch.results.items():
    print(f"Period: {period_key}")
    print(f"Trades: {len(result.trades)} rows")
    print(f"Sharpe Ratio: {result.performance['sharpe_ratio'][0]}")
```

**性能改善 (v2.0+):**
- 每個 task 的 API 呼叫從 2 次減少為 1 次
- 使用 ZIP 壓縮打包所有結果
- Performance 資料使用 Parquet 格式 (更快、更小)
```

---

#### 3.3 更新 API_REFERENCE.md

**檔案:** `packages/client/docs/API_REFERENCE.md`
**新增章節:**

```markdown
## TasksResource

### download_backtest_results() ⭐ 推薦

下載回測結果 (ZIP 打包，單次 API 呼叫)。

**自 v2.0 起為推薦方法** - 使用 ZIP 打包優化，比分別下載快 30-40%。

```python
async def download_backtest_results(
    self,
    task_id: str,
    as_dataframe: bool = True,
) -> tuple[pl.DataFrame | bytes, pl.DataFrame | bytes]
```

**參數:**
- `task_id` (str): 任務 UUID
- `as_dataframe` (bool): 是否轉換為 DataFrame (預設: True)

**返回:**
- `tuple[pl.DataFrame, pl.DataFrame]`: (trades, performance) DataFrames
  - `trades`: 交易記錄 DataFrame
  - `performance`: 績效指標 DataFrame (Parquet 格式)

**範例:**
```python
async with TradeposeClient(api_key="...") as client:
    # 單次 API 呼叫取得所有結果
    trades, perf = await client.tasks.download_backtest_results(task_id)

    print(f"Trades: {trades.shape}")
    print(f"Total Return: {perf['total_return'][0]}")
```

**性能:**
- ✅ 1 次 API 呼叫 (vs. 舊方法的 2 次)
- ✅ ZIP 壓縮傳輸
- ✅ Redis Pipeline 後端優化

---

### download_result_by_type() ⚠️ 已棄用

**已棄用** - 請改用 `download_backtest_results()`。

此方法仍可使用以保持向後相容，但效能較差。

**遷移:**
```python
# 舊方法 (2 次 API 呼叫)
trades = await client.tasks.download_result_by_type(task_id, "trades")
perf = await client.tasks.download_result_by_type(task_id, "performance")

# 新方法 (1 次 API 呼叫)
trades, perf = await client.tasks.download_backtest_results(task_id)
```
```

---

### 階段 4: 測試驗證 (優先級：HIGH)

#### 4.1 更新單元測試

**檔案:** `packages/client/tests/resources/test_tasks.py`

**新增測試:**
```python
@pytest.mark.asyncio
async def test_download_backtest_results_zip():
    """Test download_backtest_results handles ZIP correctly."""
    # Mock ZIP response
    mock_zip = create_mock_zip_with_parquet()

    async with TradeposeClient(api_key="test") as client:
        with patch.object(client._session, "get", return_value=mock_zip):
            trades, perf = await client.tasks.download_backtest_results("task-123")

            assert isinstance(trades, pl.DataFrame)
            assert isinstance(perf, pl.DataFrame)
            assert "symbol" in trades.columns
            assert "total_return" in perf.columns


@pytest.mark.asyncio
async def test_batch_tester_uses_new_download():
    """Verify BatchTester uses optimized download method."""
    tester = BatchTester(api_key="test")

    # Spy on download calls
    with patch("tradepose_client.resources.tasks.TasksResource.download_backtest_results") as mock:
        mock.return_value = (pl.DataFrame(), pl.DataFrame())

        await tester._async_download_results("task-123")

        # Should call new method once, not old method twice
        assert mock.call_count == 1
        mock.assert_called_with("task-123", as_dataframe=True)
```

#### 4.2 整合測試

**建立新測試檔案:** `packages/client/tests/integration/test_zip_migration.py`

```python
"""Integration tests for ZIP migration."""

import pytest
from tradepose_client import BatchTester, TradeposeClient
from tradepose_client.batch import Period


@pytest.mark.integration
@pytest.mark.asyncio
async def test_end_to_end_zip_download(test_strategy):
    """Test complete workflow with ZIP packaging."""
    tester = BatchTester(
        api_key=os.environ["TRADEPOSE_API_KEY"],
        server_url=os.environ.get("TRADEPOSE_SERVER_URL", "http://localhost:8000")
    )

    # Submit batch
    batch = tester.submit(
        strategies=[test_strategy],
        periods=[Period.Q1(2024)]
    )

    # Wait for completion
    batch.wait(timeout=300)

    # Verify results downloaded via ZIP
    for period_key, result in batch.results.items():
        assert result.trades is not None
        assert result.performance is not None

        # Verify performance is Parquet DataFrame
        assert isinstance(result.performance, pl.DataFrame)
        assert "total_return" in result.performance.columns

        # Verify can access metrics
        total_return = result.performance["total_return"][0]
        assert isinstance(total_return, float)


@pytest.mark.integration
@pytest.mark.asyncio
async def test_backward_compatibility():
    """Test old download_result_by_type still works."""
    async with TradeposeClient(api_key="...") as client:
        # Old method should still work
        trades = await client.tasks.download_result_by_type("task-123", "trades")
        perf = await client.tasks.download_result_by_type("task-123", "performance")

        assert trades is not None
        assert perf is not None
```

---

## 檢查清單

### 部署前檢查

- [ ] 確認遠端 Gateway 已更新至 v2.0+
- [ ] 確認 Gateway `/api/v1/tasks/{task_id}/result` 返回 ZIP
- [ ] 確認 ZIP 包含 `trades.parquet` 和 `performance.parquet`
- [ ] 確認 `performance.parquet` 是 Parquet 格式 (非 JSON)

### 程式碼修改檢查

- [ ] 更新 `BatchTester._async_download_results()`
- [ ] 更新 `BackgroundPoller._download_completed()`
- [ ] 更新 `download_result_by_type()` docstring
- [ ] 更新 ARCHITECTURE.md
- [ ] 更新 README.md
- [ ] 更新 API_REFERENCE.md

### 測試檢查

- [ ] 執行單元測試: `pytest packages/client/tests/resources/test_tasks.py`
- [ ] 執行整合測試: `pytest packages/client/tests/integration/ -m integration`
- [ ] 手動測試 BatchTester 端到端流程
- [ ] 驗證性能改善 (應減少 30-40% 下載時間)
- [ ] 驗證向後相容性 (舊程式碼仍可運作)

### 文檔檢查

- [ ] README 範例可執行
- [ ] API Reference 正確
- [ ] 遷移指南清晰
- [ ] 性能數據準確

---

## 預期效益

### 性能改善

| 指標 | 舊版 (v1.x) | 新版 (v2.0+) | 改善 |
|------|-------------|--------------|------|
| API 呼叫數 (每 task) | 2 | 1 | **-50%** |
| 網路往返 (每 task) | 2 | 1 | **-50%** |
| 下載時間 (估計) | 100% | 60-70% | **-30-40%** |
| Performance 檔案大小 | 100% (JSON) | ~50% (Parquet) | **-50%** |

### 範例場景

**10 個策略 × 4 個時期 = 40 個 tasks**

| 指標 | 舊版 | 新版 | 改善 |
|------|------|------|------|
| 總 API 呼叫數 | 80 | 40 | **-50%** |
| 預估下載時間 | 120s | 72s | **-40%** |
| 資料傳輸量 | ~500MB | ~350MB | **-30%** |

---

## 故障排除

### 問題 1: ZIP 解壓縮失敗

**錯誤訊息:**
```
zipfile.BadZipFile: File is not a zip file
```

**原因:** Gateway 尚未更新，仍返回單一 Parquet 檔案

**解決方案:**
1. 確認 Gateway 版本: `curl https://api.tradepose.com/health`
2. 等待 Gateway 更新部署
3. 暫時回退到舊版 client (使用 `download_result_by_type()`)

---

### 問題 2: Performance 資料格式錯誤

**錯誤訊息:**
```
AttributeError: 'dict' object has no attribute 'shape'
```

**原因:** 程式碼預期 JSON dict，但收到 Parquet DataFrame

**解決方案:**
```python
# 錯誤寫法 (假設是 dict)
perf["total_return"]  # ❌ KeyError if DataFrame

# 正確寫法 (DataFrame)
perf["total_return"][0]  # ✅ 取得第一列的值
```

---

### 問題 3: 舊 Gateway 不支援 ZIP 端點

**錯誤訊息:**
```
ResourceNotFoundError: 404 Not Found
```

**原因:** 連接到舊版 Gateway (<2.0)

**解決方案:**
1. 確認 `server_url` 指向正確的環境
2. 確認 Gateway 已更新
3. 暫時使用舊方法:
   ```python
   # Fallback to old method
   trades = await client.tasks.download_result_by_type(task_id, "trades")
   perf = await client.tasks.download_result_by_type(task_id, "performance")
   ```

---

## 回滾計畫

如果遇到嚴重問題，可以回滾到舊版實作：

### 回滾步驟

1. **恢復 tester.py:**
   ```python
   # 恢復為使用 download_result_by_type()
   trades_task = client.tasks.download_result_by_type(task_id, "trades")
   perf_task = client.tasks.download_result_by_type(task_id, "performance")
   ```

2. **恢復 background.py:**
   ```python
   # 恢復為兩次 API 呼叫
   asyncio.gather(
       client.tasks.download_result_by_type(task_id, "trades"),
       client.tasks.download_result_by_type(task_id, "performance")
   )
   ```

3. **版本控制:**
   ```bash
   git revert <commit-hash>
   ```

---

## 時程規劃

### 建議執行順序

1. **第 1 天:** Gateway 部署更新
   - 部署新版 Gateway (ZIP 支援)
   - 驗證 `/result` 端點返回 ZIP
   - 確認向後相容 (舊 `/results/{type}` 仍可用)

2. **第 2 天:** Client 程式碼修改
   - 更新 `BatchTester._async_download_results()`
   - 更新 `BackgroundPoller._download_completed()`
   - 執行單元測試

3. **第 3 天:** 文檔更新
   - 更新 ARCHITECTURE.md
   - 更新 README.md
   - 更新 API_REFERENCE.md

4. **第 4 天:** 測試與驗證
   - 執行整合測試
   - 手動測試端到端流程
   - 驗證性能改善

5. **第 5 天:** 發布與監控
   - 發布新版 client (v2.0.0)
   - 監控錯誤率
   - 收集性能數據

---

## 聯絡資訊

**問題回報:**
- GitHub Issues: https://github.com/your-org/tradepose-python/issues
- Email: support@tradepose.com

**文檔:**
- Client SDK: `packages/client/README.md`
- Gateway API: `docs/API.md`
- ADR: `docs/adr/001-use-zip-packaging-for-task-results.md`

---

## 版本歷史

| 版本 | 日期 | 變更 |
|------|------|------|
| 1.0 | 2025-11-25 | 初始版本 - ZIP 遷移計畫 |

---

**注意:** 此文檔將在 Gateway 更新後持續維護。請在執行修改前確認 Gateway 版本。
