iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
AI Engineering

AI Agent 上線要想清楚的事:30 天拆解 Harness 的設計取捨系列 第 4

Day 4|running 轉了一整天,是在等人還是掛了:任務狀態要記到別人接得下去

  • 分享至 

  • xImage
  •  

🚦 任務狀態要記到別人接得下去:現在做到哪、在等誰、等到了從哪裡繼續。只有一個 running,誰也接不下去。

昨天在 Day 3|工具都接上了,事還是做不完:Agent 動手前、動手時、做完後,環境各要準備什麼,我們確認環境在 Agent 動手前、動手時、做完後,都給得出它需要的東西。接下來還有另一件事:任務跑到一半停下來等人,或是跑它的程式重啟了,系統要怎麼知道現在做到哪裡?

假設客服系統裡有一個 agent 在處理退款。使用者送出申請,Harness 開始跑 Day 1 那個 loop:模型先要求查訂單,再要求算金額,最後提出「送出退款」這個工具呼叫。這個工具被設成要主管核准才能執行,Harness 攔下呼叫、送出核准請求,loop 就停在這裡等一個人。

問題在停下來之後。申請的人看到的頁面只有 running。五分鐘後可能只是主管還沒看到;隔天還在轉,就分不出是主管真的沒批,還是跑這個 loop 的 process(下面叫 worker)已經掛了。值班的人重啟 worker,新的 worker 能查到的還是只有 running,它不知道該繼續等、從查訂單重來,還是再送一次退款。

這裡的 worker 指跑這個 loop 的 process:它打模型的 API、執行模型要的工具、把結果組回下一輪。同步版就是處理那個 HTTP request 的 handler,佇列版就是從 queue 撿任務來跑的常駐程式。模型在別人的伺服器上,worker 手上只有工具和自己的記憶體。

Day 1 的 loop 每轉一圈只做一件事:模型說要呼叫哪個工具,Harness 去執行,結果塞回下一輪,幾秒鐘就一圈。主管不是這種工具。核准請求送出去之後,沒有東西會馬上回傳,主管可能幾天後才在另一個頁面按下同意。這幾天 loop 轉不了,只能停下來,而 loop 本身沒有地方寫「我停在送退款這一步、在等主管」。

這段進度全在 worker 的記憶體裡:對話走到哪、查訂單和算金額的結果、停在送退款前這一步,worker 一掛就全沒了。所以 agent 跑起來之後,下一件事是把運行狀態搬到記憶體外面,也就是持久化。這篇先做第一步:決定要記什麼、記成什麼樣。以這筆退款來說是三件事:現在做到哪、在等誰、等到了從哪裡接著做。記清楚了,畫面才分得出正在做事、等人和真的卡住,接手的 worker 才知道從哪一步繼續。至於存在哪裡、worker 掛了怎麼從存檔接回來,Day 7 會詳細講。

所以今天要看的是:任務停下來等人、或換一個 worker 接手時,Harness 要記住哪些東西? 下面照這個順序:先看這份紀錄誰要讀、該掛在對話還是任務上;再看各家 SDK 怎麼切這件事;然後把狀態分好,定出最少要存的兩份紀錄;然後看等人時 loop 怎麼切成兩半、答案怎麼回來,最後處理兩個 worker 撞在一起。

狀態只剩一個 running,誰都接不下去

圖:同一個 running 傳到畫面、排程器和新 worker,三邊都不知道工作在等誰、做到哪一步,或退款有沒有送出去。

running 是給誰看的?

退款那筆的狀態,至少有三個地方要讀:畫面要告訴使用者在等誰;排程器要知道什麼時候叫醒工作,什麼時候該當作 worker 失聯發告警;接手的 worker 要知道從哪一步繼續。這些事只藏在對話紀錄裡,其他服務很難可靠地查到。

排隊、等補資料、等核准、卡在服務故障,後續要做的事都不一樣;已經取消的工作,就算結果晚到,也不能自己繼續。這些全寫成 running,三個消費者拿到同一個字,誰也做不了下一步。

不過狀態也不用越拆越細。我會看這個差別有沒有影響通知、deadline、retry、資源使用或負責人。兩種狀態後續處理完全相同,先合併就好;一個要等使用者幾天,另一個要請管理者處理,就值得分開。後面要不要多拆一個狀態,都可以拿這個當作評估依據。

對話還在,跟任務還在跑,是兩回事

使用者還開著對話、退款還沒處理完、模型正在產生回答,這是三件事。全用一個 active=true 表示,連線一斷,就分不出哪一件還在、哪一件已經停了。所以先講清楚狀態掛在哪個東西上,還是用退款的例子:

  • Session:使用者跟這個 agent 的一段對話,含歷史。隔天回來接著聊,還是同一個 Session。
  • Run:對話裡的一件工作,例如「處理這筆退款」。同一段對話先問訂單狀態、再申請退款,就是兩個 Run。這篇講的狀態掛在 Run 上。
  • Attempt:Run 裡某一步的第幾次嘗試。送退款失敗重試,Run 還是同一個,Attempt 從 1 變 2,才看得出這步試過幾次、前幾次怎麼失敗的。

這三個名字是我為了討論取的,各家 SDK 怎麼叫,下一節對照。

一個 Run 會跑好幾輪模型和工具,也可能中途等人。模型這一輪沒有再要求工具,不代表 Run 完成了。 它可能只是在問缺的資料,這時記 waiting_input;退款真的送出去、符合完成條件,才記 completed

一步失敗是 Attempt 加一;整件事失敗要從頭重做,就開一個新 Run,記下它接的是哪筆原任務。別把舊 Run 的狀態改回 running,那會把它失敗的紀錄蓋掉。

各家怎麼切?對邊界,別對名字

我翻了幾家 SDK 的文件和原始碼,看它們怎麼切同一件事。這一節只看概念,不看具體存放邏輯。依據是 2026 年 9 月中的版本,之後可能會改。名字本身不重要,每家都不一樣,讀文件時要對的是邊界切在哪。

同一件事,各家叫什麼:Session、Run、等人

圖:各家怎麼切對話、一次任務、loop 一圈,右邊是等人記在哪。圖上的名字以各家文件為準。

最好認的邊界是「一次輸入到最後回覆」:使用者一句話或一個外部訊號進來,模型呼叫幾次工具,最後回一句話。各家的執行入口都是這個單位:OpenAI Agents SDKRunner.run()Google ADKinvocationCodex CLITurnPydantic AIAgentRunClaude Agent SDK 的一次 query()LangGraph 開源版沒有名字,要用它的 Agent Server 才有帶 statusRun

往上一層是對話:把好幾次輸入到回覆串起來,共用同一段歷史。名字不是 Session 就是 Thread,意思都一樣。

往下一層是 loop 裡的一圈:一次模型呼叫,加上它引出的工具呼叫。OpenAI Agents SDK 叫 turnmax_turns 限的就是這個;ADK 叫 step,LangGraph 叫 super-step。只有一個字要小心:Codex 的 Turn 是整個一次輸入到回覆,OpenAI 的 turn 是裡面的一圈。看到 turn,先確認是哪一個。

這篇的 Run 跟上面哪一層都不一樣。處理一筆退款,順利的話就是一次輸入到回覆:申請進來、查訂單、算金額、送退款、回覆完成。但只要模型停下來問「要退哪一筆」,或是等主管核准,這一次回覆就結束了,事情還沒做完;等使用者補了資料、主管批了,再進來的那一次,還是在做同一筆退款。框架只認「一次輸入到回覆」,不認「同一筆退款」,所以 Run 這個單位要自己定義、自己存。Attempt 也是:OpenAI Agents SDK、LangGraph、Pydantic AI 的 retry 只是數到第幾次,程式一結束就沒了;只有 Temporal 把每一次嘗試當成一筆有編號的紀錄寫進歷史。

再來是等人。loop 停在「送出退款」前面等主管的那一刻,要留下三樣東西,對應後面 Run 上的三個欄位:

  1. 這筆現在在等status):畫面和排程器問一句就知道,不用把整段對話或事件翻一遍。
  2. 在等誰、等什麼waiting_for):「送出退款」這個呼叫還沒執行,要主管點同意。
  3. 等到了從哪接checkpoint):查好的訂單、算好的金額、那個還沒執行的呼叫都留著,主管批了直接送,不用從查訂單重來。

各家框架停下來等人時,這三樣給了哪幾樣,差別在「在等」這件事是誰記的、記在哪:

  • 框架自己記成一個狀態。 Microsoft Agent Framework 的 workflow 有一種狀態就叫「有請求還沒回」,待回的請求和進度一起存進 checkpoint。三樣都有,從外面問一聲就知道在等。
  • 框架當成結果還給你,記不記你自己決定。 OpenAI Agents SDK 的 run 在這裡正常結束,回傳值多一張「等核准的工具」清單,另外給你一個可以存起來的暫停狀態;Google ADK 是在最後一個事件上標「這個工具還沒回」,但那個事件同時也算「最終回覆」,從外面看跟做完沒兩樣。所以在等誰有了,「這筆在等」要你自己寫一筆狀態,暫停狀態也要你自己存。
  • 框架寫進存檔,要查得翻存檔。 LangGraph 停下來時把進度和「在等什麼」一起存進 checkpoint,主管回覆後帶著答案從那一格重跑;Temporal 是程式碼停在「等主管訊號」那一行,等的事實寫在執行歷史,worker 可以先走,訊號來了從那行接著跑。從哪接有了;「這筆在等」LangGraph 要用它的 Agent Server 才查得到,Temporal 從外面看還是「執行中」,要翻歷史才知道。

所以三樣裡,多數框架給的是後兩樣,「這筆在等」這一樣多半要自己記。這就是 Run 上要有 statuswaiting_for 這兩欄的原因:框架不替你記,畫面和排程器就沒東西可讀。

狀態可以先怎麼分?

下面沿用一組方便說明的狀態名稱,實際可以對應到現有系統,不用照著新增一套 API。用退款當例子:

狀態 屬於 意思 退款的例子
queued 進行中 排到了,還沒有 worker 接 申請剛送進來
running 進行中 有 worker 在跑 loop 正在查訂單、算金額
waiting_input 停下來等 等使用者補資料 模型問「要退哪一筆」
waiting_approval 停下來等 動作已提出,等人核准 送出退款前等主管
blocked 停下來等 卡在憑證或必要服務,昨天說的「做不成」 金流服務連不上
cancellingcancelled 結束 正在取消 → 確認相關工作都停了 使用者撤回申請
completed 結束 符合完成條件 退款送出且收到回應
failed / expired 結束 這次嘗試失敗 / 等超過期限 主管兩天沒批
outcome_unknown 結束 外部請求送了,回應沒收到 退款送了,金流沒回

停下來等的三種都能回到 running;結束的不能再回去,要重做就另建 Run。

任務狀態要記到別人接得下去

圖:退款這筆 Run 走過的狀態。等人時記清楚等誰、期限和進度,更新都走同一套規則,畫面、排程器和新 worker 讀的才是同一份。這是示意,不用照搬。

cancellingcancelled 為什麼要分開:AutoGen Core 0.5.5 的文件說得很直接,runtime 的 stop() 不會自動取消正在處理的 handler,底下的呼叫還要配合 cancellation token,也就是一路往下傳、用來通知「請停下」的那個物件。停止接新工作和取消手上的工作是兩件事(這裡引用的是該版本,其他 SDK 要另外確認)。Temporal 也把取消(讓程式收拾)和終止(直接關掉)分成兩個操作、兩個狀態,同樣的道理。

failed 之後是另建 Attempt 還是留在 retrying,由產品決定。outcome_unknown 要先查帳再決定下一步,不能當作沒成功,也不能直接再送一次。

最小要存什麼?兩份紀錄、一個 function

把上面講的落成東西,最小的版本是兩份紀錄加一個寫入用的 function。存在資料庫、SQLite 還是檔案都可以,先看要有哪些欄位。

第一份是 Run 的目前狀態,一個 Run 一筆,會被改。用退款卡在等主管那一刻的值來看,順便標每一欄是誰在讀:

欄位 退款卡在等主管時 誰讀、拿來做什麼
status waiting_approval 畫面、排程器
waiting_for / wait_deadline approver:manager / 48 小時後 畫面顯示在等誰;排程器到期叫醒或告警
checkpoint 做到第 3 步 接手的 worker 從這裡繼續
external_ref 空,退款還沒送 接手的 worker 判斷能不能再送
version / attempt 7 / 1 寫入時擋掉過期更新;這一步第幾次嘗試
owner / lease_until 空,worker 已放掉 排程器:租約過了還是 running 就是失聯
id / session_id 連回對話

