Skip to content

feat: card-forge 模块 + 三体融合 NEKO 端配套 (M1/M2/M5)#1542

Draft
LyaQanYi wants to merge 17 commits into
Project-N-E-K-O:mainfrom
LyaQanYi:claude/card-forge-extract
Draft

feat: card-forge 模块 + 三体融合 NEKO 端配套 (M1/M2/M5)#1542
LyaQanYi wants to merge 17 commits into
Project-N-E-K-O:mainfrom
LyaQanYi:claude/card-forge-extract

Conversation

@LyaQanYi
Copy link
Copy Markdown
Collaborator

@LyaQanYi LyaQanYi commented May 26, 2026

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 核心。

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 注入时把猫娘名推给铸造后端,静默失败不影响主前端
  • 全为新增、不删除、不修改任何 NEKO 核心逻辑

模块结构

前端 card-forge/ (端口 5173)

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

后端 local_server/card_forge_server/ (端口 3001)

  • GET /arena/forge-facts:从当前猫娘 active facts.json 抽取 5 条候选事实
    • 按 id/hash 去重,可排除已铸造来源
    • 优先保证 2 条近期事实 + 1 条久远事实 + 加权随机
  • POST /arena/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.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 处推送
  • 静默失败,card-forge 未运行时无影响

启动方式

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

# 或:
uv run start_card_forge.py

URL:

停止:.\stop-card-forge.bat

私密性

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

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

验证

# 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=1 python launcher.py
# 看到 [facts_sync] pushed N facts → 200

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

Test plan

  • cd card-forge && npm install && npm run build 通过
  • 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 串测)

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

Summary by CodeRabbit

发布说明

  • New Features

    • 新增卡牌锻造系统完整实现,包括前后端、跨进程角色同步、故事生成和本地卡牌库管理
    • 添加云端事实增量同步和卡牌缓存拉取后台任务
    • 集成社交平台接入和配额掉落系统,支持云端提示和本地持久化
    • 新增一键启动脚本,方便本地开发环境快速部署
  • Documentation

    • 添加卡牌锻造服务文档,说明接口规范与配置方式
  • Chores

    • 新增Windows批处理和PowerShell启动/停止脚本
    • 补充项目配置文件和依赖声明

Review Change Stack

把卡牌铸造功能拆为 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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44b345f1-30fd-4020-9254-6e2c7cb92c53

📥 Commits

Reviewing files that changed from the base of the PR and between 6f84f5f and 2f57a50.

📒 Files selected for processing (1)
  • static/app-social-ui.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • static/app-social-ui.js

Walkthrough

新增 Card-Forge 奇遇铸造机完整子系统与配额掉落规则引擎:前端 React 应用含卡牌数据持久化与铸造 UI;后端 FastAPI 服务实现 facts 选择、LLM 故事生成与角色上下文管理;新增 Facts/卡片同步后台 worker;配额规则引擎实现关键词与字数触发、云端掉落提示;主服务集成头像同步端点与社交功能;浮动按钮改为社交平台入口。

Changes

Card-Forge 奇遇铸造机系统

Layer / File(s) Summary
前端框架与工具链配置
card-forge/index.html, card-forge/src/main.jsx, card-forge/vite.config.js, card-forge/postcss.config.js, card-forge/package.json, card-forge/tailwind.config.js
新增 HTML 入口、React 应用入口、Vite 配置(含开发代理至本地后端服务)、PostCSS/Tailwind 配置与项目依赖。
卡牌检视组件与全局样式
card-forge/src/components/CardInspectModal.jsx, card-forge/src/index.css
新增 CardInspectModal 鉴赏弹窗组件(属性/效果/故事展示、Escape 关闭、framer-motion 动画)与全局样式(霓虹边框动画、滚动条定制、暗色主题)。
锻造牌数据与本地持久化
card-forge/src/data/forgedBrawlCards.js
定义属性池、效果池、临时事件池;实现卡牌创建/归一化/组成/加载/保存/删除逻辑与 localStorage 浏览器持久化。
前端铸造流程与 UI 实现
card-forge/src/App.jsx
实现奇遇铸造机主组件:槽位构建、API facts 映射、保证规则、铸造五阶段状态机(burning/floating/storyGenerating/flipping/revealed)、后端交互、LLM 故事生成与临时回退、仓库网格展示与删除。
后端依赖与文档
local_server/card_forge_server/README.md, local_server/card_forge_server/requirements.txt
新增 README 说明服务启动、接口参数与环境变量约定;requirements.txt 定义 fastapi/uvicorn/httpx 版本。
角色上下文解析
local_server/card_forge_server/active_neko_context.py
实现 ActiveNekoContext 数据结构;safe_character_segment 校验(路径穿越、Windows 驱动器号防护);facts 路径推理(env/config/memory 优先级);lanlan prompt 模板替换。
故事生成引擎
local_server/card_forge_server/forge_story_generator.py
实现 LLM 故事生成链路:Multi-target 模型目标解析、请求日志脱敏、模型输出清洗(围栏剥离、Mojibake 修复、禁用词检测、长度截断)、超时保护与异常聚合。
Facts 选择与工具
local_server/card_forge_server/server.py(辅助函数)
实现 facts 过滤(min_importance/absorbed/exclude)、内部去重(基于 id/hash/text)、近期保证(created_at 2 天内)、远端保证(记忆时间加权)、加权随机补齐与统计返回。
铸造后端路由与入口
local_server/card_forge_server/server.py
实现 /arena/forge-facts(facts 选择与远端保证合并)、/arena/forge-card-story(故事生成与诊断日志)、/health 健康检查;uvicorn 启动配置。
启动与停止脚本
start_card_forge.py, start-card-forge.bat, stop-card-forge.bat, stop-card-forge.ps1
跨平台启动脚本(Windows PowerShell 三窗口、非 Windows 命令提示)与停止脚本(进程匹配、端口探测、敏感参数脱敏)。
主服务集成与头像同步
app/main_server.py, static/app-chat-avatar.js
新增 /card-forge/active-character 进程内缓存端点(POST 选择性更新、GET 带 avatar 返回);头像变更后同步至该端点(四处调用点);启动时 async.create_task facts_sync/card_cache worker。
系统路由
main_routers/system_router.py
新增 /api/system/client-id(cloudsave 状态读取与生成)与 /api/system/social/config(社交基础 URL 读取,默认 localhost:8080)。

配额掉落规则引擎与云端同步

Layer / File(s) Summary
配额配置与 UX 状态
config/quota_rules.yaml, main_logic/quota/ux_state.py
新增规则配置(字数阈值 800、关键词列表"铸卡/铸造/奇遇/纪念"、冷却时间);UX 状态持久化至 quota_ux_state.json(按 UTC 日期隔离、线程安全)。
云端 Drop-Hint 同步
main_logic/quota/cloud_sync.py
实现 idem_key 生成(client_id/日期/trigger_type/计数/时间戳)、fire-and-forget 异步出站、HTTP 错误容错与响应解析兜底。
配额掉落规则引擎
main_logic/quota/dropper.py, main_logic/quota/__init__.py
实现 on_text_message hook(keywords 即触、word_count 累计触发)与 on_utterance 留位逻辑;规则从 yaml 加载带缓存。
运行时 Bindings 注册
app/runtime_bindings.py
在主服务 install_runtime_bindings 中按需安装配额 hooks,包含异常处理与模块缺失管理。

Facts 增量同步与卡片缓存 Worker

Layer / File(s) Summary
Facts 同步 Worker
main_logic/facts_sync/sync_worker.py, main_logic/facts_sync/__init__.py
实现本地 facts 向 Servers 增量推送:幂等客户端注册、facts.json 读取与 min_importance/private 过滤、重要度归一化、批量 POST 上行、失败重试(max 5 次)、超限记录至 jsonl、状态原子写回。
卡片缓存 Worker
main_logic/card_cache/puller.py, main_logic/card_cache/__init__.py
定期拉取 /api/cards/mine 并缓存至 memory_dir/lanlan/cards/:client_id 请求头识别、已存在文件跳过、原子写入、启动延迟 60s、循环轮询间隔 5min。

UI 社交功能集成

Layer / File(s) Summary
社交按钮与打开逻辑
static/avatar-ui-buttons.js, static/app-ui.js, static/live2d-ui-buttons.js, static/mmd-ui-buttons.js, static/vrm-ui-buttons.js
将浮动按钮从 screen(屏幕分享)替换为 social(社交平台);新增 live2d-social-click 事件监听,查询社交配置与 client_id 后打开独立窗口。
社交 SSE 事件处理
static/app-social-ui.js, templates/index.html
新增 IIFE 模块处理 window.nekoSocial SSE 回调:未读 badge(MutationObserver 补画)、通知 toast(electronToast 回退)、硬币下坠动画、配额快照缓存、连接状态日志。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🎴 铸卡机启动呀,新世界展开喵~
配额掉落闪闪亮,facts 同步不停摇🌟
猫娘故事被 LLM 生成呀,
社交窗口打开,硬币飞舞多逍遥✨
这次变更好大啦,傲娇的我给个赞喵!

Copy link
Copy Markdown

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

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)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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
Comment thread 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
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-chat-avatar.js
Comment thread stop-card-forge.ps1
Copy link
Copy Markdown

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

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 on lines +276 to +277
systemPrompt=system_prompt,
userPrompt=user_prompt,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread local_server/card_forge_server/server.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@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)

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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 stop-card-forge.ps1
Copy link
Copy Markdown

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

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)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@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)

Copy link
Copy Markdown

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

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)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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
Copy link
Copy Markdown

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

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)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ 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 stop-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>
@LyaQanYi
Copy link
Copy Markdown
Collaborator Author

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)

@LyaQanYi LyaQanYi marked this pull request as draft May 27, 2026 01:55
给 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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between b30deff and 6a5419b.

📒 Files selected for processing (1)
  • main_routers/system_router.py

Comment thread main_routers/system_router.py
Comment thread main_routers/system_router.py Outdated
LyaQanYi and others added 2 commits May 27, 2026 22:33
- /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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between f2a9dae and fddd4e6.

📒 Files selected for processing (14)
  • app/main_server.py
  • app/runtime_bindings.py
  • config/quota_rules.yaml
  • main_logic/facts_sync/__init__.py
  • main_logic/facts_sync/sync_worker.py
  • main_logic/quota/__init__.py
  • main_logic/quota/cloud_sync.py
  • main_logic/quota/dropper.py
  • main_logic/quota/ux_state.py
  • static/app-ui.js
  • static/avatar-ui-buttons.js
  • static/live2d-ui-buttons.js
  • static/mmd-ui-buttons.js
  • static/vrm-ui-buttons.js
✅ Files skipped from review due to trivial changes (2)
  • main_logic/facts_sync/init.py
  • main_logic/quota/init.py

Comment thread main_logic/facts_sync/sync_worker.py Outdated
Comment thread main_logic/quota/dropper.py
Comment thread static/app-ui.js
配合 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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@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

📥 Commits

Reviewing files that changed from the base of the PR and between fddd4e6 and 3dadfda.

📒 Files selected for processing (3)
  • app/main_server.py
  • main_logic/card_cache/__init__.py
  • main_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

Comment thread main_logic/card_cache/puller.py
@LyaQanYi LyaQanYi changed the title feat: 新增「奇遇铸造机」独立模块 (card-forge) feat: card-forge 模块 + 三体融合 NEKO 端配套 (M1/M2/M5) May 28, 2026
修复两个真实 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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3dadfda and 35f78dc.

📒 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>
Comment thread main_logic/facts_sync/sync_worker.py
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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@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

📥 Commits

Reviewing files that changed from the base of the PR and between 07692c4 and 6f84f5f.

📒 Files selected for processing (2)
  • static/app-social-ui.js
  • templates/index.html

Comment thread static/app-social-ui.js
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant