diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3e787a5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 +max_line_length = 120 + +[*.{js,vue,json,yml,yaml,css,html}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index bbdd4d4..71854dc 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ dist-ssr/ # ===== Node ===== node_modules/ +.pnpm-store/ .vite/ *.local @@ -61,7 +62,12 @@ app-client/**/.gradle/ # ===== OS / 编辑器 ===== .DS_Store .idea/ -.vscode/ +# 团队共享的 VS Code 调试/任务/插件配置入库,其余本地设置忽略 +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/launch.json +!.vscode/tasks.json *.log .playwright-mcp/ diff --git a/.uploads/6fdf0015-e5f3-4ca6-8764-507be0f0d9f0_cloud-phone-main.7z b/.uploads/6fdf0015-e5f3-4ca6-8764-507be0f0d9f0_cloud-phone-main.7z new file mode 100644 index 0000000..c66ff85 Binary files /dev/null and b/.uploads/6fdf0015-e5f3-4ca6-8764-507be0f0d9f0_cloud-phone-main.7z differ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ce816cd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,15 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy", + "charliermarsh.ruff", + "Vue.volar", + "ms-azuretools.vscode-docker", + "redhat.vscode-yaml", + "humao.rest-client", + "EditorConfig.EditorConfig", + "eamodio.gitlens" + ], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..dd82c9c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,56 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "后端:FastAPI(调试)", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": ["app.main:app", "--reload", "--host", "127.0.0.1", "--port", "8000"], + "cwd": "${workspaceFolder}/backend", + "env": { + "CLOUD_DEVICE_BACKEND": "simulator", + "CLOUD_DEBUG": "true" + }, + "console": "integratedTerminal", + "justMyCode": true + }, + { + "name": "后端:pytest(全部)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["tests", "-q"], + "cwd": "${workspaceFolder}/backend", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "后端:pytest(当前文件)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["-q", "${file}"], + "cwd": "${workspaceFolder}/backend", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "前端:Vite Dev", + "type": "node", + "request": "launch", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["run", "dev", "--", "--host", "127.0.0.1"], + "cwd": "${workspaceFolder}/frontend", + "console": "integratedTerminal", + "skipFiles": ["/**"] + } + ], + "compounds": [ + { + "name": "全栈:后端 + 前端", + "configurations": ["后端:FastAPI(调试)", "前端:Vite Dev"], + "stopAll": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f9dcdfb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,49 @@ +{ + // ===== Python / 后端 ===== + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": ["tests"], + "python.testing.cwd": "${workspaceFolder}/backend", + "python.analysis.extraPaths": ["${workspaceFolder}/backend"], + "python.analysis.exclude": ["**/.venv/**", "**/.venv-local/**", "**/_attic/**"], + "ruff.configuration": "${workspaceFolder}/backend/pyproject.toml", + "ruff.lineLength": 120, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": false + }, + + // ===== 前端 ===== + "[vue]": { + "editor.formatOnSave": false + }, + "[javascript]": { + "editor.formatOnSave": false + }, + + // ===== 资源管理器 / 搜索噪音 ===== + "files.exclude": { + "**/__pycache__": true, + "**/*.pyc": true, + "**/.ruff_cache": true, + "**/.pytest_cache": true, + "**/_test_*.db": true + }, + "search.exclude": { + "**/node_modules": true, + "**/.venv": true, + "**/.venv-local": true, + "**/dist": true, + "**/_attic": true, + "**/.uploads": true, + "**/pnpm-lock.yaml": true + }, + "files.watcherExclude": { + "**/node_modules/**": true, + "**/.venv*/**": true, + "**/dist/**": true + }, + + // ===== 终端 ===== + "terminal.integrated.cwd": "${workspaceFolder}" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0ce14e7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,56 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "环境:一键安装依赖", + "type": "shell", + "command": "bash scripts/dev-setup.sh", + "windows": { + "command": "powershell -ExecutionPolicy Bypass -File scripts/dev-setup.ps1" + }, + "problemMatcher": [], + "presentation": { "reveal": "always", "panel": "dedicated" } + }, + { + "label": "启动:后端 + 前端", + "type": "shell", + "command": "./start-all.sh", + "windows": { + "command": "start-all.bat" + }, + "problemMatcher": [], + "presentation": { "reveal": "always", "panel": "dedicated" } + }, + { + "label": "测试:后端 pytest", + "type": "shell", + "command": "${workspaceFolder}/backend/.venv/bin/python", + "args": ["-m", "pytest", "tests", "-q"], + "options": { "cwd": "${workspaceFolder}/backend" }, + "windows": { + "command": "${workspaceFolder}\\backend\\.venv\\Scripts\\python.exe", + "args": ["-m", "pytest", "tests", "-q"] + }, + "group": { "kind": "test", "isDefault": true }, + "problemMatcher": [] + }, + { + "label": "检查:ruff + compileall", + "type": "shell", + "command": "${workspaceFolder}/backend/.venv/bin/python", + "args": ["-m", "ruff", "check", "backend/app", "tools", "--config", "backend/pyproject.toml"], + "windows": { + "command": "${workspaceFolder}\\backend\\.venv\\Scripts\\python.exe", + "args": ["-m", "ruff", "check", "backend/app", "tools", "--config", "backend/pyproject.toml"] + }, + "problemMatcher": [] + }, + { + "label": "构建:前端", + "type": "shell", + "command": "pnpm run build", + "options": { "cwd": "${workspaceFolder}/frontend" }, + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 164e0f5..32364e0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ # 1) 后端 cd backend python3 -m venv .venv && source .venv/bin/activate -pip install fastapi "uvicorn[standard]" "sqlalchemy[asyncio]" aiosqlite pydantic pydantic-settings PyJWT python-multipart httpx +pip install -r requirements-dev.txt # 版本已锁定,Python 3.11+ uvicorn app.main:app --reload # http://localhost:8000/docs # 2) 前端(另开终端;用 pnpm,仓库锁文件是 pnpm-lock.yaml。Node 需 ≥ 22.13) @@ -42,6 +42,11 @@ cd frontend && corepack enable && pnpm install && pnpm run dev # http://localhos cd backend && source .venv/bin/activate && python -m tests.smoke ``` +不想手动装环境?macOS / Linux 直接 `bash scripts/dev-setup.sh`,再 `bash start-all.sh` +(后端 :8000、前端 :5173 一起起,Ctrl+C 全停);Windows 用 +`powershell -ExecutionPolicy Bypass -File scripts\dev-setup.ps1` + `start-all.bat`。 +两个脚本都是幂等的,会自动建 `backend/.venv`、装依赖、从 `.env.example` 生成 `backend/.env`。 + `tests/smoke.py` 会自动跑完登录→建 10 台→抽查指纹→预览→单机操控→批量同步→分组→脚本回放,全绿即主流程跑通。 > 首次部署(非临时演示)请先初始化 4 个角色的正式账号: @@ -53,6 +58,17 @@ cd backend && source .venv/bin/activate && python -m tests.smoke > **非破坏性**的 `python acceptance_live.py`:不建机不删机、只用现有在跑的设备核验, > 跑完还原设备原来打开的网页;并会检出「有画面但设备其实已失联」的伪装情况。 +## IDE 直接测试(VS Code,配置已入库) + +仓库自带 `.vscode/` 共享配置,克隆后打开即可: + +1. **插件**:首次打开会提示安装推荐插件(Python / Pylance / debugpy / Ruff / Vue Volar / Docker / YAML / REST Client / EditorConfig / GitLens),点「全部安装」即可。 +2. **Python 解释器**:选 `backend/.venv/bin/python`(Windows 为 `backend\.venv\Scripts\python.exe`),测试面板会自动发现 `backend/tests`。 +3. **一键任务**(`Ctrl+Shift+B` 或「终端 → 运行任务」):`环境:一键安装依赖` → `启动:后端 + 前端` → `测试:后端 pytest` / `检查:ruff + compileall` / `构建:前端`。 +4. **F5 调试**:`后端:FastAPI(调试)`、`后端:pytest(当前文件)`、`前端:Vite Dev`,以及复合配置 `全栈:后端 + 前端`。 + +已实测(Python 3.11 + pnpm 10):`pytest tests -q` 115 项全过、`ruff check` 无告警、`pnpm run build` 成功。 + ## 一条命令起整套(Docker Compose) ```bash diff --git a/backend/app/preview.py b/backend/app/preview.py index 4feca9a..ab22f78 100644 --- a/backend/app/preview.py +++ b/backend/app/preview.py @@ -780,13 +780,13 @@ def _svg_android_home(name: str, fp: dict) -> str: week = "一二三四五六日"[now.weekday()] date_text = f"{now.month} 月 {now.day} 日 · 周{week}" parts = [ - f'' - f'' - f'' - f'' - f'' - f'' - f'', + '' + '' + '' + '' + '' + '' + '', f'', f'', '', @@ -794,11 +794,11 @@ def _svg_android_home(name: str, fp: dict) -> str: _a_statusbar(), # 日期与天气组件 f'{date_text}', - f'多云 · 24° · 空气质量 优', + '多云 · 24° · 空气质量 优', # 演示角标 - f'' - f'' - f'演示模式 · SIM', + '' + '' + '演示模式 · SIM', ] # 4 列 × 5 行图标网格 @@ -824,7 +824,7 @@ def _svg_android_home(name: str, fp: dict) -> str: for x, glyph in zip(dock_x, _A_DOCK): parts.append(_a_icon(x, dock_y + 10, 56, "#1f2b45" if glyph != "browser" else "#f8fafc", glyph)) # 手势条 - parts.append(f'') + parts.append('') return ( f'' diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 085b7ce..ee4323e 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -18,7 +18,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from .. import auth -from ..config import settings from ..database import get_db from ..models import User from ..rbac import ROLE_LEVELS, ROLE_LABELS, record_audit, require_role diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 35defe8..5e8f8e7 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,4 +1,4 @@ # 开发/CI 依赖(不随运行时镜像安装) -r requirements.txt -pytest==9.2.0 +pytest==9.1.1 ruff==0.17.3 diff --git a/backend/requirements.txt b/backend/requirements.txt index b697c36..079464a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,17 +1,17 @@ -# X86 云手机平台 —— 后端运行时依赖(精确锁定,2026-11-04 实测安装成功) +# X86 云手机平台 —— 后端运行时依赖(精确锁定,2026-11-04 在 Python 3.11 实测安装并跑通 115 项测试) # 说明:密码哈希使用标准库 hashlib.pbkdf2,避免 bcrypt 原生编译依赖。 # 升级流程:改这里 → 在干净 venv 里 pip install -r requirements.txt → 跑 pytest tests -q。 -fastapi==0.128.8 -uvicorn[standard]==0.54.0 -sqlalchemy[asyncio]==2.1.8 -aiosqlite==0.22.1 # dev/测试默认库(SQLite) -asyncpg==0.32.0 # 生产库(PostgreSQL),按 DATABASE_URL 启用 -pydantic==2.12.5 -pydantic-settings==2.16.2 -PyJWT==2.15.1 -python-multipart==0.0.33 +fastapi==0.141.1 +uvicorn[standard]==0.44.0 +sqlalchemy[asyncio]==2.0.51 +aiosqlite==0.21.0 # dev/测试默认库(SQLite) +asyncpg==0.30.0 # 生产库(PostgreSQL),按 DATABASE_URL 启用 +pydantic==2.13.4 +pydantic-settings==2.14.2 +PyJWT==2.13.0 +python-multipart==0.0.21 httpx==0.28.1 -python-dotenv==1.2.4 # tools/seed_rbac.py 读取 .env +python-dotenv==1.2.2 # tools/seed_rbac.py 读取 .env # 可选:真实 Redroid 后端需要 Docker SDK(Simulator 模式无需安装) -# 注:该版本按 7.x 线锁定,Windows 上不安装(marker 排除),Linux 侧安装后请回归真机流程。 +# 注:Windows 上不安装(marker 排除),Linux 侧安装后请回归真机流程。 docker==7.1.0 ; platform_system == "Linux" diff --git a/frontend/package.json b/frontend/package.json index b86eeaa..9027d82 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,5 +21,11 @@ "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", "vite": "^5.4.9" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "vue-demi" + ] } } diff --git a/scripts/dev-setup.ps1 b/scripts/dev-setup.ps1 new file mode 100644 index 0000000..9eade2f --- /dev/null +++ b/scripts/dev-setup.ps1 @@ -0,0 +1,44 @@ +# 一次性准备好本地开发环境(Windows):Python 虚拟环境 + 后端依赖 + backend\.env + 前端依赖。 +# 用法:powershell -ExecutionPolicy Bypass -File scripts\dev-setup.ps1 +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +$Venv = Join-Path $Root "backend\.venv" +$Py = Join-Path $Venv "Scripts\python.exe" + +Write-Host "==> 检查 Python 版本(需要 3.11+)" +python -c "import sys; sys.exit('需要 Python 3.11+,当前 ' + sys.version.split()[0]) if sys.version_info < (3, 11) else print(' Python ' + sys.version.split()[0] + ' OK')" + +Write-Host "==> 创建后端虚拟环境 backend\.venv" +if (-not (Test-Path $Py)) { + python -m venv $Venv +} + +Write-Host "==> 安装后端依赖(含 pytest / ruff)" +& $Py -m pip install -q --upgrade pip +& $Py -m pip install -q -r (Join-Path $Root "backend\requirements-dev.txt") + +Write-Host "==> 准备 backend\.env" +$EnvFile = Join-Path $Root "backend\.env" +if (-not (Test-Path $EnvFile)) { + Copy-Item (Join-Path $Root "backend\.env.example") $EnvFile + Write-Host " 已从 .env.example 生成(默认 simulator + SQLite),需要真机/云机时再改" +} + +Write-Host "==> 安装前端依赖(Node >= 22.13,包管理器 pnpm)" +if (-not (Test-Path (Join-Path $Root "frontend\node_modules"))) { + if (-not (Get-Command pnpm -ErrorAction SilentlyContinue) -and (Get-Command corepack -ErrorAction SilentlyContinue)) { + corepack enable + } + if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) { + throw "未找到 pnpm:请先执行 npm i -g pnpm" + } + Push-Location (Join-Path $Root "frontend") + pnpm install --frozen-lockfile + Pop-Location +} + +Write-Host "" +Write-Host "环境就绪。下一步:" +Write-Host " 启动整套:start-all.bat (后端 :8000 + 前端 :5173)" +Write-Host " 跑测试: backend\.venv\Scripts\python.exe -m pytest backend\tests -q" diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh new file mode 100755 index 0000000..71a13d1 --- /dev/null +++ b/scripts/dev-setup.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# 一次性准备好本地开发环境:Python 虚拟环境 + 后端依赖 + backend/.env + 前端依赖。 +# 用法:bash scripts/dev-setup.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON_BIN="${PYTHON_BIN:-python3}" +VENV="$ROOT/backend/.venv" + +echo "==> 检查 Python 版本(需要 3.11+)" +"$PYTHON_BIN" - <<'PY' +import sys +if sys.version_info < (3, 11): + sys.exit(f"需要 Python 3.11+,当前 {sys.version.split()[0]}") +print(f" Python {sys.version.split()[0]} OK") +PY + +echo "==> 创建后端虚拟环境 backend/.venv" +if [ ! -x "$VENV/bin/python" ]; then + "$PYTHON_BIN" -m venv "$VENV" +fi + +echo "==> 安装后端依赖(含 pytest / ruff)" +"$VENV/bin/python" -m pip install -q --upgrade pip +"$VENV/bin/python" -m pip install -q -r "$ROOT/backend/requirements-dev.txt" + +echo "==> 准备 backend/.env" +if [ ! -f "$ROOT/backend/.env" ]; then + cp "$ROOT/backend/.env.example" "$ROOT/backend/.env" + echo " 已从 .env.example 生成(默认 simulator + SQLite),需要真机/云机时再改" +fi + +echo "==> 安装前端依赖(Node ≥ 22.13,包管理器 pnpm)" +if [ ! -d "$ROOT/frontend/node_modules" ]; then + if ! command -v pnpm >/dev/null 2>&1 && command -v corepack >/dev/null 2>&1; then + corepack enable + fi + if ! command -v pnpm >/dev/null 2>&1; then + echo "未找到 pnpm:请先执行 npm i -g pnpm" >&2 + exit 1 + fi + (cd "$ROOT/frontend" && pnpm install --frozen-lockfile) +fi + +echo "" +echo "环境就绪。下一步:" +echo " 启动整套:bash start-all.sh (后端 :8000 + 前端 :5173)" +echo " 跑测试: cd backend && .venv/bin/python -m pytest tests -q" diff --git a/start-all.sh b/start-all.sh new file mode 100755 index 0000000..da0c3de --- /dev/null +++ b/start-all.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# 一条命令起后端 + 前端(macOS / Linux)。首次运行会自动准备环境。 +# 用法:bash start-all.sh 后端 http://localhost:8000/docs 前端 http://localhost:5173 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV="$ROOT/backend/.venv" + +if [ ! -x "$VENV/bin/python" ] || [ ! -f "$ROOT/backend/.env" ]; then + echo "==> 首次运行,先准备环境" + bash "$ROOT/scripts/dev-setup.sh" +fi + +echo "==> 启动后端 http://localhost:8000/docs" +(cd "$ROOT/backend" && "$VENV/bin/python" -m uvicorn app.main:app --host 0.0.0.0 --port 8000) & +BACKEND_PID=$! +trap 'kill "$BACKEND_PID" 2>/dev/null || true' EXIT INT TERM + +echo "==> 启动前端 http://localhost:5173(Ctrl+C 同时结束后端)" +cd "$ROOT/frontend" +pnpm run dev -- --host 0.0.0.0 --port 5173