调整技能位置

This commit is contained in:
马宝龙
2026-07-28 16:09:45 +08:00
parent 92b718c096
commit 984d0320a7
8 changed files with 127 additions and 15 deletions

View File

@@ -1,4 +0,0 @@
interface:
display_name: "全栈启动验证"
short_description: "自动启动前后端服务并完成健康检查与端到端功能验证"
default_prompt: "使用 $fullstack-start-verify 启动项目的前后端,执行健康检查和端到端验证并汇报结果。"

View File

@@ -1,30 +1,41 @@
--- ---
name: fullstack-start-verify name: fullstack-start-verify
description: 启动本地项目的前端和后端服务并通过进程状态、HTTP 健康检查、接口检查和可用时的浏览器冒烟测试验证整体功能。用于 Codex 需要运行全栈项目、验证前后端联调、排查启动失败,或确认用户流程是否端到端可用的场景。 description: 快速启动或复用本地项目的前端和后端服务并通过进程状态、HTTP 健康检查、接口检查和可用时的浏览器冒烟测试验证整体功能。用于 Codex 需要运行全栈项目、验证前后端联调、排查启动失败,或确认用户流程是否端到端可用的场景。
--- ---
# 全栈启动与验证 # 全栈启动与验证
当用户要求启动前后端、验证整体功能、跑通本地联调或确认项目是否可用时使用本技能。目标是得到可复现的验证结论,而不是只确认某个进程曾经启动过。 当用户要求启动前后端、验证整体功能、跑通本地联调或确认项目是否可用时使用本技能。默认走快速只读路径,目标是以最少的启动和检查步骤得到可复现结论,而不是只确认某个进程曾经启动过。
## 验证层级
先根据任务范围选择最低必要层级,完成后不要自动升级:
- **启动检查**:复用或启动前后端,检查两个健康地址;仅在需要确认数据库联通时增加 1 个已有的只读 API。
- **联调检查**:在启动检查基础上增加 1 至 2 个主要只读 API确认前端代理、后端路由和基础设施链路。
- **回归检查**:只有用户明确要求、代码发生相关变更或快速检查失败时,才执行构建、测试或浏览器冒烟。
启动检查不执行 `npm ci``mvn test`、前端构建、数据库迁移或浏览器操作。不要用固定 `sleep` 等待服务,统一让内置脚本轮询 HTTP 地址。
## 工作流程 ## 工作流程
1. 在启动前检查项目。 1. 在启动前检查项目。
- 阅读适用的 `AGENTS.md``README.md`、根目录环境变量示例,以及前后端的项目清单文件。 - 并行阅读适用的 `AGENTS.md``README.md`、根目录环境变量示例,以及前后端的项目清单文件;同一任务中不要重复读取已确认的配置
- 确认实际使用的包管理器和启动命令。优先使用项目文档和项目自带包装器中的命令,例如 `npm``pnpm``yarn``mvnw``gradlew``uv` 等。 - 确认实际使用的包管理器和启动命令。优先使用项目文档和项目自带包装器中的命令,例如 `npm``pnpm``yarn``mvnw``gradlew``uv` 等。
- 找到后端健康检查地址、前端地址、API 基础地址、所需的数据库或缓存服务,以及文档中提供的测试账号。 - 找到后端健康检查地址、前端地址、API 基础地址、所需的数据库或缓存服务,以及文档中提供的测试账号。
- 不要为了让检查通过而虚构凭据、覆盖 `.env`、执行迁移或重置数据。 - 不要为了让检查通过而虚构凭据、覆盖 `.env`、执行迁移或重置数据。
2. 执行启动前检查。 2. 执行启动前检查。
- 确认所需运行时和依赖目录可用。前端已有 `node_modules` 时直接启动,不要每次重复执行 `npm ci`;仅在依赖缺失或用户明确要求重装时安装。 - 确认所需运行时和依赖目录可用。前端已有 `node_modules` 时直接启动,不要每次重复执行 `npm ci`;仅在依赖缺失或用户明确要求重装时安装。
- 检查计划使用的端口。只有在进程和健康响应明确属于当前项目时才复用服务,绝不要结束未知进程。 - 检查计划使用的端口。健康地址响应必须能确认属于当前项目时,才使用 `--reuse-running` 复用服务;复用的服务不由脚本结束。无法确认归属时不要复用,也绝不要结束未知进程。
- 仅在项目文档说明了启动方式且用户已将其纳入范围时,启动 MySQL 或 Redis 等基础设施。 - 仅在项目文档说明了启动方式且用户已将其纳入范围时,启动 MySQL 或 Redis 等基础设施。
- 如果缺少必要的密钥、数据库或账号,应报告阻塞原因,不要降低验证标准。 - 如果缺少必要的密钥、数据库或账号,应报告阻塞原因,不要降低验证标准。
3. 使用 `scripts/start_and_verify.py` 启动两个应用服务。脚本默认使用 0.25 秒快速轮询,并行等待两个服务就绪。 3. 使用 `scripts/start_and_verify.py` 复用或启动两个应用服务。脚本默认使用 0.25 秒快速轮询,并行等待服务就绪;使用 `--reuse-running` 时会并行探测健康地址,只启动未就绪的服务
- 从项目根目录运行脚本。 - 从项目根目录运行脚本。
- 传入明确的工作目录、启动命令和地址。对于健康检查之外的重要 API 路由,使用 `--check-url label=url` 添加只读检查;不要重复添加已经作为 `--backend-url` 的健康地址。 - 传入明确的工作目录、启动命令和地址。对于健康检查之外的重要 API 路由,使用 `--check-url label=url` 添加只读检查;不要重复添加已经作为 `--backend-url` 的健康地址。
- 后端应使用项目专用的健康检查地址,而不是只检查 TCP 端口是否打开;前端应检查开发服务器根路径或已知路由。 - 后端应使用项目专用的健康检查地址,而不是只检查 TCP 端口是否打开;前端应检查开发服务器根路径或已知路由。
- `--check-url` 只添加必要的只读 API。后端健康地址就绪后脚本会立即并行执行这些检查同时继续等待前端避免额外 API 检查串行阻塞启动流程。
- 脚本只管理和清理自己启动的进程。只有用户明确要求服务继续运行时,才使用 `--keep-running` - 脚本只管理和清理自己启动的进程。只有用户明确要求服务继续运行时,才使用 `--keep-running`
- 排查失败时使用 `--keep-logs` 保留日志。避免把密钥放在命令行参数中。 - 排查失败时使用 `--keep-logs` 保留日志。避免把密钥放在命令行参数中。
@@ -38,11 +49,12 @@ description: 启动本地项目的前端和后端服务,并通过进程状态
--frontend-cmd "npm run dev -- --host 127.0.0.1" ` --frontend-cmd "npm run dev -- --host 127.0.0.1" `
--frontend-dir frontend ` --frontend-dir frontend `
--frontend-url http://127.0.0.1:5173/ ` --frontend-url http://127.0.0.1:5173/ `
--reuse-running `
--check-url summary=http://127.0.0.1:8080/api/v1/users/summary --check-url summary=http://127.0.0.1:8080/api/v1/users/summary
``` ```
4. 按递进层级验证应用。 4. 按递进层级验证应用。
- 快速路径先确认两个健康检查地址和 1 至 2 个主要只读 API脚本会并行执行等待和额外检查 - 启动检查先确认两个健康检查地址;仅在数据库或核心联调需要时增加 1 个主要只读 API。联调检查最多增加 2 个主要只读 API避免重复请求同一资源
- 只有代码变更、用户明确要求回归,或快速路径暴露问题时,才执行前端构建、后端测试等较慢命令。 - 只有代码变更、用户明确要求回归,或快速路径暴露问题时,才执行前端构建、后端测试等较慢命令。
- 只有涉及页面或交互变更且有浏览器工具时,才执行浏览器冒烟:加载页面、完成最小有意义的操作,同时验证页面可见结果和网络/API 结果。 - 只有涉及页面或交互变更且有浏览器工具时,才执行浏览器冒烟:加载页面、完成最小有意义的操作,同时验证页面可见结果和网络/API 结果。
- 除非用户明确要求写入流程且测试数据安全,否则不执行新增、修改、删除或数据库迁移。 - 除非用户明确要求写入流程且测试数据安全,否则不执行新增、修改、删除或数据库迁移。
@@ -58,10 +70,11 @@ description: 启动本地项目的前端和后端服务,并通过进程状态
- 服务在地址就绪前退出,属于启动失败。重试前先检查捕获的日志。 - 服务在地址就绪前退出,属于启动失败。重试前先检查捕获的日志。
- 超时不算通过。检查依赖是否可用、端口是否冲突、环境变量是否加载、代理配置和服务日志。 - 超时不算通过。检查依赖是否可用、端口是否冲突、环境变量是否加载、代理配置和服务日志。
- 只允许在定位出可修复的瞬时原因后重试一次;重试时复用已确认健康的服务,不重复安装依赖或重复启动基础设施。连续失败应报告第一个可执行的根因。
- 后端健康但前端 API 调用失败,属于集成失败。检查前端代理或基础地址,以及 CORS 配置。 - 后端健康但前端 API 调用失败,属于集成失败。检查前端代理或基础地址,以及 CORS 配置。
- 前端页面能加载但浏览器操作失败,属于用户流程失败。记录准确路由、操作、响应状态和控制台错误。 - 前端页面能加载但浏览器操作失败,属于用户流程失败。记录准确路由、操作、响应状态和控制台错误。
- 验证期间不要修复无关代码、修改生产配置或删除数据,除非用户明确扩大任务范围。 - 验证期间不要修复无关代码、修改生产配置或删除数据,除非用户明确扩大任务范围。
## 内置脚本 ## 内置脚本
`scripts/start_and_verify.py` 是一个不依赖第三方库的进程运行器,负责本流程中的启动和 HTTP 检查。脚本会为检查请求添加 `X-Request-Id`,输出验证任务 ID并行等待服务与执行额外检查。使用不常见选项前先阅读 `--help` 输出。脚本默认创建临时日志并在清理后删除;排查问题时传入 `--keep-logs` 保留日志。错误日志摘要会脱敏,但保留日志前仍需确认服务自身没有输出敏感信息。 `scripts/start_and_verify.py` 是一个不依赖第三方库的进程运行器,负责本流程中的服务复用、启动和 HTTP 检查。脚本会为检查请求添加 `X-Request-Id`,输出验证任务 ID并行探测已运行服务;后端就绪后立即执行额外检查,同时继续等待前端。使用不常见选项前先阅读 `--help` 输出。脚本默认创建临时日志并在清理后删除;排查问题时传入 `--keep-logs` 保留日志。错误日志摘要会脱敏,但保留日志前仍需确认服务自身没有输出敏感信息。

View File

@@ -0,0 +1,4 @@
interface:
display_name: "全栈启动验证"
short_description: "快速复用或启动前后端并完成分层验证"
default_prompt: "使用 $fullstack-start-verify 优先复用已确认的项目服务,快速启动缺失服务并按最低必要层级完成健康、只读接口和端到端验证。"

View File

@@ -27,6 +27,8 @@ class Service:
cwd: Path cwd: Path
url: str url: str
process: subprocess.Popen[bytes] | None = None process: subprocess.Popen[bytes] | None = None
started_by_script: bool = False
reused: bool = False
log_path: Path | None = None log_path: Path | None = None
log_file: object | None = None log_file: object | None = None
@@ -73,6 +75,11 @@ def parse_args() -> argparse.Namespace:
default=5.0, default=5.0,
help="额外接口检查的单请求超时时间,默认 5 秒", help="额外接口检查的单请求超时时间,默认 5 秒",
) )
parser.add_argument(
"--reuse-running",
action="store_true",
help="健康地址已属于当前项目时复用已运行服务,不启动或清理该服务",
)
parser.add_argument( parser.add_argument(
"--log-dir", "--log-dir",
type=Path, type=Path,
@@ -118,6 +125,7 @@ def start_service(service: Service, log_dir: Path) -> None:
creationflags=creationflags, creationflags=creationflags,
start_new_session=start_new_session, start_new_session=start_new_session,
) )
service.started_by_script = True
except Exception: except Exception:
service.log_file.close() service.log_file.close()
service.log_file = None service.log_file = None
@@ -205,6 +213,87 @@ def wait_for_services(
executor.shutdown(wait=not failed, cancel_futures=failed) executor.shutdown(wait=not failed, cancel_futures=failed)
def reuse_running_services(
services: list[Service], timeout: float, run_id: str
) -> None:
"""并行探测已运行服务,避免重复启动本项目服务。"""
if not services:
return
probe_timeout = min(max(timeout, 0.5), 1.0)
def probe(service: Service) -> tuple[Service, bool, str]:
ok, reason = request(
service.url, probe_timeout, f"{run_id}-{service.name}-reuse"
)
return service, ok, reason
with concurrent.futures.ThreadPoolExecutor(max_workers=len(services)) as executor:
results = list(executor.map(probe, services))
for service, ok, reason in results:
if ok:
service.reused = True
print(f"复用 {service.name}{service.url} [{reason}]")
def wait_for_services_and_checks(
services: list[Service],
checks: list[tuple[str, str]],
timeout: float,
interval: float,
check_timeout: float,
run_id: str,
) -> None:
"""后端就绪后立即检查 API同时继续等待前端缩短总耗时。"""
if not services:
run_extra_checks(checks, check_timeout, run_id)
return
if not checks:
wait_for_services(services, timeout, interval, run_id)
return
executor = concurrent.futures.ThreadPoolExecutor(
max_workers=len(services) + max(1, len(checks))
)
failed = True
try:
service_futures = [
(
service,
executor.submit(wait_for_service, service, timeout, interval, run_id),
)
for service in services
]
backend_future = next(
(
future
for service, future in service_futures
if service.name == "backend"
),
None,
)
pending = {future for _, future in service_futures}
check_future: concurrent.futures.Future[None] | None = None
if backend_future is None:
check_future = executor.submit(run_extra_checks, checks, check_timeout, run_id)
pending.add(check_future)
while pending:
done, pending = concurrent.futures.wait(
pending, return_when=concurrent.futures.FIRST_COMPLETED
)
for future in done:
future.result()
if future is backend_future and check_future is None:
check_future = executor.submit(
run_extra_checks, checks, check_timeout, run_id
)
pending.add(check_future)
failed = False
finally:
executor.shutdown(wait=not failed, cancel_futures=failed)
def run_extra_checks( def run_extra_checks(
checks: list[tuple[str, str]], timeout: float, run_id: str checks: list[tuple[str, str]], timeout: float, run_id: str
) -> None: ) -> None:
@@ -228,7 +317,7 @@ def run_extra_checks(
def stop_service(service: Service) -> None: def stop_service(service: Service) -> None:
process = service.process process = service.process
if process is None or process.poll() is not None: if not service.started_by_script or process is None or process.poll() is not None:
return return
if os.name == "nt": if os.name == "nt":
@@ -298,10 +387,20 @@ def main() -> int:
failure: Exception | None = None failure: Exception | None = None
try: try:
if args.reuse_running:
reuse_running_services(services, args.check_timeout, run_id)
for service in services: for service in services:
start_service(service, log_dir) if not service.reused:
wait_for_services(services, args.timeout, args.interval, run_id) start_service(service, log_dir)
run_extra_checks(checks, args.check_timeout, run_id) pending_services = [service for service in services if not service.reused]
wait_for_services_and_checks(
pending_services,
checks,
args.timeout,
args.interval,
args.check_timeout,
run_id,
)
print(f"全栈验证通过FULLSTACK_VERIFY=PASS任务 ID{run_id}") print(f"全栈验证通过FULLSTACK_VERIFY=PASS任务 ID{run_id}")
return 0 return 0
except (OSError, RuntimeError, ValueError) as error: except (OSError, RuntimeError, ValueError) as error: