feat(pokemon): add the pokemon centre (Discuz X5 plugin integration) - #150
Conversation
The DTOs of the plugin's JSON API (dart_mappable with a tolerant fromMap), the api base url, the exception its error envelope is reported with, and the strings of the new pages in all three locales.
Adds the JSON post the plugin's write calls need (dart and the android side), logs the method and url of every write and of every answer that is not ok, keeps a 404 out of the network error banner, and relaxes the android http timeouts the slow heal and sprite downloads hit.
One place attaches the session formhash, retries once when the server calls it stale and runs every call under a deadline (20s, 60s for the map list), so a request the platform client parked is reported as a network failure instead of holding a page in its busy state.
State and actions of the centre and of a battle, including the re-read of the battle after an action that lost its answer on the transport.
The centre with its four tabs, the adventure map list, the battle page and view, the detail/equipment/storage pages, and the shared widgets and helpers (state and type styling, dialogs, item paging).
Registers the adventure cache, adds the /pokemon routes and their observer, and puts the centre behind the home app bar, the home tools card and the profile page.
Android can clear the cache directory while the app runs, so it is created again before a write and a failing write only costs the cache entry, not the image.
A bar queued on a messenger whose page was just torn down trips an assertion inside it; skip it instead.
flutter test --coverage writes a coverage directory that is easy to add by accident.
Models and url building, the adventure/battle/skill/shop models and the error text mapping, the dialogs, the repository (client reuse, one formhash read per page load, party healing, a call that never answers), the cubits (lifecycle, status bar switch, item actions, the battle re-read after a lost answer), the pure helpers, and the landscape layout of the centre, the adventure page and the battle page (renumbered to 176-183).
What each folder holds, the conventions the module follows, the plugin behaviour that is easy to get wrong (site, defeat, the free heal, skill slots, the formhash, recover's 404), the test list and the coverage gaps.
… bool The analyzer of the flutter version CI runs flags ullable == true (use_if_null_to_convert_nulls_to_bools), and ?? false says the same thing.
|
Claude finished @Carinoasd's task in 8s —— View job Hi @Carinoasd, your comment only contained the Here are some things I can do on this PR (#150, the pokemon centre):
Reply with |
|
@claude 請審查這個 PR,重點看:正確性與潛在 bug、並發問題(heal 與 battle 的重讀路徑)、Dart / Kotlin 兩端 postJson 的改動、圖片快取的改動,以及安全性。請用繁體中文回覆。 |
|
Claude finished @Carinoasd's task in 1m 5s —— View job PR #150 審查:宠物中心(pokemon)我讀了 整體結構清楚,註解把插件的實際行為( 需要處理1. Kotlin
2. 圖片快取:寫檔失敗後 DB 記錄仍然留著
3.
較小的問題
已檢查、沒問題的部分
建議的處理順序
需要我直接針對 1–3 點提交修正到 |
…p asked for OkHttp retries a request on a dropped connection, which would buy something twice or add a battle turn, so the json writes go out on the non-retrying client with a one-shot body (as postForm already does). The 404 exemption is narrowed to image requests and the plugin api: a forum page that is really gone still reports.
…ed formhash to the account A write the server already carried out must not be sent again, and the formhash belongs to one account's session, so another account reads it instead of sending the previous one's. A refresh no longer clears a read that another request started.
A row written ahead of a failed file would point at a file that is not there, so the file goes first and a failed write ends the save.
The read-back after a lost answer now keeps the generation it started with, is dropped while another action runs, and every action starts a new one, so its answer cannot paint over the action that followed. A scene that arrives also drops an older failure message.
…st its answer The server may have carried the write out after all, so the money and the bag are read again before the player tries it a second time.
…ash and the overtaken read-back
審查總結我讀了網路層(Kotlin / 1.
|
|
感谢两轮审查。 第一轮的 1–3 和小项都处理了(已经推到分支,追加 7 个提交):
第二轮这三点:
另外 OkHttp 的 15/30/60 秒是全局生效(不只宠物),这点我补进 PR 描述。 |
…rts again dart_mappable throws a TypeError (an Error, not an Exception) on an unexpected field type, so the cubits' on Exception handlers let it escape; they catch Object now, like the repository does. Starting a battle also drops its finished-battle key, which repeats on the same map against the same species with the same own pokemon, so a fight the client just began is not taken for one that already ended.
|
PR #150 審查總結 這個 PR 很大,我主要看了 1. 逾時後寫入操作可能已在伺服器端生效,卻被回報為網路失敗(正確性)
2.
3. 全域 Android 逾時變更影響範圍大(效能/行為) 4. 測試缺口
5. 小風險(可選) 我沒有執行測試,以上都是靜態閱讀的結果。 |
|
審查摘要 整體結構清楚, 1.
2. 分頁載入與分類切換的競態(正確性,中)
3. 讀取 formhash 的頁面請求走預設 10 秒 client(效能/可靠性,低到中)
4. 測試缺口(低)
|
The end-of-battle answer carries no battle id and may leave the pokemon id at 0, so heal_and_flee and the flee fallback were both sent with nothing and the battle stayed on the server. The last scene that named its battle is kept, and the end scene fills in what it is missing; without either, the party is healed and the page still remembers the battle as over.
…eporting it The 20s deadline is shorter than the android client's 30s read timeout, so a write that was still in flight was reported as a failure although the server may well have carried it out (a second tap would buy again). Writes get 35s now, which lets the transport report its own, accurate failure.
PR 審查總結我讀了共用層的變更(Kotlin 1.
|
The widened client was picked from the X-Pm-Formhash header, which the read that fetches the formhash itself does not carry - and a request made without one (a formhash that could not be read) fell back to the 10s client, exactly the case the wider timeouts were for. The plugin url is checked first now.
… call a battle ended A resume or a newer action makes an answer stale; it no longer clears the busy state or counts a round, so the page cannot be unlocked while a non-idempotent write is still running. A defeat that could not be ended (no battle id anywhere) is not reported as done either; only 'the battle is already over' is.
…ost its answer buy and buyPet return a failure when the transport failed, but the server may still have carried the purchase out, so the money, the bag and the party are read again before the player tries a second time.
審查總結(PR #150 寵物中心)範圍說明:我重點讀了網路層( 結論沒有發現我有把握確認的正確性或安全性 bug,以下都是需要作者確認的觀察,不是阻擋合併的問題。 值得留意
其他部分(formhash 的快取與重試、依帳號 uid 失效、圖片快取先寫檔再寫資料庫、 🤖 Generated with Claude Code |
The whole plugin api was exempt, so a plugin that is disabled or renamed answered 404 without ever showing up in the log or the banner. Only the recover call, whose 404 means no battle is running, stays silent now; the image requests keep their own exemption.
onAppResumed read the battle back and painted it unconditionally; an action the player started while that read was in flight now owns the screen, and the stale read-back is dropped like any other.
loadMoreShop, loadMoreInventory, refreshShop and refreshInventory merged or replaced a list without checking that the category (or the page) was still the one the answer belongs to, so a switch made mid-request glued the old entries onto the new list and moved the page counter.
Code Review 總結已審查 Kotlin 傳輸層、 1. 分頁載入後,刷新會把累積的列表換成單一頁(正確性)
重現:商店捲動載入到第 2 頁以上,再購買任一道具。 建議:刷新時重新載入 1..N 頁,或刷新後重設回第 1 頁。 2.
|
…cted category loadMoreShop and loadMoreInventory accumulate pages, so a refresh (which happens after every purchase) that replaced the list with the page the player had reached dropped every page before it, and loadMore only goes forward. The refresh paths read the first page again and reset the counters. load() also read the shop without its category, so a stone category held the whole item list.
…re a formhash only for its account The write deadline has to outlast connect + read (15s + 30s on android, counted separately), so it is 45s now and the transport reports its own failure. A formhash read that finishes after the account switched no longer caches the old session's hash for the new one.
審查摘要看了 repository、網路層(Dart + Kotlin)、 🐞 背包/商店「載入更多」永遠不會附加下一頁
final next = state.inventoryPage + 1;
final result = await _repository.getInventory(page: next, ...).run();
...
if (state.inventoryPage != next) return; // 這裡請求送出時 影響:
守衛的原意(排除切換分類期間回來的舊答案)應改成與請求時的頁碼比較,例如 🧪 測試缺口
其餘部分(formhash 重試、 |
The guard added for the category race compared the jump target instead of the page the request started from, so the next page could never land and the counter never moved. The test for the list refresh caught it.
A refresh after loading more pages has to read the first page again and move the counter back with it, and only the plugin's recover 404 (and an image that is missing) may stay out of the network error banner.
審查總結這個 PR 很大(60+ 檔),我主要審查了共用層與核心邏輯:Kotlin 1. 寫操作逾時後請求不會被取消,可能造成重複扣款/重複回合(中)
建議:逾時後對寫操作走 2.
|
start guarded its success path with the generation but not its failure path, so an answer that a resume or a newer action had already made stale could still switch the page to the failure state and unlock it. The write deadline comment now also says that the formhash read happens before it.
|
@claude 逐条回一下:
本地 flutter test 1826 passed、strict analyze 干净,CI 已重跑。看一下还有问题吗? |
|
Claude finished @bbtu1's task in 16s —— View job 复核结果我对照分支上的代码看了 你的四点回复,我都认可:
之前几轮的问题,我抽查了一下:
还剩一处小的:
除此之外我没有发现新的问题。 |
It emitted the busy state and cleared it in its finally without checking the generation, so a resume or a newer action could have its busy state cleared by a heal that started before it. fightAgain needs no such guard: it has no finally of its own and leaves the flag to start.
|
@claude这条也修了: healAndFlee 进入时捕获 generation,finally 里比对,过期就不再清忙碌态(只捕获、不递增,避免顺带作废插件在途的答复)。 |
|
@claude 这条也修改了: healAndFlee进入时捕获生成,终于里比对,过渡就不再清处理态(只捕获、不递增,避免顺带作插件在途的答复)。 |
|
Claude finished @bbtu1's task in 2m 30s —— View job PR #150 再次審查
範圍:我看了分支 HEAD 上一輪的兩點:已確認
需要處理1. 背包和商店的「載入更多」在切換分類後,會把舊分類的清單蓋到新分類上(正確性,中) 位置: 現在的判斷是
商店的一般分頁同樣有這個問題(例如在 建議:改成比對清單本身,例如 2. 過期動作的傳輸失敗仍會觸發 位置:
A 已經過期, 建議:改成 3. 位置:
較小的問題
Dart / Kotlin
|
|
@claude 你來處理這塊 |
|
Claude finished @Carinoasd's task in 3m 15s —— View job 處理上一輪審查的問題
|
…he right pet on fight again - loadMore* compares the list it started from instead of the page number: a category switch or a refresh resets the page to 1, the very page most load-mores start from, so the old guard let the old category's pages through. - refreshShop's item branch compares the category it asked for. - A stale battle action that lost its answer no longer reads the battle back over a newer action's scene. - fightAgain takes the pet id from the running scene when the end scene lost it, and heals the party without one. - _runVoid reads the party back too after a lost answer (release, move, set first and rename change it). - A failed image cache write drops the truncated file. Adds four regression tests to test_181. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
宠物中心(插件
pokemon:pokemon的 JSON 接口)做成原生页面了。宠物列表、治疗、背包、商店、冒险地图、战斗、详情、装备、仓库、进化路径都在,入口放在首页标题栏、首页工具卡和个人页,路由是/pokemon/*。顺带动了几个公共文件:
postJson(Dart 和 Android 的 Kotlin 通道都加了)。插件接口和图片请求单独用一个超时更宽(15/30/60)的 client,论坛和其它请求保持原来的 10s。recover的 404 和图片的 404 只记 debug,不再当成网络错误弹提示。AdventureCache、加路由和路由回调、三个入口、三语文案,.gitignore里补了 coverage。i18n 的三个 json 只加了 key。几处是按插件的实际行为处理的:
defeat不代表战斗结束(有替补时插件会保留战斗,客户端用heal_and_flee清掉);heal免费且会回满 HP 和 PP,但插件自己不会回血,所以离开战斗页的每条路径都会治一次;技能只有 4 个槽且遗忘要求 PP 满,学技能走「先遗忘再学」;背包和仓库共用boxnum;写操作带 session 的 formhash。测试补了 8 个(
test_176到test_183),覆盖模型、文案映射、弹窗、仓库层(客户端复用、formhash 只读一次、治疗并发、请求超时)、cubit,以及横屏布局(宠物中心 4 个 tab、冒险页、战斗页,792x368 带挖孔)。本地flutter test全部通过,strict analyze 干净。