Phase:M04 — Web Chat MCP Integration & AI Join Kit 本文件定義 M04-A~M04-C 每個 Subphase 的自動 / 人工驗收流程與測試證據要求。驗收意圖見
實作規格.md;實作契約見開發設計方針.md。
最後更新:2026-09-11
X.n 對應至少一條 pytest/Vitest/Playwright test 或人工程序,於 closeout 列出檔名 + test 名稱。tests/test_p3d_*.py、tests/test_p3e_*.py 在 M04 每個 Subphase 關門時必須通過;只能加斷言、改 subset 比對,不得刪或改語意。/mcp/guide 與 briefing,斷言不含。zh-TW/en 各跑一次。| Subphase | diff 性質 | Subphase 關門要跑的 E2E |
|---|---|---|
| M04-A | 只動 tools/m04a-webchat-preflight/ 與 docs,不碰 app |
無。以人工 preflight 記錄為證據 |
| M04-B | backend:OAuth package、migration、auth 分流、可能的相容層;不碰 apps/web(authorize 頁是 server-rendered) |
無新 Playwright;以 protocol/OAuth pytest 與 B.8 人工 gate 為證據。若 4.7 落在 per-role URL 且動到前端顯示 URL,跑 p3e-mcp-browser-integration.spec.ts |
| M04-C | backend 加 guide/briefing;apps/web 兩個面板加 kit |
m04c-ai-join-kit.spec.ts(新);p3e-mcp-browser-integration.spec.ts;m02h-bilingual-site-smoke.spec.ts 若已納入 Lobby 路由則一併跑 |
合併回 main 前比照 AGENTS.md「Phase 關門」跑全套 Playwright(npm run test:e2e:docker)。
test_p3d_*.py/test_p3e_*.py 的 grant/auth fixture 建 DM、Player、pre-session DM 三種。client_id、redirect_uri=https://chatgpt.example/callback)、PKCE verifier/challenge 對、一張 active DM grant 的 plaintext AI Join Token。SECRET_TOKEN、SECRET_DC = 27(掛在一個 dm_only request)。at_oa_;DB 只有 hash。GET /mcp/guide 的 Content-Type 為 text/plain; charset=utf-8。briefing ≤ BRIEFING_MAX_CHARS(2,400;原 1,200 於 2026-09-12 隨強制流程 briefing 放寬)。M04-A — Web Chat MCP Preflight
→ M04-B — Adventure Table Web Chat Integration
→ M04-C — AI Join Kit, Server-hosted Guide & Other Client Compatibility
M04-A 的 app 側只有兩條 static/unit 測試;其餘證據全部是人工 preflight 記錄,依「ChatGPT Plus → Claude chat 個人方案」階梯對每個嘗試的平台各記一輪。以下每項對應實作規格 A.n。
M04-A_PREFLIGHT.md 開頭記錄:
tools/m04a-webchat-preflight/README.md 的 smoke 步驟,用 curl 走一遍:metadata 兩個 endpoint 200、/register 回 client_id、/authorize 表單 200、以 dm 完成 code → token、tools/list 含 dm_only_ping、post_note 回 seq、以 player 重做後 tools/list 不含 dm_only_ping;經公開 tunnel URL 打 /admin/add-tool 得 404,經 127.0.0.1:<admin_port> 打得 200;打完 /token 後檢查 JSONL 該行的 response_body 不含 access_token/refresh_token 明文。記錄 JSONL 行號。
另有兩條自動測試:
tests/test_m04a_preflight_isolation.py::test_preflight_tool_does_not_import_app,掃描 tools/m04a-webchat-preflight/*.py 不含 from app/import app。tools/m04a-webchat-preflight/test_redact.py:對 token endpoint 的 request/response 樣本、含 code= 的 Location header、含 client_secret 的 register body 各跑一次 redact(),輸出不含任何明文 secret;admin router 對非 loopback client.host 回 404。記錄表(每列一個 request):endpoint、method、平台送的關鍵欄位(遮罩後)、我們回的關鍵欄位。至少涵蓋:metadata discovery 有無、DCR 有無與 body、authorize query、token request 的 grant_type/PKCE、refresh 是否被呼叫。
記錄:Scan 觸發的每個 JSON-RPC method 依序、MCP-Protocol-Version 值、Mcp-Session-Id 有無、Accept、_meta 內容、平台對 initialize 回應中 protocolVersion 的接受情況。
get_context,記錄 request/response 與平台呈現。post_note,記錄平台的確認 UX(截圖或文字描述)、是否可設為免確認、server 端收到的 arguments。若 write 因方案被拒,記錄平台原文並依階梯進下一平台。五步各一列:操作、平台端 tool 清單(截圖或文字)、需要的使用者動作(無/Refresh/重新發佈/新 connector)、JSONL 行號。第 4 步(revoke)另記平台的錯誤呈現與是否自動重新 authorize。
wait_seconds 10/30/60(若 60 通過再試 90、120):每次記成功或平台端 timeout 訊息。結論寫成一個數字。
M04-A_PREFLIGHT.md 最後六個小標(開發設計方針 3.4)各一句結論 + 證據行號,第一個是 Target platform。這是 M04-B 開工的輸入;缺任何一項 M04-A 不得關門。判定 Platform Blocked 時,M04-A closeout 同時把 PROJECT_BRIEF 的 M04-B/M04-C 標為延後、下一步改 P4。
Claude Desktop 若有量,記 A.3 同格式;沒有就寫「未量」。Claude chat 個人方案若已作第二階正式量測則不屬本項。
tests/test_m04b_oauth_endpoints.py
test_metadata_endpoints_exist_when_required:依 preflight 結論,需要的 metadata endpoint 回 200 且欄位正確;不需要的不存在(404)。test_register_returns_client_id(若需 DCR)。test_authorize_page_bilingual:?locale=zh-TW/en 各回 200 HTML,含 token textarea;不含任何 Room/Campaign/Seat 名稱或 UUID。test_authorize_with_valid_join_token_redirects_with_code:302,Location 含 code 與原 state。test_authorize_with_bad_join_token_rejected_without_side_effect:回錯誤頁 oauth_join_token_rejected,ai_oauth_authorization_codes 計數不變。test_token_exchange_with_pkce:code + 正確 verifier → at_oa_ access token;DB 只有 hash。test_token_exchange_bad_pkce_rejected:oauth_invalid_grant,code 未被 consume。test_code_single_use:同一 code 第二次 → oauth_invalid_grant。test_refresh_rotates_access(若需 refresh):舊 refresh 作廢、新 access/refresh 綁同一 authorization_id。test_refresh_revalidates_grant_authority:以下四種狀態各一個 case——grant revoked、Seat epoch 前進(administrative reassignment/Take Back)、pre-session TTL 到期、Session End——refresh token 本身未過期,但 refresh 回 oauth_invalid_grant,且該 authorization revoked_at 被設定、零新 token row。test_standalone_has_no_oauth_routes(擴充 test_p3e_standalone_mcp.py):所有 OAuth path 404。tests/test_m04b_oauth_grant_binding.py
test_access_token_resolves_same_auth_view_as_join_token:同一 grant 的 at_ai_ 與 at_oa_ 走 authenticate_request,AIControllerAuthView 相等。test_tools_call_identical_via_both_tokens:get_session_context 兩種 token 回同一份 structuredContent。test_grant_revoke_invalidates_oauth_tokens_atomically:revoke grant 後該 grant 全部 authorization 的 access/refresh 都 401;在同一 transaction 內(以 savepoint rollback 證明不會只撤一半)。test_revoke_scoped_to_authorization_family:同 client_id 下綁 grant A 與 grant B 的兩個 authorization,revoke grant A 後,grant B 的 access/refresh 仍有效。test_seat_epoch_advance_invalidates_oauth_token:administrative reassignment 後舊 access token 401。test_session_end_invalidates_oauth_tokens:End/Abandon 後 401。test_pre_session_ttl_expiry_invalidates_oauth_token。test_oauth_token_expiry_enforced:expires_at 過後 401。tests/test_m04b_oauth_seat_invariant.py
test_oauth_flow_writes_zero_seat_rows:整個 register → authorize → token → tools/call 流程前後,campaign_seats、sessions、session_participants 的 row 內容與計數完全相同(比對整表 dump)。test_oauth_module_has_no_seat_writes:static 掃描 app/mcp/oauth/*.py 不含對 seat/session/participant repository 的 write 方法名。test_reauthorize_same_grant_revokes_previous_family:對同一張 grant 第二次 authorize 後,第一個 authorization revoked_at 被設定、其 access/refresh 401;新 family 有效。test_reauthorize_other_grant_same_client_keeps_previous_family:同 client_id 以另一張 grant 的 token authorize 後,原 grant 的 family 不受影響。test_one_active_family_per_grant:partial unique index 擋下同 grant 兩個 revoked_at IS NULL 的 authorization。test_one_authorization_one_grant_for_lifetime:無任何 endpoint 能改 ai_oauth_authorizations.grant_id(schema 無 update path;以 repository 介面枚舉證明)。tests/test_m04b_legacy_protocol.py
test_modern_path_unchanged:test_p3e_mcp_protocol.py 在同一 CI job 內全綠。test_legacy_initialize_static_and_stateless:連續兩次 initialize 回應相同;不寫 DB。test_legacy_session_id_opaque:帶任意/不帶 Mcp-Session-Id,tools/call 結果相同;server 重啟後仍可用。test_legacy_and_modern_share_catalog_and_auth:同 token 兩版本 tools/list 相同;revoke 後兩版本都 401。test_legacy_wait_does_not_hold_db:沿用 P3-A waiter starvation 替身。依 4.7 落點:
test_p3e_mcp_tools.py 既有 scope 測試即證據,加 test_oauth_source_gets_same_role_scoped_catalog。test_union_catalog_lists_all_with_role_markers、test_player_oauth_token_calling_dm_tool_rejected_no_side_effect(回 scope_denied,event 計數不變)。test_role_url_catalog_fixed、test_token_role_mismatch_with_url_rejected。wait_for_event timeout 上限test_m04b_wait_timeout_cap.py::test_mcp_wait_timeout_capped_to_preflight_value:MCP 入口 timeout 超過實測值回 validation error;Human UI long-poll endpoint 上限不變。
2026-09-12 修訂:M04-C 交付時上限改 120,本測試斷言同步改為「120 通過、120.1 拒絕」;Human UI
le=60斷言不變。
test_readme_has_no_credentials:不含 tskey-、Bearer at_、at_oa_、at_ai_ 後接非佔位字元。寫入 M04-B_CLOSEOUT.md,兩場各一表:
| 步驟 | 證據 |
|---|---|
Lobby 產生憑證/Let AI Control |
截圖或 UI 文字 |
| 平台加 connector、OAuth 貼 token | server log 的 authorize/token 行(遮罩) |
| Scan Tools 看到的清單 | 截圖或文字;與 4.7 落點一致 |
get_session_context → start_session(DM)/post_dialogue(Player) |
event seq |
Human 回應 → AI wait_for_event 收到 → AI 再回應 |
event seq |
Player 場:Human DM request_check → AI roll_pending |
roll result id |
Owner revoke/Human Take Back Control → AI 下一次呼叫失效;若平台嘗試 refresh,refresh 也被拒 |
平台端錯誤呈現 + server 401/oauth_invalid_grant log |
記錄方案、日期、protocol version、認證形態、AI 從 connector 加好到第一次成功 get_session_context 的 tool call 次數。
cd apps/server 後 pytest tests/test_m04b_*.py tests/test_p3d_*.py tests/test_p3e_*.py tests/test_m03_import_boundary.py tests/test_m03d_schema_parity.py tests/test_p3e_standalone_mcp.py 綠。0021 各一次;standalone character@head 不含四張 OAuth 表。docker compose config;前端若未動則不跑 E2E。GET /mcp/guidetests/test_m04c_mcp_guide.py
test_guide_returns_plain_text_without_auth。test_guide_supports_both_locales:兩 locale 200 且 body 不同;zh-TW 含中文。test_guide_rejects_unknown_locale:400 mcp_guide_locale_unsupported。test_guide_contains_three_join_paths:含目標平台/connector 段、Bearer 段、HTTP 契約段。test_guide_client_sample_has_applicability_notice:最小 client 段開頭含適用性聲明(en 含 connector,zh-TW 含「網頁版」)。test_guide_contains_contract_sections:含 MCP-Protocol-Version: 2026-07-28、Mcp-Method、Mcp-Name、io.modelcontextprotocol/protocolVersion、get_session_context、wait_for_event、ai_token_unauthorized。test_guide_wait_rule_matches_cap:兩個 locale 的等待規則段含 120 與「5」次,且 120 等於 WaitEventsInput.timeout 的 le(從 schema 讀,不手寫)。test_guide_contains_no_secret:不含 SECRET_TOKEN、SECRET_DC、任何 Room/Seat UUID。test_guide_does_not_touch_db。test_standalone_has_no_mcp_guide_route(擴充 test_p3e_standalone_mcp.py)。tests/test_m04c_guide_tool_parity.py
test_guide_lists_every_tool_definition。test_guide_has_no_unknown_tool_names。test_guide_module_has_no_hardcoded_tool_names:static 掃描 guide.py 不含任何 _TOOL_DEFINITIONS name 字面值。test_guide_required_params_match_schema。test_player_rules_do_not_mention_dm_tools:Player 守則段不含 request_check、post_narration、set_stage_text;含 post_action、roll_pending。tests/test_m04c_tool_descriptions.py
test_every_description_has_en_and_zh。test_description_minimum_length:en 段 ≥ 80。test_every_tool_has_specific_when_to_use_text:16 個「何時用」互不相同且不只是重述工具名。test_role_scoped_catalog_is_fixed_per_role(2026-09-12 修訂,原 test_role_scoped_catalog_unchanged):Player 12 個不變;DM 固定 13 個含 start_session,且 pre-session 與 active 的 tool_catalog 完全相同;quick_roll/whisper_dm 不在 DM、start_session 不在 Player。test_descriptions_state_when_the_call_is_accepted:start_session 寫明 pre_session_only、get_session_context 寫明前後皆可、其餘 DM 工具雙語都寫明 active_session_required。test_p3e_mcp_tools.py::test_pre_session_dm_catalog_matches_active_dm_catalog、test_p3e_pre_session_ai_dm.py(start 前後清單相同);test_pre_session_dm_gameplay_tool_returns_active_session_required 不變,仍是執行閘門的證據。briefingtests/test_m04c_briefing.py
test_pre_session_dm_briefing_mentions_start。test_active_dm_briefing_excludes_player_tools:不含 quick_roll、whisper_dm。test_active_player_briefing_excludes_dm_tools:不含 request_check、post_narration、set_stage_text;含 post_action。test_briefing_keeps_temporary_instruction_separate。test_briefing_length_cap:以 BRIEFING_MAX_CHARS(2,400)為上限。test_briefing_wait_rule_and_write_back:兩種 role 的 active_session briefing 含等待規則(120、5 次);DM 含 post_narration、Player 含 post_dialogue。test_active_briefings_are_mandatory_step_loops:含 MANDATORY DM/PLAYER LOOP、禁止停在 host chat、wait_for_event 至少出現兩次。test_dm_active_briefing_leads_with_stage_setup:DM active 含 stage_unset 與 set_stage_text。test_pre_session_briefing_states_no_join_step_and_stage_first:pre-session 明講無入席步驟且第一步 set_stage_text。test_active_briefings_carry_the_invocation_rule:含 MCP invocation rule/MCP 呼叫判定、connection failed、尚未測試。test_stage_hint_steers_dm_to_set_stage_then_wait:_stage_hint 對 DM 空 Stage→set_stage_text、有文字→wait_for_event、Player 一律 wait_for_event。test_briefing_contains_no_secret。test_p3e_pre_session_ai_dm.py(start_session 回 stage_unset=True/next_required_action="set_stage_text")、test_p3e_temporary_instruction_context.py(active Player context 回 wait_for_event)。briefing/temporary_instruction;active 斷言用 subset,斷言數不減。server/discover.instructionstests/test_m04c_discover_instructions.py:含 /mcp/guide 與 get_session_context;ttlMs/cacheScope 仍在。
apps/web/src/features/rooms/AIJoinKit.test.ts
builds a complete English DM kit with local and remote URLs:含 URL (local): {origin}/mcp、URL (remote): {public_origin}/mcp、兩個 Guide 行、token、role、Expires、目標平台 connector 段(指向公網 URL)、Bearer 段、何時用哪個 URL 的一句話;不含「未設定公網入口」。builds a Traditional Chinese Player kit and explains the missing remote URL:remoteOrigin: null 時不印公網 URL/Guide,印「尚未設定公網入口」。renders both locales with the same line structure。uses contract filename and does not leak the token。hardcodedUiCopy.test.ts 涵蓋 AIJoinKit.tsx。src/api/aiControllers.test.ts:fetchMcpPublicOrigin 不帶 Room token、null 原樣透傳。tests/test_m04c_public_origin_endpoint.py
null;route 無 dependency、只有 GET;standalone 404。擴充 LobbyAIDMGrantPanel.test.ts、PlayerAIControlPanel.test.ts:issued 後 [data-ai-join-kit] 出現、複製內容 == textarea、下載 spy 與檔名、既有 token-once 測試不動。
apps/web/e2e/m04c-ai-join-kit.spec.ts
request 打 GET /mcp/guide 200、POST /mcp get_session_context 得 pre_session(MCP 直連 PLAYWRIGHT_MCP_URL,同 P3-E spec)。GET /mcp/guide 200、POST /mcp 無 Bearer 得 401,且 GET /api/mcp/public-origin 在 E2E 環境回 null;kit 對應印「未設定公網入口」而非公網 URL。Let AI Control 後 kit 可見、role 為 Player。| 路徑 | 程序 | 必須觀察到 |
|---|---|---|
| MCP client(Bearer) | 新開 Claude Code session,工作目錄不在本 repo,只給 kit,提示語僅「依這份檔案進桌當 DM」 | claude mcp add(或等價)→ get_session_context → start_session → post_narration → wait_for_event → Human 回一句 → AI 再回 |
| 純 HTTP | 一個確定具備 outbound network 的 code-execution client(例如 Claude Code 不設定 MCP),只給 kit | GET /mcp/guide → 依範例寫 client → 同上迴圈 |
記錄 client、版本、日期、拿到 kit 到第一次成功 get_session_context 的 tool call 次數、AI 有沒有問指引已寫的事(有就是缺口,回填後再跑)。
2026-09-12 修訂(使用者拍板):上表兩條路徑延後。M04-C 的 C.8 gate 改為 ChatGPT Web Plus(目標平台)憑 Lobby/Session 產出的 kit 進桌一次,由使用者人工執行,結果記入 M04-C_CLOSEOUT.md「C.8」;同日暴露的行為缺口(request_check 友善 ref、擲骰提示進 Chat)以 commit 回填。
M04-C_CLOSEOUT.md「Compatibility」表:非目標平台的網頁版 chat、Codex CLI、Codex Desktop 各一列——平台、版本、日期、OAuth 成功與否、Scan 成功與否、卡點。全部失敗也不阻塞關門。
2026-09-12 修訂(使用者拍板):延後;closeout 不含此表。
npm test -- --run、npm run build 綠。cd apps/server 後 pytest tests/test_m04c_*.py tests/test_m04b_*.py tests/test_p3e_*.py 綠。docker compose config,加 1.1 對應 E2E。main:全套 E2E。