feat: surface integration deployment locations
This commit is contained in:
@@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
## 2026-04-06
|
## 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 真实运行模式
|
### 工作台依赖健康现在会显示 ASR 真实运行模式
|
||||||
|
|
||||||
- `collector` 的 `/v2/integrations/health` 现在会带出 ASR 的 `language_mode / runtime_device_mode / runtime_compute_type_mode / active_device / active_compute_type / model_name`。
|
- `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"
|
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]:
|
def disk_usage_payload(path: Path) -> dict[str, Any]:
|
||||||
probe = path if path.exists() else path.parent
|
probe = path if path.exists() else path.parent
|
||||||
try:
|
try:
|
||||||
@@ -3301,6 +3320,7 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
|
|||||||
return {
|
return {
|
||||||
"local_model": {
|
"local_model": {
|
||||||
"base_url": LOCAL_OPENAI_BASE_URL,
|
"base_url": LOCAL_OPENAI_BASE_URL,
|
||||||
|
**integration_deployment_payload("local_model", LOCAL_OPENAI_BASE_URL),
|
||||||
**probe_http(LOCAL_OPENAI_BASE_URL, "/models"),
|
**probe_http(LOCAL_OPENAI_BASE_URL, "/models"),
|
||||||
},
|
},
|
||||||
"cutvideo": {
|
"cutvideo": {
|
||||||
@@ -3311,17 +3331,21 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
|
|||||||
"upload_error": cutvideo_uploads.get("error", ""),
|
"upload_error": cutvideo_uploads.get("error", ""),
|
||||||
"upload_url": cutvideo_uploads.get("url", ""),
|
"upload_url": cutvideo_uploads.get("url", ""),
|
||||||
"route_mode": cutvideo_route_mode(CUTVIDEO_BASE_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": {
|
"huobao": {
|
||||||
"base_url": HUOBAO_BASE_URL,
|
"base_url": HUOBAO_BASE_URL,
|
||||||
|
**integration_deployment_payload("huobao", HUOBAO_BASE_URL),
|
||||||
**probe_http(HUOBAO_BASE_URL, "/health"),
|
**probe_http(HUOBAO_BASE_URL, "/health"),
|
||||||
},
|
},
|
||||||
"n8n": {
|
"n8n": {
|
||||||
"base_url": N8N_BASE_URL,
|
"base_url": N8N_BASE_URL,
|
||||||
|
**integration_deployment_payload("n8n", N8N_BASE_URL),
|
||||||
**probe_http(N8N_BASE_URL, "/healthz"),
|
**probe_http(N8N_BASE_URL, "/healthz"),
|
||||||
},
|
},
|
||||||
"asr": {
|
"asr": {
|
||||||
"base_url": ASR_HTTP_BASE_URL,
|
"base_url": ASR_HTTP_BASE_URL,
|
||||||
|
**integration_deployment_payload("asr", ASR_HTTP_BASE_URL),
|
||||||
"configured": asr_probe.get("configured", False),
|
"configured": asr_probe.get("configured", False),
|
||||||
"reachable": asr_probe.get("reachable", False),
|
"reachable": asr_probe.get("reachable", False),
|
||||||
"status_code": int(asr_probe.get("status_code") or 0),
|
"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": {
|
"live_recorder": {
|
||||||
"base_url": LIVE_RECORDER_BASE_URL,
|
"base_url": LIVE_RECORDER_BASE_URL,
|
||||||
|
**integration_deployment_payload("live_recorder", LIVE_RECORDER_BASE_URL),
|
||||||
**probe_http(LIVE_RECORDER_BASE_URL, "/api/healthz"),
|
**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"]["language_mode"], "auto")
|
||||||
self.assertEqual(payload["asr"]["model_name"], "base")
|
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:
|
def test_collector_deploy_script_exposes_health_retry_controls(self) -> None:
|
||||||
script_path = ROOT / "scripts" / "deploy_fnos_storyforge_collector.sh"
|
script_path = ROOT / "scripts" / "deploy_fnos_storyforge_collector.sh"
|
||||||
content = script_path.read_text(encoding="utf-8")
|
content = script_path.read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -3929,6 +3929,8 @@ function getIntegrationDetail(key) {
|
|||||||
url: String(raw?.url || raw?.base_url || ""),
|
url: String(raw?.url || raw?.base_url || ""),
|
||||||
baseUrl: String(raw?.base_url || ""),
|
baseUrl: String(raw?.base_url || ""),
|
||||||
routeMode: String(raw?.route_mode || ""),
|
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,
|
supportsUploads: raw?.supports_uploads !== undefined ? Boolean(raw?.supports_uploads) : true,
|
||||||
uploadStatusCode: Number(raw?.upload_status_code || 0),
|
uploadStatusCode: Number(raw?.upload_status_code || 0),
|
||||||
uploadError: String(raw?.upload_error || ""),
|
uploadError: String(raw?.upload_error || ""),
|
||||||
@@ -4093,10 +4095,12 @@ function getIntegrationCards() {
|
|||||||
const runtimeBadge = getAsrRuntimeBadge(detail) || "待热身";
|
const runtimeBadge = getAsrRuntimeBadge(detail) || "待热身";
|
||||||
const computeLabel = detail.activeComputeType || detail.runtimeComputeTypeMode || "auto";
|
const computeLabel = detail.activeComputeType || detail.runtimeComputeTypeMode || "auto";
|
||||||
const languageLabel = detail.languageMode || "auto";
|
const languageLabel = detail.languageMode || "auto";
|
||||||
extra = `当前转写:${runtimeBadge} · ${computeLabel} · 语言 ${languageLabel}`;
|
extra = `部署:${detail.deploymentLabel || "待确认"} · 当前转写:${runtimeBadge} · ${computeLabel} · 语言 ${languageLabel}`;
|
||||||
if (detail.modelName) {
|
if (detail.modelName) {
|
||||||
extra += ` · 当前模型:${detail.modelName}`;
|
extra += ` · 当前模型:${detail.modelName}`;
|
||||||
}
|
}
|
||||||
|
} else if (detail.deploymentLabel) {
|
||||||
|
extra = `部署:${detail.deploymentLabel}`;
|
||||||
}
|
}
|
||||||
if (detail.available && !detail.configured && isSuperAdmin()) {
|
if (detail.available && !detail.configured && isSuperAdmin()) {
|
||||||
actions = [
|
actions = [
|
||||||
|
|||||||
@@ -1489,8 +1489,11 @@ test("integration cards surface ASR runtime mode and model details", () => {
|
|||||||
assert.match(detailSource, /activeComputeType:/);
|
assert.match(detailSource, /activeComputeType:/);
|
||||||
assert.match(detailSource, /languageMode:/);
|
assert.match(detailSource, /languageMode:/);
|
||||||
assert.match(detailSource, /modelName:/);
|
assert.match(detailSource, /modelName:/);
|
||||||
|
assert.match(detailSource, /deploymentScope:/);
|
||||||
|
assert.match(detailSource, /deploymentLabel:/);
|
||||||
assert.match(statusSource, /detail\.key === "asr"/);
|
assert.match(statusSource, /detail\.key === "asr"/);
|
||||||
assert.match(statusSource, /在线 ·/);
|
assert.match(statusSource, /在线 ·/);
|
||||||
|
assert.match(cardsSource, /部署:/);
|
||||||
assert.match(cardsSource, /当前转写:/);
|
assert.match(cardsSource, /当前转写:/);
|
||||||
assert.match(cardsSource, /当前模型:/);
|
assert.match(cardsSource, /当前模型:/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user