(Don't Merge)Nekobrawl arena v2#1454
Conversation
## Summary This PR introduces the first integrated prototype of 猫娘大乱斗, including the deck builder, forged-card pipeline, new battle UI, local battle service, and launch workflow. ## Added - Added 猫娘大乱斗 deck builder page. - Added local deck saving with 18-card deck rule and max 3 copies per card. - Added favorite card marking and favorite-only filtering. - Added forged-card generation from 奇遇制造机. - Added forged cards into deck builder card pool. - Added new battle UI prototype with drag-to-Boss card play flow. - Added transparent play zones, ally play animation, battle log drawer, Combo list drawer, and Boss state visuals. - Added local battle arena server and one-click startup scripts. - Added battle arena visual assets. ## Forged Card Rules - Forged cards inherit base card name, cost, main effect, type, and normal attribute from C001-C013. - Forged card names append `(Forged)`. - Combo attribute is randomized. - Story text is currently a hardcoded temporary placeholder until the personal story system is integrated. ## Temporary / Prototype Notes - Current card appreciation/detail view is TODO. The old hover detail popup has been removed because it blocked the card grid. - Play-zone dashed outlines are temporary and should be removed after final UX is confirmed. - Star-to-Boss effect is temporary battle feedback. - Some layout experiments were used during UI positioning and should be finalized in a later pass. ## Verification - Ran `npm run build` successfully. - Manually verified local dev server responds on `localhost:5173`.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Walkthrough本 PR 新增并整合本地匹配/Forge 后端与 LLM 故事生成、ActiveNeko 上下文、主服务器 CORS/头像同步,以及完整前端 Neko Brawl 子项目(UI、战斗/组卡/锻造/音频/样式)、构建配置、启动/停止脚本与相应文档喵。 ChangesBattle Arena 对战系统
Estimated code review effort:
|
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (2)
battle-arena/src/components/BattleLog.jsx (1)
24-43: ⚡ Quick win动态对战日志建议补充
aria-live,否则读屏器不会及时播报更新喵。这个区域是实时更新内容,建议在日志容器加
aria-live="polite"和aria-atomic="false",让辅助技术能感知新增日志喵。可参考的微调喵
- <div className="flex-1 space-y-1.5 overflow-y-auto pr-1"> + <div + className="flex-1 space-y-1.5 overflow-y-auto pr-1" + aria-live="polite" + aria-atomic="false" + >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@battle-arena/src/components/BattleLog.jsx` around lines 24 - 43, The log container that renders dynamic messages in BattleLog.jsx (the div wrapping AnimatePresence that maps over logs) needs ARIA attributes so screen readers announce updates; add aria-live="polite" and aria-atomic="false" to that root container (the div containing AnimatePresence and the logs array rendering) to ensure assistive tech is notified when new entries (from logs/map) are appended.static/app-chat-avatar.js (1)
349-368: ⚡ Quick win建议给头像同步加“去重”门闩,避免重复 POST 同一张图喵。
当前多个入口都会触发同步,同一
dataUrl在一次会话里可能重复上传,带来不必要请求和日志噪音。可以在前端记一个最近同步签名,命中就直接跳过喵。可参考的最小改法喵
@@ - /** 将当前头像 dataUrl 推送到 battle-arena 后端(非关键,静默失败) */ + // 会话内最近一次同步签名,避免重复上传同一头像 + let lastSyncedAvatarSignature = ''; + + /** 将当前头像 dataUrl 推送到 battle-arena 后端(非关键,静默失败) */ function syncAvatarToBattleArena(dataUrl) { if (!dataUrl) return; var _nekoName = (typeof lanlan_config !== 'undefined' && lanlan_config.lanlan_name) ? lanlan_config.lanlan_name : ''; + var signature = _nekoName + '|' + dataUrl; + if (signature === lastSyncedAvatarSignature) return; fetch('/battle-arena/avatar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ side: 'left', dataUrl: dataUrl, name: _nekoName }) }).then(function (res) { if (!res.ok) { console.warn('[AvatarSync] POST /battle-arena/avatar failed:', res.status, res.statusText); } else { + lastSyncedAvatarSignature = signature; console.log('[AvatarSync] Avatar synced to battle-arena successfully'); } }).catch(function (err) { console.warn('[AvatarSync] Avatar sync error:', err.message || err); }); }Based on learnings: 在该仓库的 JavaScript 文件审查中,优先把问题合并为一条清单式评论,避免分散多条评论喵。
Also applies to: 393-393, 1153-1153, 1174-1174, 1313-1313
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@static/app-chat-avatar.js` around lines 349 - 368, Add a simple de-duplication gate in syncAvatarToBattleArena: keep a module-scoped variable (e.g., lastSyncedDataUrl or lastSyncedSignature) and before issuing fetch compare the incoming dataUrl/signature to that stored value and return early if identical; only update lastSyncedDataUrl to the new signature after a successful POST (inside the res.ok branch) so retries on failure still attempt upload; reference the syncAvatarToBattleArena function and the dataUrl parameter when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/main_server.py`:
- Around line 1598-1614: The set_battle_avatar and get_battle_avatar endpoints
currently accept untrusted side/dataUrl/name into _battle_arena_avatars; add
input validation and bounds: enforce side whitelist (only "left" or "right") in
both set_battle_avatar and get_battle_avatar, validate dataUrl starts with an
allowed prefix like "data:image/" and cap its length (e.g. max bytes/chars) and
similarly cap name length; if validation fails return a 4xx response (e.g. 400)
instead of storing; ensure you reference and update _battle_arena_avatars only
after passing these checks so oversized or invalid payloads cannot exhaust
memory.
In `@battle-arena/src/components/BattleArena.jsx`:
- Around line 483-499: The pollOpponent function captures playerIdRef.current
into id before awaiting the fetch, but never re-checks it after the async call;
add a guard after the await and after parsing the response (e.g., in
pollOpponent and the analogous polling function around lines 531-555) that
returns early if playerIdRef.current !== id so stale responses cannot overwrite
state; keep using setOpponent, setMatchStatus and clearing pollTimerRef.current
only when the id still matches.
- Around line 463-469: Validate postMessage origin and source in onMessage:
restrict event.origin to the allowed parent origins and verify event.source
matches the expected window (e.g., the parent iframe contentWindow) before
calling setAvatarLeft or setNameLeft; only accept messages with type ===
'neko-avatar' && side === 'left' after those checks. For pollOpponent, prevent
stale-rematch races by tying each fetch to the current session (compare the
returned id/session token to playerIdRef.current or a rematch token) or use an
AbortController per rematch to cancel previous fetches; before calling
setOpponent or setMatchStatus, verify the response belongs to the current
session (e.g., compare id !== playerIdRef.current or check a rematching flag)
and skip updates if it does not. Ensure references: onMessage, setAvatarLeft,
setNameLeft, pollOpponent, handleRematch, playerIdRef.current, setOpponent,
setMatchStatus are updated accordingly.
In `@battle-arena/src/components/CardGamePanel.jsx`:
- Around line 973-979: The merged temporary card created in the merged object
(id `card-${cardIdCounter}` built from mergedAttr/mergedPower and debuffs) is
missing essential fields `code`, `name`, and `effects`, causing
resolvePlayedCard (used by the confirm-play path) to misbehave; fix by
populating the merged card with minimal settlement-ready fields — e.g., set
`code` and `name` (derived from `dragCard`/`targetCard` or a deterministic
composite string) and a valid `effects` array (merge or synthesize the two
cards' effects or provide an empty/default effect structure compatible with
resolvePlayedCard) so that resolvePlayedCard and functions that read `effects`
(called in the confirm-play flow) can operate without errors.
- Around line 205-210: BOSS_IMAGE_SOURCES currently uses absolute local file
paths (e.g., "F:/...") which will 404 off your machine; update
BOSS_IMAGE_SOURCES to use project-relative static asset paths or import the
images at build time (e.g., replace the string values with imported module
references or "./assets/..." relative paths) so assets resolve across
environments; locate the BOSS_IMAGE_SOURCES constant in CardGamePanel.jsx and
either import the four image files at top of the file and assign those imports
to the normal/attack/damageTaken/weakDamageTaken keys, or change the values to
repository-relative public/static URLs that your bundler serves.
In `@battle-arena/src/components/NewBattleDuelUI.jsx`:
- Around line 686-715: The pointer handlers currently use only moved to control
overHand and drop logic, which causes light drags anywhere to cancel previews;
update handlePointerMove to compute overHand = isPointInHandZone(event.clientX,
event.clientY, handZoneRef.current) and set that in returnDragState (keep moved
calculation as-is), and in handlePointerUp use isPointInHandZone with the final
event coordinates (and/or the computed targetRect hit test) instead of just
moved to decide whether to setReturnFlyingCard (return animation) or call
inspectCard/onSetPreviewCard; ensure you still compute fromX/fromY/toX/toY when
returning and clear returnDragState via setReturnDragState(null).
- Line 6: The hardcoded local /@fs path in the BATTLE_BACKGROUND_SRC constant
should be replaced with a publishable asset reference: move the PNG into the
project's public/ static assets or import it from src so the bundler emits a
URL, then update the BATTLE_BACKGROUND_SRC constant in NewBattleDuelUI.jsx to
use that public-relative path or the imported URL; look for the
BATTLE_BACKGROUND_SRC constant to change and ensure the component uses the new
value for the background image.
In `@battle-arena/src/data/forgedBrawlCards.js`:
- Around line 123-128: saveForgedBrawlCards currently calls
window.localStorage.setItem directly and can throw in private
mode/quota-reached/disabled-storage; wrap the storage write in a try/catch
around the window.localStorage.setItem invocation (inside saveForgedBrawlCards)
and on failure swallow the error or log it (e.g., console.error) so the UI flow
isn’t interrupted; keep the same normalization pipeline using
normalizeForgedBrawlCard and the FORGED_BRAWL_CARDS_STORAGE_KEY, but ensure
exceptions from setItem don’t propagate.
In `@battle-arena/vite.config.js`:
- Around line 12-15: 配置中的硬编码绝对路径 ("F:/NEKO_bugfix/NekoBrawl/Gif_source")
会导致多人环境启动失败;将该硬编码项替换为由环境变量驱动并在未设置时回退或省略:在 vite.config.js 中找到 allow 数组(与
__dirname 同级),把硬编码字符串替换为基于 process.env(例如 process.env.EXTRA_ALLOW_DIR)的值,并在构建
allow 数组前做条件检查(如果变量存在则 push 进数组,否则使用项目内默认相对路径或不注入该项),确保路径使用
path.resolve/normalize 以兼容跨平台。
In `@local_server/battle_arena_server/README.md`:
- Around line 18-20: Remove the blank line inside the blockquote that currently
contains "铸造机 facts 接口依赖 `httpx`:若 `.venv` 中尚未安装,请在 N.E.K.O 根目录执行 `uv sync` 或
`uv pip install -r local_server/battle_arena_server/requirements.txt`" so the
blockquote is a single continuous paragraph (fixes MD028), and add a language
specifier (e.g., ```bash or ```text) to the fenced code block around the snippet
at the later section (lines referenced in the comment) to satisfy MD040; update
the README.md fenced block and the quoted paragraph accordingly.
In `@local_server/battle_arena_server/server.py`:
- Around line 65-68: The matched dict currently stores only the opponent
snapshot (matched: dict[str, dict]) which prevents removing both sides on leave;
change the pairing data to include the opponent's player_id (e.g., store
matched[player_id] = {"opponent_id": other_id, "opponent_snapshot": {...}}) OR
add a separate pair map (e.g., pairs: dict[str, str]) that records player_id ->
opponent_id when a match is created (adjust the matching code where matched is
set and the leave handler / polling cleanup to remove both matched[player_id]
and matched[opponent_id] using the stored opponent_id); update any places that
read matched (match-making code, leave function, polling/cleanup logic) to use
the new structure so both sides are cleared reliably.
- Around line 3-6: Update the default bind address from 0.0.0.0 to 127.0.0.1 in
both the startup examples and the actual server run call: change the commented
uvicorn CLI example "--host 0.0.0.0" to "--host 127.0.0.1" and change the
uvicorn.run/app invocation that uses host="0.0.0.0" to host="127.0.0.1" (leave a
note that 0.0.0.0 can be used for cross-device testing).
---
Nitpick comments:
In `@battle-arena/src/components/BattleLog.jsx`:
- Around line 24-43: The log container that renders dynamic messages in
BattleLog.jsx (the div wrapping AnimatePresence that maps over logs) needs ARIA
attributes so screen readers announce updates; add aria-live="polite" and
aria-atomic="false" to that root container (the div containing AnimatePresence
and the logs array rendering) to ensure assistive tech is notified when new
entries (from logs/map) are appended.
In `@static/app-chat-avatar.js`:
- Around line 349-368: Add a simple de-duplication gate in
syncAvatarToBattleArena: keep a module-scoped variable (e.g., lastSyncedDataUrl
or lastSyncedSignature) and before issuing fetch compare the incoming
dataUrl/signature to that stored value and return early if identical; only
update lastSyncedDataUrl to the new signature after a successful POST (inside
the res.ok branch) so retries on failure still attempt upload; reference the
syncAvatarToBattleArena function and the dataUrl parameter when making this
change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fcd3943b-8452-4a8d-bc18-d96f62e17fb3
⛔ Files ignored due to path filters (10)
battle-arena/package-lock.jsonis excluded by!**/package-lock.jsonbattle-arena/public/Simple_design_judging.gifis excluded by!**/*.gifbattle-arena/public/background_twisted.jpgis excluded by!**/*.jpgbattle-arena/public/celebration.gifis excluded by!**/*.gifbattle-arena/public/cry.gifis excluded by!**/*.gifbattle-arena/public/waiting.gifis excluded by!**/*.gifbattle-arena/public/waiting_idle.gifis excluded by!**/*.gifbattle-arena/public/waiting_idle2.gifis excluded by!**/*.gifbattle-arena/public/waiting_idle3.gifis excluded by!**/*.gifbattle-arena/public/waiting_idle4.gifis excluded by!**/*.gif
📒 Files selected for processing (27)
.gitignoreapp/main_server.pybattle-arena/index.htmlbattle-arena/package.jsonbattle-arena/postcss.config.jsbattle-arena/src/App.jsxbattle-arena/src/components/BattleArena.jsxbattle-arena/src/components/BattleLog.jsxbattle-arena/src/components/BottomTicker.jsxbattle-arena/src/components/CardGamePanel.jsxbattle-arena/src/components/DeckBuilderPanel.jsxbattle-arena/src/components/NekoAvatar.jsxbattle-arena/src/components/NekoCard.jsxbattle-arena/src/components/NewBattleDuelUI.jsxbattle-arena/src/components/ScoreBar.jsxbattle-arena/src/data/forgedBrawlCards.jsbattle-arena/src/index.cssbattle-arena/src/main.jsxbattle-arena/tailwind.config.jsbattle-arena/vite.config.jslocal_server/battle_arena_server/README.mdlocal_server/battle_arena_server/__init__.pylocal_server/battle_arena_server/requirements.txtlocal_server/battle_arena_server/server.pystart-battle-arena.batstart_battle_arena.pystatic/app-chat-avatar.js
| @app.post('/battle-arena/avatar') | ||
| async def set_battle_avatar(payload: dict): | ||
| """由 app-chat-avatar.js 在捕获头像后调用,存储 dataUrl 和角色名供 battle-arena 获取。""" | ||
| side = str(payload.get('side', 'left')) | ||
| data_url = str(payload.get('dataUrl', '')) | ||
| name = str(payload.get('name', '')) | ||
| if data_url: | ||
| _battle_arena_avatars[side] = {'dataUrl': data_url, 'name': name} | ||
| return {"ok": True} | ||
|
|
||
|
|
||
| @app.get('/battle-arena/avatar/{side}') | ||
| async def get_battle_avatar(side: str): | ||
| """battle-arena 前端轮询此端点获取最新头像 dataUrl 和角色名。""" | ||
| from fastapi.responses import JSONResponse | ||
| entry = _battle_arena_avatars.get(side, {}) | ||
| return JSONResponse({'dataUrl': entry.get('dataUrl', ''), 'name': entry.get('name', '')}) |
There was a problem hiding this comment.
头像同步端点缺少输入边界,容易被超大 dataUrl 或任意 side 撑爆内存喵。
Line [1601-1606] 直接把外部 side/dataUrl/name 入内存,没有 side 白名单、格式校验和长度上限喵。建议最少限制 side(left/right)并限制 dataUrl 前缀与长度(超限返回 4xx)喵。
💡 建议修复(示例)
_battle_arena_avatars: dict = {} # side -> {dataUrl, name}
+_BATTLE_AVATAR_SIDES = {"left", "right"}
+_BATTLE_AVATAR_MAX_DATA_URL_LEN = 2_000_000
`@app.post`('/battle-arena/avatar')
async def set_battle_avatar(payload: dict):
@@
- side = str(payload.get('side', 'left'))
+ side = str(payload.get('side', 'left')).strip()
data_url = str(payload.get('dataUrl', ''))
name = str(payload.get('name', ''))
- if data_url:
+ if side not in _BATTLE_AVATAR_SIDES:
+ return JSONResponse(status_code=400, content={"ok": False, "error": "invalid side"})
+ if data_url and (not data_url.startswith("data:image/") or len(data_url) > _BATTLE_AVATAR_MAX_DATA_URL_LEN):
+ return JSONResponse(status_code=413, content={"ok": False, "error": "avatar too large or invalid"})
+ if data_url:
_battle_arena_avatars[side] = {'dataUrl': data_url, 'name': name}
return {"ok": True}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/main_server.py` around lines 1598 - 1614, The set_battle_avatar and
get_battle_avatar endpoints currently accept untrusted side/dataUrl/name into
_battle_arena_avatars; add input validation and bounds: enforce side whitelist
(only "left" or "right") in both set_battle_avatar and get_battle_avatar,
validate dataUrl starts with an allowed prefix like "data:image/" and cap its
length (e.g. max bytes/chars) and similarly cap name length; if validation fails
return a 4xx response (e.g. 400) instead of storing; ensure you reference and
update _battle_arena_avatars only after passing these checks so oversized or
invalid payloads cannot exhaust memory.
| // postMessage 幁底(iframe 嵌入时由父页面推送) | ||
| const onMessage = (event) => { | ||
| const d = event.data | ||
| if (!d || d.type !== 'neko-avatar' || d.side !== 'left') return | ||
| if (d.dataUrl) { lastLeftUrl = d.dataUrl; setAvatarLeft(d.dataUrl) } | ||
| if (d.name) setNameLeft(d.name) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/BattleArena.jsx"
# show relevant region around the snippet lines
nl -ba "$file" | sed -n '420,520p'
# find onMessage definition and listener wiring
rg -n "const onMessage|addEventListener\\('message'|handleRematch|playerIdRef|fetch\\(|rematch" "$file"
# show handleRematch region
nl -ba "$file" | sed -n '520,700p'
# show matchmaking/polling region (best-effort around refs)
nl -ba "$file" | sed -n '300,430p'Repository: Project-N-E-K-O/N.E.K.O
Length of output: 110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/BattleArena.jsx"
# show relevant region around the snippet lines
nl -ba "$file" | sed -n '420,520p'
# find onMessage definition and listener wiring
rg -n "const onMessage|addEventListener\\('message'|handleRematch|playerIdRef|fetch\\(|neko-avatar|rematch" "$file"
# show handleRematch region
nl -ba "$file" | sed -n '520,700p'
# show matchmaking/polling region (best-effort around refs)
nl -ba "$file" | sed -n '300,430p'Repository: Project-N-E-K-O/N.E.K.O
Length of output: 110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/BattleArena.jsx"
sed -n '430,500p' "$file" | cat -n
echo "-----"
rg -n "const onMessage|addEventListener\\('message'|handleRematch|playerIdRef|fetch\\(|neko-avatar|rematch|setAvatarLeft|setNameLeft" "$file" || true
echo "-----"
sed -n '500,620p' "$file" | cat -n
echo "-----"
sed -n '250,430p' "$file" | cat -nRepository: Project-N-E-K-O/N.E.K.O
Length of output: 15918
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/BattleArena.jsx"
sed -n '430,500p' "$file" | cat -n
echo "-----"
rg -n "const onMessage|addEventListener\\('message'|handleRematch|playerIdRef|fetch\\(|neko-avatar|rematch|setAvatarLeft|setNameLeft" "$file" || true
echo "-----"
sed -n '500,620p' "$file" | cat -n
echo "-----"
sed -n '250,430p' "$file" | cat -nRepository: Project-N-E-K-O/N.E.K.O
Length of output: 15918
postMessage 需要校验来源并处理 rematch 期间轮询竞态喵。
battle-arena/src/components/BattleArena.jsx的onMessage只校验event.data(type === 'neko-avatar'且side === 'left'),未校验event.origin/event.source,任意可触达窗口都能伪造消息直接覆盖左侧头像和名字喵;把允许的origin收紧,并最好校验event.source是否为预期父窗口/iframecontentWindow再更新状态喵。pollOpponent在await fetch(/arena/status/${id})之后直接setOpponent/setMatchStatus,但handleRematch会清空playerIdRef.current并重新 join,且pollOpponent不检查rematching或“返回时的 id 是否仍为当前会话”,容易出现过期响应写入旧对手数据喵;返回后比对 id/会话 token,或用 AbortController 丢弃过期轮询响应喵。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/src/components/BattleArena.jsx` around lines 463 - 469, Validate
postMessage origin and source in onMessage: restrict event.origin to the allowed
parent origins and verify event.source matches the expected window (e.g., the
parent iframe contentWindow) before calling setAvatarLeft or setNameLeft; only
accept messages with type === 'neko-avatar' && side === 'left' after those
checks. For pollOpponent, prevent stale-rematch races by tying each fetch to the
current session (compare the returned id/session token to playerIdRef.current or
a rematch token) or use an AbortController per rematch to cancel previous
fetches; before calling setOpponent or setMatchStatus, verify the response
belongs to the current session (e.g., compare id !== playerIdRef.current or
check a rematching flag) and skip updates if it does not. Ensure references:
onMessage, setAvatarLeft, setNameLeft, pollOpponent, handleRematch,
playerIdRef.current, setOpponent, setMatchStatus are updated accordingly.
| const pollOpponent = useCallback(async () => { | ||
| const id = playerIdRef.current | ||
| if (!id) return | ||
| try { | ||
| const res = await fetch(`/arena/status/${id}`) | ||
| if (!res.ok) return | ||
| const { opponent: opp } = await res.json() | ||
| if (opp) { | ||
| setOpponent(opp) | ||
| setMatchStatus('matched') | ||
| clearInterval(pollTimerRef.current) | ||
| pollTimerRef.current = null | ||
| } | ||
| } catch { | ||
| // 匹配服务器未运行时静默容错 | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
重匹配时旧轮询结果可能把新对手覆盖掉喵。
pollOpponent 在发请求前先抓了一次 id,但请求回来后没有再确认这个 id 还是不是当前会话喵。用户点“测试重匹配”时,旧的 /arena/status/{oldId} 请求如果晚一点返回,就还能把 opponent 和 matchStatus 回写成旧数据喵。这里在 await 之后再补一层 playerIdRef.current === id 判断就稳很多喵。
Also applies to: 531-555
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/src/components/BattleArena.jsx` around lines 483 - 499, The
pollOpponent function captures playerIdRef.current into id before awaiting the
fetch, but never re-checks it after the async call; add a guard after the await
and after parsing the response (e.g., in pollOpponent and the analogous polling
function around lines 531-555) that returns early if playerIdRef.current !== id
so stale responses cannot overwrite state; keep using setOpponent,
setMatchStatus and clearing pollTimerRef.current only when the id still matches.
| const merged = { | ||
| id: `card-${cardIdCounter}`, | ||
| attr: mergedAttr, | ||
| power: mergedPower, | ||
| debuffs: [...new Set([...dragCard.debuffs, ...targetCard.debuffs])], | ||
| temp: true, // 合成卡为临时卡,打出后不进弃牌堆 | ||
| } |
There was a problem hiding this comment.
合成卡缺少基础字段,走“确认出牌”路径时会结算异常喵。
Line [973-979] 生成的合成卡没有 code/name/effects,而 Line [881-903] 的确认出牌会走 resolvePlayedCard(Line [811-853])按 effects 结算,所以合成卡在该路径下会出现“日志/效果/伤害不符合预期”的问题喵。给合成卡补齐最小可结算字段会更稳喵。
💡 建议修复
const merged = {
id: `card-${cardIdCounter}`,
+ code: `M${cardIdCounter}`,
+ name: '合成指令',
attr: mergedAttr,
+ comboAttr: mergedAttr,
power: mergedPower,
+ cost: Math.max(1, Math.ceil(mergedPower / 3)),
+ mainText: `对Boss造成${mergedPower}点伤害`,
+ comboText: '无',
+ effects: { main: { damage: mergedPower }, combo: {} },
debuffs: [...new Set([...dragCard.debuffs, ...targetCard.debuffs])],
temp: true, // 合成卡为临时卡,打出后不进弃牌堆
}Also applies to: 881-903, 811-853
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/src/components/CardGamePanel.jsx` around lines 973 - 979, The
merged temporary card created in the merged object (id `card-${cardIdCounter}`
built from mergedAttr/mergedPower and debuffs) is missing essential fields
`code`, `name`, and `effects`, causing resolvePlayedCard (used by the
confirm-play path) to misbehave; fix by populating the merged card with minimal
settlement-ready fields — e.g., set `code` and `name` (derived from
`dragCard`/`targetCard` or a deterministic composite string) and a valid
`effects` array (merge or synthesize the two cards' effects or provide an
empty/default effect structure compatible with resolvePlayedCard) so that
resolvePlayedCard and functions that read `effects` (called in the confirm-play
flow) can operate without errors.
| export function saveForgedBrawlCards(cards) { | ||
| if (typeof window === 'undefined') return | ||
| window.localStorage.setItem( | ||
| FORGED_BRAWL_CARDS_STORAGE_KEY, | ||
| JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean)) | ||
| ) |
There was a problem hiding this comment.
saveForgedBrawlCards 缺少写入异常兜底,存储失败会直接打断交互喵。
Line [125-128] 直接 setItem,在隐私模式、配额超限或存储被禁用时会抛异常,当前调用链会被同步打断喵。建议和读取路径一致加 try/catch 降级喵。
💡 建议修复
export function saveForgedBrawlCards(cards) {
if (typeof window === 'undefined') return
- window.localStorage.setItem(
- FORGED_BRAWL_CARDS_STORAGE_KEY,
- JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean))
- )
+ try {
+ window.localStorage.setItem(
+ FORGED_BRAWL_CARDS_STORAGE_KEY,
+ JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean))
+ )
+ } catch {
+ // 存储失败时静默降级,避免打断当前交互
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function saveForgedBrawlCards(cards) { | |
| if (typeof window === 'undefined') return | |
| window.localStorage.setItem( | |
| FORGED_BRAWL_CARDS_STORAGE_KEY, | |
| JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean)) | |
| ) | |
| export function saveForgedBrawlCards(cards) { | |
| if (typeof window === 'undefined') return | |
| try { | |
| window.localStorage.setItem( | |
| FORGED_BRAWL_CARDS_STORAGE_KEY, | |
| JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean)) | |
| ) | |
| } catch { | |
| // 存储失败时静默降级,避免打断当前交互 | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/src/data/forgedBrawlCards.js` around lines 123 - 128,
saveForgedBrawlCards currently calls window.localStorage.setItem directly and
can throw in private mode/quota-reached/disabled-storage; wrap the storage write
in a try/catch around the window.localStorage.setItem invocation (inside
saveForgedBrawlCards) and on failure swallow the error or log it (e.g.,
console.error) so the UI flow isn’t interrupted; keep the same normalization
pipeline using normalizeForgedBrawlCard and the FORGED_BRAWL_CARDS_STORAGE_KEY,
but ensure exceptions from setItem don’t propagate.
| allow: [ | ||
| __dirname, | ||
| 'F:/NEKO_bugfix/NekoBrawl/Gif_source', | ||
| ], |
There was a problem hiding this comment.
请移除硬编码的本机绝对路径,避免多人环境启动失败喵。
F:/NEKO_bugfix/NekoBrawl/Gif_source 是机器绑定路径,其他开发者环境基本不可复用,vite 本地启动流程会变得脆弱喵。建议改为环境变量驱动,并在未配置时回退到项目内路径或直接不注入该项喵。
可参考的修正示例喵
import { fileURLToPath } from 'node:url'
-import { dirname } from 'node:path'
+import { dirname, resolve } from 'node:path'
const __dirname = dirname(fileURLToPath(import.meta.url))
+const externalGifSource = process.env.NEKO_BRAWL_GIF_SOURCE
export default defineConfig({
plugins: [react()],
server: {
fs: {
allow: [
__dirname,
- 'F:/NEKO_bugfix/NekoBrawl/Gif_source',
+ ...(externalGifSource ? [resolve(externalGifSource)] : []),
],
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| allow: [ | |
| __dirname, | |
| 'F:/NEKO_bugfix/NekoBrawl/Gif_source', | |
| ], | |
| import { fileURLToPath } from 'node:url' | |
| import { dirname, resolve } from 'node:path' | |
| const __dirname = dirname(fileURLToPath(import.meta.url)) | |
| const externalGifSource = process.env.NEKO_BRAWL_GIF_SOURCE | |
| export default defineConfig({ | |
| plugins: [react()], | |
| server: { | |
| fs: { | |
| allow: [ | |
| __dirname, | |
| ...(externalGifSource ? [resolve(externalGifSource)] : []), | |
| ], | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/vite.config.js` around lines 12 - 15, 配置中的硬编码绝对路径
("F:/NEKO_bugfix/NekoBrawl/Gif_source")
会导致多人环境启动失败;将该硬编码项替换为由环境变量驱动并在未设置时回退或省略:在 vite.config.js 中找到 allow 数组(与
__dirname 同级),把硬编码字符串替换为基于 process.env(例如 process.env.EXTRA_ALLOW_DIR)的值,并在构建
allow 数组前做条件检查(如果变量存在则 push 进数组,否则使用项目内默认相对路径或不注入该项),确保路径使用
path.resolve/normalize 以兼容跨平台。
|
|
||
| > 铸造机 facts 接口依赖 `httpx`:若 `.venv` 中尚未安装,请在 N.E.K.O 根目录执行 `uv sync` 或 `uv pip install -r local_server/battle_arena_server/requirements.txt`。 | ||
|
|
There was a problem hiding this comment.
修一下 Markdown lint 警告避免文档检查噶掉喵。
这里有两个已命中的 lint 点:
- Line 18 的 blockquote 中间有空行(MD028);
- Line 84 的 fenced code block 缺少语言标记(MD040)。
这俩改动很小,但能减少文档流水线噪音,合并前顺手处理更稳喵。
Also applies to: 84-86
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 18-18: Blank line inside blockquote
(MD028, no-blanks-blockquote)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/README.md` around lines 18 - 20, Remove the
blank line inside the blockquote that currently contains "铸造机 facts 接口依赖
`httpx`:若 `.venv` 中尚未安装,请在 N.E.K.O 根目录执行 `uv sync` 或 `uv pip install -r
local_server/battle_arena_server/requirements.txt`" so the blockquote is a
single continuous paragraph (fixes MD028), and add a language specifier (e.g.,
```bash or ```text) to the fenced code block around the snippet at the later
section (lines referenced in the comment) to satisfy MD040; update the README.md
fenced block and the quoted paragraph accordingly.
| 猫娘大乱斗 — 本地对战匹配服务器 | ||
| 端口: 3001 | ||
| 启动: uvicorn server:app --host 0.0.0.0 --port 3001 --reload | ||
| 或直接: python server.py |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact content around the referenced file/lines
sed -n '1,120p' local_server/battle_arena_server/server.py
# Find any uvicorn command or host binding in the repo
rg -n --hidden --no-ignore-vcs -S "uvicorn\s+.*server:app|--host\s+0\.0\.0\.0|host\s*=\s*['\"]0\.0\.0\.0['\"]|0\.0\.0\.0\s*:\s*|0\.0\.0\.0" . || true
# Specifically check for "battle_arena" and port 3001 occurrences
rg -n --hidden --no-ignore-vcs -S "battle_arena|3001" . || trueRepository: Project-N-E-K-O/N.E.K.O
Length of output: 50379
默认监听地址请改为 127.0.0.1(别默认绑 0.0.0.0 暴露到局域网)喵。
local_server/battle_arena_server/server.py 里注释启动命令和启动代码都默认使用 --host 0.0.0.0 / host="0.0.0.0"(3001),建议默认改为 127.0.0.1,需要跨设备联调时再手动切到 0.0.0.0 喵。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/server.py` around lines 3 - 6, Update the
default bind address from 0.0.0.0 to 127.0.0.1 in both the startup examples and
the actual server run call: change the commented uvicorn CLI example "--host
0.0.0.0" to "--host 127.0.0.1" and change the uvicorn.run/app invocation that
uses host="0.0.0.0" to host="127.0.0.1" (leave a note that 0.0.0.0 can be used
for cross-device testing).
| # 等待匹配的玩家: player_id -> PlayerEntry dict | ||
| waiting_room: dict[str, dict] = {} | ||
| # 已匹配结果: player_id -> opponent snapshot dict | ||
| matched: dict[str, dict] = {} |
There was a problem hiding this comment.
当前匹配表结构没法在 leave 时把对端一起清掉喵。
matched 里只存了“对手快照”,没有存真实的对端 player_id 喵。这样 A/B 成功匹配后,如果 A 调了 leave,当前实现只能删掉 matched[A],却没法准确删掉 matched[B] 喵,B 后续轮询到的就会一直是一个已经离场的旧对手喵。这里需要把配对关系单独存起来,或者至少在匹配结果里把对端 id 一起记住喵。
Also applies to: 92-101, 318-323
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/server.py` around lines 65 - 68, The matched
dict currently stores only the opponent snapshot (matched: dict[str, dict])
which prevents removing both sides on leave; change the pairing data to include
the opponent's player_id (e.g., store matched[player_id] = {"opponent_id":
other_id, "opponent_snapshot": {...}}) OR add a separate pair map (e.g., pairs:
dict[str, str]) that records player_id -> opponent_id when a match is created
(adjust the matching code where matched is set and the leave handler / polling
cleanup to remove both matched[player_id] and matched[opponent_id] using the
stored opponent_id); update any places that read matched (match-making code,
leave function, polling/cleanup logic) to use the new structure so both sides
are cleared reliably.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
battle-arena/src/data/forgedBrawlCards.js (1)
196-201:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
saveForgedBrawlCards这里缺少写入异常兜底,会中断交互喵。Line [198-201] 直接调用
window.localStorage.setItem(...),在隐私模式、配额超限或存储被禁用时会抛异常,调用链会被同步打断喵。建议和读取路径保持一致加try/catch做降级喵。💡 建议修复
export function saveForgedBrawlCards(cards) { if (typeof window === 'undefined') return - window.localStorage.setItem( - FORGED_BRAWL_CARDS_STORAGE_KEY, - JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean)) - ) + try { + window.localStorage.setItem( + FORGED_BRAWL_CARDS_STORAGE_KEY, + JSON.stringify((Array.isArray(cards) ? cards : []).map(normalizeForgedBrawlCard).filter(Boolean)) + ) + } catch { + // 存储失败时静默降级,避免打断当前交互 + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@battle-arena/src/data/forgedBrawlCards.js` around lines 196 - 201, The saveForgedBrawlCards function currently calls window.localStorage.setItem(...) directly which can throw in private mode, quota-exceeded, or when storage is disabled; wrap the setItem call in a try/catch that mirrors the read path behavior so errors are swallowed or logged but do not propagate and break the UI, and still write the normalized array produced by Array.isArray(cards) ? cards : [] mapped through normalizeForgedBrawlCard and filtered; reference saveForgedBrawlCards, FORGED_BRAWL_CARDS_STORAGE_KEY and normalizeForgedBrawlCard when making the change.
🧹 Nitpick comments (6)
local_server/battle_arena_server/forge_story_generator.py (3)
211-221: 💤 Low value双重超时控制:
async with llm+asyncio.wait_for可能冗余喵。
create_chat_llm已经传入了timeout=FORGE_STORY_TIMEOUT_SECONDS,外层再包一层asyncio.wait_for同样超时是重复的喵。不过作为防御性编程,保留也无害喵~🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@local_server/battle_arena_server/forge_story_generator.py` around lines 211 - 221, The code applies two timeouts: a timeout passed into create_chat_llm and an outer asyncio.wait_for around llm.ainvoke inside the async with llm block; remove the redundancy by choosing one timeout mechanism—either remove the outer asyncio.wait_for wrapping llm.ainvoke (keep the timeout in create_chat_llm) or remove the timeout argument supplied to create_chat_llm and rely on asyncio.wait_for—but do not keep both. Update the call sites referencing llm, llm.ainvoke, asyncio.wait_for, create_chat_llm, and the FORGE_STORY_TIMEOUT_SECONDS constant accordingly so only a single timeout governs the operation.
175-178: ⚡ Quick win相对导入依赖
sys.path操作,建议添加注释说明喵。
from active_neko_context import ...能工作是因为 server.py 把SERVER_ROOT加到了sys.path,但如果这个模块被其他地方直接导入就会ModuleNotFoundError喵。♻️ 建议添加注释或改用显式路径
async def generate_forge_card_story(payload: dict[str, Any]) -> ForgeStoryResult: """Generate a Forged card story through NEKO's configured core LLM client. The active NEKO catgirl is authoritative: forge stories are written from the memory context of the catgirl who invited the player, not from a UI display name supplied by the battle page. """ - + # NOTE: active_neko_context is a sibling module; requires SERVER_ROOT in sys.path + # (set by server.py at startup) from active_neko_context import resolve_active_neko_context🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@local_server/battle_arena_server/forge_story_generator.py` around lines 175 - 178, The import of resolve_active_neko_context from active_neko_context relies on server.py mutating sys.path (SERVER_ROOT) and will cause ModuleNotFoundError when this module is imported elsewhere; either replace the top-level import with a package/relative import (e.g., change "from active_neko_context import ..." to a proper relative/package import matching the package layout) so it works without sys.path hacks, or keep the current import but add a clear comment above it referencing server.py, SERVER_ROOT and why sys.path is being modified so future readers know the dependency; update imports in forge_story_generator.py (the active_neko_context import line) and ensure any tests or callers import using the new package path.
52-65: 💤 Low valuemojibake 修复阈值
suspicious < 3可能误判正常中文喵。像
æ、ç、è这些字符在正常法语/德语文本中也会出现喵。不过考虑到这是卡牌故事生成场景,主要是中文,这个启发式应该问题不大喵~🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@local_server/battle_arena_server/forge_story_generator.py` around lines 52 - 65, The current heuristic in _repair_utf8_mojibake uses suspicious < 3 and can mis-trigger on valid Western texts; change the decision logic to be more robust: keep counting suspicious chars into the suspicious variable but only attempt the latin1->utf-8 repair when either (a) suspicious is >= 3 AND the string contains no CJK characters (check for any char in the CJK unicode ranges), or (b) the string contains strong mojibake markers like "ï¼" or "ã€" regardless of CJK presence; update the conditional that uses suspicious to reflect this combined rule so normal French/German accented text (and texts containing CJK) won’t be incorrectly re-decoded.local_server/battle_arena_server/active_neko_context.py (1)
48-55: 💤 Low value硬编码的 tuple 索引
character_data[5]有点脆弱喵~这里假设
get_character_data()返回的 tuple 第 6 个元素是 prompt_map,但如果上游 API 结构变化就会出问题喵。不过考虑到这是内部 API 且有 try/except 兜底,暂时可以接受喵。♻️ 可选:添加注释说明预期结构
def _resolve_prompt(config_manager: Any, lanlan_name: str, master_name: str) -> str: try: + # get_character_data() returns: (master_name, her_name, master_basic_config, + # lanlan_basic_config, name_mapping, lanlan_prompt, ...) character_data = config_manager.get_character_data() prompt_map = character_data[5] if len(character_data) > 5 and isinstance(character_data[5], dict) else {}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@local_server/battle_arena_server/active_neko_context.py` around lines 48 - 55, The function _resolve_prompt uses a hard-coded tuple index character_data[5] to extract the prompt_map from config_manager.get_character_data(), which is brittle; update the code to safely access the prompt map by checking for a well-known key or documented structure (e.g., prefer a dict return or look for a 'prompt_map' key), e.g., call get_character_data(), if it's a dict use character_data.get('prompt_map') else if it's a sequence check length and type before using index 5, and add a short comment in _resolve_prompt explaining the expected shape; ensure prompt_map variable is always a dict (fallback to {}) and keep the existing replacements and exception-safe behavior.battle-arena/src/components/neko-brawl/CardInspectModal.jsx (1)
42-57: ⚡ Quick win给鉴赏弹层补齐对话框语义与可访问名称喵。
Line 42 到 Line 57 的模态容器建议加
role="dialog"、aria-modal、aria-labelledby,Line 90 到 Line 97 的图标按钮建议补aria-label,这样读屏器定位会更稳定喵。🧩 建议改动喵
{open && card && ( <motion.div key="card-inspect-modal" + role="dialog" + aria-modal="true" + aria-labelledby="card-inspect-title" className="fixed inset-0 z-[260] flex items-center justify-center overflow-y-auto bg-black/45 px-4 py-5" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} @@ - <h2 className="mt-1 text-2xl font-black">{card.name || '未知卡牌'}</h2> + <h2 id="card-inspect-title" className="mt-1 text-2xl font-black">{card.name || '未知卡牌'}</h2> </div> <button type="button" onClick={onClose} className="flex h-10 w-10 shrink-0 items-center justify-center border-2 border-neutral-950 bg-white hover:bg-neutral-100" title="关闭" + aria-label="关闭卡牌鉴赏" >Also applies to: 88-97
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@battle-arena/src/components/neko-brawl/CardInspectModal.jsx` around lines 42 - 57, The modal wrapper in CardInspectModal.jsx should be given proper dialog semantics: add role="dialog" and aria-modal="true" on the outer motion.div (key="card-inspect-modal") and add aria-labelledby pointing to the modal title's id (create a stable id on the heading inside the motion.article, e.g., id="card-inspect-title"); ensure the motion.article keeps its onClick stopPropagation; additionally, add descriptive aria-label attributes to the icon button(s) referenced around lines 90-97 (use unique, human-readable labels like "Close card view" or "Zoom image" as appropriate) so screen readers can identify the controls.battle-arena/src/components/neko-brawl/BattleTutorialPanel.jsx (1)
47-83: ⚡ Quick win补齐教程弹层的无障碍语义和键盘关闭能力喵。
Line 51 到 Line 83 当前主要依赖鼠标关闭,读屏和键盘用户体验会打折喵。建议给弹层补
role="dialog"、aria-modal、aria-labelledby,并支持Escape关闭,同时给图标关闭按钮补aria-label喵。✨ 建议改动(最小可用)喵
+import { useEffect } from 'react' import { BookOpen, CheckCircle2, MousePointer2, Sparkles, Swords, X, Zap } from 'lucide-react' import { AnimatePresence, motion } from 'framer-motion' export default function BattleTutorialPanel({ open, onClose }) { + useEffect(() => { + if (!open) return undefined + const handleKeyDown = (event) => { + if (event.key === 'Escape') onClose?.() + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [open, onClose]) + return ( <AnimatePresence> {open && ( <motion.div key="battle-tutorial-panel" + role="dialog" + aria-modal="true" + aria-labelledby="battle-tutorial-title" className="absolute inset-0 z-[245] bg-black/20" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} @@ - <h2 className="text-lg font-black">战斗教程</h2> + <h2 id="battle-tutorial-title" className="text-lg font-black">战斗教程</h2> </div> @@ <button type="button" onClick={onClose} className="flex h-9 w-9 items-center justify-center border-2 border-neutral-900 bg-white hover:bg-neutral-100" title="关闭教程" + aria-label="关闭教程" >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@battle-arena/src/components/neko-brawl/BattleTutorialPanel.jsx` around lines 47 - 83, The panel lacks accessibility semantics and keyboard close support: update the BattleTutorialPanel render so the overlay/motion.div and the sliding panel/motion.aside include role="dialog", aria-modal="true", and set aria-labelledby to the header's id (add an id on the <h2> used as the title). Add keyboard handling on the panel (e.g., an onKeyDown on motion.aside or a useEffect that attaches a keydown listener) to call the existing onClose when Escape is pressed, and ensure the close <button> (the X icon) has an aria-label like "关闭教程"; also move focus into the panel when opened (focus the close button or panel container) and keep the existing stopPropagation onClick to avoid closing when interacting inside.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@battle-arena/src/components/neko-brawl/DeckBuilderPanel.jsx`:
- Around line 261-267: The code updates slot.deck when removing a Forged card
but the UI reads slot.cards, causing stale data; in the block that reads
DECK_LIBRARY_STORAGE_KEY and builds nextLibrary, replace the slot.deck
transformation with slot.cards (i.e., map each slot to preserve other fields and
set cards: Array.isArray(slot.cards) ? slot.cards.filter(code => code !==
card.code) : []), then write nextLibrary back to localStorage as before so
stored slots and UI are consistent.
In `@battle-arena/src/components/neko-brawl/nekoBrawlAudio.js`:
- Around line 129-137: fadeIn can throw if module-level currentBgm is nulled by
stopNekoBrawlBgm before the play().then callback runs; add a defensive null
check at the top of fadeIn (return early if audio is falsy) and inside the
play().then callback capture currentBgm into a local snapshot (e.g. const bgm =
currentBgm) and verify bgm is not null and not paused before calling fadeIn(bgm,
targetVolume) so you never call fadeIn with a null audio reference; reference
functions: fadeIn, currentBgm, stopNekoBrawlBgm, and the play().then callback.
In `@local_server/battle_arena_server/server.py`:
- Around line 379-382: Replace the string-based exception check with a proper
type check: import or otherwise make the ForgeStoryGenerationError type
available to the except block and use isinstance(exc, ForgeStoryGenerationError)
instead of comparing exc.__class__.__name__; update the except block around the
logger call (the variable exc and logger.warn/logging line) so that
non-ForgeStoryGenerationError exceptions are detected via isinstance; if the
class is currently imported inside the try, move that import to module scope or
perform a local import inside the except before the isinstance check to avoid
NameError.
---
Duplicate comments:
In `@battle-arena/src/data/forgedBrawlCards.js`:
- Around line 196-201: The saveForgedBrawlCards function currently calls
window.localStorage.setItem(...) directly which can throw in private mode,
quota-exceeded, or when storage is disabled; wrap the setItem call in a
try/catch that mirrors the read path behavior so errors are swallowed or logged
but do not propagate and break the UI, and still write the normalized array
produced by Array.isArray(cards) ? cards : [] mapped through
normalizeForgedBrawlCard and filtered; reference saveForgedBrawlCards,
FORGED_BRAWL_CARDS_STORAGE_KEY and normalizeForgedBrawlCard when making the
change.
---
Nitpick comments:
In `@battle-arena/src/components/neko-brawl/BattleTutorialPanel.jsx`:
- Around line 47-83: The panel lacks accessibility semantics and keyboard close
support: update the BattleTutorialPanel render so the overlay/motion.div and the
sliding panel/motion.aside include role="dialog", aria-modal="true", and set
aria-labelledby to the header's id (add an id on the <h2> used as the title).
Add keyboard handling on the panel (e.g., an onKeyDown on motion.aside or a
useEffect that attaches a keydown listener) to call the existing onClose when
Escape is pressed, and ensure the close <button> (the X icon) has an aria-label
like "关闭教程"; also move focus into the panel when opened (focus the close button
or panel container) and keep the existing stopPropagation onClick to avoid
closing when interacting inside.
In `@battle-arena/src/components/neko-brawl/CardInspectModal.jsx`:
- Around line 42-57: The modal wrapper in CardInspectModal.jsx should be given
proper dialog semantics: add role="dialog" and aria-modal="true" on the outer
motion.div (key="card-inspect-modal") and add aria-labelledby pointing to the
modal title's id (create a stable id on the heading inside the motion.article,
e.g., id="card-inspect-title"); ensure the motion.article keeps its onClick
stopPropagation; additionally, add descriptive aria-label attributes to the icon
button(s) referenced around lines 90-97 (use unique, human-readable labels like
"Close card view" or "Zoom image" as appropriate) so screen readers can identify
the controls.
In `@local_server/battle_arena_server/active_neko_context.py`:
- Around line 48-55: The function _resolve_prompt uses a hard-coded tuple index
character_data[5] to extract the prompt_map from
config_manager.get_character_data(), which is brittle; update the code to safely
access the prompt map by checking for a well-known key or documented structure
(e.g., prefer a dict return or look for a 'prompt_map' key), e.g., call
get_character_data(), if it's a dict use character_data.get('prompt_map') else
if it's a sequence check length and type before using index 5, and add a short
comment in _resolve_prompt explaining the expected shape; ensure prompt_map
variable is always a dict (fallback to {}) and keep the existing replacements
and exception-safe behavior.
In `@local_server/battle_arena_server/forge_story_generator.py`:
- Around line 211-221: The code applies two timeouts: a timeout passed into
create_chat_llm and an outer asyncio.wait_for around llm.ainvoke inside the
async with llm block; remove the redundancy by choosing one timeout
mechanism—either remove the outer asyncio.wait_for wrapping llm.ainvoke (keep
the timeout in create_chat_llm) or remove the timeout argument supplied to
create_chat_llm and rely on asyncio.wait_for—but do not keep both. Update the
call sites referencing llm, llm.ainvoke, asyncio.wait_for, create_chat_llm, and
the FORGE_STORY_TIMEOUT_SECONDS constant accordingly so only a single timeout
governs the operation.
- Around line 175-178: The import of resolve_active_neko_context from
active_neko_context relies on server.py mutating sys.path (SERVER_ROOT) and will
cause ModuleNotFoundError when this module is imported elsewhere; either replace
the top-level import with a package/relative import (e.g., change "from
active_neko_context import ..." to a proper relative/package import matching the
package layout) so it works without sys.path hacks, or keep the current import
but add a clear comment above it referencing server.py, SERVER_ROOT and why
sys.path is being modified so future readers know the dependency; update imports
in forge_story_generator.py (the active_neko_context import line) and ensure any
tests or callers import using the new package path.
- Around line 52-65: The current heuristic in _repair_utf8_mojibake uses
suspicious < 3 and can mis-trigger on valid Western texts; change the decision
logic to be more robust: keep counting suspicious chars into the suspicious
variable but only attempt the latin1->utf-8 repair when either (a) suspicious is
>= 3 AND the string contains no CJK characters (check for any char in the CJK
unicode ranges), or (b) the string contains strong mojibake markers like "ï¼" or
"ã€" regardless of CJK presence; update the conditional that uses suspicious to
reflect this combined rule so normal French/German accented text (and texts
containing CJK) won’t be incorrectly re-decoded.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 67be0254-b69c-477b-9faf-7ab099c3d7b3
⛔ Files ignored due to path filters (17)
battle-arena/public/neko-brawl/Background_forest.pngis excluded by!**/*.pngbattle-arena/public/neko-brawl/Boss_WeakDamageTaken_transparent.pngis excluded by!**/*.pngbattle-arena/public/neko-brawl/Boss_attack_transparent.pngis excluded by!**/*.pngbattle-arena/public/neko-brawl/Boss_damagetaken_transparent.pngis excluded by!**/*.pngbattle-arena/public/neko-brawl/Boss_normal_transparent.pngis excluded by!**/*.pngbattle-arena/public/neko-brawl/audio/bgm_answer_quickly.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/bgm_battle_loop.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/bgm_deck_builder_loop.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/bgm_deck_library_loop.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/bgm_home_brightlands_night.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/bgm_home_loop.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/sfx_card_attack.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/sfx_card_combo.wavis excluded by!**/*.wavbattle-arena/public/neko-brawl/audio/sfx_card_draw.wavis excluded by!**/*.wavbattle-arena/public/neko-brawl/audio/sfx_card_heal.mp3is excluded by!**/*.mp3battle-arena/public/neko-brawl/audio/sfx_card_shield.wavis excluded by!**/*.wavbattle-arena/public/neko-brawl/audio/sfx_card_support.wavis excluded by!**/*.wav
📒 Files selected for processing (23)
.gitignoreapp/main_server.pybattle-arena/index.htmlbattle-arena/package.jsonbattle-arena/public/neko-brawl/README.mdbattle-arena/src/components/BattleArena.jsxbattle-arena/src/components/CardGamePanel.jsxbattle-arena/src/components/neko-brawl/BattleResultOverlay.jsxbattle-arena/src/components/neko-brawl/BattleTutorialPanel.jsxbattle-arena/src/components/neko-brawl/CardInspectModal.jsxbattle-arena/src/components/neko-brawl/DeckBuilderPanel.jsxbattle-arena/src/components/neko-brawl/DeckBuilderTutorialPanel.jsxbattle-arena/src/components/neko-brawl/DeckLibraryPanel.jsxbattle-arena/src/components/neko-brawl/NekoCardBack.jsxbattle-arena/src/components/neko-brawl/NewBattleDuelUI.jsxbattle-arena/src/components/neko-brawl/README.mdbattle-arena/src/components/neko-brawl/nekoBrawlAudio.jsbattle-arena/src/data/forgedBrawlCards.jsbattle-arena/tailwind.config.jslocal_server/battle_arena_server/README.mdlocal_server/battle_arena_server/active_neko_context.pylocal_server/battle_arena_server/forge_story_generator.pylocal_server/battle_arena_server/server.py
💤 Files with no reviewable changes (1)
- battle-arena/index.html
✅ Files skipped from review due to trivial changes (5)
- battle-arena/src/components/neko-brawl/README.md
- battle-arena/public/neko-brawl/README.md
- .gitignore
- battle-arena/package.json
- local_server/battle_arena_server/README.md
| const library = JSON.parse(window.localStorage.getItem(DECK_LIBRARY_STORAGE_KEY) || '[]') | ||
| if (Array.isArray(library)) { | ||
| const nextLibrary = library.map(slot => ({ | ||
| ...slot, | ||
| deck: Array.isArray(slot.deck) ? slot.deck.filter(code => code !== card.code) : [], | ||
| })) | ||
| window.localStorage.setItem(DECK_LIBRARY_STORAGE_KEY, JSON.stringify(nextLibrary)) |
There was a problem hiding this comment.
删除 Forged 卡时写错了仓库槽位字段,cards 没被清理到喵。
Line [261-267] 现在在更新 DECK_LIBRARY_STORAGE_KEY 时操作的是 slot.deck,但仓库面板读取的是 slot.cards。这样会导致“删除后仓库仍保留该卡码”的数据不一致喵。
💡 建议修复
try {
const library = JSON.parse(window.localStorage.getItem(DECK_LIBRARY_STORAGE_KEY) || '[]')
if (Array.isArray(library)) {
const nextLibrary = library.map(slot => ({
...slot,
- deck: Array.isArray(slot.deck) ? slot.deck.filter(code => code !== card.code) : [],
+ cards: (Array.isArray(slot.cards) ? slot.cards : Array.isArray(slot.deck) ? slot.deck : [])
+ .filter(code => code !== card.code),
}))
window.localStorage.setItem(DECK_LIBRARY_STORAGE_KEY, JSON.stringify(nextLibrary))
}
} catch {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/src/components/neko-brawl/DeckBuilderPanel.jsx` around lines 261
- 267, The code updates slot.deck when removing a Forged card but the UI reads
slot.cards, causing stale data; in the block that reads DECK_LIBRARY_STORAGE_KEY
and builds nextLibrary, replace the slot.deck transformation with slot.cards
(i.e., map each slot to preserve other fields and set cards:
Array.isArray(slot.cards) ? slot.cards.filter(code => code !== card.code) : []),
then write nextLibrary back to localStorage as before so stored slots and UI are
consistent.
| function fadeIn(audio, targetVolume) { | ||
| audio.volume = 0 | ||
| const timer = window.setInterval(() => { | ||
| if (!audio || audio.paused) { | ||
| window.clearInterval(timer) | ||
| return | ||
| } | ||
| audio.volume = Math.min(targetVolume, audio.volume + BGM_FADE_STEP) | ||
| if (audio.volume >= targetVolume) window.clearInterval(timer) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1) 命中同源恢复分支里对可变 currentBgm 的直接闭包引用
rg -n -C2 'currentBgm\.play\(\)|fadeIn\(currentBgm' battle-arena/src/components/neko-brawl/nekoBrawlAudio.js
# 2) 命中 fadeIn 入口是否存在空值保护
rg -n -C2 'function fadeIn|audio\.volume = 0' battle-arena/src/components/neko-brawl/nekoBrawlAudio.jsRepository: Project-N-E-K-O/N.E.K.O
Length of output: 679
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/neko-brawl/nekoBrawlAudio.js"
echo "---- fadeIn snippet ----"
nl -ba "$file" | sed -n '110,160p'
echo "---- same-origin recovery snippet ----"
nl -ba "$file" | sed -n '140,220p'Repository: Project-N-E-K-O/N.E.K.O
Length of output: 153
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/neko-brawl/nekoBrawlAudio.js"
echo "---- stopNekoBrawlBgm snippet ----"
nl -ba "$file" | sed -n '1,120p'
rg -n "function stopNekoBrawlBgm|stopNekoBrawlBgm" "$file" -nRepository: Project-N-E-K-O/N.E.K.O
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/neko-brawl/nekoBrawlAudio.js"
echo "---- fadeIn snippet (lines 110-170) ----"
sed -n '110,170p' "$file" | cat -n -w3 -s' '
echo "---- same-origin recovery snippet (lines 140-210) ----"
sed -n '140,210p' "$file" | cat -n -w3 -s' '
echo "---- stopNekoBrawlBgm snippet (first 220 lines) ----"
sed -n '1,220p' "$file" | cat -n -w3 -s' ' | sed -n '1,220p'Repository: Project-N-E-K-O/N.E.K.O
Length of output: 192
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="battle-arena/src/components/neko-brawl/nekoBrawlAudio.js"
python3 - <<'PY'
import itertools,sys
path="battle-arena/src/components/neko-brawl/nekoBrawlAudio.js"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
def show(a,b):
for i in range(a,b+1):
if 1 <= i <= len(lines):
print(f"{i:4d} {lines[i-1].rstrip()}")
print("---- fadeIn snippet (120-170) ----")
show(120,170)
print("\n---- same-origin recovery snippet (140-210) ----")
show(140,210)
print("\n---- stopNekoBrawlBgm snippet (1-260, stop function area) ----")
show(1,260)
PYRepository: Project-N-E-K-O/N.E.K.O
Length of output: 12119
修复 currentBgm 在 play().then 回调里的竞态空引用崩溃喵!
同源恢复分支里的 then(() => fadeIn(currentBgm, targetVolume)) 会直接捕获模块级可变 currentBgm;而 stopNekoBrawlBgm() 会把 currentBgm 置为 null,从而在 fadeIn() 入口 audio.volume = 0 触发运行时异常喵。
给 fadeIn 加空值保护,并在 then 回调里使用 currentBgm 局部快照 + 一致性检查后再调用 fadeIn 喵。
🐾 建议修复喵
function fadeIn(audio, targetVolume) {
+ if (!audio) return
audio.volume = 0
const timer = window.setInterval(() => {
if (!audio || audio.paused) {
window.clearInterval(timer)
return
}
audio.volume = Math.min(targetVolume, audio.volume + BGM_FADE_STEP)
if (audio.volume >= targetVolume) window.clearInterval(timer)
}, BGM_FADE_STEP_MS)
}
if (currentBgm && currentBgmSrc === src) {
currentBgmVolume = targetVolume
if (!currentBgm.paused) {
currentBgm.volume = targetVolume
return
}
- currentBgm.play()
- .then(() => fadeIn(currentBgm, targetVolume))
+ const resumedBgm = currentBgm
+ resumedBgm.play()
+ .then(() => {
+ if (currentBgm !== resumedBgm) return
+ fadeIn(resumedBgm, targetVolume)
+ })
.catch(() => armBgmUnlockRetry(src, options))
return
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@battle-arena/src/components/neko-brawl/nekoBrawlAudio.js` around lines 129 -
137, fadeIn can throw if module-level currentBgm is nulled by stopNekoBrawlBgm
before the play().then callback runs; add a defensive null check at the top of
fadeIn (return early if audio is falsy) and inside the play().then callback
capture currentBgm into a local snapshot (e.g. const bgm = currentBgm) and
verify bgm is not null and not paused before calling fadeIn(bgm, targetVolume)
so you never call fadeIn with a null audio reference; reference functions:
fadeIn, currentBgm, stopNekoBrawlBgm, and the play().then callback.
| except Exception as exc: | ||
| error = str(exc) or type(exc).__name__ | ||
| if exc.__class__.__name__ != "ForgeStoryGenerationError": | ||
| logger.warning("forge-card-story: generation failed: %s", error) |
There was a problem hiding this comment.
用字符串比较类名来判断异常类型不太可靠喵~
如果 ForgeStoryGenerationError 被重命名或者有继承关系,这个检查就会失效喵。建议直接 import 后用 isinstance 检查喵。
🐛 建议修复
`@app.post`("/arena/forge-card-story")
async def arena_forge_card_story(body: dict[str, Any]):
"""用 NEKO 核心 LLM 配置把故事引子生成 Forged 卡牌专属小故事。"""
try:
from forge_story_generator import ForgeStoryGenerationError, generate_forge_card_story
result = await generate_forge_card_story(body if isinstance(body, dict) else {})
return JSONResponse(
{
"success": True,
"story": result.story,
"storyGenerationStatus": "ready",
"provider": result.provider,
"model": result.model,
"sourceFactId": result.source_fact_id,
}
)
except Exception as exc:
error = str(exc) or type(exc).__name__
- if exc.__class__.__name__ != "ForgeStoryGenerationError":
+ if not isinstance(exc, ForgeStoryGenerationError):
logger.warning("forge-card-story: generation failed: %s", error)
return JSONResponse(
{
"success": False,
"storyGenerationStatus": "failed",
"error": error,
}
)不过要注意,ForgeStoryGenerationError 的 import 在 try 块内部,需要把它移到外面或者在 except 块里重新 import 喵。
♻️ 完整修复方案
`@app.post`("/arena/forge-card-story")
async def arena_forge_card_story(body: dict[str, Any]):
"""用 NEKO 核心 LLM 配置把故事引子生成 Forged 卡牌专属小故事。"""
+ from forge_story_generator import ForgeStoryGenerationError, generate_forge_card_story
+
try:
- from forge_story_generator import ForgeStoryGenerationError, generate_forge_card_story
-
result = await generate_forge_card_story(body if isinstance(body, dict) else {})
return JSONResponse(
{
"success": True,
"story": result.story,
"storyGenerationStatus": "ready",
"provider": result.provider,
"model": result.model,
"sourceFactId": result.source_fact_id,
}
)
except Exception as exc:
error = str(exc) or type(exc).__name__
- if exc.__class__.__name__ != "ForgeStoryGenerationError":
+ if not isinstance(exc, ForgeStoryGenerationError):
logger.warning("forge-card-story: generation failed: %s", error)
return JSONResponse(
{
"success": False,
"storyGenerationStatus": "failed",
"error": error,
}
)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/server.py` around lines 379 - 382, Replace
the string-based exception check with a proper type check: import or otherwise
make the ForgeStoryGenerationError type available to the except block and use
isinstance(exc, ForgeStoryGenerationError) instead of comparing
exc.__class__.__name__; update the except block around the logger call (the
variable exc and logger.warn/logging line) so that non-ForgeStoryGenerationError
exceptions are detected via isinstance; if the class is currently imported
inside the try, move that import to module scope or perform a local import
inside the except before the isinstance check to avoid NameError.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@local_server/battle_arena_server/forge_story_generator.py`:
- Line 151: Replace the literal placeholder `{猫娘名}` in the prompt string (the
rule that currently reads "6. 大部分叙事内容使用第三人称,例如“她”“猫娘”“{猫娘名}”,不要全篇用第一人称。") with a
concrete example name so the model sees an actual example (e.g., use "小喵" or
"露露" in place of `{猫娘名}`); locate this string in forge_story_generator.py (the
prompt/rules list or function that builds the story prompt) and update the text
to include the real example name while preserving the rest of the sentence and
punctuation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fd921b8c-d78e-4b84-90ab-df5c75bba2e3
📒 Files selected for processing (1)
local_server/battle_arena_server/forge_story_generator.py
| "3. 保留故事引子的情绪基调和关系,不要改成相反含义。", | ||
| "4. 不要编造新的现实履历、地点、人物关系或长期承诺。", | ||
| "5. 可以把卡牌效果轻微融入动作、气氛或战斗画面。", | ||
| "6. 大部分叙事内容使用第三人称,例如“她”“猫娘”“{猫娘名}”,不要全篇用第一人称。", |
There was a problem hiding this comment.
把 {猫娘名} 改成真实示例名喵。
这里现在是普通字符串,模型实际看到的会是字面量 {猫娘名},不是当前猫娘名本身,提示词示例会失真,甚至可能把占位符学进生成结果里喵。
可直接这样改喵
- "6. 大部分叙事内容使用第三人称,例如“她”“猫娘”“{猫娘名}”,不要全篇用第一人称。",
+ f"6. 大部分叙事内容使用第三人称,例如“她”“猫娘”“{lanlan_name or '当前猫娘'}”,不要全篇用第一人称。",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/forge_story_generator.py` at line 151,
Replace the literal placeholder `{猫娘名}` in the prompt string (the rule that
currently reads "6. 大部分叙事内容使用第三人称,例如“她”“猫娘”“{猫娘名}”,不要全篇用第一人称。") with a concrete
example name so the model sees an actual example (e.g., use "小喵" or "露露" in
place of `{猫娘名}`); locate this string in forge_story_generator.py (the
prompt/rules list or function that builds the story prompt) and update the text
to include the real example name while preserving the rest of the sentence and
punctuation.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
local_server/battle_arena_server/server.py (1)
588-629:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win统一使用裁剪后的
runtime_character_hint喵这里前面已经把 query trim 成
runtime_hint了,但 Line 620 和 Line 629 又回头用了原始runtime_character_hint;只要请求里带了首尾空格,就会出现“前置校验通过,但后续解析/回包元数据显示未绑定”的假阴性喵。💡 建议修一下喵
- context = await _resolve_active_facts_context(character, runtime_character_hint) + context = await _resolve_active_facts_context(character, runtime_hint or None) @@ - runtime_hint_used = bool(runtime_character_hint and runtime_character_hint == resolved_character) + runtime_hint_used = bool(runtime_hint and runtime_hint == resolved_character)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@local_server/battle_arena_server/server.py` around lines 588 - 629, 在前面已经把请求的 runtime_character_hint 裁剪并赋给 runtime_hint 后,所有后续对比/传入的地方应统一使用 runtime_hint;具体把对 _resolve_active_facts_context 的调用参数从 runtime_character_hint 改为 runtime_hint,并把 runtime_hint_used 的计算改为使用 runtime_hint(而不是原始 runtime_character_hint),以避免因首尾空格导致的假阴性;检查 resolved_character、override_ignored 等基于该比对逻辑的变量也使用 runtime_hint 一致性比较。app/main_server.py (1)
1369-1376:⚠️ Potential issue | 🟠 Major | ⚡ Quick win收窄根
app的通配 CORS,并给头像同步端点补上输入边界喵
app/main_server.py在根app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, ...)上做了全局通配,等于把跨域边界扩散到主站所有接口,且这种“通配 + 凭据”组合在 FastAPI/Starlette 的语义上不安全/不推荐;建议仅对桌面端真实会用到的http://localhost:<port>(精确到端口)白名单开,并按需拆到专用路由/子应用喵。
另外/battle-arena/avatar(1611-1619)把side/dataUrl/name直接写入_battle_arena_avatars(无side白名单、无长度/大小限制、无条目上限/过期),容易被滥用造成内存膨胀;建议限制side(如仅left/right)、限制name/dataUrl长度(并可加条目上限与过期机制)喵。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/main_server.py` around lines 1369 - 1376, Current code registers a global permissive CORS via app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, ...) which should be narrowed and the avatar endpoint (/battle-arena/avatar) writes unvalidated side/dataUrl/name into _battle_arena_avatars; change the CORS registration to only allow precise origins (e.g. http://localhost:<port> values) and apply it to a dedicated sub-application or router used by the battle-arena frontend instead of the root app (see app.add_middleware and CORSMiddleware), and harden the avatar handler that mutates _battle_arena_avatars by: validate side against a whitelist (e.g. "left"/"right"), enforce max lengths for name and dataUrl and a max decoded image size, reject or trim overlong input, and implement entry limits and expiry (or LRU) for _battle_arena_avatars to prevent unbounded memory growth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@local_server/battle_arena_server/active_neko_context.py`:
- Line 80: The current assignment of source ("runtime-character-hint" if
runtime_hint else "neko-config") mislabels cases where debug_override is used
but runtime_hint is empty; when debug_override is present you must set source to
"debug-override" so diagnostics reflect the actual origin of facts_path. Update
the logic that sets source in active_neko_context.py to check debug_override
first (e.g., reference the debug_override variable and the facts_path
construction) and only fall back to "runtime-character-hint" or "neko-config"
when debug_override is not set.
In `@local_server/battle_arena_server/forge_story_generator.py`:
- Around line 251-261: The logs currently emit full sensitive text (storyLead,
full prompt, rawContent, story) via _forge_log; change these calls (references:
_forge_log invocation that passes storyLead, card.attrName, and other places
that log prompt/rawContent/story) to avoid dumping full text by instead logging
only request_id and length metadata (e.g., len(storyLead), len(prompt),
len(rawContent), len(story)); add an explicit debug flag (e.g., a local
FORGE_DEBUG or payload.get("debug")) that, when enabled, logs a short redacted
preview (first N characters) rather than full content; ensure all occurrences
that currently pass storyLead, prompt, rawContent, or story to _forge_log are
updated to pass either length info or the redacted preview conditional on the
debug flag.
In `@local_server/battle_arena_server/server.py`:
- Around line 738-746: 当前调用 _forge_route_log(...) 将来自 safe_body 的 storyLead
原文直接记录,会把用户内容原样写入 stdout;请移除或替换传入的 storyLead 字段(不要把 safe_body.get("storyLead")
原文传入)。改为只记录 request_id、safe_body 中的 sourceFactId/factId、以及对 storyLead 的长度或摘要(例如
len 或截断/哈希摘要)以避免泄露原文;请在调用处(函数名 _forge_route_log,变量
request_id、safe_body、card)执行此替换并确保其他日志字段保持不变。
---
Outside diff comments:
In `@app/main_server.py`:
- Around line 1369-1376: Current code registers a global permissive CORS via
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
...) which should be narrowed and the avatar endpoint (/battle-arena/avatar)
writes unvalidated side/dataUrl/name into _battle_arena_avatars; change the CORS
registration to only allow precise origins (e.g. http://localhost:<port> values)
and apply it to a dedicated sub-application or router used by the battle-arena
frontend instead of the root app (see app.add_middleware and CORSMiddleware),
and harden the avatar handler that mutates _battle_arena_avatars by: validate
side against a whitelist (e.g. "left"/"right"), enforce max lengths for name and
dataUrl and a max decoded image size, reject or trim overlong input, and
implement entry limits and expiry (or LRU) for _battle_arena_avatars to prevent
unbounded memory growth.
In `@local_server/battle_arena_server/server.py`:
- Around line 588-629: 在前面已经把请求的 runtime_character_hint 裁剪并赋给 runtime_hint
后,所有后续对比/传入的地方应统一使用 runtime_hint;具体把对 _resolve_active_facts_context 的调用参数从
runtime_character_hint 改为 runtime_hint,并把 runtime_hint_used 的计算改为使用
runtime_hint(而不是原始 runtime_character_hint),以避免因首尾空格导致的假阴性;检查
resolved_character、override_ignored 等基于该比对逻辑的变量也使用 runtime_hint 一致性比较。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a02d25dd-35c1-4deb-92ed-87b317743cb8
📒 Files selected for processing (18)
app/main_server.pybattle-arena/src/components/BattleArena.jsxbattle-arena/src/components/CardGamePanel.jsxbattle-arena/src/components/neko-brawl/BattleTutorialPanel.jsxbattle-arena/src/components/neko-brawl/CardInspectModal.jsxbattle-arena/src/components/neko-brawl/DeckBuilderPanel.jsxbattle-arena/src/components/neko-brawl/DeckBuilderTutorialPanel.jsxbattle-arena/src/components/neko-brawl/DeckLibraryPanel.jsxbattle-arena/src/components/neko-brawl/NewBattleDuelUI.jsxbattle-arena/src/data/forgedBrawlCards.jsbattle-arena/src/data/nekoBrawlAdventureDeck.jslocal_server/battle_arena_server/README.mdlocal_server/battle_arena_server/active_neko_context.pylocal_server/battle_arena_server/forge_story_generator.pylocal_server/battle_arena_server/server.pystart-battle-arena.batstop-battle-arena.batstop-battle-arena.ps1
💤 Files with no reviewable changes (2)
- stop-battle-arena.bat
- stop-battle-arena.ps1
✅ Files skipped from review due to trivial changes (1)
- local_server/battle_arena_server/README.md
| source = "env-facts-json" | ||
| elif memory_dir and safe_character_segment(lanlan): | ||
| facts_path = memory_dir / lanlan / "facts.json" | ||
| source = "runtime-character-hint" if runtime_hint else "neko-config" |
There was a problem hiding this comment.
source 来源标记会把 debug override 误记成 neko-config 喵。
Line [80] 在 debug_override 生效且 runtime_hint 为空时,facts_path 实际按 override 角色构造,但 source 仍写成 neko-config,会误导后续诊断和排障喵。
💡 建议修复喵
- source = "runtime-character-hint" if runtime_hint else "neko-config"
+ if runtime_hint:
+ source = "runtime-character-hint"
+ elif debug_override:
+ source = "character-override"
+ else:
+ source = "neko-config"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/active_neko_context.py` at line 80, The
current assignment of source ("runtime-character-hint" if runtime_hint else
"neko-config") mislabels cases where debug_override is used but runtime_hint is
empty; when debug_override is present you must set source to "debug-override" so
diagnostics reflect the actual origin of facts_path. Update the logic that sets
source in active_neko_context.py to check debug_override first (e.g., reference
the debug_override variable and the facts_path construction) and only fall back
to "runtime-character-hint" or "neko-config" when debug_override is not set.
| _forge_route_log( | ||
| request_id, | ||
| "request", | ||
| sourceFactId=safe_body.get("sourceFactId") or safe_body.get("factId"), | ||
| storyLead=safe_body.get("storyLead"), | ||
| card={ | ||
| "attrName": card.get("attrName"), | ||
| }, | ||
| ) |
There was a problem hiding this comment.
别把 storyLead 原文打进控制台喵
storyLead 来自 forge facts / 当前猫娘记忆,这里直接传给 _forge_route_log 后会被原样 json.dumps 到 stdout;一旦控制台被收集或落盘,就把用户内容直接泄出去了喵。排障留 requestId、sourceFactId 和长度/摘要通常就够用了喵。
💡 可以这样收窄日志喵
_forge_route_log(
request_id,
"request",
sourceFactId=safe_body.get("sourceFactId") or safe_body.get("factId"),
- storyLead=safe_body.get("storyLead"),
+ storyLeadChars=len(str(safe_body.get("storyLead") or "")),
card={
"attrName": card.get("attrName"),
},
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| _forge_route_log( | |
| request_id, | |
| "request", | |
| sourceFactId=safe_body.get("sourceFactId") or safe_body.get("factId"), | |
| storyLead=safe_body.get("storyLead"), | |
| card={ | |
| "attrName": card.get("attrName"), | |
| }, | |
| ) | |
| _forge_route_log( | |
| request_id, | |
| "request", | |
| sourceFactId=safe_body.get("sourceFactId") or safe_body.get("factId"), | |
| storyLeadChars=len(str(safe_body.get("storyLead") or "")), | |
| card={ | |
| "attrName": card.get("attrName"), | |
| }, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@local_server/battle_arena_server/server.py` around lines 738 - 746, 当前调用
_forge_route_log(...) 将来自 safe_body 的 storyLead 原文直接记录,会把用户内容原样写入
stdout;请移除或替换传入的 storyLead 字段(不要把 safe_body.get("storyLead") 原文传入)。改为只记录
request_id、safe_body 中的 sourceFactId/factId、以及对 storyLead 的长度或摘要(例如 len
或截断/哈希摘要)以避免泄露原文;请在调用处(函数名 _forge_route_log,变量
request_id、safe_body、card)执行此替换并确保其他日志字段保持不变。
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/neko-brawl/exploration-rules.md (1)
45-70: ⚡ Quick win建议补充初始位置示例喵~
文档里的位置说明示例都是从第 4 张、第 9 张这样的中间位置开始的喵。虽然算法部分(第 679 行)有处理边界的逻辑,但是初始位置(position = 0)的行为没有明确的例子说明喵。
建议在这个章节或者第 18 节的示例里补充一个"探索开始时,当前位置 = 0,走 3 步"的例子,让实现者更清楚初始状态的处理喵~
示例:
当前位置 = 0 (探索开始) 探索步数 = 3 路径:第 1 张 -> 第 2 张 -> 第 3 张 最终落点:第 3 张这样能让规则更完整,避免实现时对初始状态产生疑惑喵!
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/neko-brawl/exploration-rules.md` around lines 45 - 70, Add an explicit initial-position example showing how position = 0 is handled: in the "探索位置" section (or section 18's examples) insert a short snippet like "当前位置 = 0 (探索开始), 探索步数 = 3, 路径:第 1 张 -> 第 2 张 -> 第 3 张, 最终落点:第 3 张" to illustrate that position is 0-based and the first step goes to 第 1 张; reference the existing position variable/algorithm (the logic around position = 0 at the algorithm starting near line ~679) so readers can correlate the example with the boundary handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@docs/neko-brawl/exploration-rules.md`:
- Around line 45-70: Add an explicit initial-position example showing how
position = 0 is handled: in the "探索位置" section (or section 18's examples) insert
a short snippet like "当前位置 = 0 (探索开始), 探索步数 = 3, 路径:第 1 张 -> 第 2 张 -> 第 3 张,
最终落点:第 3 张" to illustrate that position is 0-based and the first step goes to 第
1 张; reference the existing position variable/algorithm (the logic around
position = 0 at the algorithm starting near line ~679) so readers can correlate
the example with the boundary handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fdd5135f-a0bb-40ce-9066-bb167cbb018a
📒 Files selected for processing (1)
docs/neko-brawl/exploration-rules.md
- 战斗触发卡移除(主牌组 BATTLE 设 0、并入事件保持 40 张;支线同步) - 事件卡:打牌检定(属性匹配;数值累加留接口)+ AI 队友象征完成 + 取双方较好结果 - 休息卡:确认休息(恢复效果留空档) - 奇遇卡:双方确认进入/跳过支线,翻完回主线,跨边界步数清 0 - 终点卡:历程记录 + 结算小故事(前端模板保底 + 后端 LLM /arena/adventure-ending) - 交互辅助:抽牌动画、已抽卡堆 + 剩余张数、探索回合计数 - 修复:4 费行动卡无法放入行动区(预选阶段误用费用门控) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- exploration-rules:§13 战斗移除、§14.1 支线进出、§23 实现状态、§25 变更记录 - 新增 pr-1454-overview(大乱斗模块总览 + §12 交互实装进展) - 新增 branch-pr1454-changes(分支整体改动总览) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@battle-arena/src/data/nekoBrawlAdventureDeck.js`:
- Around line 237-245: The problem is buildEventCheck unconditionally assigns
blueprint.check (overwriting any existing { mode: 'value', ... } config), so
value-mode checks never take effect; modify buildEventCheck (and the similar
block around the other occurrence) to only set blueprint.check when it is
undefined/null (i.e., if (!blueprint.check) or equivalent), preserving existing
blueprint.check values (especially those with mode: 'value') and otherwise
assign the attribute-mode object as currently constructed.
In `@docs/branch-pr1454-changes.md`:
- Line 214: The sentence "Git 上 `main` 没有任何 HEAD 不包含的提交(`git log HEAD..main`
为空),所以本分支可以直接 fast-forward 合并到 `main`。" is misleading; change the phrasing
around `git log HEAD..main` to state that the branch is "未落后 main,但仍是协作草稿分支" (or
similar) rather than claiming it can be directly fast-forward merged, e.g.,
replace the clause asserting "可以直接 fast-forward 合并到 `main`" with "未落后 main,但仍为
Draft/协作草稿分支,需按流程合并"; keep the existing `git log HEAD..main` check as
explanation.
- Around line 84-170: The markdown uses repository-relative "../" links which
fail the docs checker; edit docs/branch-pr1454-changes.md and replace all "../"
style links (e.g. links like ../main_logic/proactive_delivery.py) with
site-allowed absolute doc URLs or convert them to plain code-path text (e.g.
"main_logic/proactive_delivery.py") so they no longer match the
scripts/check_docs_no_relative_paths.py rule; ensure every entry that currently
begins with "../" is fixed consistently.
In `@docs/neko-brawl/pr-1454-overview.md`:
- Around line 254-274: Update the overview to match the current implementation:
change the event/battle counts to EVENT=31 and BATTLE=0 (reflecting
nekoBrawlAdventureDeck.js), remove the claim that createAdventureRun() "pins
important node positions" since createAdventureRun no longer does that, clarify
that important-node reveals are not implemented at the data layer (so
advanceAdventureRun does not persist/emit reveals), and add the new API endpoint
/arena/adventure-ending to the interface list; also ensure function references
(createAdventureRun, calculateAdventureSteps, advanceAdventureRun,
describeAdventureReveal) describe their actual behaviors (e.g.,
calculateAdventureSteps returns per-player average AP, describeAdventureReveal
is a presentation helper only).
- Line 5: The file contains upward-relative links like
"../branch-pr1454-changes.md" and "../../battle-arena/..." that escape the docs
root and break the site checker; update every such link (including occurrences
in ranges 124-140, 152-185, 192-338, 503-543, 745-792) to use
repository-docs-root-relative or absolute site paths (e.g., start with
"/docs/..." or the canonical doc base URL) so they no longer traverse above the
docs root; search for the patterns "../" and "../../battle-arena" in this
document and replace them with the correct root-relative paths consistently.
In `@local_server/battle_arena_server/server.py`:
- Around line 798-803: The route currently passes safe_body straight into
generate_adventure_ending_story without propagating the runtime character hint
or the request id; extract the runtime hint from body keys
(runtimeCharacterHint, runtime_character_hint, character) similar to
/arena/forge-card-story, call
resolve_active_neko_context(runtime_character_hint=...) to validate/resolve it,
then inject the resolved runtime hint and the generated request_id (request_id)
into safe_body (e.g., safe_body["_requestId"]=request_id and
safe_body["runtimeCharacterHint"]=resolved_hint) before awaiting
generate_adventure_ending_story(safe_body) so the generator gets both context
and the same request id for logging/troubleshooting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 891c717b-32bf-45b3-b269-0b9070bdc0d4
📒 Files selected for processing (8)
battle-arena/src/components/CardGamePanel.jsxbattle-arena/src/components/neko-brawl/NewBattleDuelUI.jsxbattle-arena/src/data/nekoBrawlAdventureDeck.jsdocs/branch-pr1454-changes.mddocs/neko-brawl/exploration-rules.mddocs/neko-brawl/pr-1454-overview.mdlocal_server/battle_arena_server/forge_story_generator.pylocal_server/battle_arena_server/server.py
🚧 Files skipped from review as they are similar to previous changes (1)
- battle-arena/src/components/CardGamePanel.jsx
- buildEventCheck 不再覆盖已配置的 check,value 累加检定可正常启用(#1) - 修复 docs 内 ../ relative-up 链接(违反 check_docs_no_relative_paths、CI 卡红),改为 inline code(Project-N-E-K-O#2 Project-N-E-K-O#4) - branch-changes 措辞:未落后 main ≠ 可直接合并,标注仍为草稿分支(Project-N-E-K-O#3) - pr-1454-overview 同步实现:EVENT 31 / BATTLE 0、重要节点未实现、补 /arena/adventure-ending(Project-N-E-K-O#5) - 结算路由透传 _requestId + 软性 runtimeCharacterHint(不强制校验,结算用当前猫娘)(Project-N-E-K-O#6) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts: # app/main_server.py
Purpose
This PR is opened as a collaborative working PR for the NekoBrawl Arena V2 prototype. It is intentionally marked Don't Merge for now so multiple contributors can review, discuss, and build on the same branch before this work is considered ready for the main N.E.K.O codebase.
Collaboration model
Project-N-E-K-O/N.E.K.O:mainrophec/N.E.K.O:NekoBrawlArenaV2rophec/N.E.K.O:NekoBrawlArenaV2first.Current scope
Notes
Verification
upstream/mainbefore opening this PR.NekoBrawlArenaV2is currently 2 commits ahead ofupstream/main.Summary by CodeRabbit
新功能
文档
杂项