feat: 修复 Redis 超时问题、固定小程序端口、新增人生事件模块及优化多个页面

- 修复 Redis 超时:添加 commons-pool2 依赖,启用 Lettuce 连接池,超时提升至 15s
- 固定 mini-program H5 端口为 5175,避免与 web 项目端口冲突
- 新增人生事件(life-event)模块:表单和详情页面
- 新增 EpicScript 灵感接口(Controller/Service/DTO)
- 优化登录、引导、主页、记录、剧本详情等多个页面
- 优化服务管理脚本和 Nginx 配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:38:35 +08:00
parent 507d1ebdab
commit 60c63850ee
36 changed files with 4545 additions and 3043 deletions
+158 -10
View File
@@ -49,11 +49,17 @@ 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"}
SKIP_DIRS = {
"node_modules", "__pycache__", ".git", "venv", ".venv", "target", "build",
"dist", ".omc", ".next", ".nuxt", "coverage", "e2e",
}
AUXILIARY_DIRS = {"tools", "scripts", "script", "bin", ".superpowers", ".gstack"}
# 框架默认端口
DEFAULT_PORTS = {
"vite": 5173,
"uniapp": 5173,
"taro": 5173,
"next": 3000,
"node-backend": 3000,
"python": 8000,
@@ -134,6 +140,8 @@ class Service:
def type_label(self) -> str:
labels = {
"vite": "Vite 前端",
"uniapp": "UniApp H5",
"taro": "Taro H5",
"next": "Next.js",
"node-backend": "Node.js 后端",
"python": "Python 后端",
@@ -162,7 +170,7 @@ def _scan_directory(current: Path, root: Path, max_depth: int, services: list):
return
# 跳过黑名单
if current.name in SKIP_DIRS:
if current != root and (current.name in SKIP_DIRS or current.name in AUXILIARY_DIRS):
return
# 尝试发现服务
@@ -206,6 +214,10 @@ def _detect_service(directory: Path) -> Optional[Service]:
deps = {**pkg_data.get("dependencies", {}), **pkg_data.get("devDependencies", {})}
scripts = pkg_data.get("scripts", {})
if _is_uniapp_project(deps, scripts):
return _build_uniapp_service(directory, pkg_data)
if _is_taro_project(deps, scripts):
return _build_taro_service(directory, pkg_data)
# Vite
if "vite" in deps:
return _build_vite_service(directory, pkg_data)
@@ -263,7 +275,7 @@ def _read_env_file(directory: Path) -> dict:
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"]:
for key in ["VITE_PORT", "VITE_APP_PORT", "PORT", "SERVER_PORT", "APP_PORT", "BACKEND_PORT"]:
if key in env_data:
try:
return int(env_data[key])
@@ -291,13 +303,61 @@ def _extract_port_from_vite_config(directory: Path) -> Optional[int]:
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))
for script_name in ["dev:h5", "h5", "dev", "start"]:
script = scripts.get(script_name, "")
match = re.search(r'--port(?:=|\s+)(\d+)', script)
if match:
return int(match.group(1))
return None
def _service_has_explicit_port(svc: Service) -> bool:
if _extract_port_from_env(svc.project_dir) is not None:
return True
if svc.project_type in {"vite", "uniapp", "taro", "next"}:
return _extract_port_from_vite_config(svc.project_dir) is not None
return False
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
svc.start_cmd = updated
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
def _extract_port_from_pom(pom_path: Path) -> Optional[int]:
"""从 pom.xml 提取 server.port"""
try:
@@ -558,6 +618,38 @@ def _find_python_entry(directory: Path) -> Optional[Path]:
# 服务构建
# ============================================================
def _is_uniapp_project(deps: dict, scripts: dict) -> bool:
dep_names = set(deps.keys())
return (
any(name.startswith("@dcloudio/") for name in dep_names)
or "uni-app" in dep_names
or any("uni " in value or value.startswith("uni") for value in scripts.values())
)
def _is_taro_project(deps: dict, scripts: dict) -> bool:
dep_names = set(deps.keys())
return (
any(name.startswith("@tarojs/") for name in dep_names)
or any("taro " in value or value.startswith("taro") for value in scripts.values())
)
def _get_package_manager(directory: Path) -> str:
if (directory / "pnpm-lock.yaml").exists():
return shutil.which("pnpm") or "pnpm"
if (directory / "yarn.lock").exists():
return shutil.which("yarn") or "yarn"
return shutil.which("npm") or "npm"
def _choose_h5_script(scripts: dict) -> str:
for name in ["dev:h5", "h5", "dev"]:
if name in scripts:
return name
return "dev"
def _get_npm_cmd(directory: Path) -> list:
"""获取可用的包管理器命令,返回 [cmd, args...] 列表"""
# 优先通过 node 直接执行 vite.js(避免 .cmd 的 shell 问题)
@@ -583,6 +675,56 @@ def _get_npm_cmd(directory: Path) -> list:
return ["npx"]
def _build_uniapp_service(directory: Path, pkg_data: dict) -> Service:
name = pkg_data.get("name", "uniapp")
if "/" in name:
name = name.split("/")[-1]
scripts = pkg_data.get("scripts", {})
port = (_extract_port_from_env(directory)
or _extract_port_from_scripts(pkg_data)
or _extract_port_from_vite_config(directory)
or DEFAULT_PORTS["uniapp"])
script = _choose_h5_script(scripts)
cmd = [_get_package_manager(directory), "run", script, "--", "--host", "127.0.0.1", "--port", str(port)]
return Service(
name=f"{name.capitalize()} 前端",
project_type="uniapp",
project_dir=directory,
port=port,
start_cmd=cmd,
health_keyword="",
health_url=f"http://localhost:{port}",
health_timeout=120,
keyword_timeout=0,
)
def _build_taro_service(directory: Path, pkg_data: dict) -> Service:
name = pkg_data.get("name", "taro")
if "/" in name:
name = name.split("/")[-1]
scripts = pkg_data.get("scripts", {})
port = (_extract_port_from_env(directory)
or _extract_port_from_scripts(pkg_data)
or _extract_port_from_vite_config(directory)
or DEFAULT_PORTS["taro"])
script = _choose_h5_script(scripts)
cmd = [_get_package_manager(directory), "run", script, "--", "--host", "127.0.0.1", "--port", str(port)]
return Service(
name=f"{name.capitalize()} 前端",
project_type="taro",
project_dir=directory,
port=port,
start_cmd=cmd,
health_keyword="",
health_url=f"http://localhost:{port}",
health_timeout=120,
keyword_timeout=0,
)
def _build_vite_service(directory: Path, pkg_data: dict) -> Service:
name = pkg_data.get("name", "前端")
if "/" in name:
@@ -1044,15 +1186,20 @@ 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):
if not svc.health_keyword:
log(f"[1/2] Log keyword skipped: {svc.name}", "INFO")
elif 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")
log(f"[1/2] 日志关键字超时: {svc.name}", "WARN")
if http_health_check(svc.health_url, svc.health_timeout):
log(f"[2/2] HTTP ready: {svc.name} ({svc.health_url})", "SUCCESS")
return True
_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")
log(f"[2/2] HTTP ready: {svc.name} ({svc.health_url})", "SUCCESS")
return True
else:
log(f"[2/2] HTTP 健康检查失败: {svc.name}", "ERROR")
@@ -1424,6 +1571,7 @@ def main():
# 发现服务
services = discover_services(root_dir=root_dir)
services = filter_services(services, args.svc_type, args.port)
services = assign_unique_ports(services)
if args.action == "discover":
log_section(f"发现的服务 (目录: {root_dir or CWD})")