diff --git a/.superpowers/brainstorm/visual-login-live/.server-info b/.superpowers/brainstorm/visual-login-live/.server-info new file mode 100644 index 0000000..ed279fb --- /dev/null +++ b/.superpowers/brainstorm/visual-login-live/.server-info @@ -0,0 +1 @@ +{"type":"server-started","port":58787,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:58787","screen_dir":"G:\\IdeaProjects\\emotion-museun\\.superpowers\\brainstorm\\visual-login-live"} diff --git a/.superpowers/brainstorm/visual-login-test/.server-info b/.superpowers/brainstorm/visual-login-test/.server-info new file mode 100644 index 0000000..275a253 --- /dev/null +++ b/.superpowers/brainstorm/visual-login-test/.server-info @@ -0,0 +1 @@ +{"type":"server-started","port":64089,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:64089","screen_dir":"G:\\IdeaProjects\\emotion-museun\\.superpowers\\brainstorm\\visual-login-test"} diff --git a/dev-services.py b/dev-services.py new file mode 100644 index 0000000..bcf9584 --- /dev/null +++ b/dev-services.py @@ -0,0 +1,1569 @@ +""" +Author: Peanut +Created: 2026-04-26 +Purpose: 通用服务管理脚本 — 在任意项目目录下自动发现、启动、停止、重启各类服务 + 支持 Vite 前端、Python 后端 (FastAPI/Flask/Django)、Node.js 后端 (Express/NestJS)、 + Java Spring Boot (Maven/Gradle)。 + 用法: python ~/.claude/dev-services.py [命令] [选项] +""" + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import time +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Optional + +try: + import psutil +except ImportError: + print("错误: 需要 psutil 库,请运行: pip install psutil") + sys.exit(1) + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +try: + import requests as _requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +# ============================================================ +# 常量 +# ============================================================ + +SCRIPT_DIR = Path(__file__).parent.resolve() +CWD = Path.cwd() +LOG_DIR = CWD / "logs" +LOG_DIR.mkdir(exist_ok=True) +SCRIPT_LOG = LOG_DIR / "dev-services.log" + +MAX_SCAN_DEPTH = 3 +SKIP_DIRS = {"node_modules", "__pycache__", ".git", "venv", ".venv", "target", "build", "dist", ".omc", ".next", ".nuxt", "coverage", "e2e"} + +# 框架默认端口 +DEFAULT_PORTS = { + "vite": 5173, + "next": 3000, + "node-backend": 3000, + "python": 8000, + "spring-boot": 8080, + "spring-boot-gradle": 8080, +} + +# ANSI 颜色 +COLORS = { + "INFO": "\033[37m", + "SUCCESS": "\033[32m", + "WARN": "\033[33m", + "ERROR": "\033[31m", + "DEBUG": "\033[36m", + "RESET": "\033[0m", + "BOLD": "\033[1m", + "DIM": "\033[2m", +} + +if sys.platform == "win32": + import ctypes + try: + ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) + except Exception: + pass + +if hasattr(sys.stdout, 'reconfigure'): + sys.stdout.reconfigure(encoding='utf-8') + +# ============================================================ +# 日志 +# ============================================================ + +def log(msg: str, level: str = "INFO"): + timestamp = datetime.now().strftime("%H:%M:%S") + color = COLORS.get(level, COLORS["INFO"]) + reset = COLORS["RESET"] + print(f"{color}[{timestamp}] [{level}]{reset} {msg}") + try: + with open(SCRIPT_LOG, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] [{level}] {msg}\n") + except Exception: + pass + + +def log_separator(): + print(f"\n{COLORS['DIM']}{'=' * 60}{COLORS['RESET']}\n") + + +def log_section(title: str): + print(f"\n{COLORS['BOLD']} {title}{COLORS['RESET']}") + print(f"{COLORS['DIM']}{'-' * 50}{COLORS['RESET']}") + + +# ============================================================ +# 服务抽象 +# ============================================================ + +@dataclass +class Service: + name: str + project_type: str + project_dir: Path + port: int + start_cmd: list + health_keyword: str + health_url: str + health_timeout: int = 30 + keyword_timeout: int = 30 + log_file: Optional[Path] = None + + @property + def log_path(self) -> Path: + if self.log_file: + return self.log_file + return LOG_DIR / f"{self.name.lower().replace(' ', '_')}.log" + + def type_label(self) -> str: + labels = { + "vite": "Vite 前端", + "next": "Next.js", + "node-backend": "Node.js 后端", + "python": "Python 后端", + "spring-boot": "Spring Boot (Maven)", + "spring-boot-gradle": "Spring Boot (Gradle)", + } + return labels.get(self.project_type, self.project_type) + + +# ============================================================ +# 项目发现 +# ============================================================ + +def discover_services(root_dir: Optional[Path] = None, max_depth: int = MAX_SCAN_DEPTH) -> list[Service]: + """递归扫描目录,发现所有服务""" + base = root_dir or CWD + services = [] + _scan_directory(base, base, max_depth, services) + return services + + +def _scan_directory(current: Path, root: Path, max_depth: int, services: list): + """递归扫描,跳过黑名单目录""" + rel_depth = len(current.relative_to(root).parts) + if rel_depth > max_depth: + return + + # 跳过黑名单 + if current.name in SKIP_DIRS: + return + + # 尝试发现服务 + svc = _detect_service(current) + if svc: + services.append(svc) + # 如果找到服务,不再深入其子目录(避免重复发现) + return + + # 递归子目录 + try: + for child in sorted(current.iterdir()): + if child.is_dir(): + _scan_directory(child, root, max_depth, services) + except PermissionError: + pass + + +def _detect_service(directory: Path) -> Optional[Service]: + """检测目录是否是项目根目录,返回 Service 或 None""" + + # 1. Java Spring Boot 优先于 Python 检测,避免辅助脚本导致误判 + pom = directory / "pom.xml" + if pom.exists(): + svc = _build_spring_boot_maven_service(directory, pom) + if svc: + return svc + + gradle = directory / "build.gradle" + gradle_kts = directory / "build.gradle.kts" + if gradle.exists() or gradle_kts.exists(): + svc = _build_spring_boot_gradle_service(directory, gradle if gradle.exists() else gradle_kts) + if svc: + return svc + + # 2. Vite 前端 + pkg = directory / "package.json" + if pkg.exists(): + try: + pkg_data = json.loads(pkg.read_text(encoding="utf-8")) + deps = {**pkg_data.get("dependencies", {}), **pkg_data.get("devDependencies", {})} + scripts = pkg_data.get("scripts", {}) + + # Vite + if "vite" in deps: + return _build_vite_service(directory, pkg_data) + # Next.js + if "next" in deps: + return _build_next_service(directory, pkg_data) + # Node.js 后端 (Express / NestJS / Koa) + if any(k in deps for k in ["express", "@nestjs/core", "koa"]): + return _build_node_backend_service(directory, pkg_data) + except (json.JSONDecodeError, Exception): + pass + + # 3. Python 后端 + req_file = directory / "requirements.txt" + pyproject = directory / "pyproject.toml" + has_python_deps = req_file.exists() or pyproject.exists() + + # 只把明确的 Python Web 项目入口当作 Python 服务,普通 src 目录不代表 Python + has_python_entry = ( + (directory / "main.py").exists() + or (directory / "manage.py").exists() + or (directory / "app.py").exists() + or (directory / "app").is_dir() + ) + + if has_python_deps or has_python_entry: + svc = _build_python_service(directory) + if svc: + return svc + + return None + + +# ============================================================ +# 配置解析 +# ============================================================ + +def _read_env_file(directory: Path) -> dict: + """读取 .env 或 .env.development,返回键值对(合并所有找到的环境文件)""" + env_data = {} + for env_name in [".env.development", ".env.local", ".env"]: + env_path = directory / env_name + if env_path.exists(): + try: + for line in env_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, _, value = line.partition("=") + env_data[key.strip()] = value.strip().strip('"').strip("'") + except Exception: + pass + return env_data + + +def _extract_port_from_env(directory: Path) -> Optional[int]: + """从 .env 文件中提取端口号""" + env_data = _read_env_file(directory) + for key in ["VITE_PORT", "PORT", "SERVER_PORT", "APP_PORT", "BACKEND_PORT"]: + if key in env_data: + try: + return int(env_data[key]) + except ValueError: + continue + return None + + +def _extract_port_from_vite_config(directory: Path) -> Optional[int]: + """从 vite.config.ts/js 中提取 server.port""" + for conf_name in ["vite.config.ts", "vite.config.js"]: + conf_path = directory / conf_name + if conf_path.exists(): + try: + content = conf_path.read_text(encoding="utf-8") + # 匹配 port: 5178 或 port:5173 + match = re.search(r'port\s*:\s*(\d+)', content) + if match: + return int(match.group(1)) + except Exception: + pass + return None + + +def _extract_port_from_scripts(pkg_data: dict) -> Optional[int]: + """从 package.json scripts 中提取 --port 参数""" + scripts = pkg_data.get("scripts", {}) + dev_script = scripts.get("dev", "") or scripts.get("start", "") + match = re.search(r'--port\s+(\d+)', dev_script) + if match: + return int(match.group(1)) + return None + + +def _extract_port_from_pom(pom_path: Path) -> Optional[int]: + """从 pom.xml 提取 server.port""" + try: + content = pom_path.read_text(encoding="utf-8") + # 查找 12341234 + match = re.search(r'\s*(\d+)\s*', content) + if match: + return int(match.group(1)) + # 查找 1234 + match = re.search(r'.*?\s*(\d+)\s*.*?', content, re.DOTALL) + if match: + return int(match.group(1)) + except Exception: + pass + return None + + +def _extract_port_from_spring_boot_yaml(directory: Path) -> Optional[int]: + """从 Spring Boot application*.yml 提取 server.port + + 1. 优先检查目录根下的 application.yml + 2. 检查直接子目录中的 src/main/resources/application*.yml(多模块 Maven 项目) + 3. 最后递归搜索其他子目录(跳过构建产物) + """ + if not HAS_YAML: + return None + + # 1. 检查目录根 + for name in ["application.yml", "application.yaml"]: + port = _extract_port_from_single_yaml(directory / name) + if port is not None: + return port + + # 2. 优先检查直接子目录中的 src/main/resources(多模块 Maven 项目常见布局) + try: + for subdir in sorted(directory.iterdir()): + if subdir.is_dir(): + resources_dir = subdir / "src" / "main" / "resources" + if resources_dir.exists(): + for name in [ + "application.yml", "application.yaml", + "application-local.yml", "application-local.yaml", + "application-dev.yml", "application-dev.yaml", + "application-prod.yml", "application-prod.yaml", + "application-test.yml", "application-test.yaml", + ]: + port = _extract_port_from_single_yaml(resources_dir / name) + if port is not None: + return port + except Exception: + pass + + # 3. 递归搜索其他子目录(跳过构建产物) + try: + for child in directory.rglob("application*.yml"): + parts = set(child.parts) + if parts & {"target", "build", "dist", "node_modules"}: + continue + depth = len(child.relative_to(directory).parts) + if depth > 10: + continue + port = _extract_port_from_single_yaml(child) + if port is not None: + return port + for child in directory.rglob("application*.yaml"): + parts = set(child.parts) + if parts & {"target", "build", "dist", "node_modules"}: + continue + depth = len(child.relative_to(directory).parts) + if depth > 10: + continue + port = _extract_port_from_single_yaml(child) + if port is not None: + return port + except Exception: + pass + + return None + + +def _extract_spring_boot_context_path(directory: Path) -> str: + """从 Spring Boot application*.yml 提取 servlet context-path""" + value = _extract_spring_boot_yaml_value(directory, ("server", "servlet", "context-path")) + return _normalize_url_path(value) if value else "" + + +def _extract_spring_boot_management_base_path(directory: Path) -> str: + """从 Spring Boot application*.yml 提取 actuator base-path""" + value = _extract_spring_boot_yaml_value(directory, ("management", "endpoints", "web", "base-path")) + return _normalize_url_path(value) if value else "/actuator" + + +def _build_spring_boot_health_url(port: int, directory: Path) -> str: + """根据 Spring Boot 上下文路径构建健康检查 URL""" + context_path = _extract_spring_boot_context_path(directory) + actuator_base_path = _extract_spring_boot_management_base_path(directory) + health_path = _join_url_paths(context_path, actuator_base_path, "health") + return f"http://localhost:{port}{health_path}" + + +def _extract_spring_boot_yaml_value(directory: Path, keys: tuple[str, ...]) -> Optional[str]: + """按优先级从 Spring Boot application*.yml/yaml 中读取嵌套配置值""" + if not HAS_YAML: + return None + + for yml_path in _iter_spring_boot_yaml_files(directory): + value = _extract_value_from_single_yaml(yml_path, keys) + if value is not None: + return str(value) + return None + + +def _iter_spring_boot_yaml_files(directory: Path) -> list[Path]: + """按稳定优先级列出 Spring Boot YAML 配置文件""" + candidates = [] + + for name in ["application.yml", "application.yaml"]: + candidates.append(directory / name) + + try: + for subdir in sorted(directory.iterdir()): + if subdir.is_dir(): + resources_dir = subdir / "src" / "main" / "resources" + if resources_dir.exists(): + for name in [ + "application.yml", "application.yaml", + "application-local.yml", "application-local.yaml", + "application-dev.yml", "application-dev.yaml", + "application-prod.yml", "application-prod.yaml", + "application-test.yml", "application-test.yaml", + ]: + candidates.append(resources_dir / name) + except Exception: + pass + + try: + for pattern in ["application*.yml", "application*.yaml"]: + for child in directory.rglob(pattern): + parts = set(child.parts) + if parts & {"target", "build", "dist", "node_modules"}: + continue + depth = len(child.relative_to(directory).parts) + if depth <= 10: + candidates.append(child) + except Exception: + pass + + seen = set() + result = [] + for path in candidates: + if path in seen or not path.exists(): + continue + seen.add(path) + result.append(path) + return result + + +def _extract_value_from_single_yaml(yml_path: Path, keys: tuple[str, ...]) -> Optional[object]: + """从单个 YAML 文件中读取嵌套配置值""" + if not HAS_YAML: + return None + try: + content = yml_path.read_text(encoding="utf-8") + content = re.sub(r'@\w[\w.\-]*@', '""', content) + for doc in yaml.safe_load_all(content): + current = doc + for key in keys: + if not isinstance(current, dict) or key not in current: + current = None + break + current = current[key] + if current is not None: + return current + except Exception: + pass + return None + + +def _normalize_url_path(value: str) -> str: + """规范化 URL 路径片段""" + path = str(value).strip() + if not path or path == "/": + return "" + return "/" + path.strip("/") + + +def _join_url_paths(*parts: str) -> str: + """拼接 URL 路径,避免重复斜杠""" + cleaned = [str(part).strip("/") for part in parts if str(part).strip("/")] + return "/" + "/".join(cleaned) + + +def _extract_port_from_single_yaml(yml_path: Path) -> Optional[int]: + """从单个 YAML 文件提取 server.port + + 自动处理 Maven @placeholder@ 占位符(替换为空字符串)。 + """ + if not HAS_YAML: + return None + try: + content = yml_path.read_text(encoding="utf-8") + # 移除 Maven 占位符: @xxx@ -> "" + content = re.sub(r'@\w[\w.\-]*@', '""', content) + for doc in yaml.safe_load_all(content): + if isinstance(doc, dict): + server = doc.get("server", {}) + if isinstance(server, dict) and "port" in server: + return int(server["port"]) + except Exception: + pass + return None + + +def _extract_port_from_gradle(gradle_path: Path) -> Optional[int]: + """从 build.gradle 提取 server.port""" + try: + content = gradle_path.read_text(encoding="utf-8") + match = re.search(r'server\.port\s*=\s*["\']?(\d+)["\']?', content) + if match: + return int(match.group(1)) + match = re.search(r'serverPort\s*=\s*["\']?(\d+)["\']?', content) + if match: + return int(match.group(1)) + except Exception: + pass + return None + + +def _has_spring_boot(pom_path: Path) -> bool: + """检查 pom.xml 是否包含 Spring Boot 依赖""" + try: + content = pom_path.read_text(encoding="utf-8") + return "spring-boot" in content.lower() or "springboot" in content.lower() + except Exception: + return False + + +def _has_spring_boot_gradle(gradle_path: Path) -> bool: + """检查 build.gradle 是否包含 Spring Boot 插件""" + try: + content = gradle_path.read_text(encoding="utf-8") + return "spring-boot" in content.lower() or "org.springframework.boot" in content + except Exception: + return False + + +def _find_python_entry(directory: Path) -> Optional[Path]: + """查找 Python 入口文件""" + candidates = ["main.py", "app.py", "app/main.py", "src/main.py", "manage.py", "wsgi.py", "asgi.py"] + for c in candidates: + p = directory / c + if p.exists(): + return p + return None + + +# ============================================================ +# 服务构建 +# ============================================================ + +def _get_npm_cmd(directory: Path) -> list: + """获取可用的包管理器命令,返回 [cmd, args...] 列表""" + # 优先通过 node 直接执行 vite.js(避免 .cmd 的 shell 问题) + vite_js = directory / "node_modules" / "vite" / "bin" / "vite.js" + if vite_js.exists(): + node_exe = shutil.which("node") or "node" + return [node_exe, str(vite_js)] + + # Windows: 尝试 .cmd + if sys.platform == "win32": + vite_cmd = directory / "node_modules" / ".bin" / "vite.cmd" + if vite_cmd.exists(): + return [str(vite_cmd)] + else: + vite_bin = directory / "node_modules" / ".bin" / "vite" + if vite_bin.exists(): + return [str(vite_bin)] + + for pkg_cmd in ["pnpm", "npm", "yarn"]: + found = shutil.which(pkg_cmd) + if found: + return [found] + return ["npx"] + + +def _build_vite_service(directory: Path, pkg_data: dict) -> Service: + name = pkg_data.get("name", "前端") + if "/" in name: + name = name.split("/")[-1] + port = (_extract_port_from_env(directory) + or _extract_port_from_vite_config(directory) + or _extract_port_from_scripts(pkg_data) + or DEFAULT_PORTS["vite"]) + npm_cmd = _get_npm_cmd(directory) + + # 判断是否是直接指向 vite 可执行文件(node+vite.js 或 vite.bin) + cmd_str = " ".join(npm_cmd).lower() + is_direct_vite = "vite" in cmd_str and "npx" not in cmd_str + + if is_direct_vite: + cmd = npm_cmd + ["--port", str(port)] + elif npm_cmd[0] in ("pnpm", "npm", "yarn"): + cmd = npm_cmd + ["run", "dev", "--", "--port", str(port)] + else: + cmd = ["npx", "vite", "--port", str(port)] + + return Service( + name=f"{name.capitalize()} 前端", + project_type="vite", + project_dir=directory, + port=port, + start_cmd=cmd, + health_keyword="ready in", + health_url=f"http://localhost:{port}", + health_timeout=30, + keyword_timeout=30, + ) + + +def _build_next_service(directory: Path, pkg_data: dict) -> Service: + name = pkg_data.get("name", "Next.js") + if "/" in name: + name = name.split("/")[-1] + port = _extract_port_from_env(directory) or _extract_port_from_scripts(pkg_data) or DEFAULT_PORTS["next"] + + return Service( + name=f"{name.capitalize()} Next.js", + project_type="next", + project_dir=directory, + port=port, + start_cmd=["npx", "next", "dev", "--port", str(port)], + health_keyword="started server", + health_url=f"http://localhost:{port}", + health_timeout=30, + keyword_timeout=30, + ) + + +def _build_node_backend_service(directory: Path, pkg_data: dict) -> Service: + name = pkg_data.get("name", "Node.js 后端") + if "/" in name: + name = name.split("/")[-1] + port = _extract_port_from_env(directory) or _extract_port_from_scripts(pkg_data) or DEFAULT_PORTS["node-backend"] + scripts = pkg_data.get("scripts", {}) + + # 尝试使用 start/dev script + if "start" in scripts: + cmd = [shutil.which("npm") or "npm", "run", "start"] + elif "dev" in scripts: + cmd = [shutil.which("npm") or "npm", "run", "dev"] + else: + # 查找入口文件 + entry = directory / "src" / "index.ts" + if not entry.exists(): + entry = directory / "src" / "index.js" + if not entry.exists(): + entry = directory / "index.ts" + if not entry.exists(): + entry = directory / "index.js" + + if entry.exists(): + if entry.suffix == ".ts": + cmd = ["npx", "ts-node", str(entry.relative_to(directory))] + else: + cmd = ["node", str(entry.relative_to(directory))] + else: + cmd = [shutil.which("npm") or "npm", "run", "start"] + + return Service( + name=f"{name.capitalize()} 后端", + project_type="node-backend", + project_dir=directory, + port=port, + start_cmd=cmd, + health_keyword="listening", + health_url=f"http://localhost:{port}", + health_timeout=30, + keyword_timeout=30, + ) + + +def _build_python_service(directory: Path) -> Optional[Service]: + """检测并构建 Python 服务""" + req_file = directory / "requirements.txt" + has_fastapi = False + has_flask = False + has_django = False + + if req_file.exists(): + try: + content = req_file.read_text(encoding="utf-8").lower() + has_fastapi = "fastapi" in content + has_flask = "flask" in content + has_django = "django" in content + except Exception: + pass + + # 检查 pyproject.toml + pyproject = directory / "pyproject.toml" + if pyproject.exists(): + try: + content = pyproject.read_text(encoding="utf-8").lower() + has_fastapi = has_fastapi or "fastapi" in content + has_flask = has_flask or "flask" in content + has_django = has_django or "django" in content + except Exception: + pass + + if not (has_fastapi or has_flask or has_django): + # 检查目录名或入口文件内容 + entry = _find_python_entry(directory) + if entry: + try: + content = entry.read_text(encoding="utf-8").lower() + has_fastapi = "fastapi" in content or "uvicorn" in content + has_flask = "flask" in content + has_django = "django" in content + except Exception: + pass + + # 如果没有明确框架,避免把辅助脚本误判成 FastAPI 服务 + if not (has_fastapi or has_flask or has_django): + return None + + port = _extract_port_from_env(directory) or DEFAULT_PORTS["python"] + dir_name = directory.name + + if has_django: + manage = directory / "manage.py" + if manage.exists(): + cmd = [sys.executable, "manage.py", "runserver", f"127.0.0.1:{port}"] + else: + cmd = [sys.executable, "-m", "django", "runserver", f"127.0.0.1:{port}"] + keyword = "Starting development server" + elif has_flask: + env_data = _read_env_file(directory) + app_module = env_data.get("FLASK_APP", "") + if app_module: + cmd = [sys.executable, "-m", "flask", "run", "--port", str(port), "--host", "127.0.0.1"] + else: + cmd = [sys.executable, "-m", "flask", "run", "--port", str(port), "--host", "127.0.0.1"] + keyword = "Running on" + else: + # FastAPI / uvicorn + # 查找 ASGI 入口 + asgi_found = False + if (directory / "main.py").exists() or (directory / "app" / "main.py").exists(): + asgi_found = True + + if asgi_found and (directory / "app").is_dir() and (directory / "app" / "main.py").exists(): + module = "app.main:app" + elif (directory / "main.py").exists(): + module = "main:app" + elif (directory / "app.py").exists(): + module = "app:app" + else: + entry = _find_python_entry(directory) + if entry: + module = entry.stem + ":app" + else: + module = "main:app" + + python_exe = sys.executable + # 尝试使用项目 venv + for venv_path in [directory / "venv", directory / ".venv"]: + venv_py = venv_path / "Scripts" / "python.exe" if sys.platform == "win32" else venv_path / "bin" / "python" + if venv_py.exists(): + python_exe = str(venv_py) + break + + cmd = [python_exe, "-m", "uvicorn", module, "--reload", "--port", str(port), "--host", "127.0.0.1"] + keyword = "Application startup complete" + + return Service( + name=f"{dir_name.capitalize()} 后端", + project_type="python", + project_dir=directory, + port=port, + start_cmd=cmd, + health_keyword=keyword, + health_url=f"http://127.0.0.1:{port}", + health_timeout=30, + keyword_timeout=60 if has_fastapi else 30, + ) + + +def _build_spring_boot_maven_service(directory: Path, pom_path: Path) -> Optional[Service]: + if not _has_spring_boot(pom_path): + return None + + port = ( + _extract_port_from_pom(pom_path) + or _extract_port_from_spring_boot_yaml(directory) + or _extract_port_from_env(directory) + or DEFAULT_PORTS["spring-boot"] + ) + dir_name = directory.name + + # 尝试提取 artifactId 作为名称 + try: + content = pom_path.read_text(encoding="utf-8") + match = re.search(r'\s*(.+?)\s*', content) + if match: + dir_name = match.group(1) + except Exception: + pass + + mvn_cmd = shutil.which("mvnw") + if not mvn_cmd: + mvnw = directory / "mvnw" + if mvnw.exists(): + mvn_cmd = str(mvnw) + else: + mvn_cmd = shutil.which("mvn") or "mvn" + + return Service( + name=f"{dir_name} 服务", + project_type="spring-boot", + project_dir=directory, + port=port, + start_cmd=[mvn_cmd, "spring-boot:run", f"-Dspring-boot.run.arguments=--server.port={port}"], + health_keyword="Started ", + health_url=_build_spring_boot_health_url(port, directory), + health_timeout=60, + keyword_timeout=120, + ) + + +def _build_spring_boot_gradle_service(directory: Path, gradle_path: Path) -> Optional[Service]: + if not _has_spring_boot_gradle(gradle_path): + return None + + port = _extract_port_from_gradle(gradle_path) or _extract_port_from_env(directory) or DEFAULT_PORTS["spring-boot-gradle"] + dir_name = directory.name + + gradlew = directory / "gradlew" + gradle_cmd = str(gradlew) if gradlew.exists() else (shutil.which("gradle") or "gradle") + + return Service( + name=f"{dir_name} 服务", + project_type="spring-boot-gradle", + project_dir=directory, + port=port, + start_cmd=[gradle_cmd, "bootRun", "--args='--server.port=" + str(port) + "'"], + health_keyword="Started ", + health_url=f"http://localhost:{port}/actuator/health", + health_timeout=60, + keyword_timeout=120, + ) + + +# ============================================================ +# 进程管理 +# ============================================================ + +def _get_pids_by_port_fallback(port: int) -> list: + """macOS/Linux: 通过 lsof 获取端口占用 PID""" + pids = [] + try: + result = subprocess.run(["lsof", "-ti", f":{port}"], capture_output=True, text=True, timeout=5) + if result.returncode == 0 and result.stdout.strip(): + for line in result.stdout.strip().splitlines(): + try: + pids.append(int(line.strip())) + except ValueError: + continue + except Exception: + pass + return pids + + +def check_port(port: int) -> tuple: + """检查端口占用,返回 (是否占用, PID列表)""" + pids = [] + try: + for conn in psutil.net_connections(kind='inet'): + if conn.laddr and conn.laddr.port == port and conn.status == psutil.CONN_LISTEN: + if conn.pid: + pids.append(conn.pid) + except psutil.AccessDenied: + pids = _get_pids_by_port_fallback(port) + except Exception: + pids = _get_pids_by_port_fallback(port) + + return (bool(pids), pids) if pids else (False, []) + + +def _pid_alive(pid: int) -> bool: + try: + psutil.Process(pid) + return True + except (psutil.NoSuchProcess, psutil.AccessDenied): + return False + + +def get_active_pid_on_port(port: int) -> Optional[int]: + in_use, pids = check_port(port) + if not in_use: + return None + for pid in pids: + if _pid_alive(pid): + return pid + return pids[0] if pids else None + + +def find_process_by_port(port: int) -> list: + """查找占用端口的存活进程""" + pids = set() + try: + for conn in psutil.net_connections(kind='inet'): + if conn.laddr and conn.laddr.port == port and conn.pid and conn.pid != os.getpid(): + try: + psutil.Process(conn.pid) + pids.add(conn.pid) + except psutil.NoSuchProcess: + continue + except psutil.AccessDenied: + for pid in _get_pids_by_port_fallback(port): + if pid != os.getpid(): + try: + psutil.Process(pid) + pids.add(pid) + except psutil.NoSuchProcess: + continue + except Exception: + for pid in _get_pids_by_port_fallback(port): + if pid != os.getpid(): + try: + psutil.Process(pid) + pids.add(pid) + except psutil.NoSuchProcess: + continue + return sorted(pids) + + +def kill_process(pid: int, graceful: bool = True): + """终止进程""" + try: + proc = psutil.Process(pid) + proc_name = proc.name() + except psutil.NoSuchProcess: + log(f"进程 {pid} 已不存在", "INFO") + return + + if graceful: + proc.terminate() + try: + proc.wait(timeout=3) + log(f"{proc_name} (PID {pid}) 已停止", "SUCCESS") + except psutil.TimeoutExpired: + proc.kill() + proc.wait(timeout=2) + log(f"{proc_name} (PID {pid}) 已强制终止", "WARN") + else: + proc.kill() + try: + proc.wait(timeout=2) + except Exception: + pass + log(f"{proc_name} (PID {pid}) 已强制终止", "WARN") + + +def stop_service(name: str, port: int): + """优雅停止指定服务""" + log(f"停止 {name} (端口 {port})...", "INFO") + pids = find_process_by_port(port) + if pids: + for pid in pids: + kill_process(pid, graceful=True) + else: + log(f"{name} 未运行", "INFO") + + +def kill_service(name: str, port: int): + """强制终止指定服务""" + log(f"强制终止 {name} (端口 {port})...", "WARN") + pids = find_process_by_port(port) + if pids: + for pid in pids: + kill_process(pid, graceful=False) + else: + log(f"{name} 未运行", "INFO") + + +# ============================================================ +# 健康检查 +# ============================================================ + +def wait_for_log_keyword(log_file: Path, keyword: str, timeout: int = 30) -> bool: + """轮询日志文件等待关键字""" + log(f'等待日志关键字: "{keyword}"', "DEBUG") + start_time = time.time() + + while time.time() - start_time < timeout: + if log_file.exists(): + break + time.sleep(0.5) + + time.sleep(2) # 等待进程开始写日志 + + while time.time() - start_time < timeout: + try: + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + if keyword in content: + return True + except Exception: + pass + time.sleep(1) + + return False + + +def http_get(url: str, timeout: int = 10) -> bool: + """HTTP GET 请求""" + if HAS_REQUESTS: + try: + resp = _requests.get(url, timeout=timeout) + return resp.status_code == 200 + except Exception: + return False + else: + try: + from urllib.request import urlopen + resp = urlopen(url, timeout=timeout) + return resp.status == 200 + except Exception: + return False + + +def http_health_check(url: str, timeout: int = 10) -> bool: + """HTTP 健康检查""" + log(f"HTTP 健康检查: {url}", "DEBUG") + start_time = time.time() + while time.time() - start_time < timeout: + if http_get(url): + return True + time.sleep(0.5) + return False + + +def check_service_health(svc: Service) -> bool: + """两阶段健康检查""" + log(f"健康检查开始: {svc.name}", "INFO") + log_file = svc.log_path + + if wait_for_log_keyword(log_file, svc.health_keyword, svc.keyword_timeout): + log(f"[1/2] 日志就绪: {svc.name}", "SUCCESS") + else: + log(f"[1/2] 日志关键字超时: {svc.name}", "ERROR") + _print_log_tail(svc.name, log_file) + return False + + if http_health_check(svc.health_url, svc.health_timeout): + log(f"[2/2] HTTP 就绪: {svc.name} ({svc.health_url})", "SUCCESS") + return True + else: + log(f"[2/2] HTTP 健康检查失败: {svc.name}", "ERROR") + _print_log_tail(svc.name, log_file) + return False + + +def _print_log_tail(name: str, log_file: Path): + """输出日志最后 20 行""" + if log_file.exists(): + try: + lines = log_file.read_text(encoding="utf-8", errors="ignore").splitlines()[-20:] + if lines: + log(f"{name} 日志最后 20 行:", "WARN") + for line in lines: + print(f" {line}") + except Exception: + pass + + +# ============================================================ +# 启动/停止 +# ============================================================ + +def start_service(svc: Service) -> bool: + """启动单个服务""" + log_section(f"启动 {svc.name} ({svc.type_label()})") + + # 确保目录存在 + if not svc.project_dir.exists(): + log(f"项目目录不存在: {svc.project_dir}", "ERROR") + return False + + # 检查端口占用 + in_use, pids = check_port(svc.port) + if in_use: + alive_pids = [pid for pid in pids if _pid_alive(pid)] + if alive_pids: + log(f"端口 {svc.port} 已被占用 (PID: {', '.join(map(str, alive_pids))}),先停止...", "WARN") + kill_service(svc.name, svc.port) + time.sleep(3) + else: + log(f"端口 {svc.port} 有残留连接 (PID 已不存在),直接启动...", "WARN") + + # 启动进程 + cmd = svc.start_cmd + log(f"启动命令: {' '.join(cmd)}", "DEBUG") + + try: + with open(svc.log_path, "w", encoding="utf-8") as f: + popen_kwargs = _background_process_options() + proc = subprocess.Popen( + cmd, + cwd=str(svc.project_dir), + stdout=f, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + **popen_kwargs, + ) + log(f"{svc.name} 进程已启动 (PID: {proc.pid})", "INFO") + except FileNotFoundError as e: + log(f"{svc.name} 启动失败: 命令不存在 — {e}", "ERROR") + _suggest_runtime_install(svc) + return False + except Exception as e: + log(f"{svc.name} 启动失败: {e}", "ERROR") + return False + + # 健康检查 + if check_service_health(svc): + log(f"{svc.name} 已就绪 {svc.health_url}", "SUCCESS") + return True + else: + log(f"{svc.name} 启动失败", "ERROR") + return False + + +def _background_process_options() -> dict: + """获取跨平台后台启动参数""" + if sys.platform == "win32": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 0 + creationflags = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW + if hasattr(subprocess, "CREATE_BREAKAWAY_FROM_JOB"): + creationflags |= subprocess.CREATE_BREAKAWAY_FROM_JOB + return { + "creationflags": creationflags, + "startupinfo": startupinfo, + } + + return { + "start_new_session": True, + } + + +def _suggest_runtime_install(svc: Service): + """给出运行时环境安装建议""" + suggestions = { + "vite": "请确保已安装 Node.js,并在项目目录下运行: npm install 或 pnpm install", + "next": "请确保已安装 Node.js,并在项目目录下运行: npm install", + "node-backend": "请确保已安装 Node.js,并在项目目录下运行: npm install", + "python": "请确保已安装 Python,并在项目目录下运行: pip install -r requirements.txt", + "spring-boot": "请确保已安装 Java JDK 和 Maven", + "spring-boot-gradle": "请确保已安装 Java JDK 和 Gradle", + } + tip = suggestions.get(svc.project_type, "") + if tip: + log(tip, "WARN") + + +def stop_service_wrapper(svc: Service): + """停止单个服务""" + log_section(f"停止 {svc.name}") + stop_service(svc.name, svc.port) + + +def restart_service(svc: Service) -> bool: + """重启单个服务""" + stop_service_wrapper(svc) + time.sleep(2) + return start_service(svc) + + +# ============================================================ +# 状态查询 +# ============================================================ + +def status_services(services: list[Service]): + """查看所有服务状态""" + log_section("服务状态") + + if not services: + log("未发现任何服务", "WARN") + log("请确保当前目录下存在可识别的项目(包含 package.json / requirements.txt / pom.xml / build.gradle)", "INFO") + log_separator() + return + + for svc in services: + pid = get_active_pid_on_port(svc.port) + if pid: + try: + proc = psutil.Process(pid) + cpu = proc.cpu_percent(interval=0.1) + mem = proc.memory_info().rss / 1024 / 1024 + status_icon = "运行中" + color = COLORS["SUCCESS"] + except (psutil.NoSuchProcess, psutil.AccessDenied): + cpu = mem = 0 + status_icon = "异常" + color = COLORS["ERROR"] + else: + cpu = mem = 0 + status_icon = "未运行" + color = COLORS["INFO"] + + reset = COLORS["RESET"] + cpu_str = f"{cpu:.1f}%" if cpu else "-" + mem_str = f"{mem:.1f}MB" if mem else "-" + dir_rel = svc.project_dir.relative_to(CWD) if svc.project_dir.is_relative_to(CWD) else svc.project_dir + print(f" {color}[{svc.name}]{reset} 类型: {svc.type_label()} 端口: {svc.port} 状态: {status_icon} 目录: {dir_rel}") + if pid: + print(f" PID: {pid} CPU: {cpu_str} 内存: {mem_str}") + + log_separator() + print(f" {COLORS['BOLD']}访问地址:{COLORS['RESET']}") + for svc in services: + print(f" {svc.name}: {COLORS['SUCCESS']}{svc.health_url}{COLORS['RESET']}") + log_separator() + print(f" {COLORS['BOLD']}日志目录:{COLORS['RESET']}") + print(f" {LOG_DIR}") + log_separator() + + +# ============================================================ +# 日志查看 +# ============================================================ + +def tail_logs(target: str, services: list[Service]): + """查看日志""" + if target == "all": + _tail_multi([svc.log_path for svc in services]) + return + + # 按名称或类型匹配 + matched = [s for s in services if target.lower() in s.name.lower() or target.lower() in s.project_type.lower()] + if len(matched) == 1: + log_file = matched[0].log_path + elif len(matched) > 1: + _tail_multi([s.log_path for s in matched]) + return + else: + # 尝试直接匹配文件 + log_file = LOG_DIR / f"{target}.log" + if not log_file.exists(): + log(f"未找到日志: {target}", "ERROR") + return + + if not log_file.exists(): + log(f"日志文件不存在: {log_file}", "ERROR") + return + + log(f"实时查看日志 (Ctrl+C 退出)...", "INFO") + + if sys.platform == "win32": + try: + subprocess.run(["powershell", "-Command", f"Get-Content -Wait -Tail 50 '{log_file}'"], check=True) + except KeyboardInterrupt: + pass + except Exception: + _tail_python(log_file) + else: + try: + subprocess.run(["tail", "-f", str(log_file)], check=True) + except KeyboardInterrupt: + pass + + +def _tail_multi(log_files: list): + """多日志同时查看""" + positions = {lf: 0 for lf in log_files} + log("实时查看所有日志 (Ctrl+C 退出)...", "INFO") + try: + while True: + new_output = False + for lf in log_files: + try: + with open(lf, "r", encoding="utf-8", errors="ignore") as f: + f.seek(positions[lf]) + lines = f.readlines() + positions[lf] = f.tell() + for line in lines: + prefix = f"{lf.stem[:3]} " + print(f"{prefix}{line.rstrip()}") + new_output = True + except Exception: + pass + if not new_output: + time.sleep(1) + except KeyboardInterrupt: + pass + + +def _tail_python(log_file: Path): + """Python tail -f""" + position = 0 + try: + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + f.seek(0, 2) + position = f.tell() + f.seek(0) + lines = f.readlines() + for line in lines[-50:]: + print(line.rstrip()) + + while True: + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + f.seek(position) + lines = f.readlines() + position = f.tell() + for line in lines: + print(line.rstrip()) + if not lines: + time.sleep(1) + except KeyboardInterrupt: + pass + + +# ============================================================ +# 服务过滤 +# ============================================================ + +def filter_services(services: list[Service], svc_type: Optional[str] = None, port: Optional[int] = None) -> list[Service]: + """按类型或端口过滤服务""" + result = services + if svc_type: + result = [s for s in result if s.project_type == svc_type or svc_type.lower() in s.project_type.lower()] + if port: + result = [s for s in result if s.port == port] + return result + + +# ============================================================ +# CLI 入口 +# ============================================================ + +def main(): + parser = argparse.ArgumentParser( + description="通用服务管理脚本 — 自动发现并管理项目中的各类服务", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +可用命令: + start 发现并启动所有服务(默认) + stop 停止所有服务 + restart 重启所有服务 + status 查看发现的服务及运行状态 + discover 仅列出发现的服务(不启动) + kill 强制终止服务 + logs 实时查看日志 + +选项: + --type 按类型过滤: vite | python | spring-boot | node-backend | next + --port 按端口号过滤 + --foreground, -f 前台模式运行(Ctrl+C 停止) + +使用示例: + dev-services 发现并启动所有服务 + dev-services discover 仅列出发现的服务 + dev-services discover server 只列出 server 目录下的服务 + dev-services start frontend 只启动前端服务 + dev-services restart server 只重启后端服务 + dev-services status 查看所有服务状态 + dev-services status web 只查看大屏服务状态 + dev-services stop 停止所有服务 + dev-services kill all 强制终止所有服务 + dev-services logs all 实时查看所有日志 + +支持的服务类型: + Vite 前端 检测到 package.json + vite 依赖 + Next.js 检测到 package.json + next 依赖 + Node.js 后端 检测到 Express / NestJS / Koa + Python 后端 检测到 FastAPI / Flask / Django + Spring Boot 检测到 pom.xml + spring-boot 依赖 + Spring Boot Gradle 检测到 build.gradle + spring-boot 插件 + +端口提取: + 1. 优先从 .env / .env.development 提取 + 2. 从 vite.config.ts / pom.xml / build.gradle 提取 + 3. 从 application*.yml 提取 server.port(Spring Boot) + 4. 使用框架默认值 + +安装方式: + 脚本位于 ~/.claude/dev-services.py + 通过 PATH 中的 dev-services 命令调用 + (Windows: %USERPROFILE%\\bin\\dev-services.cmd) +""", + ) + parser.add_argument( + "action", + nargs="?", + default="start", + choices=["start", "stop", "restart", "status", "kill", "logs", "discover"], + help="操作: start|stop|restart|status|kill|logs|discover (默认: start)", + ) + parser.add_argument( + "target", + nargs="?", + default=None, + help="目标服务 (kill/logs) 或指定服务目录 (其他命令)", + ) + parser.add_argument("--type", dest="svc_type", default=None, help="服务类型过滤: vite|python|spring-boot|node-backend") + parser.add_argument("--port", dest="port", type=int, default=None, help="端口号过滤") + parser.add_argument("--foreground", "-f", action="store_true", help="前台模式运行") + + args = parser.parse_args() + + # 解析 target:如果是存在的目录,作为服务发现的根目录;否则作为 kill/logs 的目标 + root_dir = None + if args.target and args.action not in ("kill", "logs"): + target_path = Path(args.target) + if not target_path.is_absolute(): + target_path = CWD / target_path + if target_path.is_dir(): + root_dir = target_path + else: + log(f"目标路径不存在: {args.target}", "ERROR") + return + + # 发现服务 + services = discover_services(root_dir=root_dir) + services = filter_services(services, args.svc_type, args.port) + + if args.action == "discover": + log_section(f"发现的服务 (目录: {root_dir or CWD})") + if not services: + log("未发现任何服务", "WARN") + log("支持的类型: Vite 前端, Next.js, Node.js 后端, Python (FastAPI/Flask/Django), Spring Boot (Maven/Gradle)", "INFO") + else: + for i, svc in enumerate(services, 1): + dir_rel = svc.project_dir.relative_to(CWD) if svc.project_dir.is_relative_to(CWD) else svc.project_dir + print(f" {COLORS['SUCCESS']}[{i}]{COLORS['RESET']} {COLORS['BOLD']}{svc.name}{COLORS['RESET']}") + print(f" 类型: {svc.type_label()} 端口: {svc.port}") + print(f" 目录: {dir_rel}") + print(f" 命令: {' '.join(svc.start_cmd)}") + print() + log_separator() + return + + if args.action == "status": + status_services(services) + return + + if args.action == "kill": + target = args.target or "all" + log_section("强制终止服务") + if not services: + log("未发现任何服务", "WARN") + else: + for svc in services: + if target in ("all", svc.name.lower(), svc.project_type.lower()): + kill_service(svc.name, svc.port) + log_separator() + return + + if args.action == "logs": + target = args.target or "all" + if not services: + log("未发现任何服务,无法查看日志", "WARN") + return + tail_logs(target, services) + return + + if args.action == "stop": + if not services: + log("未发现任何服务", "WARN") + return + for svc in services: + stop_service_wrapper(svc) + log_separator() + log("所有服务已停止", "SUCCESS") + return + + if args.action in ("start", "restart"): + if not services: + log("未发现任何服务", "WARN") + log("支持的类型: Vite 前端, Next.js, Node.js 后端, Python (FastAPI/Flask/Django), Spring Boot (Maven/Gradle)", "INFO") + log(f"扫描目录: {CWD} (最多 {MAX_SCAN_DEPTH} 层)", "DEBUG") + return + + if args.action == "restart": + for svc in services: + restart_service(svc) + else: + # start + if args.foreground: + _run_foreground(services) + else: + log_separator() + log(f"通用服务管理器 (目录: {CWD})", "INFO") + log_separator() + + results = [] + for svc in services: + ok = start_service(svc) + results.append((svc, ok)) + + log_separator() + if all(ok for _, ok in results): + log("所有服务已就绪", "SUCCESS") + for svc, ok in results: + print(f" {COLORS['SUCCESS']}✓ {svc.name}{COLORS['RESET']}: {svc.health_url}") + elif any(ok for _, ok in results): + log("部分服务启动失败,请检查日志", "WARN") + for svc, ok in results: + icon = f"{COLORS['SUCCESS']}✓{COLORS['RESET']}" if ok else f"{COLORS['ERROR']}✗{COLORS['RESET']}" + print(f" {icon} {svc.name}") + else: + log("所有服务启动失败", "ERROR") + return + log_separator() + return + + +def _run_foreground(services: list[Service]): + """前台模式运行""" + procs = [] + + def cleanup(): + log("正在停止服务...", "WARN") + for p in procs: + try: + p.terminate() + except Exception: + pass + for p in procs: + try: + p.wait(timeout=3) + except Exception: + p.kill() + log("所有服务已停止", "INFO") + + import signal + def handle_signal(signum, frame): + cleanup() + sys.exit(0) + + signal.signal(signal.SIGINT, handle_signal) + if sys.platform != "win32": + signal.signal(signal.SIGTERM, handle_signal) + + for svc in services: + log_section(f"启动 {svc.name} (前台模式)") + cmd = svc.start_cmd + try: + creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 + proc = subprocess.Popen( + cmd, + cwd=str(svc.project_dir), + creationflags=creationflags, + ) + procs.append(proc) + except Exception as e: + log(f"{svc.name} 启动失败: {e}", "ERROR") + + log_separator() + log("按 Ctrl+C 停止所有服务", "INFO") + log_separator() + + for p in procs: + p.wait() + + +if __name__ == "__main__": + main() diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-内容界面.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-内容界面.png new file mode 100644 index 0000000..ffcdf71 Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-内容界面.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-编辑资料.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-编辑资料.png new file mode 100644 index 0000000..cf5f151 Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-编辑资料.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-记录界面.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-记录界面.png new file mode 100644 index 0000000..8d2f66d Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-记录界面.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-首页.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-首页.png new file mode 100644 index 0000000..f5c7d39 Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/人生轨迹-首页.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-剧本列表.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-剧本列表.png new file mode 100644 index 0000000..ddbc5eb Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-剧本列表.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-大纲.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-大纲.png new file mode 100644 index 0000000..cff8b47 Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-大纲.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-正文.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-正文.png new file mode 100644 index 0000000..8ba9539 Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-正文.png differ diff --git a/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-灵感模式.png b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-灵感模式.png new file mode 100644 index 0000000..1bf93b3 Binary files /dev/null and b/mini-program-prototype/开心OS-UI设计/开心OS-UI设计/爽文生成-灵感模式.png differ