ZZD public AI gateway · zzd_public_ai_gateway_v3 Resource: BaaS · 实时与有限刷新 Canonical map: https://zzd.show/docs/ai/map.json Full handbook: https://zzd.show/llms-full.txt REST capabilities: https://zzd.show/api/v1/capabilities/ # 实时同步与有限只读刷新 先复用 [数据页](/docs/ai/branch/baas-data/) 的已授权 `space`,不用新 token。 主站编辑器可按原 Space 规则订阅;隔离作品没有受支持的通用 WS transport, 也不能把 wsUrl 改成主站借用主站身份。已授权 docs 可用下方有限只读刷新。 ## 主站:订阅、断线恢复、完整停止 classic SDK 的 `realtime()` 返回 SpaceRealtime;`onDoc/onCollection/onPath/onJob/onSpace` 返回 sub_id,`unsubscribe(id)` 移除订阅,`close()` 关闭连接。 本示例对应 classic SDK `20261011l.rest1`(保留原WS生命周期修复),原资产加载地址为 `https://zzd.show/static/js/js/space_sdk.js?v=20261011l.rest1`。 该版本的 close 会取消 SDK 自己的待重连计时器,并忽略旧连接回调; 不要将旧版本“close 后重开”的历史缺陷当成此版本现状。 下面仍由应用接管标准 socket 的 onclose,目的是限制为两次重连与60秒视图生命周期。 封装在处理任何回调前独立核对当前连接,失效连接的 message/close 不读取、不报告状态、 不安排重连。当前 close 一经接受就解除该封装归属,重复 close 也没有作用。 保留 SDK 的 ready/event 分发,且分发后再次核对归属;不修改 SDK 文件或权限规则。 ```js // recipe: document-subscription function watchDocument(space, collection, key, onValue, onStatus) { const wsURL = new URL(space.wsUrl); const expected = location.protocol === "https:" ? "wss:" : "ws:"; if (location.origin !== "https://zzd.show" || wsURL.host !== location.host || wsURL.protocol !== expected) { throw new Error("trusted_editor_ws_required"); } const rt = space.realtime(); let stopped = false, timer = null, expiry = null, reconnects = 0; let sub = null, reading = false, again = false, ownedSocket = null; function stop() { if (stopped) return; stopped = true; ownedSocket = null; clearTimeout(timer); clearTimeout(expiry); timer = expiry = null; if (sub !== null) rt.unsubscribe(sub); rt.close(); } async function refresh() { if (reading) { again = true; return; } reading = true; try { do { again = false; const reply = await space.docs.get(collection, key); if (stopped) return; if (!reply || reply.ok !== true) throw new Error("invalid_document_response"); onValue(reply.data); } while (again && !stopped); } catch (error) { if (!stopped) { stop(); onStatus(error.status ? "access_or_read_error" : "offline_reopen_view"); } } finally { reading = false; } } function ownSocket() { const ws = rt.ws; ownedSocket = ws; const deliver = ws.onmessage; const isCurrent = () => !stopped && ownedSocket === ws && rt.ws === ws; // Own this socket's finite retry policy; old callbacks have no effect. ws.onclose = event => { if (!isCurrent()) return; ownedSocket = null; // also fences duplicate close/late messages before retry if ([4403, 4404, 1008].includes(event.code)) { stop(); onStatus("access_denied"); return; } if (reconnects >= 2) { stop(); onStatus("ws_unavailable_use_read_refresh"); return; } reconnects += 1; timer = setTimeout(() => { timer = null; if (!stopped) { rt.connect(); ownSocket(); } }, 800); }; ws.onmessage = event => { if (!isCurrent() || ws.readyState !== WebSocket.OPEN) return; let message; try { message = JSON.parse(event.data); } catch (_) { return; } if (message.op === "error") { stop(); onStatus("subscription_denied_or_invalid"); return; } deliver.call(ws, event); if (!isCurrent() || ws.readyState !== WebSocket.OPEN) return; // ready -> SDK resubscribes same id; subscribed -> read full current record. if (message.op === "subscribed" && message.sub_id === sub) void refresh(); }; } sub = rt.onDoc(collection, key, () => { void refresh(); }); ownSocket(); expiry = setTimeout(() => { if (!stopped) { stop(); onStatus("refresh_paused"); } }, 60000); return stop; } ``` `const stop = watchDocument(space, "notes", "note-001", renderRecord, showSafeStatus)`; 离开视图/组件卸载立即 `stop()`,恢复原视图时重新调用,不能每次 render 新开订阅。 UI 回调应同步、无异常,不打印原响应/凭据。60 秒到期或两次重连耗尽会停止, 界面保留结果并让用户明确刷新。拒绝时不换身份、不继续 POST,也不转为绕过权限的轮询。 实际 WS:`wss://zzd.show/ws/space/{spaceId}/`,使用正常会话和可信 Origin, 浏览器 WebSocket 没有这里的自定义 bearer/header 通道。 服务器先发 `{op:"ready",space_id,...}`,SDK 发 `{op:"subscribe",sub_id,kind:"doc",collection,doc_key}`;成功为 `{op:"subscribed",sub_id,kind,selector}`,变化为 `{op:"event",event:{id,kind,collection,doc_key,payload,...}}`。 收到事件后 GET 当前文档,因为大事件可能只有摘要,重连也没有自动历史补发。 订阅成功后重新 GET,弥补断线期间变化;不把通知 payload 当完整可靠状态。 unsubscribe 发送 `{op:"unsubscribe",sub_id}`;4403/4404 或 error 消息停止。 通用 `events.poll({after_id,limit})` 是另一个需要 Space read 的主站接口,不是隔离 docs 的替代权限。 ## WS 当前不支持/暂不可达时:最多 12 次 GET 仅用于本来就有读权限的数据。不会重新提交写操作,也不在拒绝之后继续请求。 ```js // recipe: bounded-read function boundedRead(read, onValue, onStatus, isDone = () => false) { let stopped = false, timer = null, count = 0; function stop() { stopped = true; clearTimeout(timer); timer = null; } async function tick() { timer = null; if (stopped) return; count += 1; try { const value = await read(); if (stopped) return; onValue(value); if (isDone(value)) { stop(); onStatus("finished"); return; } } catch (error) { if (stopped) return; // Authorization, schema, unknown HTTP and rate errors stop immediately. if (error.status || !(error instanceof TypeError)) { stop(); onStatus("stopped_check_access_or_input"); return; } onStatus("offline_read_only"); } if (count >= 12) { stop(); onStatus("refresh_paused"); return; } timer = setTimeout(tick, 5000); } void tick(); return stop; } ``` 例如 `const stop = boundedRead(() => space.docs.get("notes", "note-001"), reply => renderRecord(reply.data), showSafeStatus)`。离开视图调用 stop。 请求串行,间隔 5 秒,最多 12 次;网络 TypeError 才继续只读恢复,任意 HTTP 错误停止。 这个通用 boundedRead 的 stop 清除自身 timer 并忽略在途 GET 的迟到结果;它不会自动 为 read 回调传控制参数。SpaceClient 可按数据页传 signal/timeoutMs:复用视图自己的 AbortController,在stop之后abort该signal可结束其REST等待;每次GET仍设置适合的正超时。 原WS还须unsubscribe/close并清示例的重连/expiry timer,REST signal不代替WS收尾。 不宣称可以取消服务器任务。恢复后读取同一key,绝不自动重放POST。