// API 桥接层：所有对后端 /api/* 的调用统一从这里发出
// M1 阶段只定义接口壳，各画板暂不消费真实数据（继续用源码 mock）
// M2 阶段画板逐个切到真实数据时，就用这里的方法

// ===== 登录拦截：仅对 /api/* 生效 =====
// 1) fetch → 注入 Authorization: Bearer <token>；响应 401 → 清 token 跳 /login
// 2) EventSource → token 写到 query ?sso_token=<token>（SSE 不能带 header）
// 3) withApiToken(url) → 给 <audio src>/<img src>/<video src> 等 DOM 直连场景用
// 注意：token 取自 window.Auth（login.jsx 暴露），调用时 lazy 读，无加载时序约束
(() => {
  if (window.__API_FETCH_PATCHED__) return;
  window.__API_FETCH_PATCHED__ = true;

  const origFetch = window.fetch.bind(window);
  const origES = window.EventSource;

  const isApi = (input) => {
    const u = typeof input === "string" ? input : (input && input.url) || "";
    return typeof u === "string" && u.startsWith("/api/");
  };
  const getToken = () => {
    try { return (window.Auth && window.Auth.getToken && window.Auth.getToken()) || ""; }
    catch (_) { return ""; }
  };
  const goLogin = () => {
    try { window.Auth && window.Auth.clearToken && window.Auth.clearToken(); } catch (_) {}
    if (window.location.pathname.startsWith("/login")) return;
    const from = encodeURIComponent(window.location.pathname + window.location.search);
    window.location.replace("/login?from=" + from);
  };

  /** 把 token 拼到 URL query；URL 已含 sso_token 时幂等，无 token 时原样返回 */
  window.withApiToken = (url) => {
    if (typeof url !== "string" || !url.startsWith("/api/")) return url;
    if (/[?&]sso_token=/.test(url)) return url;
    const token = getToken();
    if (!token) return url;
    return url + (url.includes("?") ? "&" : "?") + "sso_token=" + encodeURIComponent(token);
  };

  window.fetch = (input, init) => {
    init = init || {};
    if (isApi(input)) {
      const token = getToken();
      if (token) {
        const headers = new Headers(init.headers || {});
        if (!headers.has("Authorization")) headers.set("Authorization", "Bearer " + token);
        init = { ...init, headers };
      }
      return origFetch(input, init).then((resp) => {
        if (resp.status === 401) goLogin();
        return resp;
      });
    }
    return origFetch(input, init);
  };

  window.EventSource = function PatchedEventSource(url, config) {
    if (typeof url === "string" && url.startsWith("/api/")) {
      url = window.withApiToken(url);
    }
    return new origES(url, config);
  };
  window.EventSource.prototype = origES.prototype;
  window.EventSource.CONNECTING = origES.CONNECTING;
  window.EventSource.OPEN = origES.OPEN;
  window.EventSource.CLOSED = origES.CLOSED;
})();

const __json = async (resp) => {
  if (!resp.ok) {
    let msg = `HTTP ${resp.status}`;
    try {
      const body = await resp.json();
      msg = body.detail || body.message || msg;
    } catch (_) { /* ignore */ }
    throw new Error(msg);
  }
  return resp.json();
};

