iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0

DAY 9 已能用 /projects 查看資料。今天加入 /start project_id,讓 Bot 檢查專案 ID 與狀態,成功後記住這個 Discord 伺服器正在處理哪個專案。

今天完成的內容

  • 定義專案狀態與合法轉換
  • 擴充 Repository 的查詢與更新功能
  • 使用 guild_projects.json 保存 Guild 的目前專案
  • 建立 ProjectMeetingService
  • 建立並測試 /start project_id

專案狀態

目前有四種狀態:

程式值 JSON 顯示文字
PENDING pending 待處理
IN_PROGRESS in_progress 進行中
COMPLETED completed 已完成
CANCELLED cancelled 已取消
class ProjectStatus(str, Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    CANCELLED = "cancelled"

/start 只允許 PENDING → IN_PROGRESS。已完成、已取消或進行中的專案都不能再次啟動。狀態規則集中在 can_transition_to(),避免各處自行判斷。

Repository 加入查詢與更新

DAY 9 只能列出專案,今天再加入:

class ProjectRepository(Protocol):
    def list_projects(self) -> list[Project]: ...
    def get_project(self, project_id: str) -> Project | None: ...
    def update_status(
        self,
        project_id: str,
        status: ProjectStatus,
    ) -> Project: ...

get_project() 會移除空白並轉成大寫,因此 prj-001 也能找到 PRJ-001。update_status() 更新 projects.json,重新啟動 Bot 後仍能保留狀態。

為什麼還需要 guild_projects.json?

兩份檔案保存不同資訊:

projects.json
└── PRJ-001 目前的專案狀態

guild_projects.json
└── Guild 123456 正在使用哪個專案

同一個 Bot 可能加入多個 Discord 伺服器,所以不能只用一個全域的 current_project_id。Store 會以 Guild ID 為 key,記錄專案 ID 與 UTC 啟動時間。文章中的 Guild ID 只是格式範例。

把啟動規則放進 Service

ProjectMeetingService 集中處理啟動流程:

整理 project_id
    ↓
讀取 Guild 目前專案
    ↓
查詢並檢查專案狀態
    ↓
更新 projects.json
    ↓
寫入 guild_projects.json

Discord 指令只負責接收輸入與顯示結果,規則則能直接用單元測試驗證。

start_project() 內使用 asyncio.Lock,避免同一個 Bot 程序同時收到兩次 /start 時一起通過檢查。這把鎖無法保護多個 Bot 實例;改成多程序後,仍需要資料庫交易或跨程序鎖。

啟動時要更新兩份 JSON。若 Guild 狀態寫入失敗,程式會嘗試把專案改回原狀。這只是 JSON 階段的補救,不等於真正的交易。

建立 /start 指令

指令接收 project_id,並限制只能在 Discord 伺服器中執行:

@bot.tree.command(name="start", description="啟動指定專案的會議")
@app_commands.describe(project_id="要啟動的專案 ID,例如 PRJ-001")
async def start_command(
    interaction: discord.Interaction,
    project_id: str,
) -> None:
    if interaction.guild_id is None:
        await interaction.response.send_message(
            "❌ /start 只能在 Discord 伺服器中使用。",
            ephemeral=True,
        )
        return

接著先用 defer() 回應 Discord,再交給 Service:

await interaction.response.defer(thinking=True, ephemeral=True)

project = await meeting_service.start_project(
    interaction.guild_id,
    project_id,
)

第一次回應已由 defer() 完成,成功與失敗訊息都要改用 followup.send()。

啟動成功後,Bot 會以私密 Embed 顯示專案名稱、狀態與期限。

https://ithelp.ithome.com.tw/upload/images/20260924/20183880kHNU771s5G.png

https://ithelp.ithome.com.tw/upload/images/20260924/201838801CBexTsC0D.png

找不到 ID、Guild 已有專案,或狀態不能啟動時,Service 會回傳可直接顯示的錯誤訊息。

找不到專案 ID:PRJ-999,請先使用 /projects 查看。
這個伺服器已有進行中會議:PRJ-002。

https://ithelp.ithome.com.tw/upload/images/20260924/20183880rn7pzljUDW.png

今天完成到哪裡?

/start 會檢查專案是否存在及狀態是否合法,再更新 projects.json 與 guild_projects.json。每個 Guild 同時只保留一個目前專案,啟動規則則集中在 ProjectMeetingService。

目前仍使用 JSON 保存資料。下一篇會建立 Agent 共用的基礎類別。


上一篇
DAY 9|建立專案資料與 /projects
下一篇
DAY 11|建立共用 BaseAgent
系列文
AI 公司模擬器:Discord x Multi-Agent 架構實作 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言