From 301a199b658d89fd151a1d007149b51deadf3758 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 01:43:26 +0800 Subject: [PATCH 01/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E5=B7=A5=E5=85=B7=E9=93=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 session 历史丢失 tool_calls 导致 LLM 输出 XML 格式工具调用 - session_manager.py: add_turn() 接受可选 tool_calls 参数 - voice_pipeline.py: 保存 assistant 回复时同时保存 tool_calls - prompt_assembler.py: 组装 prompt 时恢复 tool_calls 2. 提升易用性:自动安装 Chromium - pyproject.toml: playwright 从 optional 移到必需依赖 - playwright_provider.py: 首次启动时自动检查并安装 Chromium 新用户现在只需: uv sync 无需手动运行 playwright install Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 1 + .../context_runtime/prompt_assembler.py | 8 +++-- .../context_runtime/session_manager.py | 9 ++++-- src/greywind/execution/playwright_provider.py | 29 +++++++++++++++++++ src/greywind/persona/voice_pipeline.py | 13 ++++++++- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3908f7..237bff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "pyautogui>=0.9.54", "pyperclip>=1.8.0", "pygetwindow>=0.0.9", + "playwright>=1.58.0", ] [project.optional-dependencies] diff --git a/src/greywind/context_runtime/prompt_assembler.py b/src/greywind/context_runtime/prompt_assembler.py index dde7232..cb1de4b 100644 --- a/src/greywind/context_runtime/prompt_assembler.py +++ b/src/greywind/context_runtime/prompt_assembler.py @@ -60,10 +60,14 @@ def assemble( # 最近对话 for turn in recent_dialogue: - messages.append({ + msg: Dict[str, Any] = { "role": turn["role"], "content": turn["content"], - }) + } + # 恢复 tool_calls(如果有) + if turn.get("tool_calls"): + msg["tool_calls"] = turn["tool_calls"] + messages.append(msg) # 当前用户输入(可能附带截图) if screen_image_b64: diff --git a/src/greywind/context_runtime/session_manager.py b/src/greywind/context_runtime/session_manager.py index 1c2c155..bef1421 100644 --- a/src/greywind/context_runtime/session_manager.py +++ b/src/greywind/context_runtime/session_manager.py @@ -28,13 +28,16 @@ def state(self) -> str: def state(self, value: str): self._state = value - def add_turn(self, role: str, content: str) -> None: + def add_turn(self, role: str, content: str, tool_calls: List[Dict[str, Any]] | None = None) -> None: """添加一轮对话""" - self._recent_dialogue.append({ + entry: Dict[str, Any] = { "role": role, "content": content, "timestamp": time.time(), - }) + } + if tool_calls: + entry["tool_calls"] = tool_calls + self._recent_dialogue.append(entry) if len(self._recent_dialogue) > self._max_recent * 2: self._recent_dialogue = self._recent_dialogue[-self._max_recent * 2:] diff --git a/src/greywind/execution/playwright_provider.py b/src/greywind/execution/playwright_provider.py index 0d39092..fa1ddfc 100644 --- a/src/greywind/execution/playwright_provider.py +++ b/src/greywind/execution/playwright_provider.py @@ -3,6 +3,7 @@ import asyncio import base64 +import subprocess import uuid from typing import Optional, List, Any @@ -11,6 +12,28 @@ from .base import BrowserProvider, ActionResult, TabInfo +async def _ensure_chromium_installed(): + """检查并自动安装 Chromium 浏览器二进制""" + try: + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + p.chromium.launch() + return True + except Exception: + logger.info("PlaywrightProvider: 正在安装 Chromium...") + try: + subprocess.run( + ["python", "-m", "playwright", "install", "chromium"], + capture_output=True, + check=True, + ) + logger.info("PlaywrightProvider: Chromium 安装完成") + return True + except subprocess.CalledProcessError as e: + logger.error(f"PlaywrightProvider: Chromium 安装失败 — {e}") + return False + + class PlaywrightProvider(BrowserProvider): def __init__(self, screenshot_quality: int = 50, screenshot_width: int = 1280, idle_timeout: int = 60, max_tabs: int = 10, @@ -33,6 +56,12 @@ async def connect(self) -> bool: try: from playwright.async_api import async_playwright import os + + # 首次启动:自动检查并安装 Chromium + if not await _ensure_chromium_installed(): + logger.error("PlaywrightProvider: Chromium 不可用") + return False + self._playwright = await async_playwright().start() # 持久化 user data dir:保留登录态,下次启动不用重新登录 diff --git a/src/greywind/persona/voice_pipeline.py b/src/greywind/persona/voice_pipeline.py index 7ce182e..a96331a 100644 --- a/src/greywind/persona/voice_pipeline.py +++ b/src/greywind/persona/voice_pipeline.py @@ -209,7 +209,18 @@ async def _respond(self, user_text, send_fn, send_audio_fn): ) if clean_response and not self._interrupted: - self.session.add_turn("assistant", _sanitize_llm_text(clean_response)) + # 如果有 tool_calls,也保存到 session + tool_calls_data = None + if tool_calls: + tool_calls_data = [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.function.name, "arguments": tc.function.arguments}, + } + for tc in tool_calls + ] + self.session.add_turn("assistant", _sanitize_llm_text(clean_response), tool_calls_data) elif round_idx == max_rounds and not clean_response: logger.warning("最后一轮(无 tools)LLM 返回空文本,可能模型不兼容 tool result 上下文") From a2410eb5c3b51210fa20e4f546cc3f6671f08e4b Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 10:59:40 +0800 Subject: [PATCH 02/28] =?UTF-8?q?ci:=20=E4=BF=AE=E5=A4=8D=20E2E=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=BC=93=E5=AD=98=E8=B7=AF=E5=BE=84=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20package.json=20=E6=9B=BF=E4=BB=A3=20packag?= =?UTF-8?q?e-lock.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 PR #68 CI 失败:actions/setup-node 的 cache-dependency-path 指向不存在的 package-lock.json,改为使用 package.json。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index b345c9d..6ced318 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -68,7 +68,7 @@ jobs: with: node-version: '18' cache: 'npm' - cache-dependency-path: tests/e2e/package-lock.json + cache-dependency-path: tests/e2e/package.json - name: 安装 E2E 依赖 working-directory: tests/e2e From 316d4fa53d3e550e0c602c1828aedd07b1500e3d Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 11:06:30 +0800 Subject: [PATCH 03/28] =?UTF-8?q?ci:=20=E4=BC=98=E5=8C=96=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E7=AD=96=E7=95=A5=EF=BC=8C=E5=8A=A0=E9=80=9F=20CI=20?= =?UTF-8?q?=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - uv: 启用 setup-uv 内置缓存,避免每次下载 uv 工具 - Playwright: 添加浏览器二进制缓存,命中时跳过下载 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 6ced318..3751ee6 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -19,10 +19,11 @@ jobs: - name: 检出代码 uses: actions/checkout@v4 - - name: 安装 uv + - name: 安装 uv(启用缓存) uses: astral-sh/setup-uv@v4 with: version: "latest" + enable-cache: true - name: 恢复 .venv 缓存 id: cache-venv @@ -47,10 +48,11 @@ jobs: - name: 检出代码 uses: actions/checkout@v4 - - name: 安装 uv + - name: 安装 uv(启用缓存) uses: astral-sh/setup-uv@v4 with: version: "latest" + enable-cache: true - name: 恢复 .venv 缓存 id: cache-venv @@ -74,7 +76,15 @@ jobs: working-directory: tests/e2e run: npm ci + - name: 缓存 Playwright 浏览器 + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~\AppData\Local\ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('tests/e2e/package.json') }} + - name: 安装 Playwright + if: steps.cache-playwright.outputs.cache-hit != 'true' working-directory: tests/e2e run: npx playwright install chromium From 4160f93f7e9f9f88863dbe000fee985a0188c0f7 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 11:12:18 +0800 Subject: [PATCH 04/28] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=20node=5Fmodules?= =?UTF-8?q?=20=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 缓存 tests/e2e/node_modules,命中时跳过 npm ci - 配合 npm 全局缓存,E2E 依赖安装可提速 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 3751ee6..2f38960 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -72,7 +72,15 @@ jobs: cache: 'npm' cache-dependency-path: tests/e2e/package.json + - name: 缓存 node_modules + id: cache-node-modules + uses: actions/cache@v4 + with: + path: tests/e2e/node_modules + key: node-modules-${{ runner.os }}-${{ hashFiles('tests/e2e/package.json') }} + - name: 安装 E2E 依赖 + if: steps.cache-node-modules.outputs.cache-hit != 'true' working-directory: tests/e2e run: npm ci From 59fdea526d10bc830cd74ee9d2e40e84fb96cda7 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 11:33:17 +0800 Subject: [PATCH 05/28] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20E2E=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=9A=84=20package-lock.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm ci 需要 lock 文件,之前缺失导致 CI 失败。 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/package-lock.json | 967 ++++++++++++++++++++++++++++++++++++ 1 file changed, 967 insertions(+) create mode 100644 tests/e2e/package-lock.json diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json new file mode 100644 index 0000000..32bc18f --- /dev/null +++ b/tests/e2e/package-lock.json @@ -0,0 +1,967 @@ +{ + "name": "greywind-e2e-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "greywind-e2e-tests", + "devDependencies": { + "@playwright/test": "^1.44.0", + "electron": "^30.0.0", + "pngjs": "^7.0.0", + "ws": "^8.17.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz", + "integrity": "sha512-AhL7+mZ8Lg14iaNfoYTkXQ2qee8mmsQyllKdqxlpv/zrKgfxz6jNVtcRRbQtLxtF8yzcImWdfTQROpYiPumdbw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} From d536f2efc80ff599d4ace8be2bc1a7cbbd0f95bd Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 12:32:35 +0800 Subject: [PATCH 06/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20E2E=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E8=AF=AD=E6=B3=95=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 测试名称中的引号冲突导致语法错误,将外层引号改为单引号。 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/electron.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/electron.spec.ts b/tests/e2e/electron.spec.ts index ea3b455..40f5781 100644 --- a/tests/e2e/electron.spec.ts +++ b/tests/e2e/electron.spec.ts @@ -54,7 +54,7 @@ test.describe("Electron 窗口基础渲染", () => { } }); - test("status-bar 在连接成功后显示"已连接"", async () => { + test('status-bar 在连接成功后显示"已连接"', async () => { const app = await electron.launch({ args: [ELECTRON_APP_DIR], env: { ...process.env, NODE_ENV: "test" }, From e68e8d3d092017be9b48fc0c3ddfbd65432d01a7 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 12:44:02 +0800 Subject: [PATCH 07/28] =?UTF-8?q?ci:=20=E4=BD=BF=E7=94=A8=20package-lock.j?= =?UTF-8?q?son=20=E4=BD=9C=E4=B8=BA=20node=5Fmodules=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 确保缓存 key 包含完整依赖树信息,避免缓存不完整导致 ws 模块缺失。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 120c97b..a3ea897 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -95,7 +95,7 @@ jobs: uses: actions/cache@v4 with: path: tests/e2e/node_modules - key: node-modules-${{ runner.os }}-${{ hashFiles('tests/e2e/package.json') }} + key: node-modules-${{ runner.os }}-${{ hashFiles('tests/e2e/package-lock.json') }} - name: 安装 E2E 依赖 if: steps.cache-node-modules.outputs.cache-hit != 'true' From e29a9abea4772045721624aebe416fc16a85032a Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 12:44:51 +0800 Subject: [PATCH 08/28] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=20mock=20server?= =?UTF-8?q?=20=E4=BE=9D=E8=B5=96=E9=AA=8C=E8=AF=81=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 npm list ws 检查,确认 ws 模块已正确安装。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index a3ea897..41c2c49 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -102,6 +102,10 @@ jobs: working-directory: tests/e2e run: npm ci + - name: 验证 mock server 依赖 + working-directory: tests/e2e + run: npm list ws + - name: 缓存 Playwright 浏览器 id: cache-playwright uses: actions/cache@v4 From 2fedfe019342b6cb626a6951945f9b726e542e2c Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 12:45:45 +0800 Subject: [PATCH 09/28] =?UTF-8?q?ci:=20Playwright=20=E7=BC=93=E5=AD=98=20k?= =?UTF-8?q?ey=20=E4=B9=9F=E4=BD=BF=E7=94=A8=20package-lock.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一缓存 key 使用 package-lock.json,确保缓存准确性。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 41c2c49..22ba07f 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -111,7 +111,7 @@ jobs: uses: actions/cache@v4 with: path: ~\AppData\Local\ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('tests/e2e/package.json') }} + key: playwright-${{ runner.os }}-${{ hashFiles('tests/e2e/package-lock.json') }} - name: 安装 Playwright if: steps.cache-playwright.outputs.cache-hit != 'true' From 4b95c1f95169454e26906139faea52dde747d095 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 12:53:32 +0800 Subject: [PATCH 10/28] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=20mock=20server?= =?UTF-8?q?=20=E8=BF=9E=E9=80=9A=E6=80=A7=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 E2E 测试前手动启动 mock server 并测试 health endpoint, 确认服务可访问。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 22ba07f..a5d02f7 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -106,6 +106,14 @@ jobs: working-directory: tests/e2e run: npm list ws + - name: 启动 mock server 并测试连通性 + working-directory: tests/e2e + run: | + node mock-server.js & + sleep 3 + curl -v http://127.0.0.1:12393/health || echo "Health check failed" + shell: bash + - name: 缓存 Playwright 浏览器 id: cache-playwright uses: actions/cache@v4 From d28bbeb8fb08d28d692151eeb9b551a1b77fb2c5 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 13:03:09 +0800 Subject: [PATCH 11/28] =?UTF-8?q?ci:=20=E7=A7=BB=E9=99=A4=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E7=9A=84=20mock=20server=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit global-setup.ts 已经会启动 mock server,CI 中不需要再手动启动。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index a5d02f7..22ba07f 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -106,14 +106,6 @@ jobs: working-directory: tests/e2e run: npm list ws - - name: 启动 mock server 并测试连通性 - working-directory: tests/e2e - run: | - node mock-server.js & - sleep 3 - curl -v http://127.0.0.1:12393/health || echo "Health check failed" - shell: bash - - name: 缓存 Playwright 浏览器 id: cache-playwright uses: actions/cache@v4 From d69c7d6198bb62572f942fff9fe312d8c60207fd Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 13:12:56 +0800 Subject: [PATCH 12/28] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E6=8E=A7=E5=88=B6=E5=8F=B0=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E5=92=8C=E5=BB=B6=E9=95=BF=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 捕获 Electron 浏览器控制台日志以便调试 - 增加等待时间和超时时间,给 WebSocket 更多连接时间 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/electron.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/e2e/electron.spec.ts b/tests/e2e/electron.spec.ts index 40f5781..a8c137c 100644 --- a/tests/e2e/electron.spec.ts +++ b/tests/e2e/electron.spec.ts @@ -60,12 +60,21 @@ test.describe("Electron 窗口基础渲染", () => { env: { ...process.env, NODE_ENV: "test" }, }); + // 捕获浏览器控制台日志 + app.on("window", (page) => { + page.on("console", (msg) => { + console.log(`[Browser Console] ${msg.type()}: ${msg.text()}`); + }); + }); + try { const win = await app.firstWindow(); await win.waitForLoadState("domcontentloaded"); + // 等待更长时间让 WebSocket 连接 + await win.waitForTimeout(2000); // WebSocket 连接 + health check 完成后 status-bar 变"已连接" await expect(win.locator("#status-bar")).toContainText("已连接", { - timeout: 8000, + timeout: 15000, }); } finally { await app.close(); From 7f97588a46453721caeaaf2b15977b14ae764d59 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 13:25:09 +0800 Subject: [PATCH 13/28] =?UTF-8?q?fix:=20mock=20server=20=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E5=88=B0=200.0.0.0=20=E4=BB=A5=E5=85=81=E8=AE=B8=20CI=20?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E4=B8=AD=E7=9A=84=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 mock server 从 127.0.0.1 改为 0.0.0.0,使 Electron 可以连接。 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/mock-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/mock-server.js b/tests/e2e/mock-server.js index 9f1a4ce..487ff2a 100644 --- a/tests/e2e/mock-server.js +++ b/tests/e2e/mock-server.js @@ -73,7 +73,7 @@ wss.on("connection", (ws) => { ws.on("error", () => {}); }); -server.listen(PORT, "127.0.0.1", () => { +server.listen(PORT, "0.0.0.0", () => { // 向父进程报告就绪(供 playwright globalSetup 检测) process.stdout.write(`MOCK_SERVER_READY:${PORT}\n`); }); From 35b9e4179033238a150aa9f849ad6ecf7a57608c Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 13:34:31 +0800 Subject: [PATCH 14/28] =?UTF-8?q?fix:=20WebSocket=20URL=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20localhost=20=E6=9B=BF=E4=BB=A3=20127.0.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows CI 环境可能存在 127.0.0.1 解析问题,改为使用 localhost。 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/desktop/renderer/socket-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/desktop/renderer/socket-client.js b/frontend/desktop/renderer/socket-client.js index 2ee4472..b22b3ec 100644 --- a/frontend/desktop/renderer/socket-client.js +++ b/frontend/desktop/renderer/socket-client.js @@ -1,7 +1,7 @@ /** * WebSocket 客户端 — 连接灰风后端 */ -const WS_URL = "ws://127.0.0.1:12393/ws"; +const WS_URL = "ws://localhost:12393/ws"; let ws = null; let reconnectTimer = null; let pendingAudioMeta = null; From d51485f0bb701ad9213fac87709a02b69b3ca52a Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 13:42:24 +0800 Subject: [PATCH 15/28] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E8=AF=8A=E6=96=AD=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 检查端口占用、localhost 连通性和 hosts 配置。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 22ba07f..a009af0 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -106,6 +106,24 @@ jobs: working-directory: tests/e2e run: npm list ws + - name: 诊断网络状态 + shell: pwsh + run: | + Write-Host "Checking if port 12393 is in use..." + $connection = Get-NetTCPConnection -LocalPort 12393 -ErrorAction SilentlyContinue + if ($connection) { + Write-Host "Port 12393 is in use by process: $($connection.OwningProcess)" + Get-Process -Id $connection.OwningProcess + } else { + Write-Host "Port 12393 is NOT in use" + } + + Write-Host "`nTesting localhost connectivity..." + Test-NetConnection -ComputerName localhost -Port 12393 -WarningAction SilentlyContinue + + Write-Host "`nHosts file content:" + Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String -Pattern "localhost" + - name: 缓存 Playwright 浏览器 id: cache-playwright uses: actions/cache@v4 From bfebdc95c055df9660c91d038ff9351e79a359a1 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 13:54:49 +0800 Subject: [PATCH 16/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Windows=20CI?= =?UTF-8?q?=20=E4=B8=8A=20mock=20server=20=E5=90=AF=E5=8A=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 shell: true 以支持 Windows 环境 - 添加详细日志便于调试 - 延长超时时间到 10 秒 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/global-setup.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index c5492a7..076950e 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -11,36 +11,57 @@ declare global { export default async function globalSetup() { await new Promise((resolve, reject) => { const serverPath = path.join(__dirname, "mock-server.js"); + const isWindows = process.platform === "win32"; + + // Windows 需要 shell: true 才能正确执行 node const proc = spawn("node", [serverPath, "12393"], { stdio: ["ignore", "pipe", "pipe"], + shell: isWindows, + windowsHide: true, }); let ready = false; + let stdoutBuffer = ""; + + proc.stdout?.on("data", (data: Buffer) => { + const str = data.toString(); + stdoutBuffer += str; + console.log(`[Mock Server] ${str.trim()}`); - proc.stdout.on("data", (data: Buffer) => { - if (!ready && data.toString().includes("MOCK_SERVER_READY")) { + if (!ready && str.includes("MOCK_SERVER_READY")) { ready = true; globalThis.__MOCK_SERVER__ = proc; resolve(); } }); - proc.stderr.on("data", (data: Buffer) => { + proc.stderr?.on("data", (data: Buffer) => { + const str = data.toString(); + console.error(`[Mock Server Error] ${str.trim()}`); + if (!ready) { // ws 模块缺失时在这里报错 - reject(new Error(`Mock server error: ${data.toString()}`)); + reject(new Error(`Mock server error: ${str}`)); } }); proc.on("exit", (code) => { + console.log(`[Mock Server] Exited with code ${code}`); if (!ready) { - reject(new Error(`Mock server exited early with code ${code}`)); + reject(new Error(`Mock server exited early with code ${code}, stdout: ${stdoutBuffer}`)); } }); + proc.on("error", (err) => { + console.error(`[Mock Server] Failed to start: ${err.message}`); + reject(new Error(`Mock server failed to start: ${err.message}`)); + }); + // 超时保护 setTimeout(() => { - if (!ready) reject(new Error("Mock server startup timeout")); - }, 5000); + if (!ready) { + reject(new Error(`Mock server startup timeout, stdout: ${stdoutBuffer}`)); + } + }, 10000); }); } From 85d2cf2b73bd75fd8110af28e136c9ae048f6524 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 14:04:09 +0800 Subject: [PATCH 17/28] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=20process.execP?= =?UTF-8?q?ath=20=E5=90=AF=E5=8A=A8=20mock=20server=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=20shell=20=E8=BF=9B=E7=A8=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 process.execPath 替代 'node' 命令 - 移除 shell: true,直接启动 node 进程 - 避免 shell 退出导致子进程被杀死 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/global-setup.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index 076950e..df15127 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -13,10 +13,10 @@ export default async function globalSetup() { const serverPath = path.join(__dirname, "mock-server.js"); const isWindows = process.platform === "win32"; - // Windows 需要 shell: true 才能正确执行 node - const proc = spawn("node", [serverPath, "12393"], { + // 直接使用 node 启动,Windows 下不使用 shell 以避免进程层级问题 + const proc = spawn(process.execPath, [serverPath, "12393"], { stdio: ["ignore", "pipe", "pipe"], - shell: isWindows, + detached: false, windowsHide: true, }); @@ -40,7 +40,6 @@ export default async function globalSetup() { console.error(`[Mock Server Error] ${str.trim()}`); if (!ready) { - // ws 模块缺失时在这里报错 reject(new Error(`Mock server error: ${str}`)); } }); From acbbfd14d3b0386d4e77222fd13be36f7401271e Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 14:15:04 +0800 Subject: [PATCH 18/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=BC=BA=20mock=20server?= =?UTF-8?q?=20=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加全局 uncaughtException 和 unhandledRejection 处理 - 添加 WebSocket 服务器级别错误处理 - 添加 HTTP 服务器错误处理 - 添加心跳定时器保持进程运行 - 添加详细的连接/关闭日志 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/mock-server.js | 75 ++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/tests/e2e/mock-server.js b/tests/e2e/mock-server.js index 487ff2a..5684337 100644 --- a/tests/e2e/mock-server.js +++ b/tests/e2e/mock-server.js @@ -13,6 +13,15 @@ const WebSocket = require("ws"); const PORT = parseInt(process.argv[2] || "12393", 10); +// 全局错误处理,防止进程退出 +process.on("uncaughtException", (err) => { + console.error("[Mock Server] Uncaught exception:", err.message); +}); + +process.on("unhandledRejection", (reason) => { + console.error("[Mock Server] Unhandled rejection:", reason); +}); + const server = http.createServer((req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); @@ -33,9 +42,21 @@ const server = http.createServer((req, res) => { res.end("Not found"); }); +// HTTP 服务器错误处理 +server.on("error", (err) => { + console.error("[Mock Server] HTTP server error:", err.message); +}); + const wss = new WebSocket.Server({ server, path: "/ws" }); -wss.on("connection", (ws) => { +// WebSocket 服务器错误处理 +wss.on("error", (err) => { + console.error("[Mock Server] WebSocket server error:", err.message); +}); + +wss.on("connection", (ws, req) => { + console.log("[Mock Server] WebSocket connection from:", req.socket.remoteAddress); + ws.on("message", (data) => { let msg; try { @@ -51,26 +72,44 @@ wss.on("connection", (ws) => { const reply = `你好,我是灰风。你说了:「${userText}」`; // 模拟 thinking → speaking → reply_text → idle 序列 - ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); - setTimeout(() => { - ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); - ws.send( - JSON.stringify({ - type: "reply_text", - payload: { text: reply, emotion: "neutral" }, - }) - ); - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - }, 80); + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); + setTimeout(() => { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); + ws.send( + JSON.stringify({ + type: "reply_text", + payload: { text: reply, emotion: "neutral" }, + }) + ); + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) { + // 连接可能已关闭 + } + }, 80); + } catch (e) { + // 连接可能已关闭 + } } else if (type === "interrupt") { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) {} } else if (type === "clear_history") { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) {} } // 其他消息类型静默忽略 }); - ws.on("error", () => {}); + ws.on("error", (err) => { + console.error("[Mock Server] WebSocket error:", err.message); + }); + + ws.on("close", () => { + console.log("[Mock Server] WebSocket closed"); + }); }); server.listen(PORT, "0.0.0.0", () => { @@ -79,6 +118,12 @@ server.listen(PORT, "0.0.0.0", () => { }); process.on("SIGTERM", () => { + console.log("[Mock Server] Received SIGTERM, shutting down..."); server.close(); process.exit(0); }); + +// 保持进程运行 +setInterval(() => { + // 心跳,防止进程退出 +}, 60000); From c8e9a4afa1bc5bd096317430f79f1661bfb4a756 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 14:35:44 +0800 Subject: [PATCH 19/28] =?UTF-8?q?fix:=20=E5=A2=9E=E5=BC=BA=20mock=20server?= =?UTF-8?q?=20WebSocket=20=E8=BF=9E=E6=8E=A5=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 ws.readyState 检查确保连接打开才发送 - 添加详细的错误日志 - 在事件处理器中立即设置错误处理 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/mock-server.js | 60 +++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/tests/e2e/mock-server.js b/tests/e2e/mock-server.js index 5684337..a84ee5c 100644 --- a/tests/e2e/mock-server.js +++ b/tests/e2e/mock-server.js @@ -57,24 +57,42 @@ wss.on("error", (err) => { wss.on("connection", (ws, req) => { console.log("[Mock Server] WebSocket connection from:", req.socket.remoteAddress); + // 设置 WebSocket 错误处理 + ws.on("error", (err) => { + console.error("[Mock Server] WebSocket error:", err.message); + }); + + ws.on("close", (code, reason) => { + console.log("[Mock Server] WebSocket closed:", code, reason?.toString()); + }); + ws.on("message", (data) => { let msg; try { msg = JSON.parse(data.toString()); } catch { + console.log("[Mock Server] Invalid JSON received"); return; } const { type, payload = {} } = msg; + console.log("[Mock Server] Received message:", type); if (type === "text_input") { const userText = payload.text || ""; const reply = `你好,我是灰风。你说了:「${userText}」`; // 模拟 thinking → speaking → reply_text → idle 序列 - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); - setTimeout(() => { + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); + } catch (e) { + console.error("[Mock Server] Failed to send thinking:", e.message); + } + } + + setTimeout(() => { + if (ws.readyState === ws.OPEN) { try { ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); ws.send( @@ -85,31 +103,29 @@ wss.on("connection", (ws, req) => { ); ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); } catch (e) { - // 连接可能已关闭 + console.error("[Mock Server] Failed to send reply:", e.message); } - }, 80); - } catch (e) { - // 连接可能已关闭 - } + } + }, 80); } else if (type === "interrupt") { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) {} + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) { + console.error("[Mock Server] Failed to send idle:", e.message); + } + } } else if (type === "clear_history") { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) {} + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) { + console.error("[Mock Server] Failed to send idle:", e.message); + } + } } // 其他消息类型静默忽略 }); - - ws.on("error", (err) => { - console.error("[Mock Server] WebSocket error:", err.message); - }); - - ws.on("close", () => { - console.log("[Mock Server] WebSocket closed"); - }); }); server.listen(PORT, "0.0.0.0", () => { From 197a7354e6596d36e07978528ef396749dee6b69 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 14:55:09 +0800 Subject: [PATCH 20/28] =?UTF-8?q?ci:=20=E8=A7=A6=E5=8F=91=E6=96=B0?= =?UTF-8?q?=E7=9A=84=20CI=20=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 7967d5dd0939ec3f33c66471f0e33f895c2fd33f Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 15:03:27 +0800 Subject: [PATCH 21/28] =?UTF-8?q?fix:=20=E5=B0=86=E6=95=B4=E4=B8=AA=20mock?= =?UTF-8?q?=20server=20=E5=8C=85=E8=A3=85=E5=9C=A8=20try-catch=20=E4=B8=AD?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=9B=B4=E5=A4=9A=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将整个代码包装在 try-catch 中 - 添加更多错误处理和日志 - 添加 beforeExit 和 exit 事件处理 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/mock-server.js | 290 +++++++++++++++++++++++---------------- 1 file changed, 172 insertions(+), 118 deletions(-) diff --git a/tests/e2e/mock-server.js b/tests/e2e/mock-server.js index a84ee5c..7cb9b6a 100644 --- a/tests/e2e/mock-server.js +++ b/tests/e2e/mock-server.js @@ -8,138 +8,192 @@ * 用法:node mock-server.js [port] */ -const http = require("http"); -const WebSocket = require("ws"); - -const PORT = parseInt(process.argv[2] || "12393", 10); - -// 全局错误处理,防止进程退出 -process.on("uncaughtException", (err) => { - console.error("[Mock Server] Uncaught exception:", err.message); -}); - -process.on("unhandledRejection", (reason) => { - console.error("[Mock Server] Unhandled rejection:", reason); -}); - -const server = http.createServer((req, res) => { - const url = new URL(req.url, `http://localhost:${PORT}`); - - if (url.pathname === "/health") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - character: "灰风", - engines: { vad: false, asr: false, tts: false, screen_sense: false }, - screen: { capture_interval: 3.0, monitor: "active" }, - }) - ); - return; - } - - res.writeHead(404); - res.end("Not found"); -}); - -// HTTP 服务器错误处理 -server.on("error", (err) => { - console.error("[Mock Server] HTTP server error:", err.message); -}); - -const wss = new WebSocket.Server({ server, path: "/ws" }); - -// WebSocket 服务器错误处理 -wss.on("error", (err) => { - console.error("[Mock Server] WebSocket server error:", err.message); -}); - -wss.on("connection", (ws, req) => { - console.log("[Mock Server] WebSocket connection from:", req.socket.remoteAddress); - - // 设置 WebSocket 错误处理 - ws.on("error", (err) => { - console.error("[Mock Server] WebSocket error:", err.message); +// 立即设置全局错误处理 +try { + const http = require("http"); + const WebSocket = require("ws"); + + const PORT = parseInt(process.argv[2] || "12393", 10); + + // 全局错误处理,防止进程退出 + process.on("uncaughtException", (err) => { + console.error("[Mock Server] Uncaught exception:", err.message, err.stack); + // 不退出进程 + }); + + process.on("unhandledRejection", (reason, promise) => { + console.error("[Mock Server] Unhandled rejection:", reason); + // 不退出进程 + }); + + // 防止进程自然退出 + process.on("beforeExit", (code) => { + console.log("[Mock Server] beforeExit with code:", code); }); - ws.on("close", (code, reason) => { - console.log("[Mock Server] WebSocket closed:", code, reason?.toString()); + process.on("exit", (code) => { + console.log("[Mock Server] exit with code:", code); }); - ws.on("message", (data) => { - let msg; + const server = http.createServer((req, res) => { try { - msg = JSON.parse(data.toString()); - } catch { - console.log("[Mock Server] Invalid JSON received"); - return; + const url = new URL(req.url, `http://localhost:${PORT}`); + + if (url.pathname === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + status: "ok", + character: "灰风", + engines: { vad: false, asr: false, tts: false, screen_sense: false }, + screen: { capture_interval: 3.0, monitor: "active" }, + }) + ); + return; + } + + res.writeHead(404); + res.end("Not found"); + } catch (err) { + console.error("[Mock Server] HTTP request error:", err.message); + res.writeHead(500); + res.end("Internal error"); } + }); + + // HTTP 服务器错误处理 + server.on("error", (err) => { + console.error("[Mock Server] HTTP server error:", err.message); + }); + + const wss = new WebSocket.Server({ server, path: "/ws" }); + + // WebSocket 服务器错误处理 + wss.on("error", (err) => { + console.error("[Mock Server] WebSocket server error:", err.message); + }); + + wss.on("connection", (ws, req) => { + console.log("[Mock Server] WebSocket connection from:", req.socket.remoteAddress); - const { type, payload = {} } = msg; - console.log("[Mock Server] Received message:", type); + // 设置 WebSocket 错误处理 + ws.on("error", (err) => { + console.error("[Mock Server] WebSocket error:", err.message); + }); - if (type === "text_input") { - const userText = payload.text || ""; - const reply = `你好,我是灰风。你说了:「${userText}」`; + ws.on("close", (code, reason) => { + console.log("[Mock Server] WebSocket closed:", code, reason?.toString()); + }); - // 模拟 thinking → speaking → reply_text → idle 序列 - if (ws.readyState === ws.OPEN) { + ws.on("message", (data) => { + try { + let msg; try { - ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); - } catch (e) { - console.error("[Mock Server] Failed to send thinking:", e.message); + msg = JSON.parse(data.toString()); + } catch { + console.log("[Mock Server] Invalid JSON received"); + return; } - } - setTimeout(() => { - if (ws.readyState === ws.OPEN) { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); - ws.send( - JSON.stringify({ - type: "reply_text", - payload: { text: reply, emotion: "neutral" }, - }) - ); - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) { - console.error("[Mock Server] Failed to send reply:", e.message); + const { type, payload = {} } = msg; + console.log("[Mock Server] Received message:", type); + + if (type === "text_input") { + const userText = payload.text || ""; + const reply = `你好,我是灰风。你说了:「${userText}」`; + + // 模拟 thinking → speaking → reply_text → idle 序列 + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); + } catch (e) { + console.error("[Mock Server] Failed to send thinking:", e.message); + } + } + + setTimeout(() => { + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); + ws.send( + JSON.stringify({ + type: "reply_text", + payload: { text: reply, emotion: "neutral" }, + }) + ); + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) { + console.error("[Mock Server] Failed to send reply:", e.message); + } + } + }, 80); + } else if (type === "interrupt") { + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) { + console.error("[Mock Server] Failed to send idle:", e.message); + } + } + } else if (type === "clear_history") { + if (ws.readyState === ws.OPEN) { + try { + ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); + } catch (e) { + console.error("[Mock Server] Failed to send idle:", e.message); + } } } - }, 80); - } else if (type === "interrupt") { - if (ws.readyState === ws.OPEN) { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) { - console.error("[Mock Server] Failed to send idle:", e.message); - } - } - } else if (type === "clear_history") { - if (ws.readyState === ws.OPEN) { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) { - console.error("[Mock Server] Failed to send idle:", e.message); - } + // 其他消息类型静默忽略 + } catch (err) { + console.error("[Mock Server] Message handling error:", err.message); } + }); + }); + + server.listen(PORT, "0.0.0.0", () => { + // 向父进程报告就绪(供 playwright globalSetup 检测) + process.stdout.write(`MOCK_SERVER_READY:${PORT}\n`); + }); + + // 正确处理 SIGTERM + process.on("SIGTERM", () => { + console.log("[Mock Server] Received SIGTERM, shutting down..."); + try { + server.close(); + } catch (e) { + console.error("[Mock Server] Error closing server:", e.message); + } + process.exit(0); + }); + + // 正确处理 SIGINT + process.on("SIGINT", () => { + console.log("[Mock Server] Received SIGINT, shutting down..."); + try { + server.close(); + } catch (e) { + console.error("[Mock Server] Error closing server:", e.message); } - // 其他消息类型静默忽略 + process.exit(0); }); -}); - -server.listen(PORT, "0.0.0.0", () => { - // 向父进程报告就绪(供 playwright globalSetup 检测) - process.stdout.write(`MOCK_SERVER_READY:${PORT}\n`); -}); - -process.on("SIGTERM", () => { - console.log("[Mock Server] Received SIGTERM, shutting down..."); - server.close(); - process.exit(0); -}); - -// 保持进程运行 -setInterval(() => { - // 心跳,防止进程退出 -}, 60000); + + // 保持进程运行 - 使用更可靠的方式 + const keepAlive = setInterval(() => { + console.log("[Mock Server] Keepalive ping"); + }, 30000); + + // 确保定时器不会阻止进程退出(当需要时) + keepAlive.unref?.() || clearInterval(keepAlive); + + // 重新设置一个不会阻止退出的定时器 + setInterval(() => { + // 心跳,防止进程自然退出 + }, 60000); + + console.log("[Mock Server] Starting up..."); + +} catch (err) { + console.error("[Mock Server] Fatal error during startup:", err.message, err.stack); + process.exit(1); +} From d44009afcdc1bc441c6668804e8e9d98217a7dd4 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 15:10:21 +0800 Subject: [PATCH 22/28] =?UTF-8?q?simplify:=20=E7=AE=80=E5=8C=96=20mock=20s?= =?UTF-8?q?erver=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化代码结构 - 在每个步骤添加日志 - 更容易定位崩溃位置 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/mock-server.js | 165 ++++++++++----------------------------- 1 file changed, 42 insertions(+), 123 deletions(-) diff --git a/tests/e2e/mock-server.js b/tests/e2e/mock-server.js index 7cb9b6a..df1130d 100644 --- a/tests/e2e/mock-server.js +++ b/tests/e2e/mock-server.js @@ -1,38 +1,26 @@ /** * 灰风 E2E 测试用 Mock 后端 - * - * 实现最小化的 HTTP + WebSocket 服务器,模拟 Python 后端协议: - * - GET /health → 返回 ok - * - WebSocket /ws → 接收 text_input,返回 thinking/speaking/reply_text/idle - * - * 用法:node mock-server.js [port] */ -// 立即设置全局错误处理 +console.log("[Mock Server] Script starting..."); + try { const http = require("http"); + console.log("[Mock Server] http module loaded"); + const WebSocket = require("ws"); + console.log("[Mock Server] ws module loaded, version:", WebSocket.VERSION); const PORT = parseInt(process.argv[2] || "12393", 10); + console.log("[Mock Server] Using port:", PORT); - // 全局错误处理,防止进程退出 + // 全局错误处理 process.on("uncaughtException", (err) => { console.error("[Mock Server] Uncaught exception:", err.message, err.stack); - // 不退出进程 }); - process.on("unhandledRejection", (reason, promise) => { + process.on("unhandledRejection", (reason) => { console.error("[Mock Server] Unhandled rejection:", reason); - // 不退出进程 - }); - - // 防止进程自然退出 - process.on("beforeExit", (code) => { - console.log("[Mock Server] beforeExit with code:", code); - }); - - process.on("exit", (code) => { - console.log("[Mock Server] exit with code:", code); }); const server = http.createServer((req, res) => { @@ -41,159 +29,90 @@ try { if (url.pathname === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - status: "ok", - character: "灰风", - engines: { vad: false, asr: false, tts: false, screen_sense: false }, - screen: { capture_interval: 3.0, monitor: "active" }, - }) - ); + res.end(JSON.stringify({ status: "ok" })); return; } res.writeHead(404); res.end("Not found"); } catch (err) { - console.error("[Mock Server] HTTP request error:", err.message); + console.error("[Mock Server] HTTP error:", err.message); res.writeHead(500); - res.end("Internal error"); + res.end("Error"); } }); - // HTTP 服务器错误处理 server.on("error", (err) => { - console.error("[Mock Server] HTTP server error:", err.message); + console.error("[Mock Server] Server error:", err.message); }); + console.log("[Mock Server] Creating WebSocket server..."); + const wss = new WebSocket.Server({ server, path: "/ws" }); - // WebSocket 服务器错误处理 wss.on("error", (err) => { - console.error("[Mock Server] WebSocket server error:", err.message); + console.error("[Mock Server] WSS error:", err.message); }); wss.on("connection", (ws, req) => { - console.log("[Mock Server] WebSocket connection from:", req.socket.remoteAddress); + console.log("[Mock Server] Client connected from:", req.socket?.remoteAddress); - // 设置 WebSocket 错误处理 ws.on("error", (err) => { - console.error("[Mock Server] WebSocket error:", err.message); + console.error("[Mock Server] WS error:", err.message); }); - ws.on("close", (code, reason) => { - console.log("[Mock Server] WebSocket closed:", code, reason?.toString()); + ws.on("close", () => { + console.log("[Mock Server] Client disconnected"); }); ws.on("message", (data) => { try { - let msg; - try { - msg = JSON.parse(data.toString()); - } catch { - console.log("[Mock Server] Invalid JSON received"); - return; - } + const msg = JSON.parse(data.toString()); + console.log("[Mock Server] Received:", msg.type); - const { type, payload = {} } = msg; - console.log("[Mock Server] Received message:", type); + if (msg.type === "text_input") { + const reply = `你好,我是灰风。你说了:「${msg.payload?.text || ""}」`; - if (type === "text_input") { - const userText = payload.text || ""; - const reply = `你好,我是灰风。你说了:「${userText}」`; - - // 模拟 thinking → speaking → reply_text → idle 序列 - if (ws.readyState === ws.OPEN) { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); - } catch (e) { - console.error("[Mock Server] Failed to send thinking:", e.message); - } - } + ws.send(JSON.stringify({ type: "status", payload: { state: "thinking" } })); setTimeout(() => { - if (ws.readyState === ws.OPEN) { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); - ws.send( - JSON.stringify({ - type: "reply_text", - payload: { text: reply, emotion: "neutral" }, - }) - ); - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) { - console.error("[Mock Server] Failed to send reply:", e.message); - } - } - }, 80); - } else if (type === "interrupt") { - if (ws.readyState === ws.OPEN) { try { + ws.send(JSON.stringify({ type: "status", payload: { state: "speaking" } })); + ws.send(JSON.stringify({ + type: "reply_text", + payload: { text: reply, emotion: "neutral" }, + })); ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); } catch (e) { - console.error("[Mock Server] Failed to send idle:", e.message); + console.error("[Mock Server] Send error:", e.message); } - } - } else if (type === "clear_history") { - if (ws.readyState === ws.OPEN) { - try { - ws.send(JSON.stringify({ type: "status", payload: { state: "idle" } })); - } catch (e) { - console.error("[Mock Server] Failed to send idle:", e.message); - } - } + }, 80); } - // 其他消息类型静默忽略 } catch (err) { - console.error("[Mock Server] Message handling error:", err.message); + console.error("[Mock Server] Message error:", err.message); } }); }); + console.log("[Mock Server] Starting HTTP server..."); + server.listen(PORT, "0.0.0.0", () => { - // 向父进程报告就绪(供 playwright globalSetup 检测) - process.stdout.write(`MOCK_SERVER_READY:${PORT}\n`); + console.log(`MOCK_SERVER_READY:${PORT}`); }); - // 正确处理 SIGTERM + // 防止进程退出 process.on("SIGTERM", () => { - console.log("[Mock Server] Received SIGTERM, shutting down..."); - try { - server.close(); - } catch (e) { - console.error("[Mock Server] Error closing server:", e.message); - } + console.log("[Mock Server] SIGTERM received"); + server.close(); process.exit(0); }); - // 正确处理 SIGINT - process.on("SIGINT", () => { - console.log("[Mock Server] Received SIGINT, shutting down..."); - try { - server.close(); - } catch (e) { - console.error("[Mock Server] Error closing server:", e.message); - } - process.exit(0); - }); - - // 保持进程运行 - 使用更可靠的方式 - const keepAlive = setInterval(() => { - console.log("[Mock Server] Keepalive ping"); - }, 30000); - - // 确保定时器不会阻止进程退出(当需要时) - keepAlive.unref?.() || clearInterval(keepAlive); - - // 重新设置一个不会阻止退出的定时器 - setInterval(() => { - // 心跳,防止进程自然退出 - }, 60000); + // 保持运行 + setInterval(() => {}, 60000); - console.log("[Mock Server] Starting up..."); + console.log("[Mock Server] Setup complete, waiting for connections..."); } catch (err) { - console.error("[Mock Server] Fatal error during startup:", err.message, err.stack); + console.error("[Mock Server] Fatal error:", err.message, err.stack); process.exit(1); } From 39f8ad612e54bbc903585b2aa2ba985f432dab27 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 15:47:16 +0800 Subject: [PATCH 23/28] =?UTF-8?q?fix:=20interrupt=20=E5=90=8E=E8=A1=A5?= =?UTF-8?q?=E5=8F=91=20status:idle=20=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=AD=BB=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:_respond task 被 cancel 后 finally 块因 _interrupted=True 跳过发 status:idle;ws_handler interrupt 分支也不发。 导致 test_interrupt_does_not_crash 在 CI 死锁 15 分钟。 修复:interrupt() 返回 was_running 布尔值; ws_handler 仅在确实取消了正在运行的任务时才补发 status:idle, 避免 _respond 已正常完成时发出多余的 idle 消息。 --- src/greywind/persona/voice_pipeline.py | 6 +++++- src/greywind/server/ws_handler.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/greywind/persona/voice_pipeline.py b/src/greywind/persona/voice_pipeline.py index a96331a..f9784dd 100644 --- a/src/greywind/persona/voice_pipeline.py +++ b/src/greywind/persona/voice_pipeline.py @@ -131,15 +131,19 @@ def _start_response(self, text, send_fn, send_audio_fn): self._respond(text, send_fn, send_audio_fn) ) - async def interrupt(self): + async def interrupt(self) -> bool: + """打断当前响应。返回 True 表示确实取消了正在运行的任务。""" self._interrupted = True + was_running = False if self._response_task and not self._response_task.done(): + was_running = True self._response_task.cancel() try: await self._response_task except asyncio.CancelledError: pass self._interrupted = False + return was_running async def _respond(self, user_text, send_fn, send_audio_fn): self._interrupted = False diff --git a/src/greywind/server/ws_handler.py b/src/greywind/server/ws_handler.py index 7bd3819..11b7696 100644 --- a/src/greywind/server/ws_handler.py +++ b/src/greywind/server/ws_handler.py @@ -158,7 +158,9 @@ async def send_audio(audio_bytes: bytes, payload: dict): logger.info(f"桌面操控即时切换: enabled={enabled}") elif msg_type == "interrupt": - await pipeline.interrupt() + was_running = await pipeline.interrupt() + if was_running: + await send_msg({"type": "status", "payload": {"state": "idle"}}) elif msg_type == "clear_history": pipeline.session.clear() From 04fb43f54e59d8472c35950d8b72870c647f05d6 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 16:09:23 +0800 Subject: [PATCH 24/28] =?UTF-8?q?fix:=20=E6=94=B9=E7=94=A8=20HTTP=20?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=E4=BB=A3=E6=9B=BF=20pipe=20READY=20=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E4=BF=AE=E5=A4=8D=20mock=20server=20EPIPE=20=E5=B4=A9?= =?UTF-8?q?=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:stdio: pipe + detached 模式下,parent 关闭 stdout pipe 后 child 写 console.log 触发 EPIPE → uncaughtException 里再 console.error → 又 EPIPE → Node.js 因 exception-in-exception-handler 以 exit 1 退出。 这解释了为何有 10+ 次 fix 尝试都失败:都在 mock-server.js 里加错误处理, 但根本问题在 global-setup.ts 的 pipe 设计。 修复:spawn 时改为 stdio: 'ignore'(无 pipe),用 HTTP /health 轮询就绪, 彻底消除 EPIPE 可能。 --- tests/e2e/global-setup.ts | 84 +++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index df15127..0ef2d31 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -1,66 +1,54 @@ /** * globalSetup — 在所有 Electron 测试前启动 mock 后端服务器 + * + * 使用 stdio: 'ignore' + HTTP 健康检查轮询,彻底避免 pipe EPIPE 问题。 + * 之前的 pipe 方案:parent 关闭 stdout pipe 后,child 写日志触发 EPIPE → + * uncaughtException 里再 console.error → 又 EPIPE → exit code 1。 */ import { spawn, ChildProcess } from "child_process"; +import * as http from "http"; import path from "path"; declare global { var __MOCK_SERVER__: ChildProcess | undefined; } -export default async function globalSetup() { - await new Promise((resolve, reject) => { - const serverPath = path.join(__dirname, "mock-server.js"); - const isWindows = process.platform === "win32"; - - // 直接使用 node 启动,Windows 下不使用 shell 以避免进程层级问题 - const proc = spawn(process.execPath, [serverPath, "12393"], { - stdio: ["ignore", "pipe", "pipe"], - detached: false, - windowsHide: true, +function checkHealth(port: number): Promise { + return new Promise((resolve) => { + const req = http.get(`http://127.0.0.1:${port}/health`, (res) => { + resolve(res.statusCode === 200); + res.resume(); }); - - let ready = false; - let stdoutBuffer = ""; - - proc.stdout?.on("data", (data: Buffer) => { - const str = data.toString(); - stdoutBuffer += str; - console.log(`[Mock Server] ${str.trim()}`); - - if (!ready && str.includes("MOCK_SERVER_READY")) { - ready = true; - globalThis.__MOCK_SERVER__ = proc; - resolve(); - } + req.on("error", () => resolve(false)); + req.setTimeout(1000, () => { + req.destroy(); + resolve(false); }); + }); +} - proc.stderr?.on("data", (data: Buffer) => { - const str = data.toString(); - console.error(`[Mock Server Error] ${str.trim()}`); +export default async function globalSetup() { + const serverPath = path.join(__dirname, "mock-server.js"); + const PORT = 12393; - if (!ready) { - reject(new Error(`Mock server error: ${str}`)); - } - }); + const proc = spawn(process.execPath, [serverPath, String(PORT)], { + stdio: "ignore", // 不建立 pipe,避免 EPIPE 导致 mock server 崩溃 + detached: true, // 独立运行,不受 Playwright worker 进程影响 + windowsHide: true, + }); + proc.unref(); // 不阻塞父进程退出 - proc.on("exit", (code) => { - console.log(`[Mock Server] Exited with code ${code}`); - if (!ready) { - reject(new Error(`Mock server exited early with code ${code}, stdout: ${stdoutBuffer}`)); - } - }); + globalThis.__MOCK_SERVER__ = proc; - proc.on("error", (err) => { - console.error(`[Mock Server] Failed to start: ${err.message}`); - reject(new Error(`Mock server failed to start: ${err.message}`)); - }); + // 等待 /health 端点就绪(最多 10 秒,每 200ms 轮询一次) + const deadline = Date.now() + 10_000; + while (Date.now() < deadline) { + if (await checkHealth(PORT)) { + console.log(`[Mock Server] ready on port ${PORT}`); + return; + } + await new Promise((res) => setTimeout(res, 200)); + } - // 超时保护 - setTimeout(() => { - if (!ready) { - reject(new Error(`Mock server startup timeout, stdout: ${stdoutBuffer}`)); - } - }, 10000); - }); + throw new Error(`Mock server failed to start within 10s (port ${PORT})`); } From c77e43225260d542e107789606f32235ecbd8b00 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 16:23:48 +0800 Subject: [PATCH 25/28] =?UTF-8?q?fix:=20E2E=20=E6=B5=8B=E8=AF=95=E6=97=B6?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=E5=90=8E=E7=AB=AF=E5=90=AF=E5=8A=A8=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=20mock=20server=20=E8=A2=AB=E6=9D=80?= =?UTF-8?q?=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NODE_ENV=test 时,startBackend() 会先用 netstat 查杀端口 12393 上的进程(即 mock server),再尝试启动 Python 后端(CI 无 runtime),导致 WebSocket 无处可连,前端显示"已断开 - 重连中..."。 添加 NODE_ENV 守卫,测试模式下跳过 startBackend(),mock server 保持运行,Electron 可正常连接到 ws://localhost:12393/ws。 --- frontend/desktop/main.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/desktop/main.js b/frontend/desktop/main.js index 8d49e52..7084a29 100644 --- a/frontend/desktop/main.js +++ b/frontend/desktop/main.js @@ -1203,7 +1203,10 @@ if (!gotLock) { app.whenReady().then(() => { historyFilePath = resolveHistoryFilePath(); loadHistoryFromDisk(); - startBackend(); + // NODE_ENV=test 时 mock-server.js 已在端口 12393 运行,跳过真实后端启动 + if (process.env.NODE_ENV !== "test") { + startBackend(); + } createWindow(); }); } From 1af0355056f3e807c1fe424d24a09a708e425b5c Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 16:38:43 +0800 Subject: [PATCH 26/28] =?UTF-8?q?ci:=20E2E=20job=20=E7=A6=81=E7=94=A8=20uv?= =?UTF-8?q?=20=E7=BC=93=E5=AD=98=EF=BC=8C=E4=BF=AE=E5=A4=8D=20post-step=20?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E8=B7=AF=E5=BE=84=E4=B8=8D=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E job 有 .venv 全量缓存,uv sync 通常被跳过,uv 不下载 任何包,导致 setup-uv 的 post-step 找不到缓存目录报错, 整个 run 被标记为 failure。改为 enable-cache: false 避免此问题。 --- .github/workflows/pr-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index a009af0..0c4eee8 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -65,12 +65,11 @@ jobs: - name: 检出代码 uses: actions/checkout@v4 - - name: 安装 uv(启用缓存) + - name: 安装 uv uses: astral-sh/setup-uv@v4 with: version: "latest" - enable-cache: true - cache-suffix: "pr-ci" + enable-cache: false - name: 恢复 .venv 缓存 id: cache-venv From 176ecddeea4d6c7ee86449c250f81db68a688557 Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 18:23:08 +0800 Subject: [PATCH 27/28] =?UTF-8?q?docs:=20=E8=90=BD=E7=9B=98=20DEV-107/DEV-?= =?UTF-8?q?108=EF=BC=8CE2E=20=E6=B5=8B=E8=AF=95=E9=9A=94=E7=A6=BB=20+=20CI?= =?UTF-8?q?=20cache=20post-step=20=E5=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEV-107:E2E 测试时未考虑 Electron App 自带 startBackend() 会 netstat 查杀端口进程,把 mock server 干掉后 WebSocket 无处可连 DEV-108:GitHub Actions setup-uv enable-cache=true 但实际 step 被条件跳过时,post-step 找不到缓存目录 → 整个 job 标 failure --- docs/error-books/_index.md | 2 ++ docs/error-books/build-packaging.md | 6 ++++++ docs/error-books/flow-code-habit.md | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/docs/error-books/_index.md b/docs/error-books/_index.md index 55ac163..b22e61d 100644 --- a/docs/error-books/_index.md +++ b/docs/error-books/_index.md @@ -37,6 +37,7 @@ | DEV-64 | 构建脚本数据源与运行时环境不一致 | 构建/打包 | 🟡×3 | build-packaging.md | | DEV-65 | 跨平台路径拼接用了宿主机 path API | 构建/打包 | 🟢 | build-packaging.md | | DEV-75 | 跨平台 API 降级只做初始化不做运行时兜底 | 构建/打包 | 🟢 | build-packaging.md | +| DEV-108 | CI cache post-step 在实际 step 被跳过时找不到路径报错 | 构建/打包 | 🟢 | build-packaging.md | | DEV-60 | 隔离对象但共享有状态引用 | 流式/运行时 | 🟢 | streaming-runtime.md | | DEV-66 | 实时通道断线缓冲未区分消息时效性 | 流式/运行时 | 🟢 | streaming-runtime.md | | DEV-70 | 流式清洗逻辑未处理标签跨 chunk 拆分 | 流式/运行时 | 🟡×2 | streaming-runtime.md | @@ -76,5 +77,6 @@ | DEV-100 | 多问题批量修复不分类并行 | 代码习惯 | 🟢 | flow-code-habit.md | | DEV-101 | API 边界防御缺失 | 代码习惯 | 🟢 | flow-code-habit.md | | DEV-104 | 代码搬迁只验功能可用,未检查隐式依赖 | 代码习惯 | 🟢 | flow-code-habit.md | +| DEV-107 | E2E 测试未隔离被测 App 的自启动后端,mock server 被杀死 | 代码习惯 | 🟢 | flow-code-habit.md | | REC-1~6 | 记录员典型错误(讨论没落盘等) | 通用/记录 | 🟢 | error-book-recorder.md | | DEV-94 | README 完成状态与实际启用条件/实现方案不一致 | 代码习惯 | 🟢 | flow-code-habit.md | diff --git a/docs/error-books/build-packaging.md b/docs/error-books/build-packaging.md index 4c9108e..c8caaf5 100644 --- a/docs/error-books/build-packaging.md +++ b/docs/error-books/build-packaging.md @@ -22,6 +22,12 @@ ✅ 路径拼接涉及目标平台时,用 `path.win32.join` / `path.posix.join` 显式选择,不依赖宿主机默认的 `path.join`。检查点:函数参数里有 `platform` → 路径 API 必须跟着走 > 归因:`path.join` 的行为取决于 Node 运行的 OS 而非业务逻辑的目标 OS,混淆了"构建环境"与"目标环境" +### DEV-108 CI cache post-step 在实际 step 被跳过时仍执行,找不到缓存路径报错 `🟢` + +❌ GitHub Actions 中 `setup-uv` 配置 `enable-cache: true`,但当 `.venv` 缓存命中时 `uv sync` 被跳过,uv 从未下载任何包,缓存目录不存在,post-step 自动保存缓存时报错 → 整个 job 标记为 failure +✅ 若某 job 内的"真实执行步骤"可能被条件跳过(如 `if: cache-hit != 'true'`),则该步骤依赖的缓存动作必须设 `enable-cache: false` 或加 `if` 守卫,避免 post-step 找不到目录 +> 归因 C:新场景。CI 报错只出现在 post-step,与主流程失败现象混淆,排查成本高。PR #68 + ### DEV-75 跨平台 API 降级只做初始化不做运行时兜底 `🟢` ❌ 初始化时按平台降级(`setIgnoreMouseEvents(false)`),但运行时 IPC 仍无条件调用平台不支持的选项(`forward: true`),降级形同虚设 diff --git a/docs/error-books/flow-code-habit.md b/docs/error-books/flow-code-habit.md index d42f616..9e7eac8 100644 --- a/docs/error-books/flow-code-habit.md +++ b/docs/error-books/flow-code-habit.md @@ -174,6 +174,12 @@ ✅ 写 API 端点时:①读 service 完整签名透传所有参数 ②数值字段加范围校验 ③错误语义映射正确状态码 ④错误映射写完后 grep 所有 raise/return error 逐条验证覆盖 > 从 bot_civ DEV-27 搬运 +### DEV-107 E2E 测试未隔离被测 App 的自启动后端逻辑,导致 mock server 被杀死 `🟢` + +❌ Electron App 内 `startBackend()` 无条件执行 `netstat` 查杀端口 12393 进程(mock server),再尝试启动 Python 后端(CI 无 runtime),WebSocket 连接失败。测试只验证了"mock server 能启动",没考虑 App 自身会攻击该端口 +✅ E2E 测试涉及"App 自带 server 启动逻辑"时,必须在 App 内加 `NODE_ENV=test` 守卫跳过真实后端启动。检查点:App 是否有端口清理/进程杀死逻辑?测试 mock 和 App 是否争用同一端口? +> 归因 C:新场景。测试隔离思路只覆盖了"mock server 不崩溃",未推演"App 启动后会对 mock 做什么"。PR #68 + ### DEV-104 代码搬迁只验功能可用,未检查隐式依赖在新上下文是否成立 `🟢` ❌ 把托盘菜单的 IPC handler 搬到独立设置窗口,只验证"功能能用",没推演 `win` 的生命周期在新上下文是否仍然成立。主窗口销毁后设置窗口仍在,IPC 操作已销毁的 BrowserWindow 抛异常 From ee61ee4f717ff3c66a7af873e8627ab54490c20b Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Sat, 21 Mar 2026 18:42:38 +0800 Subject: [PATCH 28/28] =?UTF-8?q?docs:=20=E4=BF=AE=E5=A4=8D=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E8=A1=A5=E5=BC=BA=E8=B7=A8=20session=20=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E8=A7=84=E5=88=99=EF=BC=8C=E5=85=A5=E5=8F=A3=E9=97=A8?= =?UTF-8?q?=E7=A6=81=20Bug=20=E4=BF=AE=E5=A4=8D=E5=BC=BA=E5=88=B6=20git=20?= =?UTF-8?q?log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:跨 session 补丁螺旋时,每次新对话 fix 计数归零, 修复门禁从未触发(DEV-83 失效根因)。 修复: 1. 入口门禁 Bug 修复流程:明确要求先 git log 数已有 fix 次数 2. 修复门禁模板:注明跨 session 也算,必须从 git log 计数不能凭记忆 --- CLAUDE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3d9ae27..6fc7e16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,7 @@ worktree:[已就绪(路径/分支名) | 需要创建 → 先执行创建命令 |----------|----------|------| | 文档更新 | 只改 .md,不涉及代码 | 直接改 | | 单文件小修 | ≤1 文件,逻辑明确 | 直接实施 → 自测 | -| Bug 修复 | 有报错/复现,≤3 文件 | 读错题本 → 实施 → CR(独立 agent)→ P0/P1 归零 → push → 开 PR | +| Bug 修复 | 有报错/复现,≤3 文件 | 读错题本 → **先 `git log --oneline \| grep -i fix` 数已有 fix 次数,≥2 次立即输出修复门禁** → 实施 → CR(独立 agent)→ P0/P1 归零 → push → 开 PR | | 多文件功能 | 跨文件但不涉及新 Module | 读错题本 → 方案确认(列验收标准+技术方案+风险点,用户确认后才能实施)→ 实施 → 自验证 → CR(独立 agent)→ P0/P1 归零 → push → 开 PR | | 新 Module | 不在 spine-now.md 冻结范围 | 走 Module 开发流程 5 步 | @@ -147,10 +147,12 @@ worktree:[已就绪(路径/分支名) | 需要创建 → 先执行创建命令 对同一功能/链路提交第 2 次 fix commit 前,**必须先输出修复门禁声明**,再写任何代码。不输出就动手 = DEV-83 违规。 +**跨 session 计数规则**:fix 次数必须从 `git log` 里数,不能因新对话而归零。新 session 处理同一链路时,必须先 `git log --oneline | grep -i fix` 检查历史,再判断是否需要输出修复门禁。 + ``` --- 修复门禁 --- 功能/链路:[一句话描述修的是什么] -已有 fix 次数:[git log --oneline 计数] +已有 fix 次数:[git log --oneline | grep -i fix 计数结果,不能凭记忆] 每次 fix 摘要:[第 N 次改了什么 → 为什么没解决] 平台验证:[已做最小复现 / 已搜 Issues / 不涉及平台 API] 门禁结论:[继续当前路径(≤2次) | 强制停下换路径(≥3次)]