Skip to content

(Don't Merge)Nekobrawl arena v2#1454

Draft
rophec wants to merge 18 commits into
Project-N-E-K-O:mainfrom
rophec:NekoBrawlArenaV2
Draft

(Don't Merge)Nekobrawl arena v2#1454
rophec wants to merge 18 commits into
Project-N-E-K-O:mainfrom
rophec:NekoBrawlArenaV2

Conversation

@rophec
Copy link
Copy Markdown
Contributor

@rophec rophec commented May 21, 2026

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

  • Base target: Project-N-E-K-O/N.E.K.O:main
  • Working branch: rophec/N.E.K.O:NekoBrawlArenaV2
  • Contributors can submit follow-up PRs into rophec/N.E.K.O:NekoBrawlArenaV2 first.
  • After those follow-up PRs are reviewed and merged into the working branch, this PR will update automatically.
  • Final merge target remains the upstream N.E.K.O repository, not only the fork.

Current scope

  • Adds the NekoBrawl Arena V2 prototype module.
  • Adds a dedicated battle arena frontend workspace.
  • Adds deck-building, forged-card, favorite-card, battle UI, combo-list, battle-log, boss-state, and one-card play interaction prototypes.
  • Adds local startup support for the battle arena workflow.

Notes

  • This PR is not ready for final merge yet.
  • Some UI/layout controls and visual effects are temporary prototype tools for evaluation and will be cleaned up before final merge.
  • Tutorial content and several production-complete flows are still pending.
  • The PR exists mainly to make collaboration and code review easier while development continues.

Verification

  • Local branch was compared against latest upstream/main before opening this PR.
  • Expected comparison size is small and focused: NekoBrawlArenaV2 is currently 2 commits ahead of upstream/main.

Summary by CodeRabbit

  • 新功能

    • 上线“猫娘大乱斗”完整前端体验:新版对局 UI、回合制卡牌战、组卡/卡库、铸造与探险模式、拖拽交互、结算与卡牌检视、教程面板与音效/BGM 控制
    • 本地匹配与头像同步:实时匹配、前端自动同步头像到本地后端
    • 本地锻造与故事生成功能:事实抽取、卡牌铸造、探险结算故事生成与本地持久化
  • 文档

    • 补充匹配服务器、探险规则与组件使用说明
  • 杂项

    • 启动/停止脚本、构建与样式配置更新、添加构建缓存忽略规则

rophec and others added 2 commits May 21, 2026 13:58
## 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`.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b161fad-d3e0-4854-abbf-3862cda19731

📥 Commits

Reviewing files that changed from the base of the PR and between 32292ae and 02a84ab.

📒 Files selected for processing (2)
  • .gitignore
  • app/main_server.py
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/main_server.py

Walkthrough

本 PR 新增并整合本地匹配/Forge 后端与 LLM 故事生成、ActiveNeko 上下文、主服务器 CORS/头像同步,以及完整前端 Neko Brawl 子项目(UI、战斗/组卡/锻造/音频/样式)、构建配置、启动/停止脚本与相应文档喵。

Changes

Battle Arena 对战系统

Layer / File(s) Summary
匹配与 Forge 后端
local_server/battle_arena_server/server.py
添加内存等待室、匹配逻辑、dummy 对手兜底与 /arena/forge-facts/arena/forge-card-story/arena/adventure-ending 路由,提供 facts 读取/过滤/去重/加权抽取与归档注入及统计/错误字段喵。
ActiveNeko 上下文 & 故事生成
local_server/battle_arena_server/active_neko_context.py, local_server/battle_arena_server/forge_story_generator.py
新增活动角色上下文解析(支持 runtime hint 与安全校验、facts 路径推导)与 Forge/探险结算故事生成链(prompt 构建、LLM 逐 tier 调用、结果清洗/裁剪/重试/错误类型),并导出结构化结果喵。
主服务器 CORS 与头像 API
app/main_server.py
在主 FastAPI 应用启用全局 CORSMiddleware 并新增进程內头像缓存及 POST/GET /battle-arena/avatar 路由(内存缓存、非持久化)喵。
前端构建与入口
battle-arena/package.json, battle-arena/vite.config.js, battle-arena/postcss.config.js, battle-arena/tailwind.config.js, battle-arena/index.html, battle-arena/src/main.jsx, .gitignore
新增前端子项目的构建配置、Vite/Tailwind/PostCSS、入口 HTML/JS 与 .vite/ 忽略规则,支撑 Vite + React 的开发流程喵。
前端主应用与战斗实现
battle-arena/src/components/*, battle-arena/src/App.jsx, battle-arena/src/data/*
新增完整 Neko Brawl React 应用:BattleArena 页面、CardGamePanel(战斗逻辑含经典/新版 UI)、NewBattleDuelUI、DeckBuilder/DeckLibrary、CardInspect、BattleLog、NekoCard/NekoAvatar/ScoreBar 等子组件,及探险牌组、锻造卡生成/归一化/本地持久化 API 喵。
音频、样式与小组件
battle-arena/src/components/neko-brawl/nekoBrawlAudio.js, battle-arena/src/index.css
新增音频播放管理(BGM/SFX、解锁重试、渐入)与全局样式(霓虹特效、毛玻璃卡片样式、滚动 ticker)喵。
启动脚本与文档
start_battle_arena.py, start-battle-arena.bat, stop-battle-arena.*, local_server/battle_arena_server/README.md, local_server/battle_arena_server/requirements.txt, docs/*
新增跨平台启动/停止脚本、本地匹配服务器文档、后端依赖声明与 PR/探索规则文档,便于本地调试与集成验证喵。

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

"新功能铺成了一条路,喵,前端后端一起跑,喵,音轨闪烁像星河,喵,锻造故事轻声吟,喵,启动脚本把窗口都唤醒喵。"

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ba0c074 and 6c6094c.

⛔ Files ignored due to path filters (10)
  • battle-arena/package-lock.json is excluded by !**/package-lock.json
  • battle-arena/public/Simple_design_judging.gif is excluded by !**/*.gif
  • battle-arena/public/background_twisted.jpg is excluded by !**/*.jpg
  • battle-arena/public/celebration.gif is excluded by !**/*.gif
  • battle-arena/public/cry.gif is excluded by !**/*.gif
  • battle-arena/public/waiting.gif is excluded by !**/*.gif
  • battle-arena/public/waiting_idle.gif is excluded by !**/*.gif
  • battle-arena/public/waiting_idle2.gif is excluded by !**/*.gif
  • battle-arena/public/waiting_idle3.gif is excluded by !**/*.gif
  • battle-arena/public/waiting_idle4.gif is excluded by !**/*.gif
📒 Files selected for processing (27)
  • .gitignore
  • app/main_server.py
  • battle-arena/index.html
  • battle-arena/package.json
  • battle-arena/postcss.config.js
  • battle-arena/src/App.jsx
  • battle-arena/src/components/BattleArena.jsx
  • battle-arena/src/components/BattleLog.jsx
  • battle-arena/src/components/BottomTicker.jsx
  • battle-arena/src/components/CardGamePanel.jsx
  • battle-arena/src/components/DeckBuilderPanel.jsx
  • battle-arena/src/components/NekoAvatar.jsx
  • battle-arena/src/components/NekoCard.jsx
  • battle-arena/src/components/NewBattleDuelUI.jsx
  • battle-arena/src/components/ScoreBar.jsx
  • battle-arena/src/data/forgedBrawlCards.js
  • battle-arena/src/index.css
  • battle-arena/src/main.jsx
  • battle-arena/tailwind.config.js
  • battle-arena/vite.config.js
  • local_server/battle_arena_server/README.md
  • local_server/battle_arena_server/__init__.py
  • local_server/battle_arena_server/requirements.txt
  • local_server/battle_arena_server/server.py
  • start-battle-arena.bat
  • start_battle_arena.py
  • static/app-chat-avatar.js

Comment thread app/main_server.py
Comment on lines +1598 to +1614
@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', '')})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

头像同步端点缺少输入边界,容易被超大 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.

Comment on lines +463 to +469
// 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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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 -n

Repository: 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 -n

Repository: Project-N-E-K-O/N.E.K.O

Length of output: 15918


postMessage 需要校验来源并处理 rematch 期间轮询竞态喵。

  • battle-arena/src/components/BattleArena.jsxonMessage 只校验 event.datatype === 'neko-avatar'side === 'left'),未校验 event.origin/event.source,任意可触达窗口都能伪造消息直接覆盖左侧头像和名字喵;把允许的 origin 收紧,并最好校验 event.source 是否为预期父窗口/iframe contentWindow 再更新状态喵。
  • pollOpponentawait 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.

Comment on lines +483 to +499
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 {
// 匹配服务器未运行时静默容错
}
}, [])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

重匹配时旧轮询结果可能把新对手覆盖掉喵。

pollOpponent 在发请求前先抓了一次 id,但请求回来后没有再确认这个 id 还是不是当前会话喵。用户点“测试重匹配”时,旧的 /arena/status/{oldId} 请求如果晚一点返回,就还能把 opponentmatchStatus 回写成旧数据喵。这里在 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.

Comment thread battle-arena/src/components/CardGamePanel.jsx
Comment on lines +973 to +979
const merged = {
id: `card-${cardIdCounter}`,
attr: mergedAttr,
power: mergedPower,
debuffs: [...new Set([...dragCard.debuffs, ...targetCard.debuffs])],
temp: true, // 合成卡为临时卡,打出后不进弃牌堆
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

合成卡缺少基础字段,走“确认出牌”路径时会结算异常喵。

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.

Comment on lines +123 to +128
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))
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +12 to +15
allow: [
__dirname,
'F:/NEKO_bugfix/NekoBrawl/Gif_source',
],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

请移除硬编码的本机绝对路径,避免多人环境启动失败喵。

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.

Suggested change
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 以兼容跨平台。

Comment on lines +18 to +20

> 铸造机 facts 接口依赖 `httpx`:若 `.venv` 中尚未安装,请在 N.E.K.O 根目录执行 `uv sync` 或 `uv pip install -r local_server/battle_arena_server/requirements.txt`。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

修一下 Markdown lint 警告避免文档检查噶掉喵。

这里有两个已命中的 lint 点:

  1. Line 18 的 blockquote 中间有空行(MD028);
  2. 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.

Comment on lines +3 to +6
猫娘大乱斗 — 本地对战匹配服务器
端口: 3001
启动: uvicorn server:app --host 0.0.0.0 --port 3001 --reload
或直接: python server.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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" . || true

Repository: 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).

Comment on lines +65 to +68
# 等待匹配的玩家: player_id -> PlayerEntry dict
waiting_room: dict[str, dict] = {}
# 已匹配结果: player_id -> opponent snapshot dict
matched: dict[str, dict] = {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

当前匹配表结构没法在 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 value

mojibake 修复阈值 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-modalaria-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-modalaria-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

📥 Commits

Reviewing files that changed from the base of the PR and between 6c6094c and 43259f8.

⛔ Files ignored due to path filters (17)
  • battle-arena/public/neko-brawl/Background_forest.png is excluded by !**/*.png
  • battle-arena/public/neko-brawl/Boss_WeakDamageTaken_transparent.png is excluded by !**/*.png
  • battle-arena/public/neko-brawl/Boss_attack_transparent.png is excluded by !**/*.png
  • battle-arena/public/neko-brawl/Boss_damagetaken_transparent.png is excluded by !**/*.png
  • battle-arena/public/neko-brawl/Boss_normal_transparent.png is excluded by !**/*.png
  • battle-arena/public/neko-brawl/audio/bgm_answer_quickly.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/bgm_battle_loop.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/bgm_deck_builder_loop.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/bgm_deck_library_loop.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/bgm_home_brightlands_night.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/bgm_home_loop.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/sfx_card_attack.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/sfx_card_combo.wav is excluded by !**/*.wav
  • battle-arena/public/neko-brawl/audio/sfx_card_draw.wav is excluded by !**/*.wav
  • battle-arena/public/neko-brawl/audio/sfx_card_heal.mp3 is excluded by !**/*.mp3
  • battle-arena/public/neko-brawl/audio/sfx_card_shield.wav is excluded by !**/*.wav
  • battle-arena/public/neko-brawl/audio/sfx_card_support.wav is excluded by !**/*.wav
📒 Files selected for processing (23)
  • .gitignore
  • app/main_server.py
  • battle-arena/index.html
  • battle-arena/package.json
  • battle-arena/public/neko-brawl/README.md
  • battle-arena/src/components/BattleArena.jsx
  • battle-arena/src/components/CardGamePanel.jsx
  • battle-arena/src/components/neko-brawl/BattleResultOverlay.jsx
  • battle-arena/src/components/neko-brawl/BattleTutorialPanel.jsx
  • battle-arena/src/components/neko-brawl/CardInspectModal.jsx
  • battle-arena/src/components/neko-brawl/DeckBuilderPanel.jsx
  • battle-arena/src/components/neko-brawl/DeckBuilderTutorialPanel.jsx
  • battle-arena/src/components/neko-brawl/DeckLibraryPanel.jsx
  • battle-arena/src/components/neko-brawl/NekoCardBack.jsx
  • battle-arena/src/components/neko-brawl/NewBattleDuelUI.jsx
  • battle-arena/src/components/neko-brawl/README.md
  • battle-arena/src/components/neko-brawl/nekoBrawlAudio.js
  • battle-arena/src/data/forgedBrawlCards.js
  • battle-arena/tailwind.config.js
  • local_server/battle_arena_server/README.md
  • local_server/battle_arena_server/active_neko_context.py
  • local_server/battle_arena_server/forge_story_generator.py
  • local_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

Comment on lines +261 to +267
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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

删除 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.

Comment on lines +129 to +137
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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.js

Repository: 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" -n

Repository: 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)
PY

Repository: Project-N-E-K-O/N.E.K.O

Length of output: 12119


修复 currentBgmplay().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.

Comment on lines +379 to +382
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

用字符串比较类名来判断异常类型不太可靠喵~

如果 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 718f0b8 and bd1331b.

📒 Files selected for processing (1)
  • local_server/battle_arena_server/forge_story_generator.py

"3. 保留故事引子的情绪基调和关系,不要改成相反含义。",
"4. 不要编造新的现实履历、地点、人物关系或长期承诺。",
"5. 可以把卡牌效果轻微融入动作、气氛或战斗画面。",
"6. 大部分叙事内容使用第三人称,例如“她”“猫娘”“{猫娘名}”,不要全篇用第一人称。",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

{猫娘名} 改成真实示例名喵。

这里现在是普通字符串,模型实际看到的会是字面量 {猫娘名},不是当前猫娘名本身,提示词示例会失真,甚至可能把占位符学进生成结果里喵。

可直接这样改喵
-            "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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bd1331b and 62615da.

📒 Files selected for processing (18)
  • app/main_server.py
  • battle-arena/src/components/BattleArena.jsx
  • battle-arena/src/components/CardGamePanel.jsx
  • battle-arena/src/components/neko-brawl/BattleTutorialPanel.jsx
  • battle-arena/src/components/neko-brawl/CardInspectModal.jsx
  • battle-arena/src/components/neko-brawl/DeckBuilderPanel.jsx
  • battle-arena/src/components/neko-brawl/DeckBuilderTutorialPanel.jsx
  • battle-arena/src/components/neko-brawl/DeckLibraryPanel.jsx
  • battle-arena/src/components/neko-brawl/NewBattleDuelUI.jsx
  • battle-arena/src/data/forgedBrawlCards.js
  • battle-arena/src/data/nekoBrawlAdventureDeck.js
  • local_server/battle_arena_server/README.md
  • local_server/battle_arena_server/active_neko_context.py
  • local_server/battle_arena_server/forge_story_generator.py
  • local_server/battle_arena_server/server.py
  • start-battle-arena.bat
  • stop-battle-arena.bat
  • stop-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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment thread local_server/battle_arena_server/forge_story_generator.py
Comment on lines +738 to +746
_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"),
},
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

别把 storyLead 原文打进控制台喵

storyLead 来自 forge facts / 当前猫娘记忆,这里直接传给 _forge_route_log 后会被原样 json.dumps 到 stdout;一旦控制台被收集或落盘,就把用户内容直接泄出去了喵。排障留 requestIdsourceFactId 和长度/摘要通常就够用了喵。

💡 可以这样收窄日志喵
     _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.

Suggested change
_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)执行此替换并确保其他日志字段保持不变。

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 62615da and c84df69.

📒 Files selected for processing (1)
  • docs/neko-brawl/exploration-rules.md

rophec and others added 3 commits May 28, 2026 01:04
- 战斗触发卡移除(主牌组 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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f1d92b3 and 47130b5.

📒 Files selected for processing (8)
  • battle-arena/src/components/CardGamePanel.jsx
  • battle-arena/src/components/neko-brawl/NewBattleDuelUI.jsx
  • battle-arena/src/data/nekoBrawlAdventureDeck.js
  • docs/branch-pr1454-changes.md
  • docs/neko-brawl/exploration-rules.md
  • docs/neko-brawl/pr-1454-overview.md
  • local_server/battle_arena_server/forge_story_generator.py
  • local_server/battle_arena_server/server.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • battle-arena/src/components/CardGamePanel.jsx

Comment thread battle-arena/src/data/nekoBrawlAdventureDeck.js
Comment thread docs/branch-pr1454-changes.md Outdated
Comment thread docs/branch-pr1454-changes.md Outdated
Comment thread docs/neko-brawl/pr-1454-overview.md Outdated
Comment thread docs/neko-brawl/pr-1454-overview.md
Comment thread local_server/battle_arena_server/server.py Outdated
LyaQanYi and others added 3 commits May 29, 2026 18:14
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants