Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dfb78fa
feat: 新增「奇遇铸造机」独立模块 (card-forge)
LyaQanYi May 26, 2026
d392b05
fix(card-forge): codex P1/P2 — 别让无 name 的同步擦掉已存猫娘名
LyaQanYi May 26, 2026
0eeb45e
fix(card-forge): 处理 CodeRabbit 9 条 + 1 nitpick
LyaQanYi May 26, 2026
841ea48
fix(card-forge): codex P1/P2 — uvicorn reload app_dir + 非 Windows 守卫
LyaQanYi May 26, 2026
10fe836
fix(card-forge): CodeRabbit 第二轮 4 条
LyaQanYi May 26, 2026
fe7ec12
fix(card-forge): _ensure_windows 改抛 RuntimeError 避免绕过 entry except
LyaQanYi May 26, 2026
93d461f
fix(card-forge): codex P1/P2 — route log 脱敏 + GET 不返 avatar dataUrl
LyaQanYi May 26, 2026
0bb37f3
fix(card-forge): CodeRabbit/Codex 第四轮 5 条
LyaQanYi May 26, 2026
b30deff
fix(card-forge): stop-card-forge.ps1 加 UTF-8 BOM
LyaQanYi May 26, 2026
6a5419b
feat(system): 暴露 /api/system/client-id + /api/system/social/config
LyaQanYi May 27, 2026
f2a9dae
fix(system): CodeRabbit 两条 minor
LyaQanYi May 27, 2026
fddd4e6
feat(M2-h/i/j): 社交按钮入口 + facts_sync worker + 配额掉落引擎(NEKO 侧)
LyaQanYi May 27, 2026
3dadfda
feat(M5-g): 卡片本地缓存 puller(NEKO → N.E.K.O.Servers)
LyaQanYi May 27, 2026
35f78dc
fix(M2-i): facts_sync 自动 register client + importance 归一化
LyaQanYi May 28, 2026
07692c4
fix(M2-i): facts_sync 异步化磁盘 IO 解 async-blocking lint
LyaQanYi May 28, 2026
6f84f5f
feat(M6-h): frontend social UI 接入 — 红点 badge + DROP_ANIMATION
LyaQanYi May 28, 2026
2f57a50
fix(M6-h): social 红点用 MutationObserver 覆盖按钮重挂
LyaQanYi May 29, 2026
de97867
feat(card-drop): 本地 → 云端开卡代理路由 (P1a)
LyaQanYi Jun 5, 2026
d7dcbad
feat(card-drop): 对话掉落开卡演出(前端 + 触发)(P1)
LyaQanYi Jun 5, 2026
8cb4b14
feat(card-drop): 收集册(卡册)+ 卡片详情 (P2)
LyaQanYi Jun 5, 2026
3c6a59a
fix(card-drop): 模态/卡册可点击(宿主 body pointer-events:none 穿透)
LyaQanYi Jun 5, 2026
65a6b18
refactor(card-drop): 候选走本地记忆 + 移除 Electron 卡册(瘦客户端)
LyaQanYi Jun 6, 2026
68dca96
feat(card-drop): NEKO 邮箱密码登录社区 + JWT 接抽卡 + 登录提示(登录打通 chunk2)
LyaQanYi Jun 6, 2026
450a301
fix(social/screen): 修好猫娘社区按钮 + 恢复屏幕共享入口
LyaQanYi Jun 6, 2026
140a2aa
feat(card-drop): NEKO Steam 登录社区 — /steam-login + /steam-callback + 前…
LyaQanYi Jun 6, 2026
874821b
fix(card-drop): 登录如实反馈 bind-client 结果,不再谎报「已存入卡册」
LyaQanYi Jun 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions app/main_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,7 @@ async def get_response(self, path, scope):
from main_routers.workshop_router import router as workshop_router # noqa
from main_routers.cookies_login_router import router as cookies_login_router # noqa
from main_routers.game_router import router as game_router # noqa
from main_routers.card_drop_router import router as card_drop_router # noqa
from main_routers.debug_router import router as debug_router, start_watchdog as _start_debug_health_watchdog # noqa
from main_routers.shared_state import init_shared_state, set_steamworks_initializer # noqa

Expand All @@ -1578,6 +1579,49 @@ async def health():
return build_health_response("main", instance_id=INSTANCE_ID)


# ── Card-Forge 跨进程当前猫娘同步端点 ────────────────────────────
# 奇遇铸造机 (card-forge) 前端会轮询本端点,拿到当前猫娘名作为
# /arena/forge-facts 的 runtime_character_hint;不要求 card-forge 后端
# 知道 NEKO 内部状态,只通过此端点把"当前 NEKO 在前台展示的猫娘"广播给它。
_card_forge_active_character: dict = {} # {dataUrl, name}


