feat: card-forge 模块 + 三体融合 NEKO 端配套 (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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Walkthrough新增 Card-Forge 奇遇铸造机完整子系统与配额掉落规则引擎:前端 React 应用含卡牌数据持久化与铸造 UI;后端 FastAPI 服务实现 facts 选择、LLM 故事生成与角色上下文管理;新增 Facts/卡片同步后台 worker;配额规则引擎实现关键词与字数触发、云端掉落提示;主服务集成头像同步端点与社交功能;浮动按钮改为社交平台入口。 ChangesCard-Forge 奇遇铸造机系统
配额掉落规则引擎与云端同步
Facts 增量同步与卡片缓存 Worker
UI 社交功能集成
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
|
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".
| systemPrompt=system_prompt, | ||
| userPrompt=user_prompt, |
There was a problem hiding this comment.
Remove full prompt/response text from forge story logs
The generator currently logs systemPrompt and userPrompt verbatim, and nearby code also logs raw model output, which includes the original memory fact text and character persona content. In production this leaks private user memories into console logs and any log collectors/crash bundles, directly contradicting the module’s stated privacy goal of not printing full fact text. Please redact these fields (log IDs/lengths only) before emitting diagnostics.
Useful? React with 👍 / 👎.
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
回应 Project-N-E-K-O#1542 CodeRabbit `PSUseBOMForUnicodeEncodedFile`: 文件含中文注释和字符串字面量,但首字节 `24 45 72` (`$Er...`) 没 BOM。 PowerShell 5.1 (Windows 10/11 默认) 在无 BOM 时按系统 ANSI (中文环境通常 GBK) 解析 .ps1,会把 UTF-8 的中文读成乱码,影响中文 Write-Host 输出和 注释 / 字符串字面量正确解析。 加 `EF BB BF` BOM,文件内容不变。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
CodeRabbit PSUseBOMForUnicodeEncodedFile 这条已加 BOM(b30deff): `stop-card-forge.ps1` 首字节加上 `EF BB BF`,内容不变。PowerShell 5.1 (Win10/11 默认) 无 BOM 时按 ANSI (中文环境通常 GBK) 解析 .ps1,会把 UTF-8 中文读成乱码。 (同批的 `forgedBrawlCards.js` / `active_neko_context.py` 两条是之前 `0bb37f3c` 已修的回声,行号飘移导致 CodeRabbit 在旧建议下重新贴 —— 已经在那个 commit 里改完,不重复处理。) Co-Authored-By: Claude Opus 4.7 (1M context) |
给 N.E.K.O.Servers(云端社交平台)和 NEKO-PC 用: - GET /api/system/client-id 返回持久 client_id(来自 state/cloudsave_local_state.json);缺失时按 build_default_cloudsave_local_state 生成并持久化。N.E.K.O.Servers 的 POST /api/clients/register 会消费这个值,把游客 / 登录态资产关联到稳定的设备身份。 - GET /api/system/social/config 返回社交服务 base URL(默认 http://localhost:8080;通过环境变量 NEKO_SOCIAL_BASE_URL 覆盖部署地址)。避免前端硬编码云端 URL。 配套契约见 N.E.K.O.Servers 仓库 .claude/contracts/api-endpoints.md 与 0N.E.K.Oserver/.claude/initial-plan.md "整体架构 / 通信" 段。 复用现有 system_router 的 _set_no_store_headers + _get_system_config_manager helper,与 /api/system/status 同款风格。无新依赖。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@main_routers/system_router.py`:
- Around line 700-705: The exception branch is returning JSONResponse directly
(JSONResponse(...)) which bypasses the no-store header previously set and may
allow caching; change the except block to use the project helper that enforces
no-cache (call _json_no_store_response(...) with the same payload and
status_code) or, if helper unavailable, call _set_no_store_headers on the
response before returning; update the except handler in system_router.py (the
block catching Exception as exc) to return _json_no_store_response({"ok": False,
"error": f"{type(exc).__name__}: {exc}"}, status_code=500) or ensure
JSONResponse has no-store headers applied.
- Around line 717-722: The code reads NEKO_SOCIAL_BASE_URL into base_url and
only calls rstrip("/"), which leaves an all-whitespace env var as an empty
string but still returns enabled=True; update the logic where base_url is
computed (the base_url assignment) to first call strip() on the env var, fall
back to the default "http://localhost:8080" when the stripped value is empty,
then apply rstrip("/") before returning so social_base_url is never an
empty/whitespace string while preserving existing trailing-slash trimming.
🪄 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: cf37aafe-35b3-4d13-a353-9d232bba0a6d
📒 Files selected for processing (1)
main_routers/system_router.py
- /system/client-id 异常分支改走 _json_no_store_response,确保 500 响应
也带 no-store 头(与 200 路径一致;避免被反代/浏览器缓存)
- /system/social/config 对 NEKO_SOCIAL_BASE_URL 空白值做兜底:
strip().rstrip("/") 后为空则回退默认 http://localhost:8080,避免
前端拿到不可用空地址
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
为 N.E.K.O.Servers PR #1(M2 facts/forge/quotas)配套的 NEKO 端三组改动, 默认禁用,需通过环境变量显式启用,不破坏现有体验。 ## M2-h 社交入口(避免新增按钮 → 替换原 screen 槽位) - `static/avatar-ui-buttons.js`:getDefaultButtonConfigs 第 354-362 行 screen → social - id='social', emoji='👥', title='猫娘网络', hasPopup=false(与 goodbye 同款 click 模式) - `static/vrm-ui-buttons.js` + `static/mmd-ui-buttons.js`:count helper 同步改 'screen' → 'social' - `static/live2d-ui-buttons.js`:删除 screen 特殊检查(dead branch) - `static/avatar-ui-buttons.js` _syncButtonStatesWithGlobalState:删除 screen active 同步 (屏幕分享状态现在仅由聊天界面底部 toolbar 的 #screenButton 维护,悬浮按钮组不再追踪) - `static/app-ui.js`:监听 `live2d-social-click`:fetch /api/system/social/config 拿云端 base URL + fetch /api/system/client-id 拿设备身份 → window.openOrFocusWindow 开独立窗口 注:screen 屏幕分享功能保留在 chat.html 底部 toolbar 的 #screenButton 上不受影响。 ## M2-i facts_sync worker(main_logic/facts_sync/sync_worker.py) - 默认禁用:NEKO_FACTS_SYNC_ENABLED=0;启用要同时配 NEKO_SOCIAL_BASE_URL - 5 分钟 sweep 一次:扫 memory/<lanlan>/facts.json,过滤 private=true / importance<0.5 - 单批 ≤50 条,<memory>/<lanlan>/facts_sync_state.json 维护"已同步 hash"集合避免重发 - 失败 5 次的 hash 落到 facts_sync_pending.jsonl + 标记放弃同步,避免无限重试 - main_server.on_startup 用 asyncio.create_task 启动 worker,首次延迟 30s 避开启动忙时 - 详细契约见 N.E.K.O.Servers/.claude/contracts/facts-sync-schema.md ## M2-j 配额掉落引擎(main_logic/quota/) - config/quota_rules.yaml — word_count(800 字符触发)+ keywords(铸卡/纪念/奇遇/留念) + emotion(默认禁用,留位) - dropper.py — on_text_message hook(必须返回 None 不抢 first-hit-wins 链)+ on_utterance - ux_state.py — 本地 quota_ux_state.json(跨日自动 reset;持有 cooldown 时间戳 + 累计字数 + 今日已掉落次数) - cloud_sync.py — POST /api/quotas/drop-hint 异步出站,sha256 idem_key 防重,fire-and-forget - 默认禁用 NEKO_QUOTA_DROPPER_ENABLED=0;只挂 hook 不真触发 - app/runtime_bindings.py 新增 quota_hooks 块(与 user_directives_sink 同款幂等装配) ## 启用方式(部署时设环境变量) ``` NEKO_SOCIAL_BASE_URL=https://your-servers.example.com NEKO_FACTS_SYNC_ENABLED=1 NEKO_QUOTA_DROPPER_ENABLED=1 ``` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@main_logic/facts_sync/sync_worker.py`:
- Around line 169-175: The code calls the blocking helper _read_json directly
from an async context (e.g., reading facts_path and state_path into facts_data
and state) which triggers ASYNC_BLOCK; offload those file IO calls to a
threadpool (use asyncio.to_thread or loop.run_in_executor) so reads/writes for
_read_json (and the corresponding write ops later in the same function around
lines handling facts_data/state and the section noted 207-220) run off the event
loop; update usages that reference facts_path, state_path, facts_data and state
to await the to_thread call and ensure any subsequent logic uses the awaited
results.
In `@main_logic/quota/dropper.py`:
- Around line 77-90: The keyword branch currently triggers _maybe_drop(...) but
does not prevent the subsequent word_count accumulation
(ux_state.add_word_count), so keyword hits still increment word_count; add logic
to skip word_count when a keyword matched: introduce a local flag (e.g.,
matched_keyword = False), set it True when a keyword is hit inside the kw_list
loop (where _maybe_drop is called), then after that keyword block check the flag
and if True skip/return before executing the word_count branch (or guard the
ux_state.add_word_count call with if not matched_keyword). Apply the same
flag/guard to the other keyword block noted (lines ~91-101) so keyword hits
never increment word_count.
In `@static/app-ui.js`:
- Around line 2167-2170: The code uses the ternary pattern "window.t ?
window.t('app.socialUnavailable') : 'fallback'" which can leak the raw i18n key
to users if window.t returns a falsy value; change each occurrence to use the
safe fallback pattern "(window.t && window.t('app.socialUnavailable')) ||
'社交服务不可用 (config fetch failed)'" (i.e., use window.t && window.t(...) then ||
fallback) for the shown call to window.showStatusToast and apply the same
replacement for the other occurrences that use window.t (replace their keys with
the same pattern) so the fallback string is used whenever translation is missing
or falsy.
🪄 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: be3a16b3-e895-4a68-a696-673565fb4d7c
📒 Files selected for processing (14)
app/main_server.pyapp/runtime_bindings.pyconfig/quota_rules.yamlmain_logic/facts_sync/__init__.pymain_logic/facts_sync/sync_worker.pymain_logic/quota/__init__.pymain_logic/quota/cloud_sync.pymain_logic/quota/dropper.pymain_logic/quota/ux_state.pystatic/app-ui.jsstatic/avatar-ui-buttons.jsstatic/live2d-ui-buttons.jsstatic/mmd-ui-buttons.jsstatic/vrm-ui-buttons.js
✅ Files skipped from review due to trivial changes (2)
- main_logic/facts_sync/init.py
- main_logic/quota/init.py
配合 Servers M5(reward + 通知 + tokens)的本地缓存层。
默认禁用:NEKO_CARD_CACHE_ENABLED=0;启用需同时配 NEKO_SOCIAL_BASE_URL。
main_logic/card_cache/{__init__, puller}.py:
- 5 分钟 sweep 一次,60s 启动延迟
- GET /api/cards/mine?limit=100(含 X-Client-Id header)
- 新卡片写到 memory/<lanlan>/cards/<id>.json(原子 tmp+rename)
- 已存在的不覆写(M5 简化版;M6 引入 since/ETag 增量)
- lanlan_name 路径 sanitize 防 directory traversal
- 401(未注册 client)安静日志;其他失败下次重试
app/main_server.py:
- on_startup hook 加 asyncio.create_task(start_card_cache_puller())
- 与 facts_sync 同款 lazy import + try/except 模式
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 `@main_logic/card_cache/puller.py`:
- Around line 108-114: The code currently inserts untrusted card_id and
insufficiently-sanitized lanlan_name into the filesystem path (variables
card_id, lanlan_name, safe_lanlan, path), allowing directory-traversal and other
path injection; fix by strictly sanitizing both values: canonicalize/normalize
and reject or transform any path separator characters ("/", "\", ".."), Windows
special chars like ":", control/whitespace, and empty/"." values (replace with a
safe default), restrict to a safe charset (e.g. alphanumerics, "-", "_") and max
length, then build the filepath and assert it resides under memory_dir (resolve
both and ensure path.resolve().is_relative_to(memory_dir.resolve()) or
equivalent) before writing. Ensure the sanitization logic is applied where
safe_lanlan is created and before using card_id in f"{card_id}.json" so path
cannot escape the intended memory_dir/cards/<safe_lanlan>/ file tree.
🪄 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: 16819ce6-4acb-4459-82f8-941bfcf07674
📒 Files selected for processing (3)
app/main_server.pymain_logic/card_cache/__init__.pymain_logic/card_cache/puller.py
✅ Files skipped from review due to trivial changes (1)
- main_logic/card_cache/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
- app/main_server.py
修复两个真实 bug,让 facts_sync_worker 首次部署能跑通: 1. **401 unauthenticated**:worker 启动直接 push facts,但 client_id 没在 Servers clients 表里注册过 → X-Client-Id 鉴权拒绝。修复:在 _sweep_once 开头加 _ensure_client_registered,幂等地 POST /api/clients/register, 缓存成功状态避免每轮重复。 2. **422 importance > 1.0**:NEKO 端 importance 是 0-10 int 评分 (memory/facts.py safe_importance default=5),Servers schema 限 [0,1] float → 422。修复:在 _select_unsynced_facts 里把 > 1.0 的 importance 归一化 ÷10 并 clamp 到 [0,1];MIN_IMPORTANCE 阈值从 0.5 调整为 5.0(NEKO scale 中等)。 L3-full e2e 验证:YUI 1 条 + 水水shui 27 条 facts,0 个 422,importance 范围 [0.5, 0.9],Servers facts 表 28 行写入成功。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
main_logic/facts_sync/sync_worker.py (1)
214-220:⚠️ Potential issue | 🟠 Major | ⚡ Quick win把这些磁盘 IO 挪到线程池里喵。
Line 214 和 Line 220 还在 async 路径里直接调
_read_json(...),而同一协程里的 Line 252 和 Line 264 也还是同步写文件;这已经触发 ASYNC_BLOCK,CI 会继续红,而且 sweep 期间会把事件循环卡住喵。🐾 建议改法喵
- facts_data = _read_json(facts_path, []) + facts_data = await asyncio.to_thread(_read_json, facts_path, []) @@ - state = _read_json(state_path, {"synced": [], "failed_counts": {}}) + state = await asyncio.to_thread( + _read_json, state_path, {"synced": [], "failed_counts": {}} + ) @@ - _append_jsonl(pending_path, { - "fact_hash": h, - "text_preview": f["text"][:80], - "failed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), - }) + await asyncio.to_thread( + _append_jsonl, + pending_path, + { + "fact_hash": h, + "text_preview": f["text"][:80], + "failed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + }, + ) @@ - _write_json_atomic(state_path, state) + await asyncio.to_thread(_write_json_atomic, state_path, state)Also applies to: 252-264
🤖 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 `@main_logic/facts_sync/sync_worker.py` around lines 214 - 220, The coroutine is performing blocking disk IO: calls to _read_json(facts_path, ...) and _read_json(state_path, ...) and the later synchronous file writes must be moved off the event loop; wrap these blocking operations with asyncio.to_thread (or loop.run_in_executor) and await them so reads/writes run in a threadpool. Concretely, replace direct calls to _read_json for facts_path and state_path and the synchronous state/write operations later in the same async function with awaited asyncio.to_thread(...) invocations (referencing _read_json, facts_path, state_path and the state write logic) so all disk IO is executed in the threadpool. Ensure return values are awaited and exceptions propagated.
🤖 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.
Duplicate comments:
In `@main_logic/facts_sync/sync_worker.py`:
- Around line 214-220: The coroutine is performing blocking disk IO: calls to
_read_json(facts_path, ...) and _read_json(state_path, ...) and the later
synchronous file writes must be moved off the event loop; wrap these blocking
operations with asyncio.to_thread (or loop.run_in_executor) and await them so
reads/writes run in a threadpool. Concretely, replace direct calls to _read_json
for facts_path and state_path and the synchronous state/write operations later
in the same async function with awaited asyncio.to_thread(...) invocations
(referencing _read_json, facts_path, state_path and the state write logic) so
all disk IO is executed in the threadpool. Ensure return values are awaited and
exceptions propagated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 73670936-d869-4483-892a-3254c4f1644b
📒 Files selected for processing (1)
main_logic/facts_sync/sync_worker.py
CodeRabbit Major (Duplicate) + NEKO repo CI async-blocking 红线指出: _sync_one_lanlan() 是 async 协程,但 4 处磁盘 IO 都是同步调用直接卡事件循环。 把 4 处都包到 asyncio.to_thread 走线程池: - L214: _read_json(facts.json) - L220: _read_json(facts_sync_state.json) - L252: _append_jsonl(pending.jsonl) — 5 次失败累计的 fact 落盘 - L264: _write_json_atomic(facts_sync_state.json) `facts_path.exists()` 那行的 Path.stat() 也是同步但 check_async_blocking.py 本地跑过 0 退出码不报,先不动;如果上 CI 后真红再补。 本地验证: - scripts/check_async_blocking.py 0 退出 - ruff check 全过 - 不改动函数签名 / 调用方,行为对调用者透明 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M6-h 端到端补完最后一段。NEKO-PC 配套 PR Project-N-E-K-O#134 已加 contextBridge 把 main 进程 的 social SSE 事件暴露为 window.nekoSocial.{onUnreadCount, onNotifyIncoming, onQuotaChanged, onDropAnimation, onConnState}。本 commit 在 NEKO Pet frontend 监听这些桥,画出实际可见的 UI。 新增 static/app-social-ui.js: 1. **红点 badge** — 监听 onUnreadCount,找 [id$="-btn-social"] 通配 selector 匹配 vrm/mmd 两种 manager 的 social 按钮,在 btnWrapper 上 append 红色 圆形 badge(右上角 -4px,min-width 18px,>99 显示"99+")。首次 unread 触发时 按钮可能还没渲染,500ms × 30 次 retry 直到画上或放弃。 2. **新通知 toast** — 监听 onNotifyIncoming,简短 "📩 新消息" toast(沿用现有 window.electronToast / showToast)。 3. **DROP_ANIMATION** — 监听 onDropAnimation,body 顶部从右上角飘下 🪙 emoji, CSS @Keyframes 240px 下坠 + 360° 旋转 + 淡出,1.6s 完成。payload.delta > 1 时连播 delta 个(最多 5 个,120ms 错开)。keyframes 懒注入到 <head>。 4. **QUOTA_CHANGED** — 不主动画 UI,snapshot 挂到 window.__nekoSocialQuotaSnapshot 留给后续 quota panel 用。 5. **CONN_STATE** — 仅 console.info/warn + 挂 window.__nekoSocialConnState 调试用。 宿主检测:非 NEKO-PC Electron 环境 window.nekoSocial 不存在,整体早退 noop, 浏览器直开 NEKO frontend 不破坏。 index.html 在 avatar-ui-buttons.js (line 302) 之后引入 app-social-ui.js。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 `@static/app-social-ui.js`:
- Around line 103-107: The current onUnreadCount handler only retries painting
when paintBadge fails, so buttons that mount after the unread event (e.g.,
elements with class '-btn-social') miss the badge; to fix, keep cachedUnread
as-is but also ensure newly added social buttons are painted immediately by
adding a MutationObserver (or mounting hook) that watches for added nodes
matching '.-btn-social' and calls paintBadge(cachedUnread) (and if paintBadge
returns false, call scheduleRetry() as before); update
window.nekoSocial.onUnreadCount, paintBadge usage, and maintain scheduleRetry
logic so late-mounted buttons get the current badge.
🪄 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: 86243289-5c06-4b8b-a659-2f284d7ad4c1
📒 Files selected for processing (2)
static/app-social-ui.jstemplates/index.html
CodeRabbit 在 Project-N-E-K-O#1542 指出 app-social-ui.js 的未读红点在"按钮后挂载"场景漏画: onUnreadCount 原本只在 paintBadge 失败(无任何 -btn-social)时才 scheduleRetry, 一旦有一个按钮已挂载就 return true,后挂的另一个按钮(vrm/mmd manager 切换 / 延迟渲染重建)拿不到当前未读数,要等下一次 unread 事件才补。 把 500ms × 30 次轮询换成 MutationObserver: - onUnreadCount 缓存 cachedUnread 并立即 paintBadge(按钮已在场即画)。 - MutationObserver 监听 document.body subtree 的 childList,新增节点匹配或 包含 [id$="-btn-social"] 时用 cachedUnread 补画。social 按钮的 id 在插入 DOM 前就已设好(avatar-ui-buttons.js:419 → vrm-ui-buttons.js:186 先 btnWrapper.appendChild(btn) 再 buttonsContainer.appendChild(btnWrapper)), 所以 querySelector 在挂载瞬间即可命中,挂载/重挂都能补画。 observer 比原轮询更稳:不依赖 15s 窗口,按钮任何时刻重挂都能补画,且去掉了 setInterval 轮询。app-social-ui.js 只在 Pet index.html 加载,Pet DOM 轻量, subtree 监听开销可忽略。 node --check 通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <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 核心。card-forge 主线与三体融合配套在文件层面不重叠,互不影响。
Part 1 — 「奇遇铸造机」card-forge 模块
把卡牌铸造功能拆为 NEKO 的一个独立子模块,可单独启用,不依赖任何原型代码。
card-forge/(Vite + React + Tailwind):奇遇铸造机面板 + 铸造卡仓库local_server/card_forge_server/(FastAPI):facts 抽取 + LLM 卡牌故事生成app/main_server.py:新增 2 个/card-forge/active-character路由,广播当前猫娘名static/app-chat-avatar.js:头像捕获/恢复/IPC 注入时把猫娘名推给铸造后端,静默失败不影响主前端模块结构
前端
card-forge/(端口 5173)/card-forge/active-character轮询当前猫娘名作为runtime_character_hint后端
local_server/card_forge_server/(端口 3001)GET /arena/forge-facts:从当前猫娘 active facts.json 抽取 5 条候选事实POST /arena/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.py新增 2 个路由:POST /card-forge/active-character:由 NEKO 主前端在头像捕获后调用GET /card-forge/active-character:供 card-forge 轮询static/app-chat-avatar.js新增 1 个函数 + 4 个调用点:syncAvatarToCardForge()在applyPreviewResult、init 内存/storage 路径、setExternalAvatar处推送启动方式
URL:
停止:
.\stop-card-forge.bat私密性
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.py现有/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 入口(条件:用户登录 + 云端可达);点击 → 浏览器打开云端 feedlocal_server/facts_sync_worker.py后台任务,把本地facts.json推到 ServersPOST /api/facts/sync(默认禁用,envNEKO_FACTS_SYNC=1启用)local_server/quota_dropper.py,本地 quota_rules.yaml + 云端 drop-hint 共同决定 Pet UI 是否触发掉落动画M5-g — 卡片本地缓存 puller(commit
3dadfda)local_server/card_cache_puller.py后台 worker,定期把云端GET /api/cards/mine拉到本地state/card_cache/,让 card-forge 仓库页可离线查看自己铸造的羁绊卡验证
Test plan
cd card-forge && npm install && npm run build通过start-card-forge.bat3 窗口启动正常Co-Authored-By: Claude Opus 4.7 (1M context)
Summary by CodeRabbit
发布说明
New Features
Documentation
Chores