Feature #1401
進行中[Branch後台][FE] 充電樁狀態改吃 WebSocket,並修正充電指令自動重送
0%
概述
背景¶
Branch Admin 「連接器 → 編輯 → 充電樁狀態」分頁(react/branch/app/(dashboard)/connector/components/connector-status.tsx)目前全靠 REST 輪詢:
-
useConnectorStatus在AVAILABLE/PREPARING/CHARGING時每 2 秒輪詢(connectorHooks.ts)。 -
chargePointHooks.useGetById為了取ocppConnectionStatus,離線時每 10 秒輪詢,並用prevOnlineRef在恢復連線時補一次 refetch。 - start/stop 後
setTimeout(3000)硬等 3 秒才 refetch。
同一份 Connector 狀態,LIFF(#1399)已改為 WebSocket 推送,後台仍是輪詢,時序無法對齊。
本案不是要後台照抄 LIFF 的顯示或操作方式。 driverState/actions[] 是車主語意,授權綁住戶關聯與 transaction owner,管理員不適用。後台維持自己的維運語意(status + ocppConnectionStatus 雙軸判斷,沿用 #1382 的區分原則),只讓資料取得管道與來源一致。
實作範圍¶
- 改吃 BE 提供的 WebSocket snapshot(endpoint 與 payload 見對應 BE ticket):
- 移除
useConnectorStatus的 2 秒refetchInterval。 - 移除
chargePointHooks的 10 秒refetchInterval與prevOnlineRef補償邏輯。 - 移除 start/stop 後的
setTimeout(3000),改由推送更新畫面。
- 移除
- WebSocket 斷線時明確標示「即時狀態已中斷」並停用充電操作按鈕,不得讓管理員對著過期畫面下指令。重連策略比照 #1399(短暫重連,失敗即明確告知),不做無限重試。
- 顯示層維持後台維運語意,不引入
driverState/actions[]。BE 新欄位到位後補上:-
FAULTED顯示實際errorCode/vendorErrorCode/vendorId,取代目前只有的「故障/請聯繫技術支援」。 - 目前交易(
currentTransactionId)與使用者(currentIdTag)。 - 離線時長(
lastHeartbeatAt)與狀態停滯時間(statusChangedAt)。 -
activeOperation未終結時停用充電按鈕,避免管理員重複送指令。
-
- 操作面沿用
POST /api/tx/charge/start/stop(ss3a 管理員身分),依 BE 提供的穩定errorCode分流錯誤訊息,不再比對message字串;保存回傳的operationId並寫入前端操作紀錄。
可先行處理(不依賴 BE)¶
修正 mutations.retry: 1。 react/branch/provider/providers.client.tsx:20 的 QueryClient 全域設定,讓 useStartCharge/useStopCharge 在失敗或逾時後自動重送一次充電指令。
這與 #1399 在 LIFF 修掉的是同一個 bug(LIFF 已改為全域 mutations.retry: 0)。充電指令非 idempotent,最危險的是逾時情境:第一次請求可能已送達 Branch、只是回應遺失,自動重試就會送出第二筆真實的充電指令,並在 audit log 產生重複紀錄。
需盤點後台其餘 mutation 是否有適合自動重試者,再決定全域關閉或逐一覆寫。
不在本次範圍¶
- Connector 列表頁的狀態顯示(仍走既有查詢;本案先處理單一 Connector 詳細狀態)。
- Engineer tools(#1380)既有的 OCPP/Modbus 指令面板。
- LIFF 端行為(#1397/#1398/#1399)。
驗收重點¶
- 後台狀態分頁不再有任何定時輪詢;狀態變化由推送即時反映。
- 充電樁離線/恢復時,
status與ocppConnectionStatus來自同一份 payload,不再出現兩者短暫矛盾。 -
FAULTED時畫面可看到實際故障代碼,而非只有「故障」。 - 送出 start/stop 後不靠固定等待;同一時間不可送出第二筆指令。
- 網路中斷時操作按鈕立即停用,恢復後需收到新 snapshot 才恢復可操作。
- 充電指令在失敗或逾時後不會被自動重送(可先行驗證)。
對應 BE ticket¶
BE 契約見 #1400。本 ticket 第 1~4 項需等 BE 完成並提供 endpoint/payload 後才能實作;「可先行處理」段落不受此限。
備註記錄(1 則)¶
#1 - 鍾正剛(2026-07-31T16:02:59Z)¶
2026-08-01 已完成「可先行處理」項目:mutation 全域關閉自動重試¶
依 Ryan 指示先處理不依賴 BE(#1400)的 retry 修正。
- Branch:
feature/redmine-1401-branch-admin-connector-ws-status(自main1f4e841開出) - Commit:
f6dd8d0 - 尚未建立 MR。
問題¶
react/branch/provider/providers.client.tsx 的 QueryClient 全域預設為 mutations: { retry: 1 }。React Query 只在 mutationFn reject 時重試,因此充電指令一旦被拒絕或逾時,就會自動再送一次。與 #1399 在 LIFF 修掉的是同一個成因。
盤點 branch admin 上全部繼承此設定的 mutation,沒有任何一支是 idempotent:
| 位置 | 操作 | 自動重試的後果 |
|---|---|---|
connector/hooks/connectorHooks.ts:112 |
/api/tx/charge/start |
重複 RemoteStart 與稽核紀錄 |
connector/hooks/connectorHooks.ts:143 |
/api/tx/charge/stop |
重複 RemoteStop |
lib/query-api/hooks/crudHooksFactory.ts:150/194/238 |
resident/connector/cp/building 的 create/update/delete | 重複建檔、重複刪除 |
bill/hooks/billHooks.ts:157 |
/api/bills/export |
重複產出帳單檔 |
bill/hooks/billHooks.ts:177 |
/api/bills/status/update |
重複狀態異動 |
invoice/hooks/invoiceHooks.ts:69 |
/api/invoice/status/update |
重複發票狀態異動 |
settings/hooks/settingsHooks.ts:43 |
/api/settings/basic-charge |
重複寫入基本費設定 |
最危險的是逾時情境:第一次請求可能已送達後端、只是回應遺失,自動重試就會送出第二筆真實的指令。
修正¶
- 全域
mutations.retry由1改為0,重試一律由使用者明確再操作一次。 -
useStartCharge/useStopCharge另外明確宣告retry: 0並加註不可移除的理由,確保全域設定日後被改動時,這兩支安全關鍵的 API 仍不會被重送。
變更檔案僅兩支,共 +23/-2。
驗證狀態¶
-
npx tsc --noEmit:通過 -
yarn build:通過 -
yarn lint:無法執行。main上 branch admin 尚未有 ESLint 設定檔,next lint會進入互動式初始化提示。此為既有狀態,與本次改動無關,本次未修改任何 lint/build 設定。 - 實機驗證:尚未執行。建議驗證方式為在充電樁離線狀態下按「開始充電」,確認 Network 只出現一次
/api/tx/charge/start。
E2E Catalog Impact¶
應為 Update(後台充電操作的回歸案例需涵蓋「指令失敗或逾時後不得自動重送」)。
但本 branch 自 main 開出,main 上尚無 documents/EMS_E2E_TEST_CASE_CATALOG.md/documents/E2E_TEST_CASE_INVENTORY.md,也沒有 .claude/skills/maintain-ems-e2e-catalog/(皆仍在未合併的 feature branch 上),因此本次無檔案可維護。待 catalog 併入 main 後補上此案例。
後續¶
#1401 其餘項目(改吃 WebSocket、移除兩條輪詢與 3 秒硬等、新增維運欄位顯示)需等 #1400 提供 endpoint 與 payload 後才能實作。
是由 鍾正剛 於 3 天 前更新
- 被阻擋 Feature #1400: [Branch後台][BE] 充電樁狀態改由 WebSocket 推送,並補齊維運欄位與操作 API 已新增
是由 鍾正剛 於 3 天 前更新
h3. 2026-08-01 已完成「可先行處理」項目:mutation 全域關閉自動重試
依 Ryan 指示先處理不依賴 BE(#1400)的 retry 修正。
- Branch:@feature/redmine-1401-branch-admin-connector-ws-status@(自 @main@ @1f4e841@ 開出)
- Commit:@f6dd8d0@
- 尚未建立 MR。
h4. 問題
@react/branch/provider/providers.client.tsx@ 的 QueryClient 全域預設為 @mutations: { retry: 1 }@。React Query 只在 @mutationFn@ reject 時重試,因此充電指令一旦被拒絕或逾時,就會自動再送一次。與 #1399 在 LIFF 修掉的是同一個成因。
盤點 branch admin 上全部繼承此設定的 mutation,沒有任何一支是 idempotent:
|. 位置 |. 操作 |_. 自動重試的後果 |
| @connector/hooks/connectorHooks.ts:112@ | @/api/tx/charge/start@ | 重複 RemoteStart 與稽核紀錄 |
| @connector/hooks/connectorHooks.ts:143@ | @/api/tx/charge/stop@ | 重複 RemoteStop |
| @lib/query-api/hooks/crudHooksFactory.ts:150/194/238@ | resident/connector/cp/building 的 create/update/delete | 重複建檔、重複刪除 |
| @bill/hooks/billHooks.ts:157@ | @/api/bills/export@ | 重複產出帳單檔 |
| @bill/hooks/billHooks.ts:177@ | @/api/bills/status/update@ | 重複狀態異動 |
| @invoice/hooks/invoiceHooks.ts:69@ | @/api/invoice/status/update@ | 重複發票狀態異動 |
| @settings/hooks/settingsHooks.ts:43@ | @/api/settings/basic-charge@ | 重複寫入基本費設定 |
最危險的是逾時情境:第一次請求可能已送達後端、只是回應遺失,自動重試就會送出第二筆真實的指令。
h4. 修正
全域 @mutations.retry@ 由 @1@ 改為 @0@,重試一律由使用者明確再操作一次。¶
@useStartCharge@/@useStopCharge@ 另外明確宣告 @retry: 0@ 並加註不可移除的理由,確保全域設定日後被改動時,這兩支安全關鍵的 API 仍不會被重送。¶
變更檔案僅兩支,共 +23/-2。
h4. 驗證狀態
- @npx tsc --noEmit@:通過
- @yarn build@:通過
- @yarn lint@:無法執行。@main@ 上 branch admin 尚未有 ESLint 設定檔,@next lint@ 會進入互動式初始化提示。此為既有狀態,與本次改動無關,本次未修改任何 lint/build 設定。
- 實機驗證:尚未執行。建議驗證方式為在充電樁離線狀態下按「開始充電」,確認 Network 只出現一次 @/api/tx/charge/start@。
h4. E2E Catalog Impact
應為 Update(後台充電操作的回歸案例需涵蓋「指令失敗或逾時後不得自動重送」)。
但本 branch 自 @main@ 開出,@main@ 上尚無 @documents/EMS_E2E_TEST_CASE_CATALOG.md@/@documents/E2E_TEST_CASE_INVENTORY.md@,也沒有 @.claude/skills/maintain-ems-e2e-catalog/@(皆仍在未合併的 feature branch 上),因此本次無檔案可維護。待 catalog 併入 @main@ 後補上此案例。
h4. 後續
#1401 其餘項目(改吃 WebSocket、移除兩條輪詢與 3 秒硬等、新增維運欄位顯示)需等 #1400 提供 endpoint 與 payload 後才能實作。
是由 陳國瑋 於 約 16 小時 前更新
#1400 BE contract 已完成實作,FE 請依下列介面開發:
-
先 POST /api/connector/admin-websocket-ticket
Request: {"connectorId":"CP001-001"}
Response: {"ticket":"...","connectorId":"CP001-001","expiresAt":"..."}
ticket 一次性、30 秒、綁定 admin identity + connectorId。 -
WebSocket
/ws/admin/connectors/{connectorId}?ticket={ticket}
- CONNECTOR_SNAPSHOT:initial fetch 與後續 shared state
- OPERATION_RESULT:只送給發起 operation 的 admin;以 pending operationId 判斷是否顯示結果 modal
- close 4401:ticket 無效/逾時/已使用
- close 4403:ticket 與 connector 不符
- close 1011:initial snapshot 失敗
連線或 initial snapshot 失敗時回 Connector 列表並顯示 modal;不得沿用舊 snapshot 執行操作。
- Start / Stop
POST /api/tx/charge/start body: {"connectorId":"CP001-001"}
POST /api/tx/charge/stop body: {"connectorId":"CP001-001","stopReason":"optional"}
不得傳 userId,BE 只採 JWT identity。
Start:
- 202:result=DISPATCHED,含 operationId/operationStatus,isEnqueued=false
- 200:私樁 slot 滿,result=ENQUEUED,operationId=null,isEnqueued=true
- 403/409:{connectorId,errorCode,message}
Stop:
- 202:result=DISPATCHED,含 operationId/operationStatus
- 403/409:同上 structured error
-
Admin Start 使用管理員自己的 BranchUser/IdTag,transaction 與帳務不會自動歸到住戶。請在「開始充電」按鈕附近明確提醒操作管理員。
-
Connector 離峰 ON 時 manual Start 會回 409;FE 顯示 BE message。OpenAPI 已補 200/202/403/409 的具體 response schema,可重新執行 yarn openapi:refresh。
是由 鍾正剛 於 約 3 小時 前更新
h3. 2026-08-03 已完成 #1401 主體:狀態改吃 WebSocket,移除全部輪詢與固定等待
- Branch:@feature/redmine-1401-admin-connector-ws-status@(自 @private@ @78c3175@ 開出)
- Commits:@cab7d32@ retry 修正、@a9db939@ WebSocket 主體
- 尚未建立 MR。
h4. Base branch 更正
前一則備註的 branch 自 @main@ 開出,但 @main@ 上沒有 #1382/#1380 的成果——描述中要移除的 @chargePointHooks@ 10 秒輪詢與 @prevOnlineRef@ 在 @main@ 上根本不存在。經 Ryan 確認,#1401 的 base 應為 @private@,因此改自 @private@ 重開分支,並把 retry 修正 cherry-pick 過來(@private@ 上原本仍是 @retry: 1@)。
h4. BE 契約以 ken 實機實測取得,非推論
@ConnectorForHqDto@ 在 OpenAPI 上只新增了 @activeOperation@,A-2 要求的 8 組維運欄位一個都沒有;WebSocket 本來就不在 OpenAPI 描述範圍內。因此直接連上 ken 實測,確認 WS snapshot 使用的是後台專用 DTO(未被任何 REST path 引用,故不會出現在 OpenAPI)。
訊息外層:@{type, connectorId, occurredAt, data}@
@CONNECTOR_SNAPSHOT@ 的 @data@ 實測欄位:
id, rawStatus, status, statusLabel, areaType, connectorType, chargePointId, ocppConnectionStatus, lastHeartbeatAt, statusChangedAt, errorCode, vendorErrorCode, vendorId, currentTransactionId, currentIdTag, currentUserId, activeOperation, queues[], enableOffPeak, currentTariff, startTime, totalEnergy, energyUsage, currentVoltage, currentCurrent, currentPower, costPredict
A-2 欄位全數到位,且不含 @driverState@,符合後台維運語意。
@OPERATION_RESULT@ 的 @data@ 實測欄位(注意 @type@ 在此是 @{code,name}@ 物件,與 @activeOperation.type@ 的純字串不同):
operationId, type:{code,name}, source, status, errorCode, message,
retryable, occurredAt, nextRetryAt
close code 實測:
|. 情境 |. code |_. reason |
| ticket 無效 | 4401 | invalid, expired, or used ticket |
| ticket 重用 | 4401 | 同上(第一次成功、第二次即被拒) |
| ticket 與 connector 不符 | 4403 | ticket connector mismatch |
穩定 errorCode 實測(皆 HTTP 409):@START_ALREADY_PENDING@、@NO_ACTIVE_TRANSACTION@、@OFF_PEAK_ENABLED@、@CONNECTOR_NOT_FOUND@。
h4. 兩點提醒 BE/後續維護者
@status@ 是衍生狀態,@rawStatus@ 才是 OCPP 狀態。 CP001-001 的 @status@ 是 @ENQUEUED@(離峰排隊中),而原本的 @getStatusInfo()@ 只認 6 個 OCPP 值,會直接掉進 default 顯示成「待命」。這是原本就存在、改用 WS 後才會浮現的顯示錯誤,本次一併修正。
202 回應比帶 @activeOperation@ 的 snapshot 還晚到。 實測 start 的 202 到達前,WS 已先推來含 @activeOperation@ 的 snapshot。因此按鈕停用必須以 snapshot 的 @activeOperation@ 為準,只看 mutation @isPending@ 會有可重複點擊的空窗。
h4. 實作內容
- 新增 @useAdminConnectorSocket@:先取一次性 ticket 再連 WS,close code 分流,比照 #1399 只重連一次,失敗即明確告知
- 新增 @admin-connector-snapshot.ts@:WS 外層與 snapshot 手寫型別(WS 不在 OpenAPI,@openapi:refresh@ 不會產生)
- 移除三處輪詢與硬等:status 2 秒輪詢、ChargePoint 10 秒輪詢與 @prevOnlineRef@ 補償、start/stop 後的 @setTimeout(3000)@
- 斷線時標示「即時狀態已中斷」並停用充電操作,恢復後需收到新 snapshot 才可操作
- 以 snapshot 的 @activeOperation@ 防重複送出指令
- start/stop 改用新契約:body 不再帶 @userId@,依 errorCode 分流,結果由 @OPERATION_RESULT@ 推送
- 補維運欄位:故障碼/廠商錯誤碼、目前交易與使用者、離線時長、狀態停滯時間、離峰佇列
- 開始充電入口標示交易與費用歸屬管理員自己,不會歸到住戶
- 新增 @NEXT_PUBLIC_ADMIN_CONNECTOR_WS_URL@(WebSocket 無法走 Next.js 的 @/ems/api/*@ rewrite,必須直連後端)
h4. 驗證狀態
- @npx tsc --noEmit@:PASS
- @yarn build@:PASS
- @npx eslint@(直接跑 flat config):PASS
- @yarn lint@:無法執行。@next lint@(Next 14)不認得 repo 內的 flat config @eslint.config.mjs@,會進入互動式初始化提示。既有狀態,本次未修改任何 lint 設定。
- E2E catalog validator:與改動前相同的 3 個既有 broken link(皆指向 @java/charge_point/documents/@,該目錄在 @private@ 上不存在),未新增任何錯誤;案例數量校驗通過。
- 實機驗證:尚未執行,依 Ryan 指示由其自行驗證。
h4. 測試過程對 ken 環境的實際影響
擷取 @OPERATION_RESULT@ 契約時,經 Ryan 授權對 CP001-002 送出一次 RemoteStart(@operationId 567@)。因現場無車輛插槍,operation 於 deadline @11:55:25@ 逾時,結果為 @TIMED_OUT@/@REMOTE_START_NOT_STARTED@,未產生任何交易,connector 已自動回到 @AVAILABLE@、@activeOperation@ 為 null。另對 CP001-001 送出的 start/stop 皆被 409 擋下,無副作用。
h4. E2E Catalog Impact
@documents/E2E_TEST_CASE_INVENTORY.md@ 新增 7 案(@private@ 上尚無 @EMS_E2E_TEST_CASE_CATALOG.md@):
|. ID |. Pri |_. 內容 |
| UI-012 | P0 | 充電指令失敗或逾時後不得自動重送 |
| UI-013 | P0 | WS 斷線立即停用操作,恢復後需新 snapshot 才開放 |
| UI-014 | P0 | @activeOperation@ 未終結時不得送出第二筆指令 |
| UI-015 | P1 | ticket 4401/4403/1011 失效路徑與重連上限 |
| UI-016 | P1 | 衍生狀態 @ENQUEUED@ 正確顯示為「排隊中」 |
| UI-017 | P1 | 依穩定 errorCode 分流並顯示 BE message |
| UI-018 | P2 | @FAULTED@ 顯示實際故障碼 |
同步更新盤點數量(149→156)與 §1.3 條件式 P0 清單。
h4. 建議實機驗證項目
CP001-001 狀態頁應顯示「排隊中」+ 離峰佇列,且 Network 不應出現任何週期性 @/api/connector/status@ 請求¶
中斷網路 → 應出現「即時狀態已中斷」且按鈕停用;恢復後需收到新 snapshot 才恢復可操作¶
對離峰 ON 的 CP001-001 按「開始充電」→ Network 只應出現一次 @/api/tx/charge/start@,並顯示 @OFF_PEAK_ENABLED@ 的 BE 訊息¶
對 CP001-002 送 start → @activeOperation@ 未終結期間按鈕應停用,無法送出第二筆¶
是由 鍾正剛 於 約 2 小時 前更新
h3. 2026-08-03 補記:死碼清理(不影響行為)
依 Ryan 指示,在同一 branch @feature/redmine-1401-admin-connector-ws-status@ 追加兩筆清理。這兩筆都沒有改動任何行為,前一則備註列出的 4 項建議實機驗證步驟仍然適用。
- @de7641e@ refactor(#1401): 清除改吃 WebSocket 後殘留的死碼(-59/+3)
- @6b24f14@ chore: 移除 connector-bk 舊版備份目錄(-1471,6 檔)
h4. de7641e:改吃 WebSocket 後殘留的死碼
|. 項目 |. 說明 |
| @useConnectorStatus@ | 改吃 WS 後已無任何呼叫端,也是 FE 最後一個 @/api/connector/status/{connectorId}@ 呼叫點。移除後前端已完全不再打這支 REST |
| @ConnectorStatus@ 的 @chargePointId@ prop | 連線狀態已由 snapshot 的 @ocppConnectionStatus@ 提供,不再需要另查 ChargePoint;同時移除 @edit/page.tsx@ 的傳值 |
| @AdminStartChargingRequest@/@AdminStopChargingRequest@/@AdminChargingCommandResponse@ | 前一筆 commit 宣告後未使用 |
| @TERMINAL_OPERATION_STATUSES@ | 前一筆 commit 定義後未使用 |
| @AdminConnectorSnapshot@ import | 只 import 未使用 |
| @SearchConnectorTxDto@/@ConnectorBillDetailDto@ | 既有死碼,同檔案順手清除 |
| @UseQueryResult@ import | 因移除 @useConnectorStatus@ 變成孤兒 |
檢查過但保留的項目(非死碼):
- @admin-connector-snapshot.ts@ 的 11 個型別(@ConnectorRawStatus@、@OperationType@、@ConnectorQueueEntry@ 等)雖然「檔案外 0 次」,但都是檔案內組合出 @AdminConnectorSnapshot@/@OperationResult@ 的建構型別,各使用 2~5 次
- @useAdminConnectorSocket.ts@ 的 @AdminConnectorSocketStatus@/@UseAdminConnectorSocketResult@ 是 hook 的公開回傳型別
- @connector-status.tsx@ 的區域變數 @isSuccess@ 是從 @OperationResult.status@ 推導,不是舊 API 的 @isSuccess@ 欄位
h4. 6b24f14:移除 connector-bk 舊版備份目錄
@react/branch/app/(dashboard)/connector-bk/@ 是舊版 connector 功能的複本。雖然 @config/menus.ts@ 與全專案零參照,但 Next.js App Router 仍會編譯它,實際產出 @/connector-bk/create@、@/connector-bk/edit@、@/connector-bk/list@ 三個可直接訪問的路由——等於後台存在三個沒人維護、也沒有入口、卻能被直接開啟的舊頁面。
刪除前確認:
@config/menus.ts@ 與全專案零參照¶
6 個檔案的 @export@ 全是 Next.js page/layout 的 default export(框架要求),不是被外部 import 的模組¶
同名的 @charger-list-table@、@charger-stats@、@ChargerStats@ 在現行 @connector/components/@ 下另有各自維護的版本,名稱撞名但檔案獨立¶
刪除後 @/connector-bk/*@ 三個路由已從 build 產出消失,全專案零殘留參照。
h4. 驗證狀態
- @npx tsc --noEmit@:PASS(exit 0)
- @yarn build@:PASS,唯一 warning 來自 @node_modules/debug@(既有,與本次無關)
- @npx eslint@(直接跑 flat config):PASS
- @yarn lint@:仍無法執行,原因同前——@next lint@(Next 14)不認得 repo 內的 flat config @eslint.config.mjs@
- 實機驗證:仍未執行,依 Ryan 指示由其自行驗證
h4. 一項未納入版控的修正
@react/branch/app/(dashboard)/connector/README.md@ 的「技術實作細節」段落仍記載已移除的 @useConnectorStatus(connectorId) - 查詢充電樁狀態@,我已改寫為 WebSocket 流程,但 @react/branch/.gitignore:43@ 有 @**//README.md@,功能層 README 全數刻意不追蹤,因此該修正只存在於本機工作目錄,未進版控*。若要納管需先調整 @.gitignore@,已超出 #1401 範圍,未擅自更動。
(同檔第 221 行也出現 @isSuccess@,但那是 2024-12-19 的歷史 changelog 條目,未修改。)
h4. E2E Catalog Impact
No catalog change。這兩筆皆為死碼移除,未新增、修改或移除任何使用者可見行為、API、業務規則或設定;前一則備註新增的 UI-012~UI-018 已完整涵蓋本次功能變更,無需增修。