197 lines
8.9 KiB
Python
197 lines
8.9 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
from fastapi import Request
|
|
from fastapi import HTTPException
|
|
|
|
|
|
PUBLIC_API_BASE_URL = os.getenv("PUBLIC_API_BASE_URL", "").strip().rstrip("/")
|
|
VALID_CLIENT_TOKEN = os.getenv("CLIENT_API_TOKEN")
|
|
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
|
|
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64").strip() or "x64"
|
|
|
|
|
|
def platform_executable_path(value: str, fallback_base_name: str) -> str:
|
|
path = str(value or "").strip().replace(chr(92), "/").strip("/")
|
|
if not path:
|
|
path = fallback_base_name
|
|
parts = path.split("/")
|
|
leaf = parts[-1]
|
|
if TARGET_PLATFORM == "windows":
|
|
if "." not in leaf:
|
|
leaf += ".exe"
|
|
elif TARGET_PLATFORM == "linux":
|
|
if leaf.casefold().endswith(".exe"):
|
|
leaf = leaf[:-4]
|
|
parts[-1] = leaf
|
|
return "/".join(parts)
|
|
|
|
|
|
RELEASE_MAIN_EXECUTABLE = platform_executable_path(
|
|
os.getenv("RELEASE_MAIN_EXECUTABLE", ""),
|
|
"bin/SimCAE",
|
|
)
|
|
PUBLISH_MAX_REQUEST_BYTES = int(os.getenv("PUBLISH_MAX_REQUEST_MB", "4096")) * 1024 * 1024
|
|
PUBLISH_MAX_FILES = int(os.getenv("PUBLISH_MAX_FILES", "20000"))
|
|
PUBLISH_MAX_FIELDS = int(os.getenv("PUBLISH_MAX_FIELDS", str(PUBLISH_MAX_FILES + 100)))
|
|
|
|
|
|
def public_api_base_url(request: Request) -> str:
|
|
if PUBLIC_API_BASE_URL:
|
|
return PUBLIC_API_BASE_URL
|
|
forwarded_proto = request.headers.get("x-forwarded-proto", "").split(",")[0].strip()
|
|
forwarded_host = request.headers.get("x-forwarded-host", "").split(",")[0].strip()
|
|
scheme = forwarded_proto or request.url.scheme
|
|
host = forwarded_host or request.headers.get("host", "")
|
|
if host:
|
|
return f"{scheme}://{host}".rstrip("/")
|
|
return str(request.base_url).rstrip("/")
|
|
|
|
|
|
def default_client_main_executable() -> str:
|
|
path = RELEASE_MAIN_EXECUTABLE.strip().replace(chr(92), "/").strip("/")
|
|
if path.casefold().startswith("bin/"):
|
|
path = path[4:]
|
|
return platform_executable_path(path, "SimCAE")
|
|
|
|
|
|
def default_client_config_values(request: Request) -> dict:
|
|
return {
|
|
"client_token": VALID_CLIENT_TOKEN or "",
|
|
"launch_token": os.getenv("CLIENT_LAUNCH_TOKEN", "SimCAE_Launch_Token_2026_ChangeMe_32Bytes"),
|
|
"request_timeout_ms": os.getenv("CLIENT_REQUEST_TIMEOUT_MS", "5000"),
|
|
"temp_folder": os.getenv("CLIENT_TEMP_FOLDER", "update_temp"),
|
|
"install_root": os.getenv("CLIENT_INSTALL_ROOT", ".."),
|
|
"main_executable": platform_executable_path(
|
|
os.getenv("CLIENT_MAIN_EXECUTABLE", ""),
|
|
default_client_main_executable(),
|
|
),
|
|
"launcher_executable": platform_executable_path(os.getenv("CLIENT_LAUNCHER_EXECUTABLE", ""), "Launcher"),
|
|
"updater_executable": platform_executable_path(os.getenv("CLIENT_UPDATER_EXECUTABLE", ""), "Updater"),
|
|
"bootstrap_executable": platform_executable_path(os.getenv("CLIENT_BOOTSTRAP_EXECUTABLE", ""), "Bootstrap"),
|
|
"health_check_timeout_ms": os.getenv("CLIENT_HEALTH_CHECK_TIMEOUT_MS", "15000"),
|
|
"platform": TARGET_PLATFORM,
|
|
"arch": TARGET_ARCH,
|
|
}
|
|
|
|
|
|
def crash_report_test_values(api_base_url: str) -> dict:
|
|
base_url = api_base_url.rstrip("/")
|
|
crash_admin_token = os.getenv("CRASH_ADMIN_TOKEN", "").strip()
|
|
admin_token = os.getenv("ADMIN_TOKEN", "").strip()
|
|
crash_report_token = (
|
|
os.getenv("CRASH_REPORT_TOKEN")
|
|
or os.getenv("SIMCAE_CRASH_CLIENT_TOKEN")
|
|
or os.getenv("CLIENT_API_TOKEN")
|
|
or ""
|
|
)
|
|
crash_symbol_token = os.getenv("CRASH_SYMBOL_TOKEN") or os.getenv("SIMCAE_SYMBOL_TOKEN") or ""
|
|
effective_admin_token = crash_admin_token or admin_token
|
|
effective_admin_token_source = "CRASH_ADMIN_TOKEN" if crash_admin_token else "ADMIN_TOKEN"
|
|
return {
|
|
"说明": "仅用于后台管理员/测试人员联调崩溃报告接口,不要复制进客户端 app_config.json。",
|
|
"server_base_url": base_url,
|
|
"tokens": {
|
|
"CRASH_REPORT_TOKEN": {
|
|
"用途": "上传崩溃报告 /api/v1/crash-reports",
|
|
"token": crash_report_token,
|
|
},
|
|
"CRASH_SYMBOL_TOKEN": {
|
|
"用途": "上传符号包 /api/v1/symbols",
|
|
"token": crash_symbol_token,
|
|
},
|
|
"CRASH_ADMIN_TOKEN": {
|
|
"用途": "查询和下载崩溃报告",
|
|
"token": crash_admin_token,
|
|
"说明": "如果为空,查询/下载接口会使用 ADMIN_TOKEN。",
|
|
},
|
|
"ADMIN_TOKEN": {
|
|
"用途": "管理后台令牌;当 CRASH_ADMIN_TOKEN 为空时也用于查询/下载崩溃报告",
|
|
"token": admin_token,
|
|
},
|
|
},
|
|
"upload_crash_report": {
|
|
"url": f"{base_url}/api/v1/crash-reports",
|
|
"authorization_header": "Authorization: Bearer <CRASH_REPORT_TOKEN>",
|
|
"token_env": "CRASH_REPORT_TOKEN",
|
|
"token": crash_report_token,
|
|
"idempotency_key_rule": "Idempotency-Key 必须等于 metadata.clientReportId",
|
|
},
|
|
"upload_symbols": {
|
|
"url": f"{base_url}/api/v1/symbols",
|
|
"authorization_header": "Authorization: Bearer <CRASH_SYMBOL_TOKEN>",
|
|
"token_env": "CRASH_SYMBOL_TOKEN",
|
|
"token": crash_symbol_token,
|
|
},
|
|
"admin_query_and_download": {
|
|
"detail_url_template": f"{base_url}/api/v1/crash-reports/{{report_id}}",
|
|
"file_url_template": f"{base_url}/api/v1/crash-reports/{{report_id}}/files/{{file_name}}",
|
|
"authorization_header": f"Authorization: Bearer <{effective_admin_token_source}>",
|
|
"token_env": effective_admin_token_source,
|
|
"token": effective_admin_token,
|
|
"file_name_examples": ["metadata.json", "crash.dmp", "attachments.zip", "server.json"],
|
|
},
|
|
"limits": {
|
|
"CRASH_SERVICE_VERSION": os.getenv("CRASH_SERVICE_VERSION", "1.0.0"),
|
|
"CRASH_METADATA_MAX_KB": os.getenv("CRASH_METADATA_MAX_KB", "256"),
|
|
"CRASH_MINIDUMP_MAX_MB": os.getenv("CRASH_MINIDUMP_MAX_MB", "128"),
|
|
"CRASH_ATTACHMENTS_MAX_MB": os.getenv("CRASH_ATTACHMENTS_MAX_MB", "64"),
|
|
"CRASH_REQUEST_MAX_MB": os.getenv("CRASH_REQUEST_MAX_MB", "200"),
|
|
"CRASH_SYMBOLS_MAX_MB": os.getenv("CRASH_SYMBOLS_MAX_MB", "512"),
|
|
},
|
|
}
|
|
|
|
|
|
def crash_report_test_text(api_base_url: str) -> str:
|
|
values = crash_report_test_values(api_base_url)
|
|
tokens = values["tokens"]
|
|
lines = [
|
|
"# 说明:下面内容仅用于后台管理员/测试人员联调崩溃报告接口。",
|
|
"# 注意:不要复制进客户端 app_config.json,也不要放进客户端配置文件。",
|
|
"",
|
|
"# 服务端基础地址。curl 示例里的 SERVER 就填这个值。",
|
|
f"SERVER_BASE_URL={values['server_base_url']}",
|
|
"",
|
|
"# 上传崩溃报告使用的 Token。",
|
|
"# 用途:POST /api/v1/crash-reports",
|
|
"# 请求头写法:Authorization: Bearer <CRASH_REPORT_TOKEN>",
|
|
f"CRASH_REPORT_TOKEN={tokens['CRASH_REPORT_TOKEN']['token']}",
|
|
f"CRASH_REPORT_URL={values['upload_crash_report']['url']}",
|
|
"",
|
|
"# 上传符号包使用的 Token。",
|
|
"# 用途:POST /api/v1/symbols",
|
|
"# 请求头写法:Authorization: Bearer <CRASH_SYMBOL_TOKEN>",
|
|
f"CRASH_SYMBOL_TOKEN={tokens['CRASH_SYMBOL_TOKEN']['token']}",
|
|
f"CRASH_SYMBOL_URL={values['upload_symbols']['url']}",
|
|
"",
|
|
"# 查询和下载崩溃报告使用的 Token。",
|
|
"# 如果 CRASH_ADMIN_TOKEN 为空,就使用 ADMIN_TOKEN。",
|
|
"# 请求头写法:Authorization: Bearer <CRASH_ADMIN_TOKEN 或 ADMIN_TOKEN>",
|
|
f"CRASH_ADMIN_TOKEN={tokens['CRASH_ADMIN_TOKEN']['token']}",
|
|
f"ADMIN_TOKEN={tokens['ADMIN_TOKEN']['token']}",
|
|
f"CRASH_REPORT_DETAIL_URL_TEMPLATE={values['admin_query_and_download']['detail_url_template']}",
|
|
f"CRASH_REPORT_FILE_URL_TEMPLATE={values['admin_query_and_download']['file_url_template']}",
|
|
"",
|
|
"# 可下载文件名。",
|
|
"CRASH_REPORT_FILE_NAMES=metadata.json,crash.dmp,attachments.zip,server.json",
|
|
"",
|
|
"# 幂等规则:Idempotency-Key 必须等于 metadata.json 里的 clientReportId。",
|
|
"CRASH_IDEMPOTENCY_KEY_RULE=Idempotency-Key must equal metadata.clientReportId",
|
|
"",
|
|
"# 崩溃报告服务版本和大小限制。",
|
|
]
|
|
for key, value in values["limits"].items():
|
|
lines.append(f"{key}={value}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def normalize_executable_name(value: str) -> str:
|
|
normalized = str(value or "").strip().replace(chr(92), "/").strip("/")
|
|
if not normalized:
|
|
return ""
|
|
if normalized.startswith("/") or ":" in normalized or any(part in ("", ".", "..") for part in Path(normalized).parts):
|
|
raise HTTPException(status_code=400, detail="主程序文件名必须是安全的相对路径")
|
|
if not normalized.casefold().endswith(".exe"):
|
|
normalized += ".exe"
|
|
return normalized
|