feat: surface integration deployment locations
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
|
||||
## 2026-04-06
|
||||
|
||||
### 依赖健康卡开始显示服务部署位置
|
||||
|
||||
- `collector` 的 `/v2/integrations/health` 现在会统一带出 `deployment_scope / deployment_label`,明确说明依赖当前跑在 `服务器 / NAS / Windows / NAS 隧道 / 未启用` 哪一侧。
|
||||
- 工作台里的依赖健康卡已经开始展示 `部署:服务器`、`部署:Windows` 这类信息,和 `ASR 在线 · GPU` 一起出现,后续迁服务时不需要再靠命令行手查。
|
||||
- 当前这套口径已经覆盖 `n8n / huobao / asr / cutvideo / live_recorder / local_model`。
|
||||
|
||||
### 工作台依赖健康现在会显示 ASR 真实运行模式
|
||||
|
||||
- `collector` 的 `/v2/integrations/health` 现在会带出 ASR 的 `language_mode / runtime_device_mode / runtime_compute_type_mode / active_device / active_compute_type / model_name`。
|
||||
|
||||
@@ -947,6 +947,25 @@ def cutvideo_route_mode(base_url: str) -> str:
|
||||
return "direct"
|
||||
|
||||
|
||||
def integration_deployment_payload(key: str, base_url: str, *, route_mode: str = "") -> dict[str, str]:
|
||||
normalized = (base_url or "").strip()
|
||||
if not normalized:
|
||||
return {"deployment_scope": "not_configured", "deployment_label": "未配置"}
|
||||
if key == "local_model":
|
||||
return {"deployment_scope": "disabled", "deployment_label": "未启用"}
|
||||
if key == "asr":
|
||||
return {"deployment_scope": "windows", "deployment_label": "Windows"}
|
||||
if key == "live_recorder":
|
||||
return {"deployment_scope": "nas", "deployment_label": "NAS"}
|
||||
if key == "cutvideo":
|
||||
if route_mode == "fnos_tunnel":
|
||||
return {"deployment_scope": "nas_tunnel", "deployment_label": "NAS 隧道"}
|
||||
return {"deployment_scope": "windows", "deployment_label": "Windows"}
|
||||
if key in {"n8n", "huobao"}:
|
||||
return {"deployment_scope": "server", "deployment_label": "服务器"}
|
||||
return {"deployment_scope": "external", "deployment_label": "外部服务"}
|
||||
|
||||
|
||||
def disk_usage_payload(path: Path) -> dict[str, Any]:
|
||||
probe = path if path.exists() else path.parent
|
||||
try:
|
||||
@@ -3301,6 +3320,7 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
|
||||
return {
|
||||
"local_model": {
|
||||
"base_url": LOCAL_OPENAI_BASE_URL,
|
||||
**integration_deployment_payload("local_model", LOCAL_OPENAI_BASE_URL),
|
||||
**probe_http(LOCAL_OPENAI_BASE_URL, "/models"),
|
||||
},
|
||||
"cutvideo": {
|
||||
@@ -3311,17 +3331,21 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
|
||||
"upload_error": cutvideo_uploads.get("error", ""),
|
||||
"upload_url": cutvideo_uploads.get("url", ""),
|
||||
"route_mode": cutvideo_route_mode(CUTVIDEO_BASE_URL),
|
||||
**integration_deployment_payload("cutvideo", CUTVIDEO_BASE_URL, route_mode=cutvideo_route_mode(CUTVIDEO_BASE_URL)),
|
||||
},
|
||||
"huobao": {
|
||||
"base_url": HUOBAO_BASE_URL,
|
||||
**integration_deployment_payload("huobao", HUOBAO_BASE_URL),
|
||||
**probe_http(HUOBAO_BASE_URL, "/health"),
|
||||
},
|
||||
"n8n": {
|
||||
"base_url": N8N_BASE_URL,
|
||||
**integration_deployment_payload("n8n", N8N_BASE_URL),
|
||||
**probe_http(N8N_BASE_URL, "/healthz"),
|
||||
},
|
||||
"asr": {
|
||||
"base_url": ASR_HTTP_BASE_URL,
|
||||
**integration_deployment_payload("asr", ASR_HTTP_BASE_URL),
|
||||
"configured": asr_probe.get("configured", False),
|
||||
"reachable": asr_probe.get("reachable", False),
|
||||
"status_code": int(asr_probe.get("status_code") or 0),
|
||||
@@ -3336,6 +3360,7 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
|
||||
},
|
||||
"live_recorder": {
|
||||
"base_url": LIVE_RECORDER_BASE_URL,
|
||||
**integration_deployment_payload("live_recorder", LIVE_RECORDER_BASE_URL),
|
||||
**probe_http(LIVE_RECORDER_BASE_URL, "/api/healthz"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -326,6 +326,71 @@ class ProductionBaselineTests(unittest.TestCase):
|
||||
self.assertEqual(payload["asr"]["language_mode"], "auto")
|
||||
self.assertEqual(payload["asr"]["model_name"], "base")
|
||||
|
||||
def test_integrations_health_exposes_deployment_labels(self) -> None:
|
||||
ctx = self._seed_context("deployment_labels", exhausted=False)
|
||||
headers = {"Authorization": f"Bearer {ctx['token']}"}
|
||||
original_n8n = self.core.N8N_BASE_URL
|
||||
original_huobao = self.core.HUOBAO_BASE_URL
|
||||
original_asr = self.core.ASR_HTTP_BASE_URL
|
||||
original_live_recorder = self.core.LIVE_RECORDER_BASE_URL
|
||||
original_cutvideo = self.core.CUTVIDEO_BASE_URL
|
||||
original_probe_http = self.core.probe_http
|
||||
original_probe_http_json = getattr(self.core, "probe_http_json", None)
|
||||
try:
|
||||
self.core.N8N_BASE_URL = "http://127.0.0.1:25670"
|
||||
self.core.HUOBAO_BASE_URL = "http://127.0.0.1:25678"
|
||||
self.core.ASR_HTTP_BASE_URL = "http://192.168.31.18:8088"
|
||||
self.core.LIVE_RECORDER_BASE_URL = "http://192.168.31.188:19106"
|
||||
self.core.CUTVIDEO_BASE_URL = "http://192.168.31.188:19186"
|
||||
|
||||
def fake_probe_http(url: str, path: str = "", timeout: float = 3.0) -> dict[str, Any]:
|
||||
return {
|
||||
"configured": True,
|
||||
"reachable": True,
|
||||
"status_code": 200,
|
||||
"error": "",
|
||||
"url": f"{url.rstrip('/')}/{path.lstrip('/')}" if path else url,
|
||||
}
|
||||
|
||||
def fake_probe_http_json(url: str, path: str = "", timeout: float = 3.0) -> dict[str, Any]:
|
||||
detail = fake_probe_http(url, path=path, timeout=timeout)
|
||||
detail["json"] = {
|
||||
"service": "storyforge-windows-asr",
|
||||
"model_name": "base",
|
||||
"language": "auto",
|
||||
"device": "auto",
|
||||
"compute_type": "auto",
|
||||
"active_device": "cuda",
|
||||
"active_compute_type": "int8_float16",
|
||||
}
|
||||
return detail
|
||||
|
||||
self.core.probe_http = fake_probe_http
|
||||
self.core.probe_http_json = fake_probe_http_json
|
||||
response = self.client.get("/v2/integrations/health", headers=headers)
|
||||
finally:
|
||||
self.core.N8N_BASE_URL = original_n8n
|
||||
self.core.HUOBAO_BASE_URL = original_huobao
|
||||
self.core.ASR_HTTP_BASE_URL = original_asr
|
||||
self.core.LIVE_RECORDER_BASE_URL = original_live_recorder
|
||||
self.core.CUTVIDEO_BASE_URL = original_cutvideo
|
||||
self.core.probe_http = original_probe_http
|
||||
if original_probe_http_json is None:
|
||||
try:
|
||||
delattr(self.core, "probe_http_json")
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.core.probe_http_json = original_probe_http_json
|
||||
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["n8n"]["deployment_label"], "服务器")
|
||||
self.assertEqual(payload["huobao"]["deployment_label"], "服务器")
|
||||
self.assertEqual(payload["asr"]["deployment_label"], "Windows")
|
||||
self.assertEqual(payload["live_recorder"]["deployment_label"], "NAS")
|
||||
self.assertEqual(payload["cutvideo"]["deployment_label"], "NAS 隧道")
|
||||
|
||||
def test_collector_deploy_script_exposes_health_retry_controls(self) -> None:
|
||||
script_path = ROOT / "scripts" / "deploy_fnos_storyforge_collector.sh"
|
||||
content = script_path.read_text(encoding="utf-8")
|
||||
|
||||
@@ -3929,6 +3929,8 @@ function getIntegrationDetail(key) {
|
||||
url: String(raw?.url || raw?.base_url || ""),
|
||||
baseUrl: String(raw?.base_url || ""),
|
||||
routeMode: String(raw?.route_mode || ""),
|
||||
deploymentScope: String(raw?.deployment_scope || ""),
|
||||
deploymentLabel: String(raw?.deployment_label || ""),
|
||||
supportsUploads: raw?.supports_uploads !== undefined ? Boolean(raw?.supports_uploads) : true,
|
||||
uploadStatusCode: Number(raw?.upload_status_code || 0),
|
||||
uploadError: String(raw?.upload_error || ""),
|
||||
@@ -4093,10 +4095,12 @@ function getIntegrationCards() {
|
||||
const runtimeBadge = getAsrRuntimeBadge(detail) || "待热身";
|
||||
const computeLabel = detail.activeComputeType || detail.runtimeComputeTypeMode || "auto";
|
||||
const languageLabel = detail.languageMode || "auto";
|
||||
extra = `当前转写:${runtimeBadge} · ${computeLabel} · 语言 ${languageLabel}`;
|
||||
extra = `部署:${detail.deploymentLabel || "待确认"} · 当前转写:${runtimeBadge} · ${computeLabel} · 语言 ${languageLabel}`;
|
||||
if (detail.modelName) {
|
||||
extra += ` · 当前模型:${detail.modelName}`;
|
||||
}
|
||||
} else if (detail.deploymentLabel) {
|
||||
extra = `部署:${detail.deploymentLabel}`;
|
||||
}
|
||||
if (detail.available && !detail.configured && isSuperAdmin()) {
|
||||
actions = [
|
||||
|
||||
@@ -1489,8 +1489,11 @@ test("integration cards surface ASR runtime mode and model details", () => {
|
||||
assert.match(detailSource, /activeComputeType:/);
|
||||
assert.match(detailSource, /languageMode:/);
|
||||
assert.match(detailSource, /modelName:/);
|
||||
assert.match(detailSource, /deploymentScope:/);
|
||||
assert.match(detailSource, /deploymentLabel:/);
|
||||
assert.match(statusSource, /detail\.key === "asr"/);
|
||||
assert.match(statusSource, /在线 ·/);
|
||||
assert.match(cardsSource, /部署:/);
|
||||
assert.match(cardsSource, /当前转写:/);
|
||||
assert.match(cardsSource, /当前模型:/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user