- Makefile:新增 验收数据库(生成→检查→库层全量,前置校验隔离库连接串)、数据库分片、浏览器测试三个入口; 格式写入 末尾就地重新生成;数据库测试 与 验收数据库 统一排除 浏览器/网络;快检 不再静默跳过类型门; 前端旅程 预检补齐四个必需变量;pytest 目标改用仓内解释器(uv run 在嵌套检出会解析到外层环境)。 - 工具/验证锁.py:验证会话持共享锁,写入口用 --执行 在独占锁内落盘,生成/格式写入/索引生成走同一闸门。 - 工具/并行数据库测试.py:按文件分片并行,缺连接串在采集前拒绝。 - 工具/构建编排.py、环境预检.py、构建资源包.py、维护索引.py 与上述口径对齐。 - .gitignore / CI / README / AGENTS:忽略构建产物、CI 与 Makefile 单一口径、README 按实际实现陈述、AGENTS 补日常入口与验证纪律。
159 lines
6.7 KiB
Python
159 lines
6.7 KiB
Python
"""核对已安装资源、显式配置和本地宿主;不会发起模型调用。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
from muse.共享.错误 import Muse错误, 环境缺失错误, 资源缺失错误, 配置错误
|
||
from muse.启动 import 构建
|
||
from muse.资源加载 import 核对资源, 读取能力目录
|
||
from muse.配置 import 读取配置
|
||
|
||
|
||
def 安全失败(动作: str, 错误: Exception) -> dict:
|
||
"""输出固定原因,不展示异常文本、命令、stdout/stderr 或配置上下文。"""
|
||
可重试 = False
|
||
详情 = {}
|
||
if isinstance(错误, subprocess.TimeoutExpired):
|
||
类别, 说明, 可重试 = "TIMEOUT", "所选检查超过时限", True
|
||
elif isinstance(错误, subprocess.CalledProcessError):
|
||
类别, 说明 = "PROCESS_FAILED", "所选检查进程非零退出"
|
||
if isinstance(错误.returncode, int):
|
||
详情 = {"exit_code": 错误.returncode}
|
||
elif isinstance(错误, PermissionError):
|
||
类别, 说明 = "PERMISSION_DENIED", "所选检查没有所需文件或执行权限"
|
||
elif isinstance(错误, FileNotFoundError):
|
||
类别, 说明 = "NOT_FOUND", "所选检查需要的文件或可执行程序不存在"
|
||
elif isinstance(错误, 资源缺失错误):
|
||
类别, 说明 = "RESOURCE_INVALID", "安装资源缺失或完整性核对失败"
|
||
elif isinstance(错误, 环境缺失错误):
|
||
类别, 说明 = "ENVIRONMENT_MISSING", "所选检查需要的环境未就绪"
|
||
elif isinstance(错误, 配置错误):
|
||
类别, 说明 = "CONFIG_INVALID", "所选检查的显式配置或版本要求不匹配"
|
||
elif isinstance(错误, Muse错误) and 错误.错误码 == "MUSE_DB_CONNECTION":
|
||
类别, 说明, 可重试 = "CONNECTION_FAILED", "数据库连接或会话初始化失败", True
|
||
elif isinstance(错误, Muse错误):
|
||
类别, 说明 = "REJECTED", "所选检查被运行合同拒绝"
|
||
elif isinstance(错误, (ValueError, KeyError, TypeError)):
|
||
类别, 说明 = "INVALID_DATA", "所选检查的数据格式、字段或版本无效"
|
||
elif isinstance(错误, OSError):
|
||
类别, 说明 = "IO_FAILED", "所选检查的文件、执行或连接操作失败"
|
||
else:
|
||
类别, 说明 = "INTERNAL_ERROR", "所选检查遇到未预期错误"
|
||
return {
|
||
"code": f"PREFLIGHT_{动作}_{类别}",
|
||
"action": 动作,
|
||
"message": 说明,
|
||
"retryable": 可重试,
|
||
"details": 详情,
|
||
}
|
||
|
||
|
||
class 预检失败(Exception):
|
||
def __init__(self, 报告: dict) -> None:
|
||
self.报告 = 报告
|
||
super().__init__(报告["message"])
|
||
|
||
|
||
def 检查Pi(node: Path, cli: Path, 预期版本: str) -> dict:
|
||
动作 = "HOST_NODE"
|
||
try:
|
||
if not node.is_file():
|
||
raise FileNotFoundError
|
||
结果 = subprocess.run(
|
||
[str(node), "--version"], capture_output=True, text=True, timeout=10, check=True
|
||
)
|
||
node_version = 结果.stdout.strip().removeprefix("v")
|
||
parts = tuple(int(v) for v in node_version.split("."))
|
||
if len(parts) != 3 or any(v < 0 for v in parts):
|
||
raise ValueError
|
||
if parts < (22, 19, 0):
|
||
raise 配置错误("Node版本未达到已声明的宿主要求")
|
||
动作 = "HOST_PI"
|
||
if not cli.is_file():
|
||
raise FileNotFoundError
|
||
with tempfile.TemporaryDirectory(prefix="muse-pi-preflight-") as 临时:
|
||
环境 = {
|
||
**os.environ,
|
||
"PI_CODING_AGENT_DIR": 临时,
|
||
"PI_OFFLINE": "1",
|
||
"PI_TELEMETRY": "0",
|
||
}
|
||
结果 = subprocess.run(
|
||
[str(node), str(cli), "--version"],
|
||
cwd=临时,
|
||
env=环境,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=15,
|
||
check=True,
|
||
)
|
||
实际 = 结果.stdout.strip()
|
||
if 实际 != 预期版本:
|
||
raise 配置错误("Pi实际版本与显式要求不符")
|
||
return {"node_version": node_version, "pi_version": 实际, "state": "version_verified"}
|
||
except Exception as 错误:
|
||
raise 预检失败(安全失败(动作, 错误)) from None
|
||
|
||
|
||
def 主() -> int:
|
||
解析 = argparse.ArgumentParser(description="检查固定资源和明确选择的运行依赖,不调用模型")
|
||
解析.add_argument("--配置", type=Path)
|
||
解析.add_argument("--数据库", action="store_true", help="明确执行所给配置的数据库连接检查")
|
||
解析.add_argument("--node", type=Path)
|
||
解析.add_argument("--pi", type=Path)
|
||
解析.add_argument("--pi-version")
|
||
参数 = 解析.parse_args()
|
||
if 参数.数据库 and 参数.配置 is None:
|
||
解析.error("数据库检查需要明确配置")
|
||
if any((参数.node, 参数.pi, 参数.pi_version)) and not all(
|
||
(参数.node, 参数.pi, 参数.pi_version)
|
||
):
|
||
解析.error("Pi检查必须同时给出node、pi和pi-version")
|
||
动作 = "RESOURCE"
|
||
try:
|
||
清单 = 核对资源()
|
||
能力 = 读取能力目录()
|
||
报告 = {
|
||
"resource_build_id": 清单["构建身份"],
|
||
"resource_label": 清单["发布身份"],
|
||
"package_version": 清单["代码版本"],
|
||
"resources_verified": len(清单["资源"]),
|
||
"roles": sorted(能力["role"]),
|
||
"operations": sorted(能力["operation"]),
|
||
"database": "not_checked",
|
||
"host": {"state": "not_checked"},
|
||
"model_calls": 0,
|
||
}
|
||
if 参数.配置:
|
||
动作 = "CONFIG"
|
||
配置 = 读取配置(参数.配置)
|
||
if 配置.资源发布身份 != 清单["构建身份"]:
|
||
raise 配置错误("配置必须固定当前资源构建身份;先无配置运行预检取得身份")
|
||
if 参数.数据库:
|
||
动作 = "DATABASE"
|
||
# 常驻连接池只在装配生命周期内打开;不进生命周期会以「连接池未就绪」必然失败。
|
||
装配 = 构建(配置)
|
||
with 装配.生命周期():
|
||
装配.要求数据库().检查连接()
|
||
报告["database"] = "connection_verified"
|
||
if 参数.node:
|
||
动作 = "HOST"
|
||
报告["host"] = 检查Pi(参数.node.resolve(), 参数.pi.resolve(), 参数.pi_version)
|
||
print(json.dumps(报告, ensure_ascii=False, indent=2))
|
||
return 0
|
||
except 预检失败 as 错误:
|
||
print(json.dumps(错误.报告, ensure_ascii=False))
|
||
except Exception as 错误:
|
||
print(json.dumps(安全失败(动作, 错误), ensure_ascii=False))
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(主())
|