@app.post('/card-forge/active-character')
async def set_card_forge_active_character(payload: dict):
"""由 app-chat-avatar.js 在捕获头像后调用,存储当前猫娘名(与头像 dataUrl)供 card-forge 获取。

用 in-payload 语义区分"省略字段(不动)" vs "显式空串(清空)":
- POST {"dataUrl": "x"} 只更新 dataUrl,保留已存 name
- POST {"name": ""} 显式清空 name
- POST {"dataUrl": "", "name": ""} 显式清空全部
前端调用方因此应该 *只在字段有意义时* 把它放进 body,避免误擦。
"""
if not isinstance(payload, dict):
return {"ok": True}
if 'dataUrl' in payload:
_card_forge_active_character['dataUrl'] = str(payload.get('dataUrl') or '')
if 'name' in payload:
_card_forge_active_character['name'] = str(payload.get('name') or '')
return {"ok": True}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@app.get('/card-forge/active-character')
async def get_card_forge_active_character(include_avatar: bool = False):
"""card-forge 前端轮询此端点获取最新猫娘名。

默认不返回 avatar dataUrl —— card-forge 前端目前每 5 秒轮询一次,只读 `name`,
把几十 KB 的 base64 dataUrl 一起回包纯属浪费。需要 avatar 的调用方
(例如未来的卡仓预览面板) 显式传 `?include_avatar=true`。
"""
from fastapi.responses import JSONResponse
payload: dict[str, str] = {
'name': _card_forge_active_character.get('name', ''),
}
if include_avatar:
payload['dataUrl'] = _card_forge_active_character.get('dataUrl', '')
return JSONResponse(payload)


@app.post('/api/beacon/shutdown')
async def beacon_shutdown():
"""Beacon 接口:用于优雅关闭服务器"""
Expand Down Expand Up @@ -1614,6 +1658,7 @@ async def beacon_shutdown():
app.include_router(music_router)
app.include_router(galgame_router)
app.include_router(game_router)
app.include_router(card_drop_router) # 对话掉落卡片:本地 → 云端 N.E.K.O.Servers 代理
app.include_router(capture_router)
app.include_router(cookies_login_router) # Cookies登录相关路由,放在最后以避免与其他API路由冲突
app.include_router(debug_router) # 诊断观测:/api/debug/health(轻量、零侵入,详见 debug_router.py 头注释)
Expand Down Expand Up @@ -2055,6 +2100,24 @@ async def _event_loop_heartbeat():
except Exception as _e:
logger.debug(f"[debug_health] start watchdog failed: {_e}")

# N.E.K.O.Servers 社交平台 facts_sync worker。默认禁用
# (NEKO_FACTS_SYNC_ENABLED=0);启用后 5 分钟 sweep 一次,把高 importance
# facts 推到云端铸造池。完整契约见 N.E.K.O.Servers/.claude/contracts/facts-sync-schema.md。
try:
from main_logic.facts_sync import start_facts_sync_worker # noqa: WPS433 (lazy import 避免循环)
asyncio.create_task(start_facts_sync_worker())
except Exception as _e:
logger.debug(f"[facts_sync] start worker failed: {_e}")

# N.E.K.O.Servers 卡片本地缓存 puller。默认禁用
# (NEKO_CARD_CACHE_ENABLED=0);启用后 5 分钟拉 Servers /api/cards/mine
# 自己的卡片到 memory/<lanlan>/cards/<id>.json。
try:
from main_logic.card_cache import start_card_cache_puller # noqa: WPS433
asyncio.create_task(start_card_cache_puller())
except Exception as _e:
logger.debug(f"[card_cache] start puller failed: {_e}")

blocking_reason = get_storage_startup_blocking_reason(_config_manager)
if blocking_reason:
_enable_main_storage_limited_mode(blocking_reason)
Expand Down
39 changes: 39 additions & 0 deletions app/runtime_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
_INSTALLED: dict[str, bool] = {
"config_runtime": False,
"user_directives_sink": False,
"quota_hooks": False,
}


Expand Down Expand Up @@ -149,3 +150,41 @@ def install_runtime_bindings() -> None:
# config_runtime block 的策略——咽掉避免 startup 二次崩,
# caller (app/__init__) 已经印过 stderr 面包屑。
pass

# ---- main_logic.agent_event_bus ← main_logic.quota dropper -----------
# N.E.K.O.Servers 社交平台配额掉落 hooks(M2-j)。挂载本身不依赖环境变量,
# 是否真触发取决于 dropper._enabled()(要求 NEKO_QUOTA_DROPPER_ENABLED=1
# + NEKO_SOCIAL_BASE_URL 已配)。默认 noop,挂着也无害。
if not _INSTALLED["quota_hooks"]:
try:
from main_logic.agent_event_bus import (
register_text_user_message_hook,
register_user_utterance_sink,
)
from main_logic.quota import on_text_message, on_utterance

# text-message hook 是 first-hit-wins,dropper 已确保返回 None
register_text_user_message_hook(on_text_message)
register_user_utterance_sink(on_utterance)
_INSTALLED["quota_hooks"] = True
except Exception as exc:
_expected_absent = {
"main_logic",
"main_logic.agent_event_bus",
"main_logic.quota",
"main_logic.quota.dropper",
"yaml", # PyYAML 缺失也算预期场景(memory-only worker)
}
_is_expected_absent = (
isinstance(exc, ModuleNotFoundError)
and getattr(exc, "name", None) in _expected_absent
)
if not _is_expected_absent:
try:
from utils.logger_config import get_module_logger
get_module_logger(__name__, "App").warning(
"install_runtime_bindings(quota_hooks) failed unexpectedly",
exc_info=True,
)
except Exception:
pass
12 changes: 12 additions & 0 deletions card-forge/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>奇遇铸造机 · NEKO</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Loading