feat: add deepseek-harness workbench plugin and console diagnosis API

- console: GET /api/workflows/{id}, /runs/{run_id}, /runs/{run_id}/diagnosis
  (journal trace + sanitized bounded log tails, workflow/run pairing enforced)
- workbench/: dsh 宿主插件(7 个工作流工具、中文系统提示词、失败监控器、
  本机回环桥接服务)+ 侧边栏面板客户端包(sidebar.footer.action 与
  shell.overlay 追加插槽)+ 降级独立面板 + 一键启动脚本
- adapters/browser: 收敛 looks_like_login_url 到共享层,修复
  jd_main_image_collector 对 gyxx_flow.accounts 的越层导入
- tests: 新端点覆盖;replay_policy 断言对齐已迁移的 catalog(repeatable)
This commit is contained in:
2026-09-04 11:10:16 +08:00
parent b124d757b0
commit 01218b2907
26 changed files with 4347 additions and 8 deletions
+147 -1
View File
@@ -546,7 +546,7 @@ def test_overview_tracks_the_current_complete_catalog_without_leaking_paths(
assert failed["last_run"]["mode"] == "execute"
assert failed["last_run"]["error"] == "critical steps failed: collect"
assert all(step["name"] and step["description"] for step in failed["steps"])
assert {step["replay_policy"] for step in failed["steps"]} == {"guarded"}
assert {step["replay_policy"] for step in failed["steps"]} == {"repeatable"}
notes_master = next(
item
for item in initial["workflows"]
@@ -3271,3 +3271,149 @@ def test_scheduler_reload_catalog_runs_before_each_tick(tmp_path: Path) -> None:
assert scheduler.tick(now) == []
assert scheduler.catalog is disabled_catalog
assert load_calls == [due_catalog, disabled_catalog]
def test_workflow_detail_returns_a_single_workflow(
console_settings: Settings,
) -> None:
service = WorkflowConsoleService(
console_settings,
launcher=FakeConsoleLauncher(),
)
detail = service.workflow_detail("content.metrics.daily")
assert detail["workflow"]["id"] == "content.metrics.daily"
assert detail["workflow"]["module"] == "content_marketing"
with pytest.raises(ConsoleNotFoundError):
service.workflow_detail("content.unknown")
def test_run_detail_returns_journal_steps(
console_settings: Settings,
) -> None:
run_id = _index_parallel_main_image_run(console_settings)
service = WorkflowConsoleService(
console_settings,
launcher=FakeConsoleLauncher(),
)
detail = service.run_detail("product.main_image.weekly", run_id)
assert detail["workflow_id"] == "product.main_image.weekly"
assert detail["run"]["run_id"] == run_id
assert detail["run"]["status"] == "failed"
assert [step["id"] for step in detail["run"]["steps"]] == [
"jd.attempt-1",
"tmall.attempt-1",
]
with pytest.raises(ConsoleNotFoundError):
service.run_detail("content.metrics.daily", run_id)
with pytest.raises(ConsoleNotFoundError):
service.run_detail("product.main_image.weekly", "missing-run")
with pytest.raises(ConsoleNotFoundError):
service.run_detail("product.main_image.weekly", "../escape")
def test_run_diagnosis_bundles_trace_and_sanitized_log_tails(
console_settings: Settings,
) -> None:
run_id = _index_parallel_main_image_run(console_settings)
layout = DataLayout(console_settings.data_root)
log_dir = layout.log_dir("product.main_image.weekly", "2026-08-02")
secret = "hunter" + "2"
(log_dir / "jd.attempt-1.log").write_text(
"采集开始\npass" + "word=" + secret + "\n浏览器崩溃\n",
encoding="utf-8",
)
(log_dir / "empty.log").write_text("", encoding="utf-8")
service = WorkflowConsoleService(
console_settings,
launcher=FakeConsoleLauncher(),
)
diagnosis = service.run_diagnosis("product.main_image.weekly", run_id)
assert diagnosis["workflow_id"] == "product.main_image.weekly"
assert diagnosis["run"]["run_id"] == run_id
journal = diagnosis["journal"]
assert journal["path"].endswith("/run.json")
assert journal["trace"]["paths"]["log"].startswith("logs/")
assert len(diagnosis["logs"]) == 1
tail = diagnosis["logs"][0]["tail"]
assert diagnosis["logs"][0]["path"].endswith("jd.attempt-1.log")
assert "浏览器崩溃" in tail
assert secret not in tail
assert "[REDACTED]" in tail
def test_run_diagnosis_handles_a_missing_journal(
console_settings: Settings,
) -> None:
context = RunContext.create(
"product.main_image.weekly",
"2026-08-02",
now=datetime(2026, 8, 2, 3, 4, 5, tzinfo=timezone.utc),
random_suffix="broken1",
)
journal = RunJournal.create(DataLayout(console_settings.data_root), context)
journal.finalize("failed", error="launch failed")
RunIndex(console_settings.data_root).index_journal(journal)
journal.path.unlink()
service = WorkflowConsoleService(
console_settings,
launcher=FakeConsoleLauncher(),
)
diagnosis = service.run_diagnosis("product.main_image.weekly", context.run_id)
assert diagnosis["journal"] == {"path": None, "mode": None, "trace": None}
assert diagnosis["logs"] == []
assert diagnosis["run"]["status"] == "failed"
def test_http_workflow_detail_and_diagnosis_routes(
console_settings: Settings,
) -> None:
run_id = _index_parallel_main_image_run(console_settings)
with _running_server(console_settings, FakeConsoleLauncher()) as server:
status, headers, content = _http_request(
server, "GET", "/api/workflows/product.main_image.weekly"
)
assert status == HTTPStatus.OK
_assert_security_headers(headers)
payload = json.loads(content.decode("utf-8"))
assert payload["workflow"]["id"] == "product.main_image.weekly"
status, _, _ = _http_request(server, "GET", "/api/workflows/unknown")
assert status == HTTPStatus.NOT_FOUND
status, headers, content = _http_request(
server,
"GET",
f"/api/workflows/product.main_image.weekly/runs/{run_id}",
)
assert status == HTTPStatus.OK
_assert_security_headers(headers)
payload = json.loads(content.decode("utf-8"))
assert payload["run"]["run_id"] == run_id
status, _, content = _http_request(
server,
"GET",
f"/api/workflows/product.main_image.weekly/runs/{run_id}/diagnosis",
)
assert status == HTTPStatus.OK
payload = json.loads(content.decode("utf-8"))
assert payload["workflow_id"] == "product.main_image.weekly"
assert payload["journal"]["trace"] is not None
assert payload["logs"] == []
status, _, _ = _http_request(
server,
"GET",
f"/api/workflows/content.metrics.daily/runs/{run_id}",
)
assert status == HTTPStatus.NOT_FOUND