const API = {
  jobs: {
    list:   () => fetch("/api/jobs").then(__json),
    detail: (id) => fetch(`/api/jobs/${encodeURIComponent(id)}`).then(__json),
    result: (id) => fetch(`/api/jobs/${encodeURIComponent(id)}/result`).then(__json),
    artifacts: (id) => fetch(`/api/jobs/${encodeURIComponent(id)}/artifacts`).then(__json),
    cancel: (id) => fetch(`/api/jobs/${encodeURIComponent(id)}/cancel`, { method: "POST" }).then(__json),
    retry:  (id) => fetch(`/api/jobs/${encodeURIComponent(id)}/retry`,  { method: "POST" }).then(__json),
    delete: (id, purge = true) => fetch(`/api/jobs/${encodeURIComponent(id)}?purge=${purge}`, { method: "DELETE" }).then(__json),
    patchRewriteText: (jobId, chunkId, text) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/rewrite-text`,
      { method: "PATCH", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({ rewrite_text: text }) }
    ).then(__json),
    patchAigcPrompt: (jobId, chunkId, prompt) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/aigc-prompt`,
      { method: "PATCH", headers: {"Content-Type":"application/json"},
        body: JSON.stringify({ aigc_prompt: prompt }) }
    ).then(__json),
  },
  replicate: {
    // POST /api/jobs/{parent_job_id}/replicate
    // body: {credential_id, target_duration_sec?, aspect_ratio?}
    // 视频：返回 {child_job_id, parent_job_id, platform, model_name, status}
    // 图文：body 改成 {template_id?, image_count?}，返回 {child_job_id, kind:"image_replicate", ...}
    start: (parentJobId, body) => fetch(
      `/api/jobs/${encodeURIComponent(parentJobId)}/replicate`,
      { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) },
    ).then(__json),
    // 图文复刻 child 跑完后拿 plan
    getResult: (childJobId) => fetch(
      `/api/jobs/${encodeURIComponent(childJobId)}/replicate-result`,
    ).then(__json),
    // 用户 review 完 plan，触发实际出图（图文复刻 only）
    generateImages: (childJobId, credentialId) => fetch(
      `/api/jobs/${encodeURIComponent(childJobId)}/generate-images`,
      { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ credential_id: credentialId }) },
    ).then(__json),
    // 人手编辑图文复刻的二创标题 / 文案；body: {title?, caption?}
    editImageCopy: (childJobId, body) => fetch(
      `/api/jobs/${encodeURIComponent(childJobId)}/replicate-edit-copy`,
      { method: "PATCH", headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body) },
    ).then(__json),
  },
  analyze: {
    link: (payload) => fetch("/api/analyze/link", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).then(__json),
    upload: (formData) => fetch("/api/analyze/upload", { method: "POST", body: formData }).then(__json),
    hub: (payload) => fetch("/api/analyze/hub", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).then(__json),
  },
  providers: {
    list: () => fetch("/api/providers").then(__json),
  },
  credentials: {
    list: (provider) => fetch(`/api/credentials${provider ? `?provider=${provider}` : ""}`).then(__json),
    get: (id) => fetch(`/api/credentials/${id}`).then(__json),
    create: (body) => fetch("/api/credentials", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
    update: (id, body) => fetch(`/api/credentials/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
    delete: (id) => fetch(`/api/credentials/${id}`, { method: "DELETE" }).then(__json),
    activate: (id) => fetch(`/api/credentials/${id}/activate`, { method: "POST" }).then(__json),
    test: (id) => fetch(`/api/credentials/${id}/test`, { method: "POST" }).then(__json),
    probe: (body) => fetch("/api/credentials/probe", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
  },
  templates: {
    list: (params = {}) => {
      const q = new URLSearchParams(params).toString();
      return fetch(`/api/templates${q ? `?${q}` : ""}`).then(__json);
    },
    get: (id) => fetch(`/api/templates/${id}`).then(__json),
    create: (body) => fetch("/api/templates", {
      method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
    }).then(__json),
    update: (id, body) => fetch(`/api/templates/${id}`, {
      method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
    }).then(__json),
    delete: (id) => fetch(`/api/templates/${id}`, { method: "DELETE" }).then(__json),
    duplicate: (id) => fetch(`/api/templates/${id}/duplicate`, { method: "POST" }).then(__json),
  },
  rulePacks: {
    list: () => fetch("/api/rule-packs").then(__json),
    health: () => fetch("/api/rule-packs/health").then(__json),
    get: (id) => fetch(`/api/rule-packs/${encodeURIComponent(id)}`).then(__json),
    upload: (formData) => fetch("/api/rule-packs/upload", { method: "POST", body: formData }).then(__json),
  },
  keywords: {
    list: (params = {}) => {
      const q = new URLSearchParams(params).toString();
      return fetch(`/api/keywords${q ? `?${q}` : ""}`).then(__json);
    },
    stats: () => fetch("/api/keywords/stats").then(__json),
    classify: (text, industry) => fetch("/api/keywords/classify", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text, industry }),
    }).then(__json),
  },
  hub: {
    videos: (params = {}) => {
      const q = new URLSearchParams(params).toString();
      return fetch(`/api/hub/videos${q ? `?${q}` : ""}`).then(__json);
    },
    get: (awemeId) => fetch(`/api/hub/videos/${encodeURIComponent(awemeId)}`).then(__json),
  },
  inbox: {
    list: (params = {}) => {
      const q = new URLSearchParams(params).toString();
      return fetch(`/api/inbox${q ? `?${q}` : ""}`).then(__json);
    },
    pull: (params = {}) => {
      const q = new URLSearchParams(params).toString();
      return fetch(`/api/inbox/pull${q ? `?${q}` : ""}`).then(__json);
    },
  },
  library: {
    // 暂未实现后端，画板纯前端组装
    tree: () => Promise.resolve({ tree: [] }),
  },
  aigcMix: {
    health: () => fetch("/api/aigc-mix/health").then(__json),
    start: (body) => fetch("/api/aigc-mix", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
    list: () => fetch("/api/aigc-mix/jobs").then(__json),
    get: (id) => fetch(`/api/aigc-mix/jobs/${encodeURIComponent(id)}`).then(__json),
    videoUrl: (id) => window.withApiToken(`/api/aigc-mix/jobs/${encodeURIComponent(id)}/video`),
  },
  seedLibrary: {
    health: () => fetch("/api/seed-library/health").then(__json),
    refresh: () => fetch("/api/seed-library/refresh", { method: "POST" }).then(__json),
    listClips: (limit = 200) => fetch(`/api/seed-library/clips?limit=${limit}`).then(__json),
    listStale: () => fetch("/api/seed-library/stale").then(__json),
    sidecar: (clipId) => fetch(`/api/seed-library/clips/${encodeURIComponent(clipId)}/sidecar`).then(__json),
    usage: () => fetch("/api/seed-library/usage").then(__json),
    ingestAll: (force = false) => fetch(`/api/seed-library/ingest-all?force=${force}`, {
      method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
    }).then(__json),
    ingestOne: (clipId, force = false) => fetch(
      `/api/seed-library/clips/${encodeURIComponent(clipId)}/ingest?force=${force}`,
      { method: "POST" }
    ).then(__json),
    gc: () => fetch("/api/seed-library/gc", { method: "POST" }).then(__json),
    match: (text, top_k = 3) => fetch("/api/seed-library/match", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text, top_k, min_score: 0.5 }),
    }).then(__json),
    generate: (body) => fetch("/api/seed-library/generate-and-ingest", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
    writeKh: (clipId, body) => fetch(`/api/seed-library/clips/${encodeURIComponent(clipId)}/kh`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
    videoUrl: (clipId) => window.withApiToken(`/api/seed-library/clips/${encodeURIComponent(clipId)}/video`),
    keyframeUrl: (clipId, index) => window.withApiToken(`/api/seed-library/clips/${encodeURIComponent(clipId)}/keyframe/${index}`),
    deleteClip: (clipId) => fetch(`/api/seed-library/clips/${encodeURIComponent(clipId)}`, {
      method: "DELETE",
    }).then(__json),
    // 批量上传视频 (multipart formData, file field name = "files")
    uploadClips: (formData) => fetch("/api/seed-library/upload", {
      method: "POST", body: formData,
    }).then(__json),
  },
  imageLibrary: {
    health: () => fetch("/api/image-library/health").then(__json),
    refresh: () => fetch("/api/image-library/refresh", { method: "POST" }).then(__json),
    list: () => fetch("/api/image-library/notes").then(__json),
    get: (noteId) => fetch(`/api/image-library/notes/${encodeURIComponent(noteId)}`).then(__json),
    deleteNote: (noteId) => fetch(`/api/image-library/notes/${encodeURIComponent(noteId)}`,
      { method: "DELETE" }).then(__json),
    deleteImage: (noteId, name) => fetch(
      `/api/image-library/notes/${encodeURIComponent(noteId)}/img/${encodeURIComponent(name)}`,
      { method: "DELETE" }
    ).then(__json),
    imageUrl: (noteId, name) =>
      window.withApiToken(`/api/image-library/notes/${encodeURIComponent(noteId)}/img/${encodeURIComponent(name)}`),
    // 批量上传图片 (multipart formData, file field name = "files", optional "title")
    uploadImages: (formData) => fetch("/api/image-library/upload", {
      method: "POST", body: formData,
    }).then(__json),
  },
  bgmLibrary: {
    health: () => fetch("/api/bgm-library/health").then(__json),
    audiences: () => fetch("/api/bgm-library/audiences").then(__json),
    listStyles: () => fetch("/api/bgm-library/styles").then(__json),
    addStyle: (name, audienceSlugs = []) => {
      const fd = new FormData();
      fd.append("name", name);
      fd.append("audience_slugs", (audienceSlugs || []).join(","));
      return fetch("/api/bgm-library/styles", { method: "POST", body: fd }).then(__json);
    },
    updateStyleAudiences: (name, audienceSlugs) => {
      const fd = new FormData();
      fd.append("audience_slugs", (audienceSlugs || []).join(","));
      return fetch(`/api/bgm-library/styles/${encodeURIComponent(name)}`,
        { method: "PUT", body: fd }).then(__json);
    },
    deleteStyle: (name) => fetch(
      `/api/bgm-library/styles/${encodeURIComponent(name)}`,
      { method: "DELETE" }
    ).then(__json),
    recommendForAudience: (audience, limit = 50) => fetch(
      `/api/bgm-library/recommend?audience=${encodeURIComponent(audience)}&limit=${limit}`
    ).then(__json),
    listTracks: (audience = "", style = "") => {
      const qs = new URLSearchParams();
      if (audience) qs.set("audience", audience);
      if (style) qs.set("style", style);
      const q = qs.toString();
      return fetch(`/api/bgm-library/tracks${q ? "?" + q : ""}`).then(__json);
    },
    uploadTracks: (formData) => fetch("/api/bgm-library/tracks/upload", {
      method: "POST", body: formData,
    }).then(__json),
    updateTrack: (trackId, audienceSlugs, styleNames, note = "") => {
      const fd = new FormData();
      fd.append("audience_slugs", (audienceSlugs || []).join(","));
      fd.append("style_names", (styleNames || []).join(","));
      fd.append("note", note);
      return fetch(`/api/bgm-library/tracks/${encodeURIComponent(trackId)}`,
        { method: "PUT", body: fd }).then(__json);
    },
    deleteTrack: (trackId) => fetch(
      `/api/bgm-library/tracks/${encodeURIComponent(trackId)}`,
      { method: "DELETE" }
    ).then(__json),
    streamUrl: (trackId) => window.withApiToken(`/api/bgm-library/tracks/${encodeURIComponent(trackId)}/stream`),
  },
  postprocess: {
    health: () => fetch("/api/postprocess/health").then(__json),
    cameras: () => fetch("/api/postprocess/cameras").then(__json),
    scanClip: (clipId) => fetch(`/api/postprocess/scan-clip/${encodeURIComponent(clipId)}`,
      { method: "POST" }).then(__json),
    scrubClip: (clipId, profile = null) => fetch(`/api/postprocess/scrub-clip/${encodeURIComponent(clipId)}`,
      { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ profile }) }).then(__json),
    scanLibrary: () => fetch("/api/postprocess/scan-library", { method: "POST" }).then(__json),
    scrubLibrary: (profile = null) => fetch("/api/postprocess/scrub-library", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ profile }),
    }).then(__json),
    scanImageLibrary: () => fetch("/api/postprocess/scan-image-library", { method: "POST" }).then(__json),
  },
  replicateTable: {
    get: (jobId) => fetch(`/api/jobs/${encodeURIComponent(jobId)}/replicate-table`).then(__json),
  },
  textRewrite: {
    startLink: (body) => fetch("/api/text-rewrite/link", {
      method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
    }).then(__json),
    listTemplates: () => fetch("/api/text-rewrite/templates").then(__json),
    list: () => fetch("/api/text-rewrite/jobs").then(__json),
    get: (id) => fetch(`/api/text-rewrite/jobs/${encodeURIComponent(id)}`).then(__json),
    result: (id) => fetch(`/api/text-rewrite/jobs/${encodeURIComponent(id)}/result`).then(__json),
    matchTable: (id) => fetch(`/api/text-rewrite/jobs/${encodeURIComponent(id)}/match-table`).then(__json),
    rerunWithTemplate: (id, templateId) => fetch(
      `/api/text-rewrite/jobs/${encodeURIComponent(id)}/rerun-rewrite`,
      { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ rewrite_template_id: templateId }) }
    ).then(__json),
    rejectMatch: (id, cid) => fetch(
      `/api/text-rewrite/jobs/${encodeURIComponent(id)}/chunks/${encodeURIComponent(cid)}/reject-match`,
      { method: "POST" }
    ).then(__json),
    manualBind: (id, cid, clipId) => fetch(
      `/api/text-rewrite/jobs/${encodeURIComponent(id)}/chunks/${encodeURIComponent(cid)}/manual-bind`,
      { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ clip_id: clipId }) }
    ).then(__json),
    generateShot: (id, cid, body) => fetch(
      `/api/text-rewrite/jobs/${encodeURIComponent(id)}/chunks/${encodeURIComponent(cid)}/generate-shot`,
      { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
    ).then(__json),
    patchRewrite: (id, cid, body) => fetch(
      `/api/text-rewrite/jobs/${encodeURIComponent(id)}/chunks/${encodeURIComponent(cid)}/rewrite`,
      { method: "PATCH", headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body) }
    ).then(__json),
    streamUrl: (id) => `/api/text-rewrite/jobs/${encodeURIComponent(id)}/stream`,
  },
  compose: {
    health: () => fetch("/api/compose/health").then(__json),
    defaults: () => fetch("/api/compose/defaults").then(__json),
    bgmList: () => fetch("/api/compose/bgm-list").then(__json),
    start: (parentJobId, body = {}) => fetch(`/api/jobs/${encodeURIComponent(parentJobId)}/compose`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then(__json),
    get: (composeId) => fetch(`/api/compose/${encodeURIComponent(composeId)}`).then(__json),
    listForParent: (parentJobId) => fetch(`/api/jobs/${encodeURIComponent(parentJobId)}/compose-jobs`).then(__json),
    videoUrl: (composeId) => window.withApiToken(`/api/compose/${encodeURIComponent(composeId)}/video`),
    manifestUrl: (composeId) => window.withApiToken(`/api/compose/${encodeURIComponent(composeId)}/manifest`),
    bgmStreamUrl: (filename) => window.withApiToken(`/api/compose/bgm-stream/${encodeURIComponent(filename)}`),
    ttsPreviewUrl: (text, voice = "zh-CN-XiaoxiaoNeural", rate = "+10%") =>
      window.withApiToken(`/api/compose/tts-preview?text=${encodeURIComponent(text)}&voice=${encodeURIComponent(voice)}&rate=${encodeURIComponent(rate)}`),
  },
  shotQc: {
    rejectMatch: (jobId, chunkId) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/reject-match`,
      { method: "POST" }
    ).then(__json),
    clearRejections: (jobId, chunkId) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/clear-rejections`,
      { method: "POST" }
    ).then(__json),
    generateShot: (jobId, chunkId, body) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/generate-shot`,
      { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(body) }
    ).then(__json),
    patchPrompt: (jobId, chunkId, prompt) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/aigc-prompt`,
      { method: "PATCH", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ aigc_prompt: prompt }) }
    ).then(__json),
    // patchRewriteText 已移到 API.jobs（避免命名空间错位）
    matchCandidates: (jobId, chunkId, top_k = 10) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/match-candidates?top_k=${top_k}`
    ).then(__json),
    manualBind: (jobId, chunkId, clipId) => fetch(
      `/api/jobs/${encodeURIComponent(jobId)}/chunks/${encodeURIComponent(chunkId)}/manual-bind`,
      { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ clip_id: clipId }) }
    ).then(__json),
  },
};

// SSE 进度订阅 + 活性指示（从老 app.js:subscribeProgress 提炼）
// onEvent: (kind, payload) => void，kind ∈ {"progress","status","done","stale","error"}
// 返回 dispose 函数
function subscribeJobProgress(jobId, onEvent) {
  let disposed = false;
  let lastEventAt = Date.now();
  let es = null;
  let livenessTimer = null;
  let pollTimer = null;

  const touch = () => { lastEventAt = Date.now(); };
  const checkLiveness = () => {
    if (disposed) return;
    const gap = Date.now() - lastEventAt;
    if (gap >= 5000) {
      onEvent("stale", { seconds: Math.floor(gap / 1000), severe: gap > 30000 });
    }
  };
  livenessTimer = setInterval(checkLiveness, 1000);

  // SSE 主通道
  try {
    es = new EventSource(`/api/jobs/${encodeURIComponent(jobId)}/stream`);
    es.addEventListener("progress", (ev) => {
      touch();
      try { onEvent("progress", JSON.parse(ev.data)); } catch (_) {}
    });
    es.addEventListener("status", (ev) => {
      touch();
      try { onEvent("status", JSON.parse(ev.data)); } catch (_) {}
    });
    es.addEventListener("done", (ev) => {
      touch();
      try { onEvent("done", JSON.parse(ev.data)); } catch (_) {}
    });
    es.onerror = (e) => {
      if (!disposed) onEvent("error", { error: String(e && e.message || e) });
    };
  } catch (err) {
    onEvent("error", { error: String(err) });
  }

  // 5 秒兜底轮询
  pollTimer = setInterval(async () => {
    if (disposed) return;
    try {
      const job = await API.jobs.detail(jobId);
      touch();
      if (job.progress) onEvent("progress", job.progress);
      if (job.status === "completed" || job.status === "failed") {
        onEvent("done", { status: job.status, error: job.error });
      }
    } catch (_) { /* 404 等兜底 */ }
  }, 5000);

  return () => {
    disposed = true;
    clearInterval(livenessTimer);
    clearInterval(pollTimer);
    try { es && es.close(); } catch (_) {}
  };
}

Object.assign(window, { API, subscribeJobProgress });