第二份是 變更紀錄,每改一次狀態就追加一筆,不改舊的。同一筆退款從等主管到完成,會留下這幾筆:

seq from → to event actor at
7 running → waiting_approval ApprovalRequested worker-7 09-18 10:02
8 waiting_approval → running Approved user:manager 09-20 09:15
9 running → completed ToolFinished worker-3 09-20 09:16

seq 跟著 Run 的 version 走。接手做完的是 worker-3,不是原本的 worker-7,從紀錄就看得出來;要帶原因就多一欄 reason。

寫入只走一個 function,UI、worker、排程器都不直接改 Run:

# pseudocode:所有狀態變更都經過這裡
def transition(run_id, expected_version, event, actor, reason=None, **fields):
    with store.transaction():
        run = store.load_run(run_id)
        new_status = next_status(run.status, event)   # 規則集中在這一個 function
        if new_status is None:
            return rejected(run.status, event)        # 例如 completed + RetryRequested

        # 條件更新:只有版本還是 expected_version 才寫得進去
        ok = store.update_run_if_version(
            run_id, expected_version,
            status=new_status, version=expected_version + 1, **fields)
        if not ok:
            return conflict()                         # 別人先改了,重新讀再決定

        store.append_transition(run_id, seq=expected_version + 1,
                                from_status=run.status, to_status=new_status,
                                event=event, actor=actor, reason=reason, at=now())
        return accepted(new_status)

條件更新和追加紀錄要在同一個 transaction 裡,不然 Run 改了、紀錄沒寫,或反過來,之後就對不起來。

呼叫 transition() 的有三種程式:跑 loop 的 worker、收畫面操作的 API server、看期限的排程器。用這筆退款走一遍:

誰呼叫 什麼時候 event 結果
worker loop 跑到「送出退款」被攔下 ApprovalRequested running → waiting_approval,一起填 waiting_for、wait_deadline、checkpoint
API server 主管在後台按同意 Approved waiting_approval → running,Run 回 queue
worker 退款送出、拿到回應 ToolFinished running → completed,external_ref 補上退款單號
排程器 wait_deadline 過了主管還沒批 Expired waiting_approval → expired,發告警
worker 使用者已取消,工具結果才回來 ToolFinished 維持 cancelled,結果另外記
API server 有人對已完成的 Run 按重試 RetryRequested 拒絕;要重做另建 Run,連回原任務

每一次呼叫都帶著自己讀到的 version 進來,被人搶先改過就拿到 conflict,重讀再決定;後面「兩個 worker 同時接手」那節會用到。

next_status() 就是這張表左三欄對右欄的規則,集中在一個 function 裡,這種 function 常叫 reducer。規則集中,UI、API server、worker 和排程器才不會各自解讀。尤其任務已經取消,工具才回報完成,紀錄可以留,狀態不能改回執行中。

至少要有的就這幾樣:狀態掛在 Run 上,而不是掛在連線或 process 上;等誰、等到什麼時候;進度存在哪、外部操作的識別是什麼;一個版本號讓過期的更新被拒絕;每次變更留一筆。欄位可以再加,這幾樣少一個,前面某個讀者就做不了下一步。

等人時,loop 切成兩半,答案從另一個 request 回來

退款要等主管核准,可能等上好幾天。同步版的 request 撐不了三天,佇列版讓 worker 掛著等也不划算。所以等人的時候,loop 要切成兩半,中間沒有任何 process 在等:

worker A
  loop 跑到「送出退款」,被攔下
  → transition(ApprovalRequested):status=waiting_approval、waiting_for=主管、
    checkpoint=目前的對話和那個還沒執行的工具呼叫
  → 發核准請求(待辦、通知),然後結束,process 可以死
  (幾天。沒有 process 在等,排程器只看 wait_deadline 有沒有過)
主管在後台按同意 → 一個全新的 HTTP request 打進 API server
  → transition(Approved):status=running → Run 丟回 queue
worker B(另一個 process)
  撿到 Run → 讀 checkpoint → 執行那個待送的「送出退款」→ loop 接著跑

後端做過金流 callback 或 webhook 的人對這個形狀很熟:發請求和收回應的不是同一個 process,靠一個 id 和一筆存起來的狀態接起來。Run 表就是那筆狀態。接回來的那半,程式大概長這樣:

# pseudocode:主管按下同意
def on_approval(run_id, expected_version):
    transition(run_id, expected_version, event="Approved", actor="user:manager")
    queue.push(run_id)

# pseudocode:worker 撿到 Run
def resume(run_id):
    run = store.load_run(run_id)
    state = load_checkpoint(run.checkpoint)      # 對話到目前為止、待執行的工具呼叫
    result = execute_tool(state.pending_call)    # 送出退款
    continue_loop(state, result)                 # 回到 Day 1 的 loop

各家框架差在前半怎麼存:前面三種立場裡,記成狀態和寫進存檔的,暫停本來就存著;當結果交給你的,要自己把回傳的暫停狀態存起來。LangChain 的 runtime 文章講的「等人時釋放執行資源、之後從 checkpoint 接手」就是這件事,這是 LangSmith Deployment 和 Agent Server 的能力,不是裝了 LangGraph 就有。

回來之後哪些動作能沿用、哪些要重做,還有 checkpoint 要存成什麼才撐得過 process 死掉,是 Day 7 的題目,這裡先不展開。

兩個 worker 同時接手,怎麼不蓋掉彼此?

前面講的都是「重啟後接得下去」。還有一個相反的狀況:舊 worker 沒死透,新 worker 也接手了,兩邊同時改同一筆狀態。

Run 的 version 那一欄就是為這件事準備的:transition() 裡的 update_run_if_version 是條件更新,目前版本仍是 7 才能更新成 8,版本檢查和寫入放在同一個原子操作,一個 worker 寫成功後,另一個就得重新讀取再決定。ownerlease_until 則讓接手的 worker 先確認租約過期,才能把 owner 改成自己。Hermes Agent 的做法是在 SQLite 裡放一張租約表,記錄這段對話目前由誰在跑、什麼時候到期,其他 process 要等租約過期才能接手。

這些只保護資料庫裡的更新。已經送出去的退款、寄出的信、第三方寫入,versioncheckpoint 都撤不回,要靠 external_ref 去查、下游去重或人工對帳,才知道重做會不會多生效一次。從 checkpoint 接回來也有同樣的問題:LangGraph 的文件明講恢復時那一格從頭重跑,暫停前做過的外部動作可能再做一次。所以送外部請求之前,先把要用的識別(例如退款的 idempotency key)寫進 external_ref,接手的人才查得到有沒有送過。

可以先從哪裡做起?

前幾天的寫法,對正在自己刻 Harness 的人來說可能有點抽象。所以從這篇開始,每篇最後固定留一節:如果你從頭實作 Harness,這篇講的東西要從哪裡切入、怎麼開始比較好,再對回上面的說明,整套怎麼運作會比較好懂。

假設你照 Day 1 從頭刻 Harness,loop 已經跑得起來:一個 process 收到訊息,把對話放在一個 list 裡,打模型、跑工具、回覆。接下來照這個順序加:

先給對話一個身分。 把對話紀錄搬出記憶體,用 session id 存起來,一開始一張表或一個 JSON 檔都行。每個 request 帶 session id 進來,Harness 照 id 把歷史載回來、接上新訊息、跑 loop,跑完寫回去。這步做完,process 重啟後對話還在,同一個使用者開好幾個對話也不會混在一起。Session 之間預設互相看不到:A 對話的歷史不會進到 B 的模型輸入,要跨對話記住東西(使用者偏好、上次做過什麼),得另外定寫入規則和有效期,那是 Day 12 的題目。歷史越長越貴,什麼時候該摘要、摘要能不能蓋掉原文,Day 11 再談。

再把工作從對話裡拆出來。 loop 第一次要停下來等人,或工具一跑就是幾分鐘,就是開 Run 表的時候。先把這篇的兩份紀錄和 transition() 做出來,狀態用 queued / running / completed / failed 四個起步;等人時填 waiting_forwait_deadline,畫面和排程器一起讀這幾欄。用現成框架也一樣:先看它停下來等人時給了三樣裡的哪幾樣,缺的補在 Run 表,框架交回來的暫停狀態存起來就是 checkpoint。已經有 workflow engine,就讓它管等待、派工和恢復,Harness 接上任務目標、工具和完成條件就好。

然後測幾個容易出事的時間點。 兩個 worker 同時接手、取消後才收到工具結果、完成後又收到舊核准。這些事件可以記下來,但不能讓已結束的任務自己重新開始。上線後看各狀態待了多久、多少超過 deadline、多少次更新被拒絕,就知道要不要再拆狀態。

短而同步、沒有外部寫入、整段重跑也便宜的工作,做到第一步就夠,Run 表可以晚點開。需要等人、跨 request,或工具很昂貴,再補更細的等待原因。

這兩份紀錄後面幾天會一直被加東西:Day 5 講狀態是由哪些事件推出來的、順序亂了誰說了算;Day 6 把取消放進同一條順序;Day 7 講 checkpoint 要存成什麼、worker 消失後怎麼從存檔接回來。今天不用一次想完,先把 Session 和 Run 的邊界定下來,後面加的東西才有地方放。

你可以怎麼確認自己讀懂了?

回到那筆退款:主管還沒核准,worker 就先退出了,這一定算失敗嗎?新的 worker 只讀到 running,又能不能直接再送一次退款?

等人可以是正常狀態,前提是等待對象、期限和接續位置都有保存;只有 running 則不足以判斷該從哪一步繼續,更不能據此重做外部操作。

任務狀態要記到別人接得下去:現在做到哪、在等誰、等到了從哪裡繼續。 名稱可以依產品調整,畫面、排程器和接手的 worker 要讀到同一個意思。

今天的變更紀錄每改一次狀態追加一筆,順序由 seq 定。明天看的是事件本身:工具結果同時回來、畫面斷線重連,畫面、worker 和模型各自收到的順序可能不一樣,得先講好哪一份紀錄說了算。

參考資料

  1. Microsoft AutoGen|Agent and Agent Runtime:0.5.5 版本的 stop() 不會自動取消執行中的 handler;用來說明 cancellingcancelled 要分開記。
  2. OpenAI Agents SDK|Running agentsSession 等幾種接續對話的方式,以及 turn 的定義(一次模型呼叫加它引出的工具呼叫);文件是 v0.22.3 當時的內容。
  3. OpenAI Codex|sdk/typescript/src/events.tsThreadTurn 的定義,Turn 涵蓋回應一句 prompt 的所有事件。
  4. Google ADK|agents/invocation_context.pyinvocationstep 的定義寫在 docstring 裡,含層級示意。
  5. Claude Agent SDK|Sessionssession_idresumefork_session 的用法;一次任務沒有獨立的名字。
  6. LangGraph|Checkpointersthread 的定義,以及 checkpoint 以 super-step 為單位。
  7. Pydantic AI|pydantic_ai/agent 模組AgentRun 物件;retry 只是計數器。
  8. Temporal|Activity ExecutionRetry Policies:每次嘗試是一筆 Activity Task Execution,attempt 編號在事件歷史裡;用來說明 Attempt 要不要有身分。
  9. Microsoft Agent Framework|_workflows/_events.pyWorkflowRunState 的七個值,其中兩個代表有請求還沒回;引用的是 main 分支該 commit。
  10. LangGraph|Interruptsinterrupt() 把 graph 狀態和待回的 Interrupt 存進 checkpoint,恢復時 node 從頭重跑。
  11. OpenAI Agents SDK|result.pyrun_state.pyinterruptions 列出待核准的工具,RunState 是可序列化的暫停邊界。
  12. Google ADK|events/event.pylong_running_tool_ids 標記未回的工具,is_final_response() 在這種情況也回 True
  13. Temporal|Workflow message passing:workflow 用 signal 等外部輸入;等待期間不占 worker 是 Temporal 的執行模型,不代表其他框架也如此。
  14. Temporal|Workflow Execution:Workflow Id 與 Run Id 的分工,以及 Cancellation 和 Termination 的差別。
  15. Hermes Agent|hermes_state_common.pysession_turn_leases 租約表,記錄目前由誰在跑、何時到期。
  16. LangChain|The runtime behind production deep agents:受管理的 runtime 如何在等人時保存狀態、釋放資源;這是部署服務的能力,不是套件本身的。

上一篇
Day 3|工具都接上了,事還是做不完:Agent 動手前、動手時、做完後,環境各要準備什麼
系列文
AI Agent 上線要想清楚的事:30 天拆解 Harness 的設計取捨4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言