# 使用 TAS API 電話連結服務製作電話問券調查系統

## 使用說明

您可以直接複製本文件中的提示詞內容，在最上方填寫您偏好的程式語言與框架後，送給 Google Gemini、GitHub Copilot 或 ChatGPT 等 AI 工具。AI 就能根據這份詳細的規格，為您產出架構完整且符合規範的應用程式原始碼。

---

## 📋 前置動作：取得 API 關鍵信息

在開始開發應用程式之前，需要透過 TAS API 的「登記註冊」API 完成以下前置步驟：

### 1️⃣ 建議：撰寫獨立的前置動作程式

**推薦做法**：將前置動作（服務號碼註冊與查詢）獨立為一個單獨程式執行，原因如下：
- ✅ 這些操作通常只需執行一次（或偶爾更新）
- ✅ 將其與主應用分離，使主應用邏輯更簡潔
- ✅ 便於後續維護與調試（獨立的責任邊界）
- ✅ 可使用 Postman、curl 或簡單的客戶端工具直接測試

### 2️⃣ 前置步驟流程

**步驟 A：取得 API-KEY**
- 從 TAS API 平台申請或獲取您的 API-KEY
- 此金鑰將用於所有後續 API 呼叫的身份驗證

**步驟 B：透過 REST API Client 執行 POST /reg —— 申請服務號碼**

使用 Postman 或其他 REST API 工具（如 curl、Insomnia）執行以下請求：

```http
POST https://tasapi.cht.com.tw/apis/CHTIoT/phone-conn/v1/reg HTTP/1.1
Content-Type: application/json
X-API-KEY: your-api-key-here

{
  "appName": "test Phone 001-20231226",
  "appDesc": "test phone description"
}
```

**成功回應**：
```json
{
  "serviceNumber": "*****6113",
  "SNKey": "G7P*******************5FL",
  "start": "2026-03-20T06:58:58Z"
}
```

此時您將獲得：
- **serviceNumber**：租用的服務號碼（後續撥打電話時使用）
- **SNKey**：與該服務號碼對應的 MQTT 訂閱識別碼（關鍵信息，注意大寫）
- **start**：服務啟用時間

**步驟 C：若遺忘 SNKey 或服務號碼，透過 GET /reg 查詢**

使用 REST API 工具執行以下請求，檢索目前 API-KEY 所有租用的服務號碼資訊：

```http
GET https://tasapi.cht.com.tw/apis/CHTIoT/phone-conn/v1/reg HTTP/1.1
X-API-KEY: your-api-key-here
```

**成功回應**：
```json
[
  {
    "appName": "test Phone 001-20231226",
    "appDesc": "test phone description",
    "serviceNumber": "*****6113",
    "SNKey": "G7P*******************5FL",
    "start": "2026-03-20T06:58:58Z",
    "rentStatus": "Accessible"
  }
]
```

回應說明：
- **serviceNumber**：租用的服務號碼
- **SNKey**：MQTT 訂閱識別碼（注意大寫）
- **appName**：應用名稱
- **appDesc**（可選）：應用說明
- **start**：服務啟用時間
- **rentStatus**：服務租用狀態（`Accessible`=可用，`Lock`=已鎖定）

### 3️⃣ 將關鍵信息保存到 config.yaml

完成上述步驟後，將獲得的 `serviceNumber` 和 `sn_key` 填入應用程式的設定檔：

```yaml
# config.yaml
callout_url: "https://tasapi.cht.com.tw/apis/CHTIoT/phone-conn/v1/callout"
phoneConfig_url: "https://tasapi.cht.com.tw/apis/CHTIoT/phone-conn/v1/phoneConfig"
mqtt_url: "wss://tasapi-qa.cht.tw/ws/mqtt/v1/"
service_number: "*****6113"          # ← 來自 POST /reg 回應的 serviceNumber
sn_key: "G7P*******************5FL"                 # ← 來自 POST /reg 回應的 SNKey（注意大寫）
api_key: "your-api-key-here"
```

---

## 📋 AI 提示詞範本

### 開發環境與技術堆疊指定

```
指定的程式語言： [必填] [請填入程式語言，例如：Java, Python, Node.js]
  ⚠️ 若未指定，請詢問使用者：「請問您希望使用哪種程式語言？」

指定的 MQTT 函式庫/框架： [建議填寫] [請填入 MQTT 套件，例如：Eclipse Paho, paho-mqtt, mqtt.js]
  💡 若未指定，請根據上述程式語言推薦合適的 MQTT 函式庫（例如 Java 推薦 Eclipse Paho，Python 推薦 paho-mqtt）

指定的 HTTP 函式庫/框架： [建議填寫] [請填入 HTTP 套件，例如：OkHttp3, requests, axios]
  💡 若未指定，請根據上述程式語言推薦合適的 HTTP 函式庫（例如 Java 推薦 OkHttp3，Python 推薦 requests）

其他補充： [選填]
  📝 若使用者暫未提供，可詢問是否需要特殊需求（例如：非同步架構、設定檔獨立為 config.yaml、詳細的日誌記錄、錯誤重試機制等）
```

### AI 角色與任務說明

請扮演一位資深的軟體架構師與開發工程師。請根據以下提供的「TAS API 與 MQTT 語音 Callout 整合實作規格書」，使用上方指定的技術堆疊，為我產生完整的服務應用程式架構、包含設定檔解析、MQTT 連線控制、API 請求與業務邏輯處理的實作程式碼。

> 💡 **詳細技術參考**：本規格書基於中華電信 TAS API 官方文件編製。如需詳細查閱 API 格式、MQTT 協議細節或完整訊息範例，請參考文末的「參考資源」部分。

---

## 📖 TAS API 與 MQTT 語音 Callout 整合實作規格書

本規格書描述如何整合中華電信 TAS API（電話連結服務）實現自動語音撥出（Callout）與互動系統。本內容基於官方 API 文件 (YAML 規格) 和 MQTT 協議規格編製。

**系統流程**：程式需先透過 HTTP POST 啟動電話通告，接著透過 MQTT 協定與系統進行非同步的語音互動與狀態接收。

---

### 1️⃣ 系統組態與連線設定檔 (config.yaml) 規格

應用程式應設計為從外部檔案（例如 `config.yaml`）讀取以下設定：

| 設定項目 | 說明 | 範例值 |
|---------|------|---------|
| `callout_url` | Callout API 端點（Call-Out 情境使用） | `https://tasapi.cht.com.tw/apis/CHTIoT/phone-conn/v1/callout` |
| `phoneConfig_url` | Phone Config API 端點（Call-In 情境使用） | `https://tasapi.cht.com.tw/apis/CHTIoT/phone-conn/v1/phoneConfig` |
| `mqtt_url` | MQTT WebSocket 端點 | `wss://tasapi-qa.cht.tw/ws/mqtt/v1/` |
| `mqtt_reconnect_delay_seconds` | 首次重連延遲秒數，採用指數退避策略，最多重試 5 次 | `1` |
| `service_number` | 租用的發話服務號碼 | 用戶所租號碼 |
| `api_key` | 用戶的 API 金鑰 | 用戶 API-KEY |
| `sn_key` | 配合 MQTT 訂閱的識別碼（來自 GET /reg 或 POST /reg 的 SNKey 欄位） | 用戶 SNKey |
| `result_output` | 執行結果儲存檔名 | `call_ask_result.txt` |
| `phones` | 目標電話號碼陣列（Callout 用） | JSON 陣列 |
| `questions` | 依序提問的問題清單（Callout 用） | JSON 陣列 |

---

### 2️⃣ MQTT 連線規範

#### 連線協定與加密
- **偏好使用 MQTT 5 版本**，以 WebSocket 方式連線
- **加密需求**：TLSv1.2 以上

#### 憑證驗證
- **建議做法**：使用作業系統本身的信任憑證庫清單，並定期更新
- **嚴禁做法**：不要在程式碼中寫死（Pinning）伺服器憑證，避免伺服器每年更換憑證時造成連線失敗

#### 驗證登入
- MQTT 連線的**帳號**（username）與**密碼**（password）皆需填入 `API-KEY`

#### 連線品質
- 所有訂閱與發佈的 **QoS 必須設定為 0**（沒有 ACK 確認訊息）

---

### 🔒 安全性最佳實踐

#### API 金鑰管理
- **絕不硬編碼**：API-KEY 和 service_number 必須從外部配置檔案讀取
- **環境變數替代**：支援從環境變數讀取敏感信息
- **存取控制**：確保 config.yaml 檔案只有應用程式有讀取權限（Unix: `chmod 600 config.yaml`）

#### 敏感信息日誌過濾
1. **API-KEY 隠碼**：`abc123def456` → `abc123***`（只保留前 6 位）

#### TLS/SSL 安全
- **禁止 Pinning**：使用系統憑證庫而非硬編碼伺服器憑證
- **驗證主機名稱**：確保 TLS handshake 時驗證伺服器身份
- **最低版本**：TLSv1.2 以上，禁用 SSLv3, TLSv1.0, TLSv1.1

#### MQTT 連線安全
- **強密碼套件**：推薦 `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`
- **禁止繞過驗證**：不要設定 `insecure_skip_verify = true`

#### 輸入驗證
| 項目 | 規則 | 範例 |
|-----|------|------|
| 電話號碼 | 10~15 位數字 | `^[0-9]{10,15}$` |
| Node 名稱 | 遵守命名規則 + 禁保留字 | `^[A-Za-z][A-Za-z0-9_]{0,15}$` |

---

### 3️⃣ MQTT 主題 (Topic) 定義

程式需動態將 `${SNKey}` 替換為 `config.yaml` 內的 `sn_key`。

#### 訂閱 (Subscribe)

| Topic | 用途 |
|-------|------|
| `phone-conn/calloutResult/${SNKey}` | 接收電話呼叫的最終處理結果 |
| `phone-conn/callEvent/${SNKey}` | 接收電話接通、掛斷的事件，以及使用者的按鍵或語音輸入請求 |
| `phone-conn/callActionDebug/${SNKey}` | 接收發佈指令發生錯誤時的除錯訊息 |

#### 發佈 (Publish)

| Topic | 用途 |
|-------|------|
| `phone-conn/callAction/${SNKey}` | 回傳 JSON 格式的指令，控制系統播放語音或接收輸入 |

**重要**：發佈訊息時必須使用獨立的執行緒（例如 Thread Pool）以避免阻塞連線。

**超時提示**：使用 MQTT 與系統互動時，若系統約 20 秒沒有收到應用程式的回應，系統會自動播放音樂給用戶聽，避免通話中出現冷場。因此在 MQTT 斷線重連或等待系統回應時，務必在 20 秒內完成操作，確保用戶體驗。

---

### 4️⃣ 業務處理邏輯

#### 多Session管理機制

當應用程式需要同時處理多個 callout 對像的互動行為，應實作多 Session 管理機制以正確追蹤每一個被叫號碼的通話狀態。

##### 建議實作方案

**使用 tags 屬性進行 Session 管理**：

1. **發起 Callout 時設置 tags**：
   ```json
   {
     "serviceNumber": "your_service_number",
     "phones": ["0212345577", "0987654321", "0912345678"],
     "tags": ["user_001", "user_002", "user_003"],
     "ivrData": {
       "node": "MAIN",
       "nextNode": "CUSTOM"
     }
   }
   ```
   - 每個 `tags` 元素與對應的 `phones` 元素一一對應
   - tag 可包含業務相關的識別碼（如用戶ID、訂單號、客戶代碼等）
   - 每個 tag 最多 1024 個字元

2. **維護 Session 對照表**：
   ```
   sessionMap = {
     "user_001": {
       "phone": "****5577",
       "status": "calling",
       "currentQuestion": 0,
       "answers": [],
       "sessionId": "...",
       "createdAt": "..."
     },
     "user_002": {
       "phone": "****4321",
       "status": "calling",
       ...
     },
     ...
   }
   ```

3. **MQTT 訊息接收與 Session 定位**：
   - 每個 MQTT 訊息（callEvent、calloutResult 等）都會包含 `tag` 屬性
   - 透過 tag 值直接定位到對應的 Session：`session = sessionMap[message.tag]`
   - 更新該 Session 的狀態與數據

4. **Session 生命週期管理**：
   - **初始化**：發起 callout 時建立 session 記錄
   - **狀態轉換**：根據 MQTT 訊息更新 session 狀態（calling → answered → processing → completed）
   - **清理**：通話結束後（收到 callHangUp 事件）保存結果並清除 session

5. **多 Callout 群組並行管理**：
   - 若需要同時維護多個 callout 群組，可在 sessionMap 上層加入 groupId 維度
   - 結構：`sessionMap[groupId][tag]`
   - 便於追蹤不同批次的通話

##### 優點
- ✓ 每個被叫號碼有清晰的身份認證
- ✓ 可包含業務邏輯相關的信息
- ✓ Session 管理簡潔清晰，易於除錯
- ✓ 支援任意規模的並行通話

#### A. 啟動 Callout 通告 (HTTP)

1. 發出 HTTP **POST** 請求到 `callout_url`
2. **Header 必須帶入**：`X-API-KEY: {api_key}`
3. **批量處理**：若 `phones` 陣列超過 20 筆號碼，程式必須實作分批發送的邏輯
4. **ivrData 設定**：請設定為 `"node": "MAIN", "nextNode": "CUSTOM"`，讓電話接通後透過 MQTT 互動

##### HTTP 請求範例

```http
POST /apis/CHTIoT/phone-conn/v1/callout HTTP/1.1
Host: tasapi.cht.com.tw
Content-Type: application/json
X-API-KEY: your-api-key-here
Content-Length: 487

{
  "serviceNumber": "your_service_phone_number",
  "phones": [
    "02123456xx",
    "09006543xx"
  ],
  "ivrData": {
    "node": "MAIN",
    "nextNode": "CUSTOM"
  },
  "ringingTimeout": 25
}
```

