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/ # 读取和保存已绑定的数据 只复制符合当前环境的初始化段,再使用“文档读写”。记录名/集合来自当前绑定契约, 例子中的 notes/note-001 只是业务自定的稳定名称,不授予访问权。无需新凭据。 ## 主站编辑器:取得非秘密 context 在正常 `https://zzd.show` 编辑器已有会话内加载 classic ``。 此版本查询参数区分同一已有 classic 资产;确认加载的是这版再使用下述生命周期示例, 不需要创建连接凭据或猜测 ESM 模块地址。 使用原作品的整数 Show ID,不能把 12 字符 public_id 当此整数。 下面只读已有 context 的 spaceId/绑定状态,既不 ensure,也不创建后端: ```js // recipe: editor-context async function editorSpace(showId) { if (location.origin !== "https://zzd.show") throw new Error("open_original_editor"); if (!Number.isSafeInteger(showId) || showId <= 0) throw new Error("show_id_required"); const response = await fetch("/puzzle/api/baas/project/context/?show_pk=" + showId, { method: "GET", credentials: "include", headers: {Accept: "application/json"} }); if (!response.ok) throw Object.assign(new Error("context_unavailable"), {status: response.status}); const envelope = await response.json(); const ctx = envelope.data; if (envelope.ok !== true || !ctx) throw new Error("binding_missing"); const spaceId = ctx.space && ctx.space.space_id; const app = ctx.backend_app || {}; if (!spaceId || String(app.show_pk) !== String(showId)) throw new Error("binding_mismatch"); if (app.status !== "active") throw new Error("backend_disabled"); // Owner/member session route: intentionally omit backendAppId here. return new SpaceClient({spaceId: String(spaceId), baseUrl: location.origin, wsUrl: "wss://" + location.host + "/ws/space/" + encodeURIComponent(spaceId) + "/"}); } ``` 例如 `const space = await editorSpace(existingShowId)`。 context HTTP 为 `GET /puzzle/api/baas/project/context/?show_pk=...`, 响应 `{ok:true,data:{space:{space_id:"..."},backend_app:{id:"...",show_pk:7,status:"active"},...}}` 或 `{ok:true,data:null}`。ID 不是授权证据;后续调用仍检查当前会话/Space 规则。 ## 隔离作品:只使用原已生成绑定的文档能力 沿用编辑器生成的 BaaS 集成片段所给的非秘密 `spaceId/backendAppId/showId`, 由 AI 从原绑定选择这三个字段用于同一作品;不要复制整个上下文,也不要从主站抓 cookie。 隔离页没有可通用查询 owner context 的入口。缺绑定时回原编辑器核对,不猜 ID。 加载原片段的 classic SDK 后使用: ```js // recipe: isolated-docs function isolatedDocs(binding) { const runtime = (window.seen && window.seen.runtime) || {}; if (!runtime.access || runtime.access.mode !== "isolated_show") throw new Error("isolated_context_required"); if (!binding || !binding.spaceId || !binding.backendAppId || !binding.showId) { throw new Error("binding_missing"); } return new SpaceClient({spaceId: String(binding.spaceId), backendAppId: String(binding.backendAppId), showId: String(binding.showId), baseUrl: ""}); } ``` `isolatedDocs(existingBinding)` 返回的对象只在这里使用 `docs.get/getData/getRecord/list/set/update/delete`。 提供 `backendAppId` 会把 **docs** URL 改为 `/puzzle/api/baas/runtime/space/{spaceId}/docs/{operation}/`,并发送 `X-Backend-App-Id`、`X-Backend-Show-Id`;所以不能把这组参数随手加到主站 owner 示例。 `baseUrl:""` 是当前隔离 origin,不要改为主站绕过隔离。原 Show scoped session、 精确 app/Show/Space、启用状态、已授予集合及原 Space 权限每次都会重新检查。 写入 Origin 由浏览器生成,必须精确匹配当前隔离 origin,不能手工伪造。 不能由此推导 rtdb、jobs、events、集合定义管理、通用服务或 WS 可在隔离页使用。 ## 文档读写:一次意图、稳定 key、明确结果 ```js // recipe: document-read-write async function readNote(space, collection, key, requestOptions) { const reply = await space.docs.get(collection, key, requestOptions); if (!reply || reply.ok !== true) throw new Error("document_response_invalid"); return reply.data; // null, or {doc_key, data, updateTime}; not the whole envelope } async function saveNoteOnce(space, collection, key, desired, requestOptions) { // Call only for an explicit authorized save; no automatic POST retry. if (!desired || typeof desired !== "object" || Array.isArray(desired)) throw new Error("invalid_data"); const reply = await space.docs.set(collection, key, desired, requestOptions); if (!reply || reply.ok !== true || !reply.data || reply.data.doc_key !== key) { throw new Error("save_result_unknown"); } return reply.data; } ``` 主站 owner 路径是 `/puzzle/api/space/{spaceId}/docs/get/` 和 `docs/set/`。 SDK 的 GET 使用 query `collection,doc_key`;POST 使用 FormData `collection,doc_key,data=JSON.stringify(desired)`,带 `credentials:"include"` 和 Accept JSON。 SDK 自动取可用 CSRF 并在 POST 添加 `X-CSRFToken`;业务代码不抄写、存储或打印它。 它不发送 developer bearer。`baseUrl` 只控制 REST;默认 WS 仍取当前 location, 改变 baseUrl 不会改变 WS,也不会产生跨域权限。 实际响应:读有记录 `{ok:true,data:{doc_key:"note-001",data:{title:"第一条"},updateTime:"..."}}`; 读空记录 `{ok:true,data:null}`;写 `{ok:true,data:{doc_key:"note-001"}}`。 `getRecord` 返回 record/null;`getData` 返回其中业务 data/null;`list` 返回 `{ok:true,data:[record,...]}`,limit 1–200。SDK 虽传 cursor,目前服务端不消费它, 不能声称可用 cursor 完整遍历。`rtdb.get` 在正常受权主站返回 `{ok:true,data:null|{path,value,updateTime}}`,`getValue` 才解出 value。 同一用户意图固定 collection/key/desired(时间戳也固定),set 替换对象,update 浅合并。 重复 set 同值仍可能产生新事件/时间,不是 exactly-once;没有 docs CAS 或文档幂等键。 网络失败时保留原 key 和 desired,恢复后 GET 对照;已相同就报告已保存,不再 POST。 不同时先呈现差异,再由明确保存动作提交;不自动覆盖并发编辑。不要用新 UUID 重放同一意图。 删除、重置另需明确意图;这份示例不执行它们。 ## 按需结束当前 SpaceClient 请求的本地等待 普通调用不变;不用改构造器、登录、Space绑定或用户界面步骤。需要随视图退出/用户取消 结束等待时,传单次 `{signal, timeoutMs}`。例如 `space.docs.get(collection,key,{signal,timeoutMs:10000})`。 不指定或 timeoutMs=0 不启用新超时;正数是 fetch 和 response body 共用的绝对期限, 不会在响应头到达后重新计时。支持数字字符串;负数、NaN、Infinity、空字符串/非数字类型 以 `TypeError/code=invalid_request_options` 在发送前拒绝。SpaceClient 本项使用 timeoutMs, 不把运行层其它opts/alias或新能力推定到这里。 所有 REST 方法保留原业务参数顺序,在最后加可选 requestOptions: | 调用 | 参数末尾 | | --- | --- | | docs.get/getRecord/getData/delete | `(collection,key,requestOptions)` | | docs.set/update | `(collection,key,dataOrPatch,requestOptions)` | | docs.list | `(collection,opts,requestOptions)` | | rtdb.get/getNode/getValue | `(path,requestOptions)` | | rtdb.set/update | `(path,valueOrPatch,requestOptions)`;对象形式 `(node,requestOptions)` 或 `(node,undefined,requestOptions)` | | events.poll | `(opts,requestOptions)` | | jobs.enqueue/get | `(kind,payload,opts,requestOptions)` / `(jobId,requestOptions)` | | collections.list/upsert/delete | `(requestOptions)` / `(collection,meta,requestOptions)` / `(collection,requestOptions)` | | baas.dashboard/context/binding/apiPuzzle | `(opts,requestOptions)` | | baas.ensure | `(payload,requestOptions)`;仍需原管理授权,本示例不调用它 | 已有 docs.list/events/jobs/baas 的 opts 也可带 signal/timeoutMs;独立尾部同名字段优先, 尾部明确 timeoutMs=0 可以关闭 embedded 正超时。rtdb对象形式的 value/patch保留在node内。 控制字段不会进入 query/FormData/JSON业务载荷;顶层 signal/timeoutMs为保留控制名, 嵌套文档 data、rtdb value、job payload 内的同名业务字段仍原样保留。 内部 `_get(path,params,requestOptions)` / `_post(path,body,requestOptions)` 同样支持, 使用现有公开方法即可,不据此猜新路由。 ```js // recipe: space-request-lifecycle function readSpaceView(space, collection, key, showValue, showStatus) { // Browser AbortController owns this view's wait, not the session or Space. const controller = new AbortController(); let closed = false; const done = space.docs.get(collection, key, { signal: controller.signal, timeoutMs: 10000 }).then(reply => { if (!closed) showValue(reply.data); }).catch(error => { if (!closed) showStatus(error && error.code === "request_timeout" ? "read_timeout" : "read_failed"); }); return {done, close() { closed = true; controller.abort(); }}; } async function saveSpaceIntent(space, collection, key, desired, signal) { try { const ack = await space.docs.set(collection, key, desired, {signal, timeoutMs: 15000}); return {state: "saved", record: ack.data}; } catch (error) { if (error && error.outcome_unknown === true) { return {state: "unknown", next: "read_original_key_before_any_resubmit"}; } throw error; } } ``` 在视图卸载调用返回对象的 close 并可 await done;重复close安全,迟到结果不更新视图。 这个close属于示例视图,不是SpaceClient撤销接口。多请求只有显式共用同一个signal才一起结束; 普通并发各用独立signal。示例使用浏览器已有AbortController;缺此全局时可使用仅timeoutMs的 单次请求,仍可逻辑结束等待,但不能声称物理取消。SDK不要求无控制调用携带新的signal。 外部signal结束:`name=AbortError, code=request_aborted`;正超时: `name=TimeoutError, code=request_timeout`。控制错误不假装HTTP401/403,消息不带业务正文或秘密。 预先aborted时0fetch且outcome_unknown=false;GET结束不标写未知。 POST开始后abort/timeout会带 `outcome_unknown=true`:操作可能已执行,取消的是本地等待, 不是服务器写入或job。saveSpaceIntent只汇报unknown,不补发POST;保留原key/desired, 恢复后先GET核对已保存状态,处理差异后再由明确动作提交。 服务器HTTP错误的原status/code/data和原非JSON行为保持;失败也不自动重试写操作。 即使fetch或响应体无视abort,SDK也结束调用方等待并接住迟到拒绝;每次结局清理本人timer/listener。 底层fetch继续运行时不能宣称已取消服务端执行。AppRuntime服务方法/状态namespace的能力仍按 原各自上下文契约,本节只描述SpaceClient REST。 ## 用户状态 namespace:仅在注入 transport 实际可用时 这与 collection/document 是不同模型。使用已允许 namespace,SDK 自动传绑定和现有身份: ```js // recipe: identity-state async function saveBoundStateOnce(namespace, desired) { const runtime = (window.seen && window.seen.runtime) || {}; const api = runtime.identity_api || {}; const sdk = window.ZZDSpaceSDK || window.AppRuntime; if (api.available === false || !api.state || !sdk || !sdk.data) { throw new Error("state_transport_unavailable"); } const current = await sdk.data.get(namespace); return sdk.data.set(namespace, desired, {expectedRevision: current.revision}); } ``` SDK `data.get` 解包为 `{namespace,exists,data,revision,updated_at,identity}`; `data.set` 解包为 `{namespace,data,revision,updated_at}`,使用 PUT JSON `{backend_app_id,show_pk,data,expected_revision}`,请求头为 `X-Backend-App-Id`、`X-Show-Pk`。 409 `revision_conflict` 必须先重新读取/处理差异,不能换 revision 静默覆盖。 当前隔离上下文明确关闭此 transport;上述函数会在网络请求前停止。 不能靠导入 ESM、配置任意 URL 或复用 docs header 将它“打开”。 下一步:实时变化只读 [实时与刷新](/docs/ai/branch/baas-realtime/); 失败处理见 [BaaS 短入口](/docs/ai/branch/baas/)。