fix(cloudsave): degrade when local state is unavailable (#1673) #4503
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Analyze | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| # Static-check jobs only need to read the checked-out source. | |
| permissions: | |
| contents: read | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Two parallel jobs — split for blast-radius clarity: | |
| # | |
| # python-lint → "the code itself is wrong" (semantic / async / leaks) | |
| # python-conventions → "the project's architecture / i18n / API rules" | |
| # | |
| # When one fails, the failure name immediately tells you whether to look at the | |
| # code change itself or at how it intersects project conventions. Both jobs run | |
| # in parallel, both block merge, neither needs the other's setup. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| jobs: | |
| python-lint: | |
| name: Python lint (ruff + async-blocking + no-loguru + no-tkinter + no-temperature + prompt-hygiene) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Python 3.11 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Install ruff | |
| # ruff is the only dev dep we need for the lint job; a full uv sync | |
| # pulls heavy native extensions (playwright, numpy, etc.) that are | |
| # unnecessary for static checks. | |
| run: uv tool install ruff==0.15.4 | |
| - name: ruff check (incl. ASYNC210/220/221/222/251) | |
| run: ruff check . | |
| - name: Forbid blocking calls in async def bodies | |
| # Custom AST checker, broader than ruff's ASYNC* rules: | |
| # * gaps flake8-async doesn't catch: Thread/Process.join, | |
| # queue.Queue.get, raw socket recv/accept/connect | |
| # * extra blocking stdlib/3p calls: PIL.Image.open, | |
| # pyautogui.screenshot, Fernet encrypt/decrypt, shutil.*, | |
| # json.load, plus bare-import forms (sleep, rmtree, urlopen) | |
| # * depth-1 transitive: a sync helper whose body hits any of the | |
| # above, called directly from async, is also flagged. | |
| run: python scripts/check_async_blocking.py | |
| - name: Forbid loguru / structlog / logbook imports | |
| # Logging is unified through utils.logger_config (RobustLoggerConfig). | |
| # Re-introducing a third-party logging frontend fragments the surface | |
| # (formatter, sinks, file naming, multi-process behaviour) and breaks | |
| # plugin/main parity. This check fails the build on any such import. | |
| run: python scripts/check_no_loguru.py | |
| - name: Forbid `tkinter` imports | |
| # Architecture rule: frontend/backend are split, ALL GUI operations | |
| # belong on the Electron frontend (Node.js renderer/main), not the | |
| # Python backend. The backend is a headless HTTP/async service — | |
| # it must stay relocatable (remote deployment, headless CI) and | |
| # not own a Tcl/Tk event loop. Past incident: PR #1014's Windows | |
| # tk screenshot overlay crashed the whole app under Nuitka builds | |
| # without `--enable-plugin=tk-inter` (SystemExit from tk.Tk() | |
| # escaped the asyncio worker, killed uvicorn). For dialogs use | |
| # Electron's `dialog` (or platform-native shell bridges in | |
| # storage_location_router.py); for screenshot framing use | |
| # Electron's desktopCapturer region path. | |
| run: python scripts/check_no_tkinter.py | |
| - name: Forbid `temperature=` kwargs on LLM client calls (memory + utils) | |
| # Project policy: do NOT pass `temperature=` to create_chat_llm / | |
| # ChatOpenAI / wrappers in memory_server.py + memory/ + utils/. The | |
| # default (None) omits the field — required for o1/o3/gpt-5-thinking/ | |
| # Claude extended-thinking, and avoids per-call-site temperature drift | |
| # across memory tasks. See memory/__init__.py and .agent/rules/ | |
| # neko-guide.md for the rationale. | |
| run: python scripts/check_no_temperature.py | |
| - name: Enforce prompt-i18n conventions (inline-EN-only + multilang-in-config) | |
| # Two rules in one walker: | |
| # - INLINE_PROMPT_NON_EN: any string at an LLM call site or in a | |
| # module-level *PROMPT/*INSTRUCTION/*SYSTEM constant must be | |
| # English-bodied (CJK ratio <30%). Embedded short examples in | |
| # CJK are allowed under the threshold. | |
| # - I18N_NOT_IN_CONFIG: multi-language dicts (≥2 lang keys, must | |
| # include 'en') belong in config/prompts_*.py, not regular code. | |
| # Per-line `# noqa: <CODE>` suppression supported. See PR #974 for | |
| # the reference incident that motivated this lint. | |
| run: python scripts/check_prompt_hygiene.py | |
| python-conventions: | |
| name: Python conventions (i18n-sync + docs + api-trailing-slash×2 + module-layering) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| # i18n-sync needs full history to diff against the merge-base of | |
| # origin/main. Other steps don't care, but fetch-depth: 0 has | |
| # negligible cost on this repo. | |
| # | |
| # The checkout@v5 bump is the actual fix here: the old @v4 tripped a | |
| # credential-handling regression against the runner's git 2.54 on the | |
| # full-history fetch ("could not read Username", git exit 128), which | |
| # broke this job on every PR. (fetch-tags has no effect under | |
| # fetch-depth: 0 — checkout fetches tags regardless — so we don't set | |
| # it; the fix is purely the version bump.) | |
| fetch-depth: 0 | |
| - name: Set up Python 3.11 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Verify i18n locale files move in lockstep (PR-only) | |
| # Diff-based: when ANY static/locales/*.json or | |
| # frontend/plugin-manager/src/i18n/locales/*.ts changes, ALL files | |
| # in the group must change AND every hunk must occupy the same line | |
| # ranges across all languages. Skipped on direct push to main — | |
| # there's nothing to diff against. | |
| if: github.event_name == 'pull_request' | |
| run: python scripts/check_i18n_sync.py --base "origin/${{ github.base_ref }}" | |
| - name: Forbid relative-up markdown links inside docs/ | |
| # docs/ ships through VitePress with itself as the deploy root; | |
| # any markdown link target starting with '..' resolves outside the | |
| # site and breaks deploy. We've shipped this regression more than | |
| # once — this check fails the build before the next attempt lands. | |
| # Fix is to inline the path as code (`foo/bar.js`) instead of a | |
| # link, or move the referenced content into docs/. | |
| run: python scripts/check_docs_no_relative_paths.py | |
| - name: Forbid trailing-slash route paths on FastAPI decorators (backend) | |
| # Project convention: every backend HTTP/WebSocket endpoint is | |
| # declared WITHOUT a trailing slash. Avoids Starlette's absolute-URL | |
| # 307 redirect under reverse proxies — root cause of the PR #938 | |
| # chara_manager regression: nginx/etc that don't transparently | |
| # forward Host send the redirect Location to 127.0.0.1:<internal>, | |
| # which the LAN browser can't reach (ERR_CONNECTION_REFUSED). | |
| # The lint exempts the literal '/' root page and explicit alias pairs | |
| # (same function carrying both '/foo' and '/foo/'). See | |
| # .agent/rules/neko-guide.md (§"API URL 末尾不带斜杠") and | |
| # main_routers/characters_router.py docstring. | |
| run: python scripts/check_api_trailing_slash.py | |
| - name: Forbid trailing-slash /api/... URL literals (frontend) | |
| # Counterpart to check_api_trailing_slash.py: the backend may declare | |
| # /api/foo without trailing slash, but if frontend code calls | |
| # fetch('/api/foo/') the same 307 → ERR_CONNECTION_REFUSED happens. | |
| # Regex sniffer over static/, frontend/, templates/. Recognises | |
| # prefix builders ('/api/foo/' + id, `/api/foo/${id}`) and exempts | |
| # them; flags only standalone literals ending in '/'. Suppress with | |
| # // noqa: API_TRAILING_SLASH if calling a third-party API that | |
| # genuinely requires the slash. | |
| run: python scripts/check_frontend_api_trailing_slash.py | |
| - name: Enforce top-level module layering (no inversions, no cycles) | |
| # Top-level packages have a strict ordering — only higher layers may | |
| # depend on lower ones (config/steamworks → utils → memory/main_logic | |
| # → main_routers → plugin → brain → app). The walker descends into | |
| # function bodies and string-form dynamic imports | |
| # (importlib.import_module / __import__), so deferred / conditional | |
| # imports cannot smuggle in a layer inversion or cycle. Run | |
| # `python scripts/check_module_layering.py --show-layers` to print | |
| # the hierarchy. Companion unit test: tests/unit/test_module_layering.py. | |
| run: python scripts/check_module_layering.py |