##### HTTP 成功回應 (HTTP 200)

```json
{
  "groupId": "0233335678.1801de67-61a6-4eca-881a-4c75f37ce588",
  "id": [
    "1f2dbc3e;tw.voip.IoTIMS"
  ],
  "message": "callout processing",
  "status": "ok"
}
```

##### HTTP 失敗回應範例

無效 API-KEY：
```json
{
    "msg": "authentication error",
    "status": "err"
}
```

服務號碼未註冊：
```json
{
    "msg": "you cannot access serviceNumber XXXXXXXX",
    "status": "err"
}
```

#### B. 處理通話狀態 (訂閱 calloutResult)

解析收到的 JSON，根據 `status` 判斷結果：

| status 值 | 說明 |
|-----------|------|
| `answered` | 已接聽 |
| `reject` | 拒接／忙線 |
| `busy` | 市話佔線 |
| `timeout` | 未接聽 |
| `notfound` | 空號 |
| `failed` | 發生錯誤 |

**動作**：結果應紀錄下來準備寫入報表。

##### calloutResult 訊息範例

**已接聽狀態** (訂閱 `calloutResult/${SNKey}`)
```json
{
  "groupId": "0233335678.1801de67-61a6-4eca-881a-4c75f37ce588",
  "id": "1f2dbc3e;tw.voip.IoTIMS",
  "phone": "****7777",
  "status": "answered",
  "statusCode": 200,
  "time": "2026-03-24T05:26:37.623859240Z"
}
```

**未接聽狀態**
```json
{
  "groupId": "0233335678.1801de67-61a6-4eca-881a-4c75f37ce588",
  "id": "1f2dbc3e;tw.voip.IoTIMS",
  "phone": "****7777",
  "status": "timeout",
  "statusCode": 487,
  "time": "2026-03-24T05:26:37.623859240Z"
}
```

#### C. 處理通話事件與互動請求 (訂閱 callEvent)

解析訊息中的 `type` 屬性：

**若為 `calloutEvent`**：
- 處理 `event` 為 `CallEstablished`（接通）或 `CallHangUp`（掛斷）

**若為 `calloutRequest`**：
- 代表系統請求下一步動作
- 訊息中會帶有 `phone` 屬性（被叫電話號碼），用來區分目前是哪一通電話的互動階段

#### D. 執行電話問卷邏輯 (發佈 callAction)

##### 第一則訊息
當從 `callEvent` 收到 `"type": "calloutRequest"` 且 `"node": "MAIN"` 時，代表互動開始。

##### ID 匹配機制
**重要**：每次回傳至 `callAction` 的 JSON 中，`id` 欄位值**必須與最近一次從 `callEvent` 收到的 `id` 完全相同**。

> 這些訊息是 1 對 1 成對的，請務必在 Session 管理中更新目前最新的 `id`。

##### 提問迴圈

1. **依序提問**：將 `questions` 陣列中的問題透過 `callAction` 發送
2. **訊息屬性**：
   - `"text": "問題內容"`
   - `"collectText": true`（收集語音輸入）
   - 自訂一個 `"node"` 名稱

##### Node 命名規則

- **允許字元**：大小寫英文、數字、底線
- **規則**：`^[A-Za-z][A-Za-z0-9_]*$`
- **首字**：必為英文
- **最長**：16 字元
- **保留字**：禁用 `MAIN`, `CUSTOM`, `END`

##### 回答處理

1. 當收到用戶回答時（從 `callEvent` 的 `node` 屬性中，用逗號分隔的文字擷取），將答案記錄下來
2. **錯誤重試**：若回答無法匹配或出現 `no_match`，必須使用**同樣的 node 重新再問一次**該問題

##### 掛斷機制

所有問題詢問完畢後，最後一則回傳的 `callAction` 訊息中，請加入 `"nextNode": "END"` 讓系統自動掛斷電話。

##### 訊息追蹤機制

**關鍵概念**：
1. **ID 成對配對**：從 callEvent 收到的 `id` 與發送 callAction 時的 `id` 必須完全相同
2. **Node 名稱追蹤**：前一次發送到 callAction 的 `node` 名稱，會包含在下一次收到的後續訊息中
   - 應用程式設定 `"node": "ask01"`，用戶輸入後系統回覆 `"node": "ask01,1"`
   - 應用程式可依此 node 名稱判斷用戶是回答哪個問題，決定下一步邏輯
3. **事件通知**：電話接通/掛斷時會有獨立的 `event` 事件通知（type: "event"）

**訊息流向範例**：
```
應用程式 → callAction {node: "ask01", text: "...", id: "abc123"}
           ↓
系統播放問題並收集輸入
           ↓
系統 → callEvent {node: "ask01,1", id: "def456", type: "calloutRequest"}
應用程式接收後，從 node 值「ask01,1」中：
  - 前半部分「ask01」：識別是同一個問題節點
  - 後半部分「1」：用戶的輸入值
           ↓
應用程式判斷下一步（例如進入下一個問題）
           ↓
應用程式 → callAction {node: "ask02", text: "...", id: "def456"}
```

##### 互動流程 JSON 訊息範例

以下是一個完整的通話互動流程示例（數字按鍵輸入場景），展示 node 名稱如何在訊息中被追蹤和使用：

**1. 收到電話接通事件** (訂閱 `callEvent`)
```json
{
  "event": "CallEstablished",
  "id": "1f2dbc3e;tw.voip.IoTIMS",
  "phone": "****7777",
  "time": "2026-03-24T05:26:37.623869240Z",
  "type": "event"
}
```

**2. 收到初始互動請求** (訂閱 `callEvent`)
```json
{
  "id": "1f2dbc3e;tw.voip.IoTIMS.ctK",
  "node": "MAIN",
  "phone": "****7777",
  "time": "2026-03-24T05:26:38.533981588Z",
  "type": "calloutRequest"
}
```

**3. 發送第一個問題** (發佈 `callAction`)
```json
{
  "node": "ask01",
  "collectDTMFTimeout": "10",
  "nextNode": "CUSTOM",
  "id": "1f2dbc3e;tw.voip.IoTIMS.ctK",
  "text": "今日來店用餐請問覺得滿意嗎？ 滿意請按 1，覺得可以更好請按 2",
  "collectDTMF": true
}
```

**4. 收到用戶輸入回覆** (訂閱 `callEvent`)
```json
{
  "id": "1f2dbc3e;tw.voip.IoTIMS.gVT",
  "node": "ask01,1",
  "phone": "****7777",
  "time": "2026-03-24T05:26:46.776983114Z",
  "type": "calloutRequest"
}
```
> 注意：`node` 屬性中以逗號分隔，前半部分 `ask01` 是提問的 node，後半部分 `1` 是用戶的輸入

**5. 發送第二個問題** (發佈 `callAction`)
```json
{
  "node": "ask02",
  "nextNode": "CUSTOM",
  "id": "1f2dbc3e;tw.voip.IoTIMS.gVT",
  "text": "請問對工作人員的服務覺得滿意嗎？ 滿意請按 1，覺得可以更好請按 2",
  "promptMode": "F"
}
```

**6. 收到用戶輸入回覆** (訂閱 `callEvent`)
```json
{
  "id": "1f2dbc3e;tw.voip.IoTIMS.p5V",
  "node": "ask02,1",
  "phone": "****7777",
  "time": "2026-03-24T05:26:54.421212335Z",
  "type": "calloutRequest"
}
```

**7. 發送最終訊息並結束通話** (發佈 `callAction`)
```json
{
  "node": "byeNode",
  "nextNode": "END",
  "id": "1f2dbc3e;tw.voip.IoTIMS.p5V",
  "text": "好的，謝謝您的回覆",
  "promptMode": "F"
}
```

**8. 收到電話掛斷事件** (訂閱 `callEvent`)
```json
{
  "event": "CallHangUp",
  "id": "1f2dbc3e;tw.voip.IoTIMS",
  "phone": "****7777",
  "time": "2026-03-24T05:27:09.989922887Z",
  "type": "event"
}
```

#### F. 錯誤處理與重試機制

##### HTTP 請求錯誤重試

1. **重試策略**：若 HTTP 請求失敗（網路錯誤、超時），應實作指數退避重試
   - 首次重試延遲：1 秒
   - 最大重試次數：3 次
   - 每次延遲翻倍：1秒 → 2秒 → 4秒

2. **不重試的情況**：
   - `status: "err"` 且 `msg: "authentication error"` (無效 API Key) — 需要修正設定
   - `status: "err"` 且 `msg: "you cannot access serviceNumber"` (服務號碼未授權) — 需要註冊或確認服務號碼
   - HTTP 4xx 錯誤（除 408, 429）— 客戶端錯誤，不應重試
   - HTTP 401, 403 — 認證或授權失敗

3. **重試邏輯範例**：
   ```
   for retry_count in 0..2:
     try:
       response = POST /callout with exponential_backoff(retry_count)
       if response.resultCode == "0000":
         return success
       elif is_retryable_error(response.resultCode):
         wait(exponential_backoff_time)
         continue
       else:
         return failure_not_retryable
     except NetworkError:
       if retry_count < 2:
         wait(exponential_backoff_time)
       else:
         return failure_max_retries
   ```

##### MQTT 連線掉線與重連

1. **連線監測**：監聽 MQTT 連線狀態變化
2. **自動重連**：實作指數退避重試機制
   - 最大重試次數：5 次
   - 首次重試延遲：1 秒
   - 延遲序列：1秒 → 2秒 → 4秒 → 5秒 → 5秒（超過 5 秒後不再增加）
   - 總耗時：約 17-18 秒內完成，確保在系統 20 秒超時前恢復連線
3. **重連上限**：超過 5 次重試後進入告警狀態
4. **訊息隊列**：重連前暫存待發送的訊息，重連成功後立即發送

##### 無效訊息處理

1. **訊息 ID 不匹配**：
   - 若收到的 callEvent 中的 id 與目前 Session 的 id 不符，應記錄警告日誌
   - 不要發送錯誤配對的 callAction

2. **超時無回應**：
   - 若發送 callAction 後 30 秒內未收到對應的 calloutRequest，應視為失敗
   - 記錄此通話為異常，建議人工審查

3. **重複訊息**：
   - 若收到重複的訊息 ID，應進行去重處理，避免重複操作

#### E. 輸出執行結果報表

在通話結束後，將每一通電話（phone）的最終狀態、問題與回答輸出至 `config.yaml` 內 `result_output` 指定的文字檔。

**CSV 格式範例**：

```csv
phone,phone_status,question1,answer1,question2,answer2
0212345577,answered,來店消費的目的是什麼?,慶生,喜歡
```

---

### 加碼實作項目：5️⃣ Call-In 互動流程（Phone Config API）

#### 概述

若是要處理 Call-Out 之後用戶沒有接到電話之後再回撥，或是客戶撥打電話查詢資訊的情況，要配合使 phoneConfig API 實作 Call-In 的服務情境。

Call-In 是指用戶主動撥入服務號碼的場景，與 Call-Out（系統主動撥打）不同。

- **Call-Out**：系統發起電話呼叫，用戶被動接聽
- **Call-In**：用戶主動撥入服務號碼

在 Call-In 場景中，應用程式先透過 Phone Config API 設定好靜態菜單（一次性設定），之後當用戶撥入時，系統會根據 Phone Config 提供初始菜單。用戶的按鍵選擇或語音輸入會透過 MQTT 的 `request` 訊息回報給應用程式，應用程式再根據用戶選擇透過 `callAction` 進行互動處理。

#### Call-In 與 Call-Out 對比

| 特性 | Call-Out（呼叫） | Call-In（來電） |
|------|-----------------|----------------|
| **發起方** | 系統主動撥打 | 用戶主動撥入 |
| **電話選單內容** | 透過 callAction 動態訊息設定 | 透過 Phone Config API 預先設定或是設定 nextNode: CUSTOM 以 callAction 動態訊息設定 |
| **首次訊息** | MQTT calloutRequest（node: MAIN） | MQTT request（node: MAIN） |
| **MQTT 訊息類型** | `type: "calloutRequest"` | `type: "request"` |
| **HTTP 端點** | `/phone-conn/v1/callout` | `/phone-conn/v1/phoneConfig` (只設定一次) |
| **適用場景** | 問卷調查、通知 | 客服熱線、資訊查詢 |

#### 設定靜態菜單（Phone Config API）

**目的**：一次性呼叫以設定用戶撥入服務號碼時的初始節點，後續電話中回應的訊息文字內容由 MQTT 程式動態發送。

**設定方式**：透過另一支只要執行一次的獨立程式透過 Phone Config API 將選單設定發送給系統進行設定，簡化配置為只包含 `node` 和 `nextNode`，讓用戶輸入後透過 MQTT 交由應用程式動態決定回應內容。

**HTTP 請求範例**：

```http
POST /apis/CHTIoT/phone-conn/v1/phoneConfig HTTP/1.1
Host: tasapi.cht.com.tw
Content-Type: application/json
X-API-KEY: your-api-key-here

{
  "serviceNumber": "02XXXXXXXX",
  "ivrData": [
    {
      "node": "MAIN",
      "nextNode": "CUSTOM"
    }
  ],
  "ringingTimeout": 25
}
```

**HTTP 成功回應**：
```json
{
  "status": "ok"
}
```

> **說明**：
> - Phone Config 中只設定 `node: "MAIN"` 和 `nextNode: "CUSTOM"`
> - 不需在 config 中指定 text，文字由 MQTT 程式動態發送
> - 用戶首次撥入時，系統會向應用程式發送 MQTT request 訊息
> - 應用程式根據 `type` 判斷是 Call-In 還是 Call-Out，動態發送 callAction 訊息

#### Call-In 互動流程

**1. 用戶撥入服務號碼**

系統根據 Phone Config 的設定，準備好初始節點。

**2. 通話接聽事件** (訂閱 `callEvent`)

系統接聽用戶撥入的電話後，會發送「通話接聽」事件訊息

```json
{
  "event": "CallEstablished",
  "id": "00903289;tw.voip.IoTIMS",
  "phone": "****1234",
  "time": "2026-03-24T10:30:00.000000000Z",
  "type": "event"
}
```

**3. 應用程式收到首次 request 訊息（node: MAIN）**

收到 MQTT request 訊息：
```json
{
  "id": "00903289;tw.voip.IoTIMS.Tr1",
  "type": "request",
  "phone": "****1234",
  "node": "MAIN",
  "time": "2026-03-24T10:30:00.000000000Z"
}
```

> **關鍵區分**：
> - `type: "request"` → **Call-In 場景**（用戶主動撥入）
> - `type: "calloutRequest"` → **Call-Out 場景**（系統主動撥打）
>
> 首次訊息都是 `node: "MAIN"`，應用程式根據 `type` 值判斷通話類型，動態發送準備好的文字訊息。

**4. 應用程式根據 type 判斷並回應**

```java
public void handleMqttMessage(JsonObject message) {
    String messageType = message.getString("type");
    String nodeValue = message.getString("node");
    String id = message.getString("id");
    
    // 判斷是 Call-In 還是 Call-Out
    if ("request".equals(messageType)) {
        // ★ Call-In 場景（來電）
        handleCallInRequest(id, nodeValue);
    } else if ("calloutRequest".equals(messageType)) {
        // ★ Call-Out 場景（呼叫）
        handleCallOutRequest(id, nodeValue);
    }
}

private void handleCallInRequest(String id, String nodeValue) {
    // Call-In 場景的邏輯
    if ("MAIN".equals(nodeValue)) {
        // 首次進入，發送菜單文字，並隶合 collectDTMF 接收按鍵
        publishCallAction(id, "mainMenu", 
            "今日營業時間查詢請按 1。填寫用餐問卷請按 2", 
            "CUSTOM", true);
    }
}

private void handleCallOutRequest(String id, String nodeValue) {
    // Call-Out 場景的邏輯（動態下發問卷等）
    if ("MAIN".equals(nodeValue)) {
        publishCallAction(id, "ask01", 
            "今日來店用餐請問覺得滿意嗎？滿意請按 1", 
            "CUSTOM", false);
    }
    // ... 其他 Call-Out 邏輯
}
```

**應用程式發送 callAction 訊息** (發佈 `callAction`)

```json
{
  "id": "00903289;tw.voip.IoTIMS.Tr1",
  "node": "mainMenu",
  "text": "今日營業時間查詢請按 1。填寫用餐問卷請按 2",
  "nextNode": "CUSTOM",
  "collectDTMF": true
}
```

> ⚠️ **重要**：注意 `id` 欄位值 `"00903289;tw.voip.IoTIMS.Tr1"` 與步驟 3 中從 `callEvent` 收到的 request 訊息的 id **完全相同**。這確保了訊息的配對。

**5. 用戶按鍵輸入（按 1：查詢營業時間）**

系統接收到用戶按鍵後，發送新的 request 訊息，應用程式收到以下範例訊息，在 node 屬性標示用戶輸入的數值：
```json
{
  "id": "00903289;tw.voip.IoTIMS.sRu",
  "type": "request",
  "phone": "****1234",
  "node": "mainMenu,1",
  "time": "2026-03-24T10:31:00.000000000Z"
}
```

應用程式收到後，判斷用戶按鍵值 `1`，執行查詢邏輯：

```java
private void handleCallInRequest(String id, String nodeValue) {
    String[] parts = nodeValue.split(",");
    String keyInput = parts.length > 1 ? parts[1] : "";
    
    if ("1".equals(keyInput)) {
        // 用戶按 1：查詢營業時間
        String businessHours = queryBusinessHours(); // 查詢資料庫
        publishCallAction(id, "businessHours", "今日營業時間：09:00 - 22:00", "END", false);
    } else if ("2".equals(keyInput)) {
        // 用戶按 2：填寫問卷
        publishCallAction(id, "survey", "請問今日消費滿意度如何？滿意請按 1", "END", false);
    }
}
```

**6. 發送 callAction 回應**

```json
{
  "id": "00903289;tw.voip.IoTIMS.sRu",
  "node": "businessHours",
  "text": "今日營業時間：09:00 - 22:00",
  "nextNode": "END"
}
```

系統播放完畢後掛斷電話。nextNode 設定為 END 代表播放完畢之後掛斷電話。

**7. 通話掛斷事件** (訂閱 `callEvent`)

```json
{
  "event": "CallHangUp",
  "id": "00903289;tw.voip.IoTIMS",
  "phone": "****1234",
  "time": "2026-03-31T09:59:05.468958391Z",
  "type": "event"
}
```

#### isWaitText 完整使用範例

**場景**：用戶要查詢營業時間，但需要從資料庫查詢（耗時操作），使用 isWaitText 機制避免超時。

**流程**：

**第一步：後 callEvent 收到首次 request 訊息（node: MAIN）**
```json
{
  "id": "15ff96c7;tw.voip.IoTIMS.2nq",
  "type": "request",
  "phone": "****5678",
  "node": "MAIN",
  "time": "2026-03-24T10:35:00.000000000Z"
}
```

**第二步：發送菜單訊息（帶 collectDTMF: true 收集用戶按鍵）**

發佈 `callAction`：

```json
{
  "id": "15ff96c7;tw.voip.IoTIMS.2nq",
  "node": "mainMenu",
  "text": "今日營業時間查詢請按 1。填寫用餐問卷請按 2",
  "nextNode": "CUSTOM",
  "collectDTMF": true
}
```

系統播放菜單並等待用戶輸入。

**第三步：用戶按 1，收到 request 訊息（node: mainMenu,1）**
```json
{
  "id": "15ff96c7;tw.voip.IoTIMS.grt",
  "type": "request",
  "phone": "****5678",
  "node": "mainMenu,1",
  "time": "2026-03-24T10:36:00.000000000Z"
}
```

**第四步：立即回應「等待訊息」（避免 20 秒超時）**
```json
{
  "id": "15ff96c7;tw.voip.IoTIMS.grt",
  "isWaitText": true,
  "text": "正在查詢營業時間資訊，請稍候",
  "promptMode": "F",
  "repeat": 1
}
```

此時系統會：
1. 播放「正在查詢營業時間資訊，請稍候」
2. 播放完畢後，自動播放等候音樂（最多 4 分鐘）
3. 應用程式有充足時間進行資料庫查詢

**第五步：應用程式查詢資料完成（例如 5 秒後）**

透過資料庫取得營業時間：`09:00 - 22:00`

**第六步：發送「完成操作訊息」**
```json
{
  "id": "15ff96c7;tw.voip.IoTIMS.grt",
  "node": "businessHours",
  "text": "營業時間為上午九點至晚上十點，謝謝詢問",
  "nextNode": "END",
  "promptMode": "F"
}
```

系統會：
1. 停止播放等候音樂
2. 播放「營業時間為上午九點至晚上十點，謝謝詢問」
3. 掛斷電話

#### 實作建議

1. **統一的 MQTT 訊息處理邏輯**：
   - 首次訊息（Call-In 和 Call-Out）都是 `node: "MAIN"`
   - 只需根據 `type` 欄位判斷是 `"request"` 還是 `"calloutRequest"`
   - 根據判斷結果動態發送 text 內容

2. **Thread Pool 實現非同步處理**：
   ```java
   ExecutorService executor = Executors.newFixedThreadPool(5);
   
   // 收到 request 後立即發送等待訊息
   publishWaitMessage(id, "正在查詢...");
   
   // 在後台執行耗時操作
   executor.submit(() -> {
       try {
           String result = queryDatabase();
           // 查詢完成後發送完成訊息
           publishFinalMessage(id, result);
       } catch (Exception e) {
           logger.error("查詢失敗", e);
       }
   });
   ```

3. **超時檢查**：
   - 等待訊息播放完畢後，系統播放等候音樂
   - 應用程式應在 4 分鐘內發送完成操作訊息
   - 若超過 4 分鐘未回應，系統會自動掛斷

4. **日誌記錄** (選擇性功能)：
   - 記錄等待訊息發送時間
   - 記錄完成訊息發送時間
   - 計算實際處理耗時

---

## ✅ 開始使用

1. 複製上方「開發環境與技術堆疊指定」至「AI 角色與任務說明」的區塊
2. 填寫您的程式語言、框架與特殊需求
3. 將完整內容送給 AI 工具（Google Gemini、GitHub Copilot、ChatGPT 等）
4. AI 將依照規格書中明確的：
   - API JSON 格式限制
   - 保留字規範
   - 問卷狀態機邏輯
   
   為您建構出正確的 MQTT 互動應用程式

---

## 參考資源

### RESTful API 文件
- **TAS API YAML 規格**：https://tasapi.cht.com.tw/yaml/download/tas.yaml

### MQTT 文件
- **MQTT 協議說明**：https://tasapi.cht.com.tw/tas/yaml/download/mqtt_protocol_specification.md

### 其他資源
- **範例程式碼與使用手冊**：https://tasapi.cht.com.tw/tas/api/tas_resource

---

## 附註

儲存的回覆僅供檢視
