feat: card-forge 模块 + 三体融合配套 + 对话掉落卡片闭环 & 社区登录 (M1/M2/M5) - #1542
Conversation
把卡牌铸造功能拆为 NEKO 的一个独立子模块,可单独启用,不依赖其他原型代码。 ## 模块结构 **前端** `card-forge/` (Vite + React + Tailwind, 端口 5173) - 奇遇铸造机面板:5 槽事件抽取 → 选择 → LLM 故事生成 → 铸造动画 → 入库 - 铸造卡仓库:网格展示 + CardInspectModal 鉴赏 - 通过 `/card-forge/active-character` 同步当前猫娘名 **后端** `local_server/card_forge_server/` (FastAPI, 端口 3001) - `GET /arena/forge-facts`:从当前猫娘 active facts.json 抽取 5 条候选事实 (按 id/hash 去重,可排除已铸造来源) - `POST /arena/forge-card-story`:用 NEKO 核心 LLM 配置 (summary / agent) 把 storyLead 生成卡牌故事 - 只读 `facts.json` / `facts_archive.json`,不修改 NEKO 核心 - LLM 复用 `utils.llm_client.create_chat_llm()`,不在本模块硬编码服务商 **主服务桥接** (`app/main_server.py`,加 2 个路由) - `POST /card-forge/active-character`:由 NEKO 主前端在头像捕获后调用 - `GET /card-forge/active-character`:供 card-forge 轮询当前猫娘名 **头像同步钩子** (`static/app-chat-avatar.js`,加 1 函数 + 4 调用点) - `syncAvatarToCardForge` 在头像捕获/恢复/IPC 注入时把当前猫娘名推给后端 - 静默失败:card-forge 未运行时不影响主前端 ## 启动 ```powershell # 一键启动 3 个窗口 (主服务 / 铸造后端 / 铸造前端) .\start-card-forge.bat # 或 Python: uv run start_card_forge.py ``` URL: - card-forge: http://localhost:5173 - 主服务: http://localhost:48911 - 铸造后端: http://localhost:3001/health ## 私密性 facts 含个人化内容,请勿把铸造后端暴露到公网;日志不打印完整 fact text。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedReview was skipped as selected files did not have any reviewable changes. 💤 Files selected but had no reviewable changes (1)
⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
You can disable this status message by setting the Use the checkbox below for a quick retry:
Walkthrough本次变更新增 Card Forge 独立服务与 React 界面,接入 facts、故事生成、社区 OAuth、credits、缓存同步和社交 UI;同时扩展屏幕共享、语言上下文、平台适配、教程、本地化及相关测试。 ChangesCard Forge 与社区整合
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dfb78fa7c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
回应 Project-N-E-K-O#1542 codex review: - **P1** `set_card_forge_active_character`:载荷里 dataUrl 有值但 name 为空时 原本会把已存的 name 清空。改成只更新载荷里实际给出的字段。 - **P2** `card-forge/src/App.jsx` 轮询:之前 name 为空时不动 state,服务端 缓存清空(如主服务重启)后前端仍显示旧猫娘名。改成空 name 时也把 本地 activeCharacterName 置 null,保持与服务端一致。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
两条 codex 意见都已修(d392b05):
Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (1)
card-forge/src/App.jsx (1)
549-599: 💤 Low value重复查找可以提取出来喵~
本喵注意到
forgeMachineSlots.find(s => s.id === machinePickedId)在渲染时被调用了好几次呢(Lines 578, 583, 596),虽然数组很小,但还是可以提取成一个变量让代码更清爽一点喵!const pickedSlot = forgeMachineSlots.find(s => s.id === machinePickedId) // 然后用 pickedSlot?.name, pickedSlot?.storyLead 等🤖 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 `@card-forge/src/App.jsx` around lines 549 - 599, The JSX repeatedly calls forgeMachineSlots.find(s => s.id === machinePickedId) which is redundant; extract that lookup once into a local constant (e.g., pickedSlot) near where machinePhase/machinePickedId are in scope and replace all occurrences (uses in the motion div: name, summary, storyLead) with pickedSlot?.name, pickedSlot?.summary, pickedSlot?.storyLead (and keep using machineForgedCard?.name/story where appropriate) so the component only does the array find once.
🤖 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 1589-1595: The POST handler set_card_forge_active_character
currently skips updating _card_forge_active_character unless name or dataUrl are
truthy, which prevents callers from clearing values; change the logic in
set_card_forge_active_character to always update _card_forge_active_character
with the provided values (use the extracted data_url and name even if empty) so
the cache can be explicitly cleared (i.e., remove the if guard and perform
_card_forge_active_character.update({'dataUrl': data_url, 'name': name})
unconditionally).
In `@card-forge/src/App.jsx`:
- Around line 171-195: The fetch in requestForgeCardStory lacks timeout
handling; wrap the POST with an AbortController and pass controller.signal to
fetch, start a timer (e.g., setTimeout) to call controller.abort() after a
reasonable timeout, and clear the timer on success or error; keep existing
behavior (return null) when aborted or on error. Locate requestForgeCardStory
and ensure you still build the body with buildForgeStoryRequest and process the
response into composeForgedCardStory, but add the AbortController creation,
signal usage, timer cleanup, and proper catch handling for the abort scenario.
In `@card-forge/src/data/forgedBrawlCards.js`:
- Around line 114-117: The generated card uses loose truthiness for
storyGenerationStatus and sourceKind which conflicts with
composeForgedCardStory's trim() semantics and omits factHash: update the logic
that sets storyGenerationStatus to treat an all-whitespace string as not-ready
(e.g., check event.story && event.story.trim().length > 0 or
event.generatedStory?.trim()) and update sourceKind computation to consider
event.factHash in addition to event.factId (e.g., sourceKind: event.sourceKind
|| (event.sourceFactId || event.factId || event.factHash ? 'fact' :
'temporary')); apply the same fixes where these fields are set (the duplicate
assignment block around storyGenerationStatus/sourceKind).
In `@local_server/card_forge_server/active_neko_context.py`:
- Line 71: The current assignment "lanlan = runtime_hint or debug_override or
active_lanlan" lets an unverified runtime_hint override the sanctioned active
character; change this to only accept a runtime_hint if it has been validated
against the authoritative active character source (e.g., the server-synced
active role) or require runtime_hint == active_lanlan before using it, otherwise
fall back to debug_override or active_lanlan; apply the same validation to the
other uses around the block that reference runtime_hint (lines ~78–80) so only
verified/whitelisted hints can alter the facts.json path.
In `@local_server/card_forge_server/forge_story_generator.py`:
- Around line 66-89: The logs emitted by _forge_log currently include full
sensitive texts (e.g., storyLead, persona summary, full prompts, model_output,
final story); update _log_value and its callers to redact sensitive fields and
return only safe metadata (e.g., length, sha256 hash, and a short preview up to
~100 chars) instead of full content; ensure keys that match sensitive patterns
("api_key", "secret", "ssn", "person", "persona", "memory", "storyLead",
"prompt", "model_output", "final_story") are masked entirely or replaced with a
redacted marker and metadata, and apply the same behavior in all uses of
_log_value/_forge_log (including the other ranges mentioned) so printed payloads
contain no full PII, only length/hash/preview.
In `@local_server/card_forge_server/server.py`:
- Around line 659-660: The script's __main__ block calls
uvicorn.run("server:app", host="0.0.0.0", ...) which binds to all interfaces;
change it to default to 127.0.0.1 and only set host to 0.0.0.0 when an explicit
environment variable (e.g. CARD_FORGE_ALLOW_EXTERNAL=true) is present; update
the __main__ logic around uvicorn.run to read that env var (or a similarly named
flag) and choose host = "127.0.0.1" by default or host = "0.0.0.0" when the env
var is truthy, keeping port and reload behavior unchanged and ensuring the code
uses the uvicorn.run call in the __main__ block you modified.
- Around line 41-46: The CORSMiddleware is currently open to all origins and the
server is bound to 0.0.0.0, risking exposure of sensitive endpoints; change the
app.add_middleware(CORSMiddleware, allow_origins=[...]) call to a tight
whitelist (at minimum "http://127.0.0.1:5173" and "http://localhost:5173",
optionally extended via an environment variable), and update the
uvicorn.run(...) default host from "0.0.0.0" to "127.0.0.1" (or read
host/origins from env vars), and add simple access control checks (e.g.,
token/secret or same-origin check) to the handlers serving /arena/forge-facts
and /arena/forge-card-story to ensure only authorized local frontends can
retrieve facts/story.
In `@static/app-chat-avatar.js`:
- Around line 354-363: The function syncAvatarToCardForge currently returns
early when dataUrl is falsy which prevents sending name-only updates to
/card-forge/active-character; change syncAvatarToCardForge so it does not return
immediately on a missing dataUrl but instead builds a payload that always
includes the character name (using lanlan_config.lanlan_name) and includes
dataUrl only when present, then POST that payload to
/card-forge/active-character (preserving the existing headers and catch
behavior); alternatively add a small helper (e.g., syncNameToCardForge) and call
it when dataUrl is absent to ensure name-only synchronization.
---
Nitpick comments:
In `@card-forge/src/App.jsx`:
- Around line 549-599: The JSX repeatedly calls forgeMachineSlots.find(s => s.id
=== machinePickedId) which is redundant; extract that lookup once into a local
constant (e.g., pickedSlot) near where machinePhase/machinePickedId are in scope
and replace all occurrences (uses in the motion div: name, summary, storyLead)
with pickedSlot?.name, pickedSlot?.summary, pickedSlot?.storyLead (and keep
using machineForgedCard?.name/story where appropriate) so the component only
does the array find once.
🪄 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: 8d4e58c4-5098-4668-8353-96ba1b3565ef
⛔ Files ignored due to path filters (1)
card-forge/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (22)
app/main_server.pycard-forge/index.htmlcard-forge/package.jsoncard-forge/postcss.config.jscard-forge/src/App.jsxcard-forge/src/components/CardInspectModal.jsxcard-forge/src/data/forgedBrawlCards.jscard-forge/src/index.csscard-forge/src/main.jsxcard-forge/tailwind.config.jscard-forge/vite.config.jslocal_server/card_forge_server/README.mdlocal_server/card_forge_server/__init__.pylocal_server/card_forge_server/active_neko_context.pylocal_server/card_forge_server/forge_story_generator.pylocal_server/card_forge_server/requirements.txtlocal_server/card_forge_server/server.pystart-card-forge.batstart_card_forge.pystatic/app-chat-avatar.jsstop-card-forge.batstop-card-forge.ps1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d392b0512a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/main_server.py`:
- Around line 1594-1595: payload.get('dataUrl') and payload.get('name') are
being wrapped with str(...), so if the JSON contains null (Python None) they
become the literal string 'None'; change the extraction to treat None as an
empty string before converting to str — e.g. read raw = payload.get('dataUrl') /
payload.get('name'), set value = '' if raw is None else str(raw), and assign to
data_url and name respectively; update the lines around the data_url and name
assignments to use this None-checking flow so only real values are stringified.
🪄 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: 7354057f-3886-4c75-b196-4f374db52730
📒 Files selected for processing (2)
app/main_server.pycard-forge/src/App.jsx
回应 Project-N-E-K-O#1542 CodeRabbit + Codex 评审,所有问题一并修复: ## Major (安全/隐私) - **server.py CORS**: 把 `allow_origins=["*"]` 收紧到本机 `127.0.0.1:5173 / localhost:5173` 白名单;通过 `NEKO_CARD_FORGE_ALLOWED_ORIGINS` 环境变量可扩展。allow_methods/headers 也从 `*` 收到具体值。 - **server.py 绑定**: 默认 `host="127.0.0.1"`,通过 `NEKO_CARD_FORGE_HOST` 才允许 0.0.0.0; 端口同理走 `NEKO_CARD_FORGE_PORT`。facts 含个人化记忆,绝不应该默认监听所有网卡。 - **forge_story_generator 日志脱敏**: 新增 `_mask_sensitive_text` 与 `_FORGE_SENSITIVE_FIELDS` 白名单,storyLead/systemPrompt/userPrompt/rawContent/story/ lanlanPromptPreview 这些字段在 `_forge_log` 中只输出长度 + 40 字符短预览, 不再把完整 prompt 与故事正文打到控制台。 - **active_neko_context hint 校验**: 新增 `_known_character_names`,把 `runtime_character_hint` 和 `character_override` 限制在已配置猫娘名集合内, unknown 一律降级到 active_lanlan,避免被任意 hint 拼接成不存在的 facts.json 路径。 - **stop-card-forge.ps1**: 杀进程前先用 CIM 取 CommandLine,只杀匹配 `launcher.py / card_forge_server / card-forge` 这三个 pattern 的进程, 避免别人在同端口跑的 Vite/uvicorn 被误杀。 ## Minor (正确性) - **main_server.py POST**: 改用 `'X' in payload` 语义,既不擦旧值 (codex P1),又允许调用方显式 `{"name": ""}` 清空 (CodeRabbit)。 - **app-chat-avatar.js sync**: 放宽 dataUrl 闸门,允许 name-only 同步; body 只塞有值字段,配合服务端 in-payload 语义,不会无脑发空串擦掉缓存。 - **forgedBrawlCards.js**: storyGenerationStatus / sourceKind 判定改用 `string?.trim()` 真值,与 `composeForgedCardStory` 的 `trim()` 行为对齐; sourceKind 同时识别 factHash,避免只带 hash 的事实来源被错判为 temporary。 - **App.jsx requestForgeCardStory**: 加 `AbortController` 30 秒超时, 防止 LLM 响应卡死时铸造动画无限转圈。 - **App.jsx nitpick**: 提取重复的 `forgeMachineSlots.find(s => s.id === machinePickedId)` 到 `pickedSlot` 局部变量。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
CodeRabbit + Codex 这一轮 9 条 review + 1 nitpick 已全部处理(0eeb45e),没有 push back 的。 Major(安全/隐私)
Minor(正确性)
Co-Authored-By: Claude Opus 4.7 (1M context) |
按 CodeRabbit prompt 自己说的 "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason":
如果哪条 maintainer 看了仍然觉得不到位,欢迎指出具体不足,我再补。 Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0eeb45e637
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
回应 Project-N-E-K-O#1542 codex review on 0eeb45e: - **P1** `local_server/card_forge_server/server.py`: `uvicorn.run("server:app", reload=True)` 从项目根 (`uv run local_server/card_forge_server/server.py`) 起时,reload worker 是 fork 出的子进程,不继承 `__main__` 里给 sys.path 加的 SERVER_ROOT,会找不到 `server` 模块。 显式传 `app_dir=str(SERVER_ROOT)` 让 uvicorn 把目录加到 worker 自己的 sys.path 上。 - **P2** `start_card_forge.py`: 脚本硬编码 `powershell.exe` 和 `subprocess.CREATE_NEW_CONSOLE`, 在 macOS/Linux 上会直接抛 FileNotFoundError / ValueError,且没有有意义的等价行为 (没有跨平台的"开三个新终端各跑一条命令"的统一 API)。加 `_ensure_windows()` 早判, 非 Windows 抛 SystemExit + 打印三条手动命令,避免误以为脚本只是卡住。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Codex 这一轮 2 条都处理(841ea48):
Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
card-forge/src/data/forgedBrawlCards.js (1)
179-184:⚠️ Potential issue | 🟠 Major | ⚡ Quick win给
localStorage写入加异常兜底,别让持久化把整页弄炸喵。这里直接
setItem,一旦遇到隐私模式、存储被禁用或 quota 满了就会抛异常;而App.jsx里每次铸造/删除后都会走到这段,核心流程会被这个运行时错误打断喵。🐾 可直接套用的小修复喵
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 { + // localStorage 不可用或空间不足时静默降级,避免打断铸造流程 + } }🤖 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 `@card-forge/src/data/forgedBrawlCards.js` around lines 179 - 184, The saveForgedBrawlCards function currently calls window.localStorage.setItem unguarded which can throw (private browsing, disabled storage, quota exceeded) and break the app; wrap the setItem call inside a try/catch in saveForgedBrawlCards, optionally check for window and window.localStorage first, and on error swallow it (or log via console.error/processLogger) so failures become a no-op and do not interrupt the core flow that calls normalizeForgedBrawlCard.card-forge/src/App.jsx (1)
695-755:⚠️ Potential issue | 🟠 Major | ⚡ Quick win把可选槽位交互改成真正可聚焦的按钮(支持 Tab/Enter/Space)喵
- 在
card-forge/src/App.jsx里这些槽位用motion.div + onClick承接“选择事件→再确认”,但没有tabIndex/role/aria-*,也没有onKeyDown兜底 Enter/Space,键盘用户会卡住主流程喵。🐾 一个最小改法喵
- <motion.div + <motion.button + type="button" key={slot.id} layout exit={{ opacity: 0 }} onClick={() => handleMachineCardClick(slot.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleMachineCardClick(slot.id) + } + }} className={`forge-card-wrapper relative flex-1 rounded-2xl border p-3 flex flex-col items-center min-h-[340px] cursor-pointer transition-all duration-200 ${ isPicked && machinePhase === 'confirming' ? 'border-violet-400/60 bg-violet-500/10 ring-2 ring-violet-400/30' : isTemporary ? 'border-amber-300/25 bg-amber-950/20' : isRecentGuaranteed ? 'border-emerald-300/70 bg-emerald-950/30 ring-2 ring-emerald-300/35 shadow-[0_0_26px_rgba(110,231,183,0.22)]' : isDistantGuaranteed ? 'border-orange-300/75 bg-orange-950/30 ring-2 ring-orange-300/40 shadow-[0_0_28px_rgba(251,146,60,0.24)]' : 'border-emerald-300/20 bg-slate-950/45' }`} - > + > <div className="mb-2 flex w-full items-center justify-between gap-2"> <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-widest">No.{index + 1}</span> <span className={`rounded-full border px-2 py-0.5 text-[9px] font-black ${ isTemporary ? 'border-amber-300/35 bg-amber-500/15 text-amber-100' : 'border-emerald-300/35 bg-emerald-500/15 text-emerald-100' }`}>{sourceLabel}</span> </div> <div className={`flex-1 w-full rounded-xl border bg-white/[0.03] flex flex-col items-center justify-center p-3 ${ isRecentGuaranteed ? 'border-emerald-200/25 shadow-inner shadow-emerald-900/20' : isDistantGuaranteed ? 'border-orange-200/30 shadow-inner shadow-orange-900/25' : 'border-white/8' }`}> <div className="w-10 h-10 rounded-full bg-violet-500/10 flex items-center justify-center text-lg mb-3">🎴</div> <p className="text-sm font-bold text-white text-center">{slot.name}</p> <p className="text-[10px] text-gray-400 text-center mt-2 leading-relaxed">{slot.summary}</p> </div> {factDebugStamp && ( <div className={`pointer-events-none absolute bottom-2 right-2 rounded-full border px-2 py-0.5 text-[10px] font-black shadow-lg ${ isDistantGuaranteed ? 'border-orange-300/60 bg-orange-500/15 text-orange-100 shadow-orange-950/30' : isRecentGuaranteed ? 'border-emerald-300/60 bg-emerald-500/15 text-emerald-100 shadow-emerald-950/30' : 'border-slate-300/25 bg-slate-950/70 text-slate-200 shadow-black/25' }`}> {factDebugStamp} </div> )} <AnimatePresence> {isPicked && machinePhase === 'confirming' && ( <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 8 }} className="mt-2 w-full rounded-lg bg-violet-500/20 border border-violet-400/30 px-3 py-2 text-center" > <p className="text-xs text-violet-200 font-bold">确定选择这个事件吗?</p> <p className="text-[10px] text-violet-300/70 mt-1">再次点击确认</p> </motion.div> )} </AnimatePresence> - </motion.div> + </motion.button>
- 用键盘 Tab / Shift+Tab 定位槽位,再用 Enter/Space 完整跑一遍“选择事件→再确认”,确保主流程可用喵。
🤖 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 `@card-forge/src/App.jsx` around lines 695 - 755, The slot cards are implemented as a non-focusable motion.div with only onClick (motion.div + onClick and handleMachineCardClick), so keyboard users cannot Tab to them or activate them via Enter/Space; make each slot a real accessible button by adding tabIndex={0}, role="button", appropriate aria attributes (e.g., aria-pressed or aria-label using slot.id/slot.name and reflecting isPicked/machinePhase), and implement onKeyDown that calls handleMachineCardClick(slot.id) when Enter or Space is pressed (preventDefault for Space to avoid page scroll); keep the existing onClick behavior and preserve the visual focus styles so keyboard focus is visible.
♻️ Duplicate comments (1)
local_server/card_forge_server/active_neko_context.py (1)
55-60:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
known_names取不到时不要退回去信任外部 hint 喵。这里一旦
_known_character_names()异常就会返回空集,而 Line 91-95 只在known_names非空时才丢弃非法值;结果就是白名单失效时,runtime_character_hint/character_override又能覆盖active_lanlan,把facts.json重新指向调用方提供的目录名喵。既然当前函数在 Line 78 已经拿到了权威的active_lanlan,白名单不可用时应直接忽略这些外部 hint,而不是回退到 legacy 行为喵。🐾 建议改法
known_names = _known_character_names(config_manager) runtime_hint = safe_character_segment(runtime_character_hint) debug_override = safe_character_segment(character_override) if known_names: if runtime_hint and runtime_hint not in known_names: runtime_hint = None if debug_override and debug_override not in known_names: debug_override = None + else: + runtime_hint = None + debug_override = NoneAlso applies to: 84-95
🤖 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/card_forge_server/active_neko_context.py` around lines 55 - 60, The _known_character_names() helper currently returns an empty set on exception which is treated as “no whitelist” by the caller and thus allows external hints (runtime_character_hint / character_override) to override active_lanlan; change the failure path to return a sentinel (e.g., None) instead of an empty set and update the caller logic around known_names (the block checking known_names and lines handling runtime_character_hint / character_override) to treat None as “whitelist unavailable” and therefore ignore any external hints and keep the authoritative active_lanlan value; locate references to config_manager.get_character_data(), prompt_map, _known_character_names(), known_names, runtime_character_hint, character_override and active_lanlan to make the paired 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 `@stop-card-forge.ps1`:
- Around line 46-49: The skip log currently prints the full $cmdLine which may
expose sensitive data; change the Write-Host call in the block after
Get-ProcessCommandLine / Test-CardForgeProcess to log a redacted preview instead
(e.g., show only the first and/or last N characters or replace likely secrets
with a fixed mask) and include $port and $processId as before; implement the
redaction by creating a small helper/inline transform (e.g., redactCommandLine
or a short snippet) that trims or masks sensitive segments of $cmdLine before
passing it to Write-Host so the log shows a safe preview rather than the full
command line.
---
Outside diff comments:
In `@card-forge/src/App.jsx`:
- Around line 695-755: The slot cards are implemented as a non-focusable
motion.div with only onClick (motion.div + onClick and handleMachineCardClick),
so keyboard users cannot Tab to them or activate them via Enter/Space; make each
slot a real accessible button by adding tabIndex={0}, role="button", appropriate
aria attributes (e.g., aria-pressed or aria-label using slot.id/slot.name and
reflecting isPicked/machinePhase), and implement onKeyDown that calls
handleMachineCardClick(slot.id) when Enter or Space is pressed (preventDefault
for Space to avoid page scroll); keep the existing onClick behavior and preserve
the visual focus styles so keyboard focus is visible.
In `@card-forge/src/data/forgedBrawlCards.js`:
- Around line 179-184: The saveForgedBrawlCards function currently calls
window.localStorage.setItem unguarded which can throw (private browsing,
disabled storage, quota exceeded) and break the app; wrap the setItem call
inside a try/catch in saveForgedBrawlCards, optionally check for window and
window.localStorage first, and on error swallow it (or log via
console.error/processLogger) so failures become a no-op and do not interrupt the
core flow that calls normalizeForgedBrawlCard.
---
Duplicate comments:
In `@local_server/card_forge_server/active_neko_context.py`:
- Around line 55-60: The _known_character_names() helper currently returns an
empty set on exception which is treated as “no whitelist” by the caller and thus
allows external hints (runtime_character_hint / character_override) to override
active_lanlan; change the failure path to return a sentinel (e.g., None) instead
of an empty set and update the caller logic around known_names (the block
checking known_names and lines handling runtime_character_hint /
character_override) to treat None as “whitelist unavailable” and therefore
ignore any external hints and keep the authoritative active_lanlan value; locate
references to config_manager.get_character_data(), prompt_map,
_known_character_names(), known_names, runtime_character_hint,
character_override and active_lanlan to make the paired 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 Plus
Run ID: 3d4788e4-e1a6-4839-8350-64dd76db62b2
📒 Files selected for processing (8)
app/main_server.pycard-forge/src/App.jsxcard-forge/src/data/forgedBrawlCards.jslocal_server/card_forge_server/active_neko_context.pylocal_server/card_forge_server/forge_story_generator.pylocal_server/card_forge_server/server.pystatic/app-chat-avatar.jsstop-card-forge.ps1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 841ea486af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
回应 Project-N-E-K-O#1542 第二轮 review: - **CodeRabbit major** `stop-card-forge.ps1`: `[skip]` 日志直接打印别人家进程的完整 CommandLine,可能泄漏 token / 密钥 / 隐私路径。新增 `Get-SafeCommandPreview`, 改为 60 字符截短预览,避免随手粘贴日志泄漏第三方进程参数。 - **CodeRabbit outside-diff** `forgedBrawlCards.js` `saveForgedBrawlCards`: `localStorage.setItem` 在私密浏览/被禁用/quota 超出时会抛 DOMException, 会让铸造主流程跟着崩。改为 try/catch,失败 console.warn 后跳过持久化, 内存里的卡片仍然可用。 - **CodeRabbit outside-diff** `active_neko_context.py` `_known_character_names`: 异常时返回 `set()` 等同于"无白名单",caller 会放行所有 hint —— 违反了校验初衷。 改为返回 `None` 表示"配置不可用",caller 据此保守拒绝 hint/override, 回退到 active_lanlan。空 set 仍表示"配置 0 猫娘",此时不强行校验以不阻挡开发。 - **Codex P2** `App.jsx` `loadForgeMachineSlots`: `exclude_fact_ids` / `exclude_hashes` 以前从整个 forgedInventory 算,但 inventory 可能含跨猫娘卡牌,会让当前猫娘的可用 fact 池被错误地缩水。改为按 `card.sourceCharacter === activeCharacterName` 过滤 (历史无 sourceCharacter 的 旧卡保持向后兼容)。 `npm run build` + `ast.parse` 双通过。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
第二轮 review 4 条都处理(10fe836):
`npm run build` + `ast.parse` 双通过。 Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@start_card_forge.py`:
- Around line 38-53: The SystemExit raised by _ensure_windows() inherits from
BaseException and thus bypasses the existing except Exception handler (around
the entry-point try/except at line 111), so the "Press Enter to close..." prompt
is never shown on non-Windows systems; fix this by changing the entry-point
exception handler to catch BaseException instead of Exception (i.e., replace
except Exception with except BaseException) so SystemExit is caught and the
cleanup/wait prompt runs, or alternatively handle the wait inside
_ensure_windows() and avoid raising SystemExit—locate _ensure_windows and the
try/except at the program entry to apply the 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 Plus
Run ID: 17147a12-30b7-4e8b-b311-32e44c452d2f
📒 Files selected for processing (2)
local_server/card_forge_server/server.pystart_card_forge.py
回应 Project-N-E-K-O#1542 CodeRabbit: `raise SystemExit(...)` 抛的是 BaseException,会跳过 __main__ 块里的 `except Exception`,导致非 Windows 双击运行时少了 "Press Enter to close" 暂停 —— 窗口瞬间关闭,用户根本看不到 macOS/Linux 的手动命令提示。 改成 `raise RuntimeError(msg)`,由 entry 的 `except Exception` 接住, 统一走"打印 + 等回车 + 重抛"路径。同时去掉 msg 里硬编码的 "[startup error]" 前缀,避免和 entry 自己加的前缀重复。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
CodeRabbit 这条小修(fe7ec12): `_ensure_windows` 原来 `raise SystemExit(msg)` 抛的是 BaseException,绕过了 `main` 块里 `except Exception` 的 "Press Enter to close" 分支 —— 双击运行时窗口瞬间关闭。改成 `raise RuntimeError(msg)`,让 entry 的 except 接住,统一走"打印 + 等回车 + 重抛"路径。顺便去掉 msg 里的 `[startup error]` 前缀避免和 entry 自己加的重复。 `ast.parse` 通过。 Co-Authored-By: Claude Opus 4.7 (1M context) |
Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe7ec12433
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
回应 Project-N-E-K-O#1542 codex review on fe7ec12: - **P1** `local_server/card_forge_server/server.py`: 之前在 forge_story_generator.py 里已对 storyLead/prompt/story 等敏感字段加了脱敏 (`_mask_sensitive_text`), 但 server.py 路由层另有一个 `_forge_route_log`,处理 POST 入口请求的日志, 直接 json.dumps storyLead 全文。新增 `_mask_route_sensitive` 让 route log 也走与 generator 对齐的"40 字符预览 + len" 脱敏路径,避免请求级日志泄漏。 - **P2** `app/main_server.py` GET `/card-forge/active-character`: 每次轮询都回 几十 KB 的 base64 dataUrl,但 card-forge 前端 (App.jsx setActiveCharacterName) 只读 `name`。改为默认只返回 `name`,需要 avatar 的调用方显式传 `?include_avatar=true`。5 秒轮询的带宽和 JSON 序列化开销直接砍掉一个数量级。 `ast.parse` 双通过。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Codex 第三轮 2 条都处理(93d461f):
`ast.parse` 双通过。 Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
local_server/card_forge_server/active_neko_context.py (1)
28-35:⚠️ Potential issue | 🟠 Major | ⚡ Quick win修正
safe_character_segment:拦截:以防 Windows 下路径段语义被绕过喵
local_server/card_forge_server/active_neko_context.py里safe_character_segment目前只拦了/,\\,..,\x00,未拦:,在 Windows 下把C:/C:foo这类值当作段拼到memory_dir / lanlan / "facts.json"时会改变拼接语义,导致落点不再稳定约束在memory_dir下喵。🐾 可收敛的修正喵
- if any(part in value for part in ("/", "\\", "..", "\x00")): + if any(part in value for part in ("/", "\\", "..", "\x00", ":")): return None
_build_context在known_names == set()(空集合)时会跳过runtime_character_hint/character_override的白名单校验,直接用runtime_hint or debug_override or active_lanlan作为lanlan喵;由于 facts_path 仍然依赖safe_character_segment(lanlan)才会落盘,所以在补齐:拦截后该“空集合跳过校验”的影响会被显著收敛喵。🤖 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/card_forge_server/active_neko_context.py` around lines 28 - 35, safe_character_segment currently allows ':' which lets Windows-style segments like "C:" or "C:foo" escape memory_dir; update safe_character_segment to also reject any name containing ':' (in the same check that currently rejects "/", "\\", "..", "\x00") so it returns None for strings with ':'; ensure the change is applied to the function named safe_character_segment in active_neko_context.py and consider downstream use in facts_path/_build_context (where lanlan gets passed through safe_character_segment) so that paths remain constrained under memory_dir.
🧹 Nitpick comments (1)
stop-card-forge.ps1 (1)
40-46: ⚡ Quick win建议在截断前先脱敏敏感参数喵~
虽然截断到 60 字符减少了泄露风险,但如果命令行的前 60 字符包含
--token=xxx、--password=yyy这类敏感参数,仍然会被打印到终端喵。参考项目中已有的_mask_sensitive_text模式,建议在截断前先用正则替换敏感参数值喵。🔒 建议的脱敏增强方案喵
function Get-SafeCommandPreview { param([string]$CommandLine, [int]$MaxLength = 60) if (-not $CommandLine) { return "(unknown)" } - $trimmed = $CommandLine.Trim() + # 先脱敏敏感参数,再截断 + $masked = $CommandLine.Trim() ` + -replace '(?i)(--?(token|secret|password|apikey|key)\s*[:=]\s*)(\S+)', '$1***' ` + -replace '(?i)((token|secret|password|apikey|key)=)([^\s&]+)', '$1***' - if ($trimmed.Length -le $MaxLength) { return $trimmed } - return $trimmed.Substring(0, $MaxLength) + "…" + if ($masked.Length -le $MaxLength) { return $masked } + return $masked.Substring(0, $MaxLength) + "…" }🤖 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 `@stop-card-forge.ps1` around lines 40 - 46, Get-SafeCommandPreview currently trims and truncates the command but does not redact sensitive parameters; update Get-SafeCommandPreview to first run the $CommandLine through a masking step (using regex patterns similar to the project's _mask_sensitive_text) to replace values for flags like --token=, --password=, --secret=, -p\s+\S+ and any bearer/token headers with a fixed placeholder (e.g. "<redacted>") and only then apply Trim() and the length truncation logic (honoring the $MaxLength parameter) so no sensitive values appear even if they are within the first 60 characters.
🤖 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 `@card-forge/src/data/forgedBrawlCards.js`:
- Around line 180-190: The current check reads window.localStorage outside the
try/catch and can still throw SecurityError; move the access to
window.localStorage inside the try block and treat missing/throwing cases the
same as other errors: wrap the retrieval of window.localStorage and the
subsequent setItem call in the try, use FORGED_BRAWL_CARDS_STORAGE_KEY and
normalizeForgedBrawlCard as before, and on any exception (including when
localStorage is inaccessible) fall back to the existing console.warn to skip
persistence without breaking createForgedBrawlCard/setForgedInventory flow.
In `@local_server/card_forge_server/active_neko_context.py`:
- Around line 48-67: The current _known_character_names() collapses a missing or
non-dict character_data[5] into an empty set, which then lets callers (e.g.
safe_character_segment()/the runtime_character_hint/character_override
validation) skip checks and accept arbitrary hints; change the function so that
if character_data lacks index 5 or character_data[5] is not a dict it returns
None (signal "whitelist unavailable"), and only return set() when
character_data[5] exists and is a dict (possibly empty) — keep the try/except
but ensure prompt_map is treated as absent vs present to preserve caller
semantics.
---
Outside diff comments:
In `@local_server/card_forge_server/active_neko_context.py`:
- Around line 28-35: safe_character_segment currently allows ':' which lets
Windows-style segments like "C:" or "C:foo" escape memory_dir; update
safe_character_segment to also reject any name containing ':' (in the same check
that currently rejects "/", "\\", "..", "\x00") so it returns None for strings
with ':'; ensure the change is applied to the function named
safe_character_segment in active_neko_context.py and consider downstream use in
facts_path/_build_context (where lanlan gets passed through
safe_character_segment) so that paths remain constrained under memory_dir.
---
Nitpick comments:
In `@stop-card-forge.ps1`:
- Around line 40-46: Get-SafeCommandPreview currently trims and truncates the
command but does not redact sensitive parameters; update Get-SafeCommandPreview
to first run the $CommandLine through a masking step (using regex patterns
similar to the project's _mask_sensitive_text) to replace values for flags like
--token=, --password=, --secret=, -p\s+\S+ and any bearer/token headers with a
fixed placeholder (e.g. "<redacted>") and only then apply Trim() and the length
truncation logic (honoring the $MaxLength parameter) so no sensitive values
appear even if they are within the first 60 characters.
🪄 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: d1321547-040b-4041-877d-5d74a5211a28
📒 Files selected for processing (5)
card-forge/src/App.jsxcard-forge/src/data/forgedBrawlCards.jslocal_server/card_forge_server/active_neko_context.pystart_card_forge.pystop-card-forge.ps1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93d461fccf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
回应 Project-N-E-K-O#1542 第四轮 review: - **CodeRabbit minor** `forgedBrawlCards.js` saveForgedBrawlCards: 访问 `window.localStorage` 本身也可能抛 SecurityError (源被禁用、Safari ITP), 把"探测"也挪进 try 内。 - **CodeRabbit major** `active_neko_context.py` _known_character_names: 之前 `character_data[5]` 不是 dict 时折成空 set,被 caller 当 "0 只猫娘" 跳过白名单校验,把 hint validation 又打开了缺口。改为 malformed shape 直接返 None,跟 exception 路径一起归到"配置不可用,保守拒绝 hint"。 - **CodeRabbit outside-diff** `active_neko_context.py` safe_character_segment: 没拒绝 `:`。Windows 上 `Path('D:/foo') / 'C:bar' / 'facts.json'` 会被 pathlib 直接重置成 `C:bar/facts.json`,绕过 memory_dir 根。把 `:` 加入 禁止字符。 - **Codex P1** `forge_story_generator.py`: `create_chat_llm()` 在 try 外面, 它本身抛异常时 token 不会被 finally 重置,后续 tier 重试会带错误的 active character。把 LLM 构造挪进 try。 - **CodeRabbit nitpick** `stop-card-forge.ps1` Get-SafeCommandPreview: 只截 60 字符不脱敏,前 60 字里若含 `--token=xxx` 仍会落日志。先用正则 把 token/password/secret/api_key/access_key/auth/Bearer/Authorization 替换成 `<redacted>` 再截短。 `npm run build` + `ast.parse` 双通过。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
第四轮 review 5 条都处理(0bb37f3):
`npm run build` + `ast.parse` 双通过。 Co-Authored-By: Claude Opus 4.7 (1M context) |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
local_server/card_forge_server/active_neko_context.py (1)
58-72:⚠️ Potential issue | 🟠 Major | ⚡ Quick win空白白名单状态仍会放行任意角色提示喵。
这里把空
prompt_map继续表示成set(),而_build_context()只在elif known_names:分支做成员校验,所以白名单为空时会跳过校验并接受任意runtime_character_hint/character_override,仍然能把facts.json从active_lanlan挪走,和文件头以及local_server/card_forge_server/server.py:69-85的“默认跟随当前猫娘”契约冲突喵。🐾 建议修正喵
- Return semantics (must be distinguished by caller): - - `None` → whitelist 不可用:config_manager 异常,或 character_data 的 - `prompt_map` 槽位 (index 5) 缺失 / 不是 dict。无法判定"什么算合法猫娘" - 就只能保守拒绝所有 hint / override,回退到 `active_lanlan`。 - - `set()` → 配置可读但里面就是 0 只猫娘 (NEKO 还没初始化完才会到这步)。 - - `{...}` → 正常白名单,hint 必须在集合内。 + Return semantics: + - `None` → whitelist 不可用,或当前没有任何可接受的猫娘名;caller + 必须保守拒绝所有 hint / override 并回退到 `active_lanlan`。 + - `{...}` → 正常白名单,hint / override 必须在集合内。 @@ - prompt_map = character_data[5] - return {str(name).strip() for name in prompt_map.keys() if isinstance(name, str) and name.strip()} + prompt_map = character_data[5] + names = {str(name).strip() for name in prompt_map.keys() if isinstance(name, str) and name.strip()} + return names or None🤖 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/card_forge_server/active_neko_context.py` around lines 58 - 72, The whitelist builder currently returns an empty set for an empty prompt_map which downstream `_build_context()` treats as "no known_names" and thus skips validation, allowing any `runtime_character_hint`/`character_override`; change the logic in the function that calls `config_manager.get_character_data()` (in active_neko_context.py) so that after computing prompt_map and building the set (`{str(name)...}`) you return None when the resulting set is empty (i.e., treat malformed/empty prompt_map the same as unavailable), ensuring `_build_context()` sees None and enforces the fallback to `active_lanlan` instead of accepting arbitrary hints.
🤖 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 `@stop-card-forge.ps1`:
- Around line 37-41: The file contains non-UTF-8-BOM encoding and triggers
PSUseBOMForUnicodeEncodedFile; update stop-card-forge.ps1 to be saved as UTF-8
with BOM (EF BB BF) so PowerShell recognizes the Chinese comments and avoids
encoding warnings—open the script (e.g. where $ErrorActionPreference is set or
the $sensitiveParamPatterns array is declared) and re-save the file in your
editor/CI tooling with UTF-8 with BOM encoding, then commit the re-encoded file.
---
Duplicate comments:
In `@local_server/card_forge_server/active_neko_context.py`:
- Around line 58-72: The whitelist builder currently returns an empty set for an
empty prompt_map which downstream `_build_context()` treats as "no known_names"
and thus skips validation, allowing any
`runtime_character_hint`/`character_override`; change the logic in the function
that calls `config_manager.get_character_data()` (in active_neko_context.py) so
that after computing prompt_map and building the set (`{str(name)...}`) you
return None when the resulting set is empty (i.e., treat malformed/empty
prompt_map the same as unavailable), ensuring `_build_context()` sees None and
enforces the fallback to `active_lanlan` instead of accepting arbitrary hints.
🪄 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: b8c1501d-795a-466c-885f-33191701610f
📒 Files selected for processing (4)
card-forge/src/data/forgedBrawlCards.jslocal_server/card_forge_server/active_neko_context.pylocal_server/card_forge_server/forge_story_generator.pystop-card-forge.ps1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 529c8254e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if needs_persist: | ||
| cm.save_cloudsave_local_state(state) |
There was a problem hiding this comment.
Use the fenced initializer for cache identity
When the card-cache worker first runs while cloudsave_local_state.json is absent, another process can create or update that state after this function loads its default but before this save. The stale full-state write then replaces concurrent sequence numbers, manifest fingerprints, timestamps, and potentially credentials, risking sequence reuse on a later cloud export. Use ensure_cloudsave_client_credentials(), which re-reads under cloud_apply_fence, rather than independently loading and saving the state.
Useful? React with 👍 / 👎.
| return _CloudIdentityLookup(None, 503, "unavailable") | ||
|
|
||
| if response.status_code >= 400: | ||
| failure = "unavailable" if response.status_code >= 500 else "rejected" |
There was a problem hiding this comment.
Preserve sessions when identity checks are rate-limited
When /api/users/me returns a transient 408, 425, or 429, this classifies the response as rejected; resolve_saved_oauth_status() can then delete a legacy session or an OAuth session without a refresh token. The earlier review fixed these statuses for /oauth2/token, but fresh evidence here is that the preceding identity lookup still maps every sub-500 failure to rejected. Classify transient identity-response statuses as unavailable so an auth-status check cannot turn a temporary rate limit into a logout.
Useful? React with 👍 / 👎.
| const applyForgeMachineLoad = useCallback(async () => { | ||
| setForgeMachineLoading(true) | ||
| const result = await loadForgeMachineSlots() | ||
| setForgeMachineSlots(result.slots) |
There was a problem hiding this comment.
Discard stale forge-slot loads
When the active character changes while a facts request is in flight, or the user closes the machine during loading and resetForgeMachine() starts another load, the older request can finish last and unconditionally replace the newer character's slots and clear its loading state. The user can then see and forge a fact belonging to the previously active character. Track a request generation or captured character key and apply these setters only for the latest still-current load.
Useful? React with 👍 / 👎.
| if path.exists(): | ||
| continue # M5:已存在不覆写;M6 加 updated_at 比对再覆写 | ||
| try: | ||
| _write_json_atomic(path, card) |
There was a problem hiding this comment.
Move card-cache writes off the event loop
On the first successful pull, this loop can synchronously create directories, serialize JSON, and write and replace as many as 100 card files. Because the worker is started with asyncio.create_task() on the main server loop, a slow user-data volume or a large batch stalls all HTTP and WebSocket handling until these writes finish. Run the batch's filesystem work through asyncio.to_thread() rather than calling _write_json_atomic() directly.
Useful? React with 👍 / 👎.
| character_reference_data_url = "" | ||
| avatar_url = _card_face_avatar_url(name) if name else "" | ||
| if name and include_avatar: | ||
| card_face_data_url = _read_card_face_data_url(name) |
There was a problem hiding this comment.
Offload card-face encoding from the forge event loop
On an include_avatar=true request after the card face is first loaded or changed, _read_card_face_data_url() synchronously searches the filesystem, reads the entire image, and base64-encodes it in this async route. A large card-face file or slow user-data volume therefore blocks the standalone forge server from handling facts and story-generation requests; move this file read and encoding into asyncio.to_thread() like the main-server snapshot lookup above it.
Useful? React with 👍 / 👎.
* chore: 清理 #1542 合入的两处死代码 - main_logic/agent_event_bus.py:删除 WS-broadcaster seam(_ws_broadcaster / register_ws_broadcaster / broadcast_ws_event)以及 character_runtime 的注册点。 该 seam 当初是为了让 quota dropper / card_drop_router 推 WS 事件时不必 import app.main_server(check_module_layering 报的 L2→L6 倒挂 + plugin→main_logic→app →brain→plugin 循环),但 #1542 内部的 7b62315「退役旧对话掉落直抽路径(券经济 取代)」已经把两个消费者一起删了。现状:broadcast_ws_event 零调用方、零测试, main_logic/ 与 main_routers/ 下已无任何 from app.main_server import,seam 连 「防将来再违规」的用途都不剩。既有 WS 广播(character_runtime 内三处直调 _broadcast_to_all_connected)不受影响。 - utils/document_parser.py:删除 _open_checked_zip 里的 backslash + vbaProject.bin 分支。变异验证确认它不可达——把 raise 换成 canary 后 tests/unit/test_document_parser.py 35 项仍全绿,说明整个套件从未执行到该分支(zipfile 读出的 info.filename 里不含 反斜杠)。删除前后 macro-backslash.docx 用例的错误码一致,宏样本仍由 _reject_macro_members 拦下。 验证:check_module_layering / check_core_contracts / check_prompt_hygiene 退出码均 0; ruff 通过;uv run pytest tests/unit -q → 6790 passed, 26 skipped, 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert: 撤回 document_parser 的删除 — 复核后确认 bot review 是对的 Greptile P1 / Codex P2 都指出删掉 _open_checked_zip 里的 backslash + vbaProject.bin 分支会破坏错误码。复核后确认它们成立,根因比二者描述的更具体: zipfile.ZipInfo.__init__ 里的分隔符规范化是**平台条件**的: if os.sep != "/" and os.sep in filename: filename = filename.replace(os.sep, "/") Windows 上 os.sep == "\\" → 条件成立 → 反斜杠被规范化成正斜杠 → 该分支不可达; POSIX 上 os.sep == "/" → 条件为假 → 反斜杠原样保留 → 该分支可达且必要, 删掉会让 _validate_zip_member_name 先抛 invalid_zip_member, test_rejects_zip_path_traversal_and_xml_entities 的 macro-backslash 用例会红。 实测同一份 _zip_bytes_with_backslash_member 样本: Windows 实际 namelist → [..., 'word/vbaProject.bin'] 模拟 os.sep='/' namelist → [..., 'word\vbaProject.bin'] 我之前的变异验证(把 raise 换成 canary 后 35 项全绿)和全量 pytest 都只在 Windows 上跑,CI 的 unit-tests.yml 也是 windows-latest——「该分支在 Windows 上 从未被执行」这个观察本身成立,但把它推广成「不可达」是错的。项目有 docker 部署,Linux 是真实生产环境。 保留 agent_event_bus 死 seam 的删除:那部分与平台无关,证据独立成立。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* refactor(settings): 移除无人消费的 NEKO_SERVERS_DESKTOP_CLIENT_ID 第二真相源 #1542 在 plugin/settings.py 里定义了 NEKO_SERVERS_DESKTOP_CLIENT_ID,并把它同时 加进 __all__ 与 PUBLIC_SYSTEM_CONFIG_KEYS(后者会经 PLUGIN_SYSTEM_CONFIG_GET 暴露 给任何已装插件读取)。但真正消费这个配置的 main_routers/community_oauth.py:84 是直接读 os.environ,从未 import 过这个常量——两份定义的兜底逻辑还不一致: settings : os.getenv(..., "neko-servers-desktop-dev").strip() or 默认值 community_oauth : 多一层 `raw != "neko-desktop"` 防误配(拒绝复用插件市场 client) 也就是说改 settings 那份不会有任何效果,是个会误导人的埋雷。 方向只能是删 settings 那份,不能反过来让 community_oauth 读它: scripts/check_module_layering.py 定义 L3=main_routers、L4=plugin,低层不能 import 高层(现有 main_routers/ 下确实零个 `from plugin` import)。 - 删除 NEKO_SERVERS_DESKTOP_CLIENT_ID 常量定义、__all__ 条目、 PUBLIC_SYSTEM_CONFIG_KEYS 条目(该值是桌面端社区 OAuth 的 client id, 插件用不到,本就不属于"插件公共配置") - 把原注释里的防误配意图改挂到 NEKO_AUTH_CLIENT_ID 上,并指明该值归 main_routers/community_oauth.py 所有 - 新增 test_desktop_client_id_is_owned_here_and_rejects_plugin_market_client, 把"误配成 neko-desktop 时必须回落"这条意图钉死(原先只有 `assert "neko-desktop" not in body["auth_url"]` 的间接覆盖) 核实:全仓 grep 该 key 只有 community_oauth 读 env、settings 三处定义/注册、 以及 test_community_oauth 的一处 monkeypatch(env)——settings 那份零消费; plugin/tests 里对 PUBLIC_SYSTEM_CONFIG_KEYS 的测试是整体 monkeypatch 替换元组, 不钉具体成员,删一个 key 不受影响。 验证:变异验证——删掉 community_oauth 的 `raw != "neko-desktop"` 后新测试立刻变红, 还原后 29 passed;uv run pytest tests/unit -q → 6791 passed, 26 skipped; uv run pytest plugin/tests -q → 2752 passed, 20 skipped。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style(test): docstring 改英文以过 DOCSTRING_CJK 门(背景说明移到注释) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(warthunder): 数据层 CORS 回到 fail-closed,移除隐式 Origin 兜底 上游 #2371(5dcec54d2)把 wt_server 的 CORS 做成显式白名单 + 默认拒绝: create_http_server(..., cors_origins=()) 默认空集,CLI 提供 --cors-origin。 #1542 在此之上加了一个模块级 _ALLOWED_CORS_ORIGINS 兜底:cors_origins 为空时 回退到主服务端口的 loopback Origin 集合。而两条启动路径都不传 cors_origins (adapters/data_layer_process.py:180 的 in-process 路径、:372-379 的子进程 cmd 只带 --host/--port),所以这个兜底 100% 生效 —— 等于把上游刚定的默认拒绝 改回了默认放行。 数据层没有浏览器消费者::8112 的调用方是 adapters/telemetry_client.py 的 Python HTTP 客户端和 data_layer_process 的 health check,都不受 CORS 约束; plugin/plugins/neko_warthunder/ui/panel.tsx 里 grep 不到任何 fetch/axios/ XMLHttpRequest,它走的是插件后端。所以放行范围应当为空。 改动: - 删除 _read_port_config / _read_main_server_port / _build_allowed_cors_origins 与模块级 _ALLOWED_CORS_ORIGINS(-71 行),连带删掉因此变孤儿的 import platform - _cors() 与 do_OPTIONS() 去掉 `if not allowed_origins: allowed_origins = _ALLOWED_CORS_ORIGINS` 兜底,只认 server.cors_origins - 保留 _cors() 里 `getattr(self, "server", None)` 的防御(裸 handler 可测) 测试:5 个断言兜底行为的用例(only_echoes_approved_neko_origins / uses_configured_main_server_port / accepts_normalized_http_origins_on_port_80 / supports_legacy_main_server_port_env / uses_electron_port_config)换成 3 个 断言 fail-closed 语义的用例:默认不放行任何 Origin、显式配置后只回显白名单内的、 以及一条反回归守卫(模块不得再出现 _ALLOWED_CORS_ORIGINS 等符号)。 验证:变异验证——把兜底加回去后 test_data_layer_cors_is_closed_by_default 与 test_data_layer_cors_has_no_implicit_origin_fallback 两条立刻变红,还原后 6 passed; uv run pytest plugin/tests -q → 2752 passed, 20 skipped; uv run pytest tests/unit -q → 6788 passed, 26 skipped, 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style(test): docstring 改英文以过 DOCSTRING_CJK 门(背景说明移到注释) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
review 反馈(codex P1)。删掉塌回行后, tests/unit/test_memory_language_resolution.py:: test_zh_tw_fact_extraction_reuses_zh_template_body 必然红 —— 它断言的正是 被删掉的那个行为。CI 已经报了。 这是我的疏漏:只跑了自己新建的测试文件,没去找**断言旧行为**的既有测试。 grep 时我搜的是 FACT_EXTRACTION(只命中 memory/facts.py 的一句 docstring), 没搜 getter 名字。 改断言而非删测试,因为那条测试守的东西仍然有效:注释里"nothing prepended that would make the two diverge"防的是 #1542 试过并回退的做法——用简体模板 加一条强制输出繁体的指令来凑。新版本保留这层意图: assert traditional != simplified assert simplified not in traditional # 不许是 zh 正文外面包一层 用例改名为 test_zh_tw_fact_extraction_uses_its_own_template,docstring 写清 断言为什么反转、以及反转后仍在守什么。 ## 影响面复核(这次彻底 grep) - 全 tests/ 搜 zh-TW 与 zh 等值断言:另外两处与本批无关 (test_source_locale 的区域探测、test_topic_llm_enrichment 的 `_select_lang_template`——后者在 main_logic/activity/,有自己正确的 zh-* → zh → en 回落) - 跑遍所有引用 prompts_memory 的 13 个单测文件:744 passed - test_fact_extraction_prompts_resolve_per_locale 同样覆盖 zh-TW,但它断言的 是 `"text"` 与 watermark,繁体模板两者都满足,所以不受影响 Refs #2500 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(memory): 补齐 fact 抽取的繁中 prompt 模板 issue #2500 第 3 步第一批。选 fact 抽取打头是因为它的输出会长期落盘 (facts.json)并进 BM25/embedding 检索——指令语言错了污染的是存量数据, 而不只是一次回复。 ## 调用点早就传全码了 issue 描述里说「fact 抽取回到短码」对这条路径不准确:memory/facts.py:605 和 :2136 一直在传 `get_global_language_full()`。繁中用户的 zh-TW 完整抵达 getter,被塌掉的地方是 `_localized_fact_extraction_prompt` 里一行 per-call-site 的 `template_key = "zh" if lang_key == "zh-TW"` —— 从调用点 看不见。所以本批**不需要改任何传参**,只需补模板 + 删那行(那行的注释是 #2568 留的路标,明确写了"补完模板就删")。 ## 两处翻译约定 - **conversation watermark 保持简体**。`======以下为对话======` / `======以上为对话======` 在全部 7 个既有 locale 里都是简体字面量(en/ja/ko /ru/es/pt 也是),因为它是 runtime 匹配的固定标记而非面向用户的文案 —— developer-notes.md 的 "Prompt watermark" 规则说的就是这个。 - **已知事实池分隔符照常翻译**。AI-aware 模板里那对分隔符每个 locale 都本地化 了,所以繁中版也翻。 台湾用语而非纯字形转换:資訊 / 擷取 / 陣列 / 回傳 / 暱稱 / 螢幕 / 使用者 / 紀錄 / 沉澱 / 通道。JSON 欄位名与枚举值(entity、event_when、user_observation 等)保持 ASCII —— 抽取器要把它们解析回来,翻掉会坏解析而不是改措辞。 ## 测试 新增 tests/unit/test_fact_extraction_zh_tw.py(34 项):模板存在、确实是繁体 (不是 zh 副本也不是 en)、zh 模板未被改动、全码解析到繁体、五种繁中变体 (zh-TW/zh-Hant/zh-HK/tchinese/zh_TW)都命中、四种简中变体仍命中简体、 watermark 在每个 locale 都是简体那对、占位符集合与 zh 一致、机读 token 保持 ASCII、其余 6 个 locale 不受影响。 六条变异全部被抓:恢复塌回行(12 红)、zh-TW 段里 擷取→提取 / 資訊→信息、 watermark 改成繁体、丢掉 {CONVERSATION}、把 entity 翻译掉。 积压从 339 降到 337。 Refs #2500 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(memory): 更新断言 zh-TW 复用 zh 模板的既有契约测试 review 反馈(codex P1)。删掉塌回行后, tests/unit/test_memory_language_resolution.py:: test_zh_tw_fact_extraction_reuses_zh_template_body 必然红 —— 它断言的正是 被删掉的那个行为。CI 已经报了。 这是我的疏漏:只跑了自己新建的测试文件,没去找**断言旧行为**的既有测试。 grep 时我搜的是 FACT_EXTRACTION(只命中 memory/facts.py 的一句 docstring), 没搜 getter 名字。 改断言而非删测试,因为那条测试守的东西仍然有效:注释里"nothing prepended that would make the two diverge"防的是 #1542 试过并回退的做法——用简体模板 加一条强制输出繁体的指令来凑。新版本保留这层意图: assert traditional != simplified assert simplified not in traditional # 不许是 zh 正文外面包一层 用例改名为 test_zh_tw_fact_extraction_uses_its_own_template,docstring 写清 断言为什么反转、以及反转后仍在守什么。 ## 影响面复核(这次彻底 grep) - 全 tests/ 搜 zh-TW 与 zh 等值断言:另外两处与本批无关 (test_source_locale 的区域探测、test_topic_llm_enrichment 的 `_select_lang_template`——后者在 main_logic/activity/,有自己正确的 zh-* → zh → en 回落) - 跑遍所有引用 prompts_memory 的 13 个单测文件:744 passed - test_fact_extraction_prompts_resolve_per_locale 同样覆盖 zh-TW,但它断言的 是 `"text"` 与 watermark,繁体模板两者都满足,所以不受影响 Refs #2500 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
本 PR 已扩展至三块(按维护者要求统一收敛在一个 PR 内迭代):
dfb78fa→b30deff,9 个 commit + 多轮 CodeRabbit / Codex review fix):把卡牌铸造拆为独立子模块(Vite + React + Tailwind 前端 + FastAPI 后端 + 主服务桥接 hook)。6a5419b→3dadfda):为配合 N.E.K.O.Servers#1 的 M1 / M2 / M5 里程碑追加的 NEKO 端 endpoints / worker / 入口,全部默认禁用,环境变量启用,零侵入 NEKO 核心。de97867→874821b,9 个 commit):把「对话里掉落卡片 → 开卡演出 → 收集」做成端到端闭环,补齐 NEKO↔猫娘社区 登录(邮箱密码 + Steam),并修好本 PR Part 2 社交按钮落地时遗留的三处问题 + bind-client 如实反馈。配套云端 PR:N.E.K.O.Servers#4。card-forge 主线 / 三体融合配套 / 对话掉落在文件层面基本互不重叠(Part 3 的「方案A」修复会触及 Part 2 的社交按钮入口)。
Part 1 — 「奇遇铸造机」card-forge 模块
把卡牌铸造功能拆为 NEKO 的一个独立子模块,可单独启用,不依赖任何原型代码。
frontend/card-forge/(Vite + React + Tailwind):奇遇铸造机面板 + 铸造卡仓库local_server/card_forge_server/(FastAPI):facts 抽取 + LLM 卡牌故事生成app/main_server/web_app.py:新增 2 个/card-forge/active-character路由,广播当前猫娘名static/app/app-chat-avatar.js:头像捕获/恢复/IPC 注入时把猫娘名推给铸造后端,静默失败不影响主前端模块结构
前端
frontend/card-forge/(端口 5173)/card-forge/active-character轮询当前猫娘名作为runtime_character_hint后端
local_server/card_forge_server/(端口 3001)GET /forge/facts:从当前猫娘 active facts.json 抽取 5 条候选事实POST /forge/card-story:用 NEKO 核心 LLM 配置 (summary / agent) 把 storyLead 生成卡牌专属小故事facts.json/facts_archive.json,不修改 NEKO 核心utils.llm_client.create_chat_llm(),不在本模块硬编码 OpenAI / Gemini / DeepSeek 等服务商主服务桥接
app/main_server/web_app.py新增 2 个路由:POST /card-forge/active-character:由 NEKO 主前端在头像捕获后调用GET /card-forge/active-character:供 card-forge 轮询static/app/app-chat-avatar.js新增 1 个函数 + 4 个调用点:syncAvatarToCardForge()在applyPreviewResult、init 内存/storage 路径、setExternalAvatar处推送启动方式
URL:
停止:
.\scripts\card-forge\stop-card-forge.bat生产构建走仓库根的
build_frontend.bat/build_frontend.sh(已包含 card-forge)。私密性
facts 含个人化内容,请勿把铸造后端暴露到公网;日志不打印完整 fact text。
Part 2 — 三体融合 N.E.K.O.Servers 配套(M1 / M2 / M5)
M1-j —
/system/client-id+/system/social/config(commit6a5419b,f2a9dae)复用
main_routers/system_router/(已拆为包)现有/system/status风格(同_set_no_store_headers+_get_system_config_manager,无新依赖):GET /api/system/client-id返回 NEKO 持久client_id(来自state/cloudsave_local_state.json,缺失时按build_default_cloudsave_local_state生成并持久化);云端POST /api/clients/register消费它,把游客 / 登录态资产关联到稳定设备身份GET /api/system/social/config返回云端 base URL(默认http://localhost:8080;环境变量NEKO_SOCIAL_BASE_URL覆盖),避免前端把云端 URL 硬编码M2-h/i/j — Pet 社交入口 + facts_sync worker + 配额掉落(commit
fddd4e6)screen按钮改为 social 入口(条件:用户登录 + 云端可达);点击 → 浏览器打开云端 feedmain_logic/facts_sync/sync_worker.py后台任务,把本地facts.json推到 ServersPOST /api/facts/sync(默认禁用,envNEKO_FACTS_SYNC_ENABLED=1启用)main_logic/quota/dropper.py,本地 quota_rules.yaml + 云端 drop-hint 共同决定 Pet UI 是否触发掉落动画M5-g — 卡片本地缓存 puller(commit
3dadfda)main_logic/card_cache/puller.py后台 worker,定期把云端GET /api/cards/mine拉到本地state/card_cache/,让 card-forge 仓库页可离线查看自己铸造的羁绊卡验证
Part 3 — 对话掉落卡片闭环 + 社区登录 + 遗留修复(commit
de97867→874821b)瘦客户端:NEKO 只管 ① 对话触发掉落 ② 从本地记忆给 5选1 候选 ③ 显示开卡演出;卡片生成(稀有度 / 编号 / 故事 / 卡面)+ 卡册都在云端(配套 N.E.K.O.Servers#4)。
对话掉落开卡演出(P1 / P2 + 瘦客户端重构)
main_routers/card_drop_router.py:GET /api/card-drop/candidates本地读memory/<角色>/facts.json偏重要加权抽 5(不足用预设补满,不走云端);POST /api/card-drop/draw代理云端/api/cards/draw补X-Client-Id(登录则带 JWT)。static/forge-drop-overlay.js+static/forge-drop-tokens.js(样式由脚本内联注入,无独立 css 文件):居中模态 5选1 → 凝结 → 按稀有度揭晓演出(多重叮 / 屏闪 / 震屏 / 粒子 +No.编号+ 云端生成卡面);app-websocket.js收 WScard_drop_available分发,dropper 触发时 emit。body{pointer-events:none}点击穿透导致模态点不动(.cd-backdrop显式pointer-events:auto+ 高 z-index)。卡册改去猫娘社区 web 看,移除 Electron 卡册。社区登录(chunk2 邮箱 + chunk3 Steam)
/api/card-drop/{auth-status,login,register,logout}:登录态存~/.../N.E.K.O/community_auth.json;login/register 成功存 JWT + bind-client 迁移游客卡;draw 带Authorization→ 卡归账号。redirect_to+ 严格 localhost 白名单):/api/card-drop/steam-login返回云端 authorize URL(带redirect_to=本机回调);/api/card-drop/steam-callback消费一次性 pending(防会话固定)→ 拉/api/users/me→ 存会话;前端「🎮 用 Steam 登录」按钮electronShell.openExternal开浏览器 + 轮询 auth-status。{bound,error}经 auth-status / login / steam-callback 透出,前端显示绿(已存入)/ 琥珀(此设备已绑到别的账号,卡留在原账号)。「方案A」— Part 2 社交按钮三处遗留修复
Part 2(M2-h)把 Pet 的
screen槽位改成「猫娘社区」社交按钮,落地时留了三个问题,一并修掉:app.socialOpenFailed/socialUnavailable/socialDisabled+ 递增LOCALE_VERSION触发客户端缓存刷新(否则弹原始 key)。socialOpenFailed—— 改用既有electronShell.openExternal在系统默认浏览器开社区(原openOrFocusWindow对跨源外站抛错)。screen槽位、且 decisions 里「并入 mic popup」从没真正实现 → 把屏幕共享开关加进语音 popup 二级菜单首项 + 修toggleScreenShare读#screenButton。验证
浏览器(Claude_Preview 驱动真 NEKO :48911 + docker 云端)端到端验过:开卡演出全档配色 / 号段正确;社区按钮 browser / electron 两路径开正确 URL;mic popup 屏幕共享开关渲染 / 置灰 / 录音态启用;邮箱登录卡真迁入账号(云端
/cards/mineowner_kind=user);同设备换号登录 → 409 → 琥珀告警;Steam 按钮全链路(steam-login → openExternal → 轮询入册);i18n 三 key 中文 + 插值;零 console 报错。Steam OpenID 验签那一跳需真STEAM_API_KEY+ 真 Steam 账号,未端到端验。Test plan
cd frontend/card-forge && npm install && npm run build通过build_frontend.sh/build_frontend.bat含 card-forge 段,产出frontend/card-forge/dist/index.htmlscripts\card-forge\start-card-forge.bat3 窗口启动正常redirect_to白名单容器内验STEAM_API_KEY+ 真账号)Co-Authored-By: Claude Opus 4.7 (1M context)
Summary by CodeRabbit
回归报告 / Regression Report
补充:文件结构收敛(commit
07f3e39)card-forge/从仓库根移到frontend/card-forge/,并把它的 vite 构建接进build_frontend.sh/build_frontend.bat;4 个一键启停脚本从根目录移到scripts/card-forge/;删除design-qa.md(一次性 QA 记录,内容是另一台机器的绝对路径)与无引用的static/sounds/forge/rarity-sr.mp3(运行时、README 和契约测试统一使用rarity-sr.wav)。frontend/、由build_frontend.*构建」的既有约定;收敛后仓库根恢复到本分支之前的状态。/card-forge/active-character等 HTTP 路由未改动;stop-card-forge.ps1是端口/进程匹配、无相对路径依赖。跟随更新的有 3 个静态契约测试的路径断言、3 个语种的docs/**/guide/project-structure.md,以及local_server/card_forge_server/README.md里指向脚本新位置的说明。start_card_forge.py的PROJECT_ROOT由.parent改为.parents[2]、start-card-forge.bat改用for %%I in ("%~dp0..\..")解析仓库根,两者路径解析错误会导致一键启动失效(Windows 侧需实机确认)。另外 desktop build workflow 会因此新增一次 card-forge 的npm ci+vite build,而该产物目前没有消费方,跟踪见 card-forge 接入 build_frontend 后构建产物没有消费方,桌面包里拿不到 #2483。check_module_layering/check_docs_no_relative_paths/check_core_contracts/check_no_tkinter/check_docstring_no_cjk五个门禁脚本通过;ruff 通过;bash -n build_frontend.sh通过;新路径下实测npm test(3 例)与npm run build(产出dist/index.html)正常。不拆分理由 / Why Not Split
本 PR 已经按三个阶段在同一贡献者分支中连续演进:card-forge 独立模块、N.E.K.O.Servers M1/M2/M5 配套、对话掉落卡片闭环与社区登录。后两部分依赖前一部分的 client_id、社交入口、当前猫娘同步和卡片展示契约;拆成多个 PR 会导致跨仓 N.E.K.O.Servers 配套和本 PR 的前后端契约无法在同一分支上端到端验证。当前 PR 保持 draft,统一收敛 review/CI 后再进入最终评审。
Note
High Risk
Large cross-cutting integration (main startup workers, memory locale replay, OAuth/card-drop proxies, CI gates) with optional cloud paths; misconfiguration or worker lifecycle bugs could affect startup, memory processing, or social flows.
Overview
Adds the card-forge React/Vite app and wires it into NEKO through main-server routes, build scripts, and docs, plus broader CI coverage for the new card-drop, OAuth, facts-sync, and cache contract tests.
Main server exposes
/card-forge/active-character(CORS-gated GET/POST for the forge UI’s runtime catgirl hint), mountscard_drop_routerand community OAuth routers, whitelists that path during storage limited-mode, and starts/stops facts_sync and card_cache background workers after runtime init and on shutdown. Agent event bus now registers the app WebSocket broadcaster fromcharacter_runtimeso quota/card-drop paths can fan out without importingapp.main_server.Memory pipeline accepts optional
languageon/cache,/process,/renew, and/settle; activates request locale and persists it on post-turn outbox replay. Fact-extraction prompts gain explicit per-locale JSONtextoutput rules.Runtime bindings install quota dropper hooks (noop unless env-enabled).
config/quota_rules.yamldefines local UX drop rule defaults. Minor logging tweaks on limited-mode guards and agent-event errors.Reviewed by Cursor Bugbot for commit b9eeafb. Bugbot is set up for automated code reviews on this repo. Configure here.