Skip to content

feat: card-forge 模块 + 三体融合配套 + 对话掉落卡片闭环 & 社区登录 (M1/M2/M5) - #1542

Merged
wehos merged 189 commits into
Project-N-E-K-O:mainfrom
LyaQanYi:claude/card-forge-extract
Jul 28, 2026
Merged

feat: card-forge 模块 + 三体融合配套 + 对话掉落卡片闭环 & 社区登录 (M1/M2/M5)#1542
wehos merged 189 commits into
Project-N-E-K-O:mainfrom
LyaQanYi:claude/card-forge-extract

Conversation

@LyaQanYi

@LyaQanYi LyaQanYi commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

本 PR 已扩展至三块(按维护者要求统一收敛在一个 PR 内迭代):

  1. 主线 — 「奇遇铸造机」独立模块 card-forge(commit dfb78fab30deff,9 个 commit + 多轮 CodeRabbit / Codex review fix):把卡牌铸造拆为独立子模块(Vite + React + Tailwind 前端 + FastAPI 后端 + 主服务桥接 hook)。
  2. 三体融合 N.E.K.O.Servers 配套(commit 6a5419b3dadfda):为配合 N.E.K.O.Servers#1 的 M1 / M2 / M5 里程碑追加的 NEKO 端 endpoints / worker / 入口,全部默认禁用,环境变量启用,零侵入 NEKO 核心。
  3. 对话掉落卡片闭环 + 社区登录 + 遗留修复(commit de97867874821b,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 注入时把猫娘名推给铸造后端,静默失败不影响主前端
  • 全为新增、不删除、不修改任何 NEKO 核心逻辑

模块结构

前端 frontend/card-forge/ (端口 5173)

  • 奇遇铸造机面板:5 槽事件抽取 → 选择确认 → LLM 故事生成 → 铸造动画 → 入库
  • 铸造卡仓库:网格展示已铸造的羁绊卡 + CardInspectModal 鉴赏
  • 通过 /card-forge/active-character 轮询当前猫娘名作为 runtime_character_hint

后端 local_server/card_forge_server/ (端口 3001)

  • GET /forge/facts:从当前猫娘 active facts.json 抽取 5 条候选事实
    • 按 id/hash 去重,可排除已铸造来源
    • 优先保证 2 条近期事实 + 1 条久远事实 + 加权随机
  • POST /forge/card-story:用 NEKO 核心 LLM 配置 (summary / agent) 把 storyLead 生成卡牌专属小故事
  • 只读 facts.json / facts_archive.json不修改 NEKO 核心
  • LLM 复用 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 处推送
  • 静默失败,card-forge 未运行时无影响

启动方式

# 一键启动 3 个窗口 (主服务 / 铸造后端 / 铸造前端)
.\scripts\card-forge\start-card-forge.bat

# 或:
uv run scripts\card-forge\start_card_forge.py

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)

全部默认禁用,由环境变量或 core_config 控制启用;零侵入 NEKO 核心。

M1-j — /system/client-id + /system/social/config(commit 6a5419b, 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

  • M2-h:Pet 窗口 screen 按钮改为 social 入口(条件:用户登录 + 云端可达);点击 → 浏览器打开云端 feed
  • M2-imain_logic/facts_sync/sync_worker.py 后台任务,把本地 facts.json 推到 Servers POST /api/facts/sync(默认禁用,env NEKO_FACTS_SYNC_ENABLED=1 启用)
  • M2-j:配额掉落引擎 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 仓库页可离线查看自己铸造的羁绊卡

验证

# system endpoints
curl http://localhost:48911/api/system/client-id
# {"ok":true,"client_id":"<UUID4 hex>"}

curl http://localhost:48911/api/system/social/config
# {"ok":true,"social_base_url":"http://localhost:8080","enabled":true}

NEKO_SOCIAL_BASE_URL=https://prod.example.com python launcher.py
# /api/system/social/config 返回新 URL

# facts_sync worker
NEKO_FACTS_SYNC_ENABLED=1 python launcher.py
# 看到 [facts_sync] pushed N facts → 200

# card cache puller
NEKO_CARD_CACHE_ENABLED=1 python launcher.py
# state/card_cache/*.json 文件出现

Part 3 — 对话掉落卡片闭环 + 社区登录 + 遗留修复(commit de97867874821b

瘦客户端:NEKO 只管 ① 对话触发掉落 ② 从本地记忆给 5选1 候选 ③ 显示开卡演出;卡片生成(稀有度 / 编号 / 故事 / 卡面)+ 卡册都在云端(配套 N.E.K.O.Servers#4)。

对话掉落开卡演出(P1 / P2 + 瘦客户端重构)

  • 本地代理 main_routers/card_drop_router.pyGET /api/card-drop/candidates 本地memory/<角色>/facts.json 偏重要加权抽 5(不足用预设补满,不走云端);POST /api/card-drop/draw 代理云端 /api/cards/drawX-Client-Id(登录则带 JWT)。
  • 原生 JS 宿主 static/forge-drop-overlay.js + static/forge-drop-tokens.js(样式由脚本内联注入,无独立 css 文件):居中模态 5选1 → 凝结 → 按稀有度揭晓演出(多重叮 / 屏闪 / 震屏 / 粒子 + No.编号 + 云端生成卡面);app-websocket.js 收 WS card_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 → 卡归账号。
  • Steam(需配套云端 oauth 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。
  • bind-client 如实反馈:原来 409(设备已绑别的账号)被静默吞、UI 还谎报「已存入卡册」;改为捕获 {bound,error} 经 auth-status / login / steam-callback 透出,前端显示绿(已存入)/ 琥珀(此设备已绑到别的账号,卡留在原账号)。

「方案A」— Part 2 社交按钮三处遗留修复

Part 2(M2-h)把 Pet 的 screen 槽位改成「猫娘社区」社交按钮,落地时留了三个问题,一并修掉:

  • i18n:8 语言补 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/mine owner_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.html
  • scripts\card-forge\start-card-forge.bat 3 窗口启动正常
  • card-forge 头像同步 / 5 条 forge-facts / LLM forge-card-story 跑通
  • CardInspectModal 展示锻造故事
  • system endpoints 返回 client_id + social_base_url
  • 三体 e2e 测试(待本机起 Servers + NEKO + NEKO-PC 串测)
  • 对话掉落开卡演出(5选1 → 凝结 → 揭晓)浏览器验,全档配色 / 号段正确
  • 邮箱登录卡迁入账号 + 同设备换号 → 409 琥珀告警
  • Steam 登录前端链路(steam-login → openExternal → 轮询入册);redirect_to 白名单容器内验
  • 「方案A」三处(i18n / 开窗 openExternal / 屏幕共享入口)浏览器验
  • 真 Steam OpenID 验签(需 STEAM_API_KEY + 真账号)

Co-Authored-By: Claude Opus 4.7 (1M context)

Summary by CodeRabbit

  • 新功能
    • 新增“奇遇铸造机”,支持从角色记忆生成卡牌、故事与本地卡牌收藏。
    • 新增猫娘社区入口、内嵌窗口、OAuth 登录及积分/铸造券体验。
    • 新增屏幕共享快捷控制、语音联动与稀有度掉落动画音效。
    • 新增活动角色与头像同步、卡牌缓存及事实同步能力。
  • 体验改进
    • 优化移动端按钮布局、无障碍标签、教程内容和多语言支持。
    • 改进请求级语言处理,提升多语言记忆与提示词一致性。
  • 文档
    • 补充铸造机使用、启动方式及项目结构说明。

回归报告 / Regression Report

  • 改动了什么:本 PR 新增 card-forge 独立前后端、本地 card-drop 代理、facts/card cache/quota 相关后台 worker 与系统桥接端点,并把社交入口、当前猫娘同步、开卡演出和社区登录链路接到现有 NEKO 主服务与前端中。
  • 理由 / 必要性:这些改动是 N.E.K.O.Servers 三体融合与奇遇铸造机闭环所需的 NEKO 侧配套;本地只负责候选、同步、代理和展示,云端负责账号、卡片生成、卡册与持久化。
  • 改动前后的表现对比:改动前 NEKO 没有 card-forge 独立模块、对话掉落卡片闭环、社区登录态桥接、facts 上行和卡片缓存拉取;改动后这些能力默认保持禁用或按入口触发,未配置对应环境变量时不主动向云端发送数据。
  • 潜在回归点:主服务启动 hook、websocket 事件广播、头像同步、社交按钮入口、i18n locale 缓存、facts/card cache worker 的本地文件读写、card-drop 登录和 draw 代理。已通过本地构建、静态检查、相关单测、浏览器端到端手动验证,以及多轮 CodeRabbit/Codex review 修复降低风险;Steam OpenID 真账号验签仍需真实 STEAM_API_KEY 环境补验。

补充:文件结构收敛(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)。
  • 理由 / 必要性:本 PR 之前在仓库根新增了 6 个条目,破坏了「前端项目放 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.pyPROJECT_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
  • 验证:221 个 forge/card/social 单测通过;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), mounts card_drop_router and 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 from character_runtime so quota/card-drop paths can fan out without importing app.main_server.

Memory pipeline accepts optional language on /cache, /process, /renew, and /settle; activates request locale and persists it on post-turn outbox replay. Fact-extraction prompts gain explicit per-locale JSON text output rules.

Runtime bindings install quota dropper hooks (noop unless env-enabled). config/quota_rules.yaml defines 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.

把卡牌铸造功能拆为 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>
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • tests/unit/test_voice_start_failure_static.py
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 00a1e933-5aea-4692-b01c-1ccabbd14dcb

📥 Commits

Reviewing files that changed from the base of the PR and between 3b319fa and 529c825.

📒 Files selected for processing (1)
  • tests/unit/test_voice_start_failure_static.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

本次变更新增 Card Forge 独立服务与 React 界面,接入 facts、故事生成、社区 OAuth、credits、缓存同步和社交 UI;同时扩展屏幕共享、语言上下文、平台适配、教程、本地化及相关测试。

Changes

Card Forge 与社区整合

Layer / File(s) Summary
Card Forge 服务与铸造链路
local_server/card_forge_server/*, main_logic/card_forge_facts.py, frontend/card-forge/*, scripts/card_forge/*
新增 facts 选择、LLM 故事生成、FastAPI 路由、React 铸造流程、卡片缓存、账本和启动脚本。
社区认证与 credits
main_routers/card_drop_router.py, main_routers/community_oauth.py, main_logic/forge_credit_ledger.py, utils/config_manager/*
新增受控会话同步、OAuth PKCE、凭据持久化、credits 状态流转及 client proof。
后台同步与运行时
main_logic/facts_sync/*, main_logic/card_cache/*, app/main_server/*, main_logic/quota/*
新增 facts/card worker、生命周期管理、配额状态、事件广播和运行时绑定。
社交界面与屏幕共享
static/social-embed.js, static/forge-drop-overlay.js, static/app/app-audio-capture.js, static/app/app-ui/surface-floating-controls.js
新增社区嵌入窗口、掉落徽章与动画、屏幕共享联动、角色参考图同步及浮动按钮调整。
平台适配与本地化
static/js/card_maker.js, plugin/plugins/neko_warthunder/*, utils/language_utils.py, static/locales/*
新增制卡嵌入模式、插件端口/CORS适配、请求级语言上下文、语言解析和多语言教程文案。
测试、构建与文档支持
tests/unit/*, tests/frontend/*, .github/workflows/*, docs/*, build_frontend.*
补充服务、认证、同步、前端静态契约、平台行为、CI触发和生产构建边界测试。

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: enhancement

Poem

铸造机亮起霓虹光,喵
记忆化作卡牌藏
社区之门 OAuth 开
小票飞向按钮旁
猫娘挥爪,故事香喵

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题覆盖了 card-forge、社交登录和对话掉落三块主改动,和变更集一致喵
Description check ✅ Passed 描述按模板补全了 Summary、回归报告、不拆分理由和测试信息,结构完整喵
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread app/main_server.py Outdated
Comment thread card-forge/src/App.jsx Outdated
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

两条 codex 意见都已修(d392b05):

  • P1 set_card_forge_active_character:改成只更新载荷里实际给出的字段。空 name 的纯 dataUrl 同步不再擦掉已存猫娘名。
  • P2 card-forge/src/App.jsx 轮询:空 name 时把 activeCharacterName 也置 null,服务端缓存清空(如主服务重启)后前端跟着同步,不会卡在旧名上。

npm run build 通过。


Co-Authored-By: Claude Opus 4.7 (1M context)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 13c5ccb and dfb78fa.

⛔ Files ignored due to path filters (1)
  • card-forge/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • app/main_server.py
  • card-forge/index.html
  • card-forge/package.json
  • card-forge/postcss.config.js
  • card-forge/src/App.jsx
  • card-forge/src/components/CardInspectModal.jsx
  • card-forge/src/data/forgedBrawlCards.js
  • card-forge/src/index.css
  • card-forge/src/main.jsx
  • card-forge/tailwind.config.js
  • card-forge/vite.config.js
  • local_server/card_forge_server/README.md
  • local_server/card_forge_server/__init__.py
  • local_server/card_forge_server/active_neko_context.py
  • local_server/card_forge_server/forge_story_generator.py
  • local_server/card_forge_server/requirements.txt
  • local_server/card_forge_server/server.py
  • start-card-forge.bat
  • start_card_forge.py
  • static/app-chat-avatar.js
  • stop-card-forge.bat
  • stop-card-forge.ps1

Comment thread app/main_server.py Outdated
Comment thread frontend/card-forge/src/App.jsx
Comment thread card-forge/src/data/forgedBrawlCards.js Outdated
Comment thread local_server/card_forge_server/active_neko_context.py Outdated
Comment thread local_server/card_forge_server/forge_story_generator.py
Comment thread local_server/card_forge_server/server.py
Comment thread local_server/card_forge_server/server.py Outdated
Comment thread static/app/app-chat-avatar.js
Comment thread scripts/card_forge/stop-card-forge.ps1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread local_server/card_forge_server/forge_story_generator.py
Comment thread local_server/card_forge_server/server.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between dfb78fa and d392b05.

📒 Files selected for processing (2)
  • app/main_server.py
  • card-forge/src/App.jsx

Comment thread app/main_server.py Outdated
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

CodeRabbit + Codex 这一轮 9 条 review + 1 nitpick 已全部处理(0eeb45e),没有 push back 的。

Major(安全/隐私)

  • CORS server.py: allow_origins["*"] 收紧到本机 127.0.0.1:5173 / localhost:5173 白名单,可经 NEKO_CARD_FORGE_ALLOWED_ORIGINS 扩展。allow_methods/headers 同步收窄。
  • 绑定 server.py:__main__: 默认 host="127.0.0.1",需要外网暴露才通过 NEKO_CARD_FORGE_HOST 显式打开,与 README 私密性声明一致。
  • 日志脱敏 forge_story_generator.py: 新增 _mask_sensitive_text + 敏感字段白名单。storyLead / systemPrompt / userPrompt / rawContent / story / lanlanPromptPreview_forge_log 里只输出 [40 字符预览]…(len=N),不再把完整 prompt 与故事正文落控制台。
  • hint 校验 active_neko_context.py: 新增 _known_character_names,runtime_character_hint / character_override 必须在已配置猫娘名里,未知值降级到 active_lanlan,堵掉"任意 hint 拼 facts.json 路径"的路径。
  • stop 脚本 stop-card-forge.ps1: 杀进程前先用 CIM 取 CommandLine 匹配 launcher.py / card_forge_server / card-forge,不再按端口盲杀,避免误伤同端口的别人项目。

Minor(正确性)

  • POST 端点 main_server.py: 改用 'X' in payload 语义,同时满足 codex P1(不擦旧值)和 CodeRabbit(允许显式清空)。payload.get('X') or '' 也对齐了 None 防御一条。
  • 同步钩子 app-chat-avatar.js: 放宽 dataUrl 闸门支持 name-only 同步,body 只塞有值字段,与服务端 in-payload 语义对齐。
  • forgedBrawlCards.js: storyGenerationStatus / sourceKind 改用 string?.trim() 真值,与 composeForgedCardStorytrim() 对齐;sourceKind 同时识别 factHash,只带 hash 的事实不再被误判为 temporary。
  • AbortController App.jsx: /arena/forge-card-story 加 30 秒超时,防止 LLM 卡住时铸造动画转圈不停。
  • nitpick: 提取了重复的 forgeMachineSlots.find(s => s.id === machinePickedId)pickedSlot 局部变量。

npm run build 通过,8 个文件、+171/-25。


Co-Authored-By: Claude Opus 4.7 (1M context)

@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

Note for CodeRabbit / maintainer: 这一轮 inline 评论在新 commit `0eeb45e6` 上重新跑了 review,但每条 body 跟上一轮逐字一致,连建议里给出的 diff 也仍然是修复前的旧代码片段。已逐条核对,所有发现都已在 `0eeb45e6` 中处理,不再改代码。

按 CodeRabbit prompt 自己说的 "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason":

重复发现 已在 `0eeb45e6` 修复的位置 现状
`main_server.py:1604` POST 保留脏旧状态 main_server.py 1591-1604 改用 `'X' in payload` 语义,POST `{"name": ""}` 即显式清空 already fixed
`main_server.py:1595` None → "None" 防御 同上,值取用是 `str(payload.get('X') or '')`,`None or '' → ''` already fixed
`App.jsx:205` 缺超时保护 App.jsx 171-203 已加 `AbortController` 30s 超时 + `finally clearTimeout` already fixed
`forgedBrawlCards.js:117` 真值/factHash forgedBrawlCards.js 105-120 / 130-160 `hasGeneratedStory` 用 `trim()`、`hasFactSource` 包含 factHash,两处函数都已改 already fixed
`active_neko_context.py:96` hint 校验 active_neko_context.py 48-95 新增 `_known_character_names` 白名单,unknown hint 降级到 `active_lanlan` already fixed
`forge_story_generator.py:121` 日志泄漏全文 forge_story_generator.py 66-115 新增 `_mask_sensitive_text` + `_FORGE_SENSITIVE_FIELDS` 白名单,敏感字段只输出 `[40 字符]…(len=N)` already fixed
`server.py:65` CORS `["*"]` server.py 41-66 改为 `127.0.0.1:5173 / localhost:5173` 白名单 + `NEKO_CARD_FORGE_ALLOWED_ORIGINS` env override already fixed
`server.py:660` 默认 0.0.0.0 绑定 server.py `main` 默认 `127.0.0.1`,`NEKO_CARD_FORGE_HOST` 才允许 0.0.0.0 already fixed
`app-chat-avatar.js:371` dataUrl 闸门阻断 name-only app-chat-avatar.js 354-372 早返回改为 `if (!dataUrl && !_nekoName)`,body 只塞有值字段 already fixed
`stop-card-forge.ps1:59` 按端口盲杀 stop-card-forge.ps1 16-65 新增 `Get-ProcessCommandLine` + `Test-CardForgeProcess`,只杀匹配 `launcher.py / card_forge_server / card-forge` pattern 的进程 already fixed

如果哪条 maintainer 看了仍然觉得不到位,欢迎指出具体不足,我再补。


Co-Authored-By: Claude Opus 4.7 (1M context)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread local_server/card_forge_server/server.py
Comment thread scripts/card_forge/start_card_forge.py
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

Codex 这一轮 2 条都处理(841ea48):

  • P1 server.py:683 uvicorn reload: 显式传 app_dir=str(SERVER_ROOT),让 reload fork 出来的 worker 子进程能找到 `server` 模块。原来从项目根 `uv run local_server/card_forge_server/server.py` 起时 worker 不继承 `main` 里加的 sys.path。
  • P2 start_card_forge.py Windows-only: 加 `_ensure_windows()` 早判,非 Windows 抛 SystemExit 并打印三条手动命令,不再让 PowerShell 路径直接报 FileNotFoundError。

python -m ast 双 parse 通过。


Co-Authored-By: Claude Opus 4.7 (1M context)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 = None

Also 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

📥 Commits

Reviewing files that changed from the base of the PR and between d392b05 and 0eeb45e.

📒 Files selected for processing (8)
  • app/main_server.py
  • card-forge/src/App.jsx
  • card-forge/src/data/forgedBrawlCards.js
  • local_server/card_forge_server/active_neko_context.py
  • local_server/card_forge_server/forge_story_generator.py
  • local_server/card_forge_server/server.py
  • static/app-chat-avatar.js
  • stop-card-forge.ps1

Comment thread scripts/card-forge/stop-card-forge.ps1 Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread card-forge/src/App.jsx Outdated
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

第二轮 review 4 条都处理(10fe836):

  • CodeRabbit major `stop-card-forge.ps1`: 新增 `Get-SafeCommandPreview`,跳过日志改成 60 字符截短,不再打印别人家进程的完整 CommandLine (可能含 token / 私密路径)。
  • CodeRabbit outside-diff `forgedBrawlCards.js` `saveForgedBrawlCards`: `localStorage.setItem` 加 try/catch,私密浏览 / quota 超出时只 console.warn 不让铸造流程崩。
  • CodeRabbit outside-diff `active_neko_context.py` `_known_character_names`: 异常路径从 `set()` 改成 `None`,caller 据此把 hint/override 全置 None 回落到 `active_lanlan` — 不再让"config 短暂不可用"被误读为"放行所有 hint"。
  • Codex P2 `App.jsx` `loadForgeMachineSlots`: exclude 按 `card.sourceCharacter === activeCharacterName` 过滤,跨猫娘库的 fact id/hash 不再错误地缩水当前猫娘的可用池。

`npm run build` + `ast.parse` 双通过。


Co-Authored-By: Claude Opus 4.7 (1M context)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0eeb45e and 841ea48.

📒 Files selected for processing (2)
  • local_server/card_forge_server/server.py
  • start_card_forge.py

Comment thread start_card_forge.py Outdated
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

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)

@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

Note for CodeRabbit / maintainer: 这条是上一轮 `stop-card-forge.ps1` CommandLine 泄漏意见的回声(行号从 49 飘到 61 是因为我加了 `Get-SafeCommandPreview` helper 把后面行号都推高了),实际已在 `10fe836c` 处理。当前 stop-card-forge.ps1 第 60 行 已经是 `Write-Host ... Preview: {2}` 输出 60 字符截短预览,不再打印任何完整 CommandLine。


Co-Authored-By: Claude Opus 4.7 (1M context)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread local_server/card_forge_server/server.py
Comment thread app/main_server.py Outdated
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

Codex 第三轮 2 条都处理(93d461f):

  • P1 `server.py`: `forge_story_generator.py` 早就脱敏了 `storyLead/prompt/story`,但 server.py 路由层另有 `_forge_route_log` 处理 POST 入口日志,直接 json.dumps storyLead 全文。新增 `_mask_route_sensitive` 让 route log 也走 "40 字符预览 + len" 脱敏,与 generator 那条对齐。
  • P2 `main_server.py` GET `/card-forge/active-character`: 每次轮询返几十 KB base64 dataUrl,但前端只读 `name`。改为默认不返 dataUrl,需要的调用方 `?include_avatar=true` 显式取。5 秒一次轮询带宽直接砍一个数量级。

`ast.parse` 双通过。


Co-Authored-By: Claude Opus 4.7 (1M context)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.pysafe_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_contextknown_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

📥 Commits

Reviewing files that changed from the base of the PR and between 841ea48 and fe7ec12.

📒 Files selected for processing (5)
  • card-forge/src/App.jsx
  • card-forge/src/data/forgedBrawlCards.js
  • local_server/card_forge_server/active_neko_context.py
  • start_card_forge.py
  • stop-card-forge.ps1

Comment thread card-forge/src/data/forgedBrawlCards.js Outdated
Comment thread local_server/card_forge_server/active_neko_context.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread local_server/card_forge_server/forge_story_generator.py Outdated
回应 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>
@LyaQanYi

Copy link
Copy Markdown
Collaborator Author

第四轮 review 5 条都处理(0bb37f3):

  • CodeRabbit minor `forgedBrawlCards.js` `saveForgedBrawlCards`: 访问 `window.localStorage` 本身也可能抛 SecurityError(源被禁用 / Safari ITP),把探测也挪进 try。
  • CodeRabbit major `active_neko_context.py` `_known_character_names`: `character_data[5]` malformed shape (缺失/非 dict) 之前折成 `set()` 被 caller 当成"0 只猫娘"跳过校验,等同于无白名单放行 hint。改为 malformed 也返 `None`,与 exception 一起归到"保守拒绝"。
  • 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 字里含 `--token=xxx` 仍会落日志。新增正则脱敏 `--token/password/secret/api_key/access_key/auth`、`Bearer XXX`、`Authorization: ...` → ``,再截短。

`npm run build` + `ast.parse` 双通过。


Co-Authored-By: Claude Opus 4.7 (1M context)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.jsonactive_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

📥 Commits

Reviewing files that changed from the base of the PR and between 93d461f and 0bb37f3.

📒 Files selected for processing (4)
  • card-forge/src/data/forgedBrawlCards.js
  • local_server/card_forge_server/active_neko_context.py
  • local_server/card_forge_server/forge_story_generator.py
  • stop-card-forge.ps1

Comment thread scripts/card-forge/stop-card-forge.ps1 Outdated
@wehos
wehos marked this pull request as ready for review July 28, 2026 03:34
@wehos
wehos merged commit 92ed281 into Project-N-E-K-O:main Jul 28, 2026
8 of 9 checks passed
@github-project-automation github-project-automation Bot moved this from 0.9.0 to Done in Release Plan Jul 28, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +65 to +66
if needs_persist:
cm.save_cloudsave_local_state(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

wehos added a commit that referenced this pull request Jul 28, 2026
* 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>
wehos added a commit that referenced this pull request Jul 28, 2026
* 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>
wehos added a commit that referenced this pull request Jul 28, 2026
* 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>
wehos added a commit that referenced this pull request Jul 30, 2026
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>
wehos added a commit that referenced this pull request Jul 30, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

react-neko-chat 的 vite.config.js / .d.ts 是 tsc 生成物却被提交,且实际生效的是生成物而非 .ts 源文件

5 participants