Skip to content

fix(proactive): 修复语音模式 mini-game 邀请「现在不想玩」反复出现 (#1641) #28

fix(proactive): 修复语音模式 mini-game 邀请「现在不想玩」反复出现 (#1641)

fix(proactive): 修复语音模式 mini-game 邀请「现在不想玩」反复出现 (#1641) #28

Workflow file for this run

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