@dp-bohrium/widget-sdk 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  本文件只记录影响使用者的变更:运行时 API、事件订阅、Manifest 要求。
4
4
 
5
+ ## 0.4.1
6
+
7
+ ### 修复
8
+
9
+ **1. 解除已释放监听与资源的强引用**
10
+
11
+ - 手动释放 render、event 或第三方资源登记后,生命周期不再持续强引用原资源及其闭包;调用方继续持有释放句柄时,也不会因此保留已释放资源,避免长期运行的 Widget 反复切换监听器后累积内存。
12
+ - 重复登记和重复释放继续保持幂等,活动资源仍在 Widget 停用时清理;公开 API、回调与释放语义不变。
13
+
14
+ **2. 对齐 Panel 文件保存指南与授权边界**
15
+
16
+ - npm 包首页指向官方文档站,便于从包元数据发现教程与 API 参考。
17
+ - README 与 Agent 索引明确:file-preview 使用当前文件引用,获独立 `files.write` grant 的 Panel 使用 Host 签发的工作区文件引用;SDK 仍通过既有 `context.files.read/write` 接口传递字节,不新增文件操作。
18
+ - [文件能力指南](https://dev.dp.tech/bohrium-widget-sdk/guides/capabilities/files) 补充 Panel 编辑示例,覆盖同文件 render 保留草稿、Session 切换、保存快照与冲突/未知结果;缺少授权、完整内容或读取版本时保持只读。
19
+ - SDK 与 contracts 同步为 `0.4.1`,保持文件请求、回执与公开类型兼容。
20
+
21
+ ## 0.4.0
22
+
23
+ ### 新增
24
+
25
+ **1. 增加文件读取与条件保存接口**
26
+
27
+ - 新增 context.files.read 与 context.files.write。保存使用 expectedRevision 和稳定 mutationId,由 Host 执行权限、预算、原子替换与去重。
28
+ - 旧 Host 只返回 content 与 encoding 时,读取仍然成功。游标、截断标记和版本是可选扩展,SDK 不补造这些字段。
29
+ - 发出保存后若超时、中止、断线或回执无法确认,返回 write-outcome-unknown,不自动重试。
30
+
5
31
  ## 0.3.0
6
32
 
7
33
  ### 破坏性变更
package/README.md CHANGED
@@ -174,7 +174,9 @@ SDK 发布包会生成 `dist/index.d.ts` 与对应的 declaration map,并通
174
174
 
175
175
  | grant 名 | Manifest 申请键 | SDK 调用 |
176
176
  | --- | --- | --- |
177
- | `files.list` / `files.metadata` / `files.read` | `files` | `context.capabilities.request({ operation, input })` |
177
+ | `files.list` / `files.metadata` | `files` | `context.capabilities.request({ operation, input })` |
178
+ | `files.read` | `files.read` | `context.files.read({ file, offset, length, encoding, expectedRevision? })` |
179
+ | `files.write` | `files.write` | `context.files.write({ file, content, encoding, expectedRevision, mutationId })` |
178
180
  | `files.download` | `files.download` | `context.files.download({ file, suggestedName })` |
179
181
  | `events.read` | `events`(需列出 `topics`) | 低层分页订阅;`context.events.on(…)` 是 Host 主动推送,不需要该 grant |
180
182
  | `tasks.watch` | `tasks.watch` | `context.tasks.watch({ taskId, cursor, limit })` |
@@ -204,8 +206,9 @@ if (result.ok) {
204
206
 
205
207
  跨能力约定:
206
208
 
207
- - 失败以 `{ ok: false, reason, message, retryable }` 正常兑现;只有客户端超时、中止、
208
- dispose 和畸形响应才 reject。
209
+ - Host 拒绝以 `{ ok: false, reason, message, retryable }` 正常兑现;本地状态、输入、客户端超时、
210
+ 中止、dispose 和畸形响应可 reject。`files.write` 发出后的结果不确定时兑现为
211
+ `write-outcome-unknown`,处理方式见下文。
209
212
  - 中止与超时统一用 `{ signal, timeoutMs }`,复用既有 MessageChannel 能力生命周期。
210
213
  - Widget 永远拿不到 Host 凭据,也不应直接调用浏览器 `fetch`:是否弹窗、记住 Origin
211
214
  授权、跟随重定向还是拒绝,全部由 Host 决定。
@@ -230,6 +233,68 @@ if (result.ok) {
230
233
  提交,重新订阅需显式传回 `events.cursor`。工具 Hook 观察的是**已经发生的**服务端
231
234
  事实,不是执行前拦截器。
232
235
 
236
+ ### 读取和保存当前文件
237
+
238
+ 作者 Manifest 申请 `"capabilities": { "files": { "read": true, "write": true } }`,Host 仍需独立
239
+ 授权两个能力。`files.write` 对 Host 签发引用指向的已有文件作整文件条件替换:在 `file-preview`
240
+ 中引用来自 `request.content.file`;在获得写 grant 的 Panel 中引用来自 `data.root` 或 `files.list`
241
+ 的结果。不能传路径、URL、自造的文件 id 或指定另存位置。Host 校验引用、作用域、
242
+ 写权限和实际字节预算,成功回执表示执行端已确认持久化。SDK 不负责 ASE、晶格或文件格式转换,
243
+ 插件应使用自己的序列化器生成最终字节。
244
+
245
+ `FileReadInput` / `FileReadResult`、`FileWriteInput` / `FileWriteResult`、`FileRevision`、
246
+ `FileMutationId` 与两个 `*CapabilityResult` 类型均可从 SDK 根入口导入。`revision` 是文件内容的
247
+ 不透明版本,与渲染快照的 `RenderRequest.revision` 无关。下面的 `file` 来自当前文件预览:
248
+
249
+ ```ts
250
+ const loaded = await context.files.read({
251
+ file, offset: 0, length: 64_000, encoding: "utf8",
252
+ ...(file.revision ? { expectedRevision: file.revision } : {}),
253
+ });
254
+ if (!loaded.ok) return showReadFailure(loaded.reason);
255
+ if (!loaded.data.revision) return showReadOnlyFile(loaded.data.content);
256
+ if (loaded.data.eof === false || loaded.data.truncated === true) {
257
+ return showFileNeedsChunkedReading();
258
+ }
259
+
260
+ const expectedRevision = loaded.data.revision;
261
+ const content = serializeEditedModel(loaded.data.content);
262
+ // 一次逻辑保存只生成一次;核对或重试同一内容时复用。
263
+ const mutationId = crypto.randomUUID();
264
+ const saved = await context.files.write({
265
+ file, content, encoding: "utf8", expectedRevision, mutationId,
266
+ }, { signal: saveController.signal, timeoutMs: 15_000 });
267
+ if (saved.ok) {
268
+ markSaved(saved.data.file, saved.data.revision);
269
+ } else {
270
+ keepUnsavedChanges(saved.reason, mutationId);
271
+ }
272
+ ```
273
+
274
+ `read` 兼容旧 Host 的 `{ content, encoding }` 结果;`offset`、`bytesRead`、`nextOffset`、`eof`、
275
+ `truncated` 和 `revision` 均为可选扩展。SDK 原样返回结果,只验证已提供字段,不补造游标或版本。
276
+ 没有 `revision` 时内容可展示,但不能据此保存;带 `expectedRevision` 的读取必须返回有效且匹配的
277
+ 版本,不能降级为无版本读取。Host 必须从同次读取的内容快照签发版本,不能另用 metadata/stat 补取。
278
+
279
+ 有游标的 `read` 按原始字节计数:使用 `nextOffset` 继续;若首块带版本,后续块携带对应
280
+ `expectedRevision`,防止混合版本。`utf8` 字节区间可能切断多字节字符,不能用重新编码后的文本长度推断 `bytesRead`。
281
+ 二进制文件或需完整解码的分块文本使用 `base64`,先将每块解码为字节并拼接,再解码文本。
282
+ `base64` 使用标准、带正确 padding 的编码;空文件和空字符串替换均合法。
283
+ 上面的整文件保存示例以 Host 返回完整内容为前提;缺少完整性标记时,插件需依据 Host 的读取约定
284
+ 确认内容完整,不能把一个未完成的分块直接替换为整个文件。
285
+
286
+ `write` 必填读取时的 `expectedRevision` 和一次逻辑保存的 `mutationId`(1–128 个字符,只含
287
+ 字母、数字、`_ . : -` 且以字母或数字开头)。`revision-conflict` 表示当前文件已变化,应重新读取
288
+ 并由用户合并;相同 mutationId 携带不同内容会返回 `mutation-conflict`。保存成功后更新本地文件引用
289
+ 和基础版本;未修改内容的保存可以返回相同版本。
290
+ 游标元信息不是保存的前提;只有读取内容所绑定的有效版本才能用作 `expectedRevision`。
291
+
292
+ SDK 不自动重试保存。调用前已中止、客户端未就绪或输入非法会 reject,且不发送请求;缺少 grant
293
+ 返回 `permission-denied`。发出后超时、中止、断线或畸形回执都不能证明未写入,返回
294
+ `{ ok: false, reason: "write-outcome-unknown", retryable: false }`。此时保留未保存内容、原基础版本
295
+ 和 mutationId,通过 Host 核对文件版本或用**同一 mutationId、同一内容**确认结果;不要直接生成新 id
296
+ 重试,也不要提前标记已保存。新的编辑内容属于新的逻辑保存,应使用新的 mutationId。
297
+
233
298
  ## 测试
234
299
 
235
300
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export type { WidgetActivationEvent, WidgetActivationEvents, WidgetContribution, WidgetEventName, WidgetFilePreviewContribution, WidgetPanelContribution, WidgetPluginContributions, WidgetPluginManifest, WidgetToolPhase, } from "@dp-bohrium/widget-contracts";
1
+ export type { FileMutationId, FileReadInput, FileReadResult, FileRevision, FileWriteInput, FileWriteResult, WidgetActivationEvent, WidgetActivationEvents, WidgetContribution, WidgetEventName, WidgetFilePreviewContribution, WidgetPanelContribution, WidgetPluginContributions, WidgetPluginManifest, WidgetToolPhase, } from "@dp-bohrium/widget-contracts";
2
2
  export { isWidgetActivationEvent, isWidgetEventName, WIDGET_ACTIVATION_EVENTS, WIDGET_EVENT_NAMES, } from "@dp-bohrium/widget-contracts";
3
3
  export type { CompileWidgetPluginManifestOptions } from "./manifest.js";
4
4
  export { compileWidgetPluginManifest } from "./manifest.js";
5
5
  export type { HostUserEmail, HostUserEmailResult, HostUserProfile, HostUserProfileResult, WidgetHostUserClient, } from "./runtime/service.js";
6
- export type { FileDownloadCapabilityResult, TaskWatchCapabilityResult, UiStateReadCapabilityResult, UiStateWriteCapabilityResult, WidgetActivation, WidgetCapabilityApi, WidgetContext, WidgetDisposable, WidgetEventApi, WidgetFileApi, WidgetHandle, WidgetHostOptions, WidgetModule, WidgetRenderContext, WidgetRenderHandler, WidgetTaskApi, WidgetUiApi, WidgetUiStateApi, } from "./widget.js";
6
+ export type { FileDownloadCapabilityResult, FileReadCapabilityResult, FileWriteCapabilityResult, TaskWatchCapabilityResult, UiStateReadCapabilityResult, UiStateWriteCapabilityResult, WidgetActivation, WidgetCapabilityApi, WidgetContext, WidgetDisposable, WidgetEventApi, WidgetFileApi, WidgetHandle, WidgetHostOptions, WidgetModule, WidgetRenderContext, WidgetRenderHandler, WidgetTaskApi, WidgetUiApi, WidgetUiStateApi, } from "./widget.js";
7
7
  export { runWidgetHost } from "./widget.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as createWidgetClient, l as WIDGET_SDK_VERSION } from "./runtime-Drr96Awx.js";
1
+ import { i as createWidgetClient, l as WIDGET_SDK_VERSION } from "./runtime-RyAEhkyn.js";
2
2
  import { WIDGET_ACTIVATION_EVENTS, WIDGET_EVENT_NAMES, isWidgetActivationEvent, isWidgetEventName } from "@dp-bohrium/widget-contracts";
3
3
  //#region src/manifest.ts
4
4
  function compiledContribution(contribution) {
@@ -46,7 +46,8 @@ function compileWidgetPluginManifest(manifest, options = {}) {
46
46
  //#region src/lifetime.ts
47
47
  function createWidgetLifetime(onError) {
48
48
  const controller = new AbortController();
49
- const resources = /* @__PURE__ */ new Map();
49
+ const registrations = /* @__PURE__ */ new WeakMap();
50
+ const resources = /* @__PURE__ */ new Set();
50
51
  const pending = /* @__PURE__ */ new Set();
51
52
  let closePromise;
52
53
  function run(callback) {
@@ -60,15 +61,20 @@ function createWidgetLifetime(onError) {
60
61
  return task;
61
62
  }
62
63
  function add(resource) {
63
- const existing = resources.get(resource);
64
+ const existing = registrations.get(resource);
64
65
  if (existing) return existing;
66
+ let retained = resource;
65
67
  let disposed;
66
68
  const handle = { dispose() {
67
69
  if (disposed) return disposed;
68
- disposed = run(() => typeof resource === "function" ? resource() : resource.dispose());
70
+ const current = retained;
71
+ retained = void 0;
72
+ resources.delete(handle);
73
+ disposed = run(() => typeof current === "function" ? current() : current?.dispose());
69
74
  return disposed;
70
75
  } };
71
- resources.set(resource, handle);
76
+ registrations.set(resource, handle);
77
+ resources.add(handle);
72
78
  if (controller.signal.aborted) handle.dispose();
73
79
  return handle;
74
80
  }
@@ -87,6 +93,177 @@ function createWidgetLifetime(onError) {
87
93
  };
88
94
  }
89
95
  //#endregion
96
+ //#region src/runtime/files.ts
97
+ const failureReasons = /* @__PURE__ */ new Set([
98
+ "unsupported",
99
+ "permission-denied",
100
+ "resource-limit",
101
+ "invalid-reference",
102
+ "timeout",
103
+ "internal-error",
104
+ "revision-conflict",
105
+ "mutation-conflict",
106
+ "write-outcome-unknown",
107
+ "network-consent-denied",
108
+ "network-policy-denied",
109
+ "network-transport-unavailable",
110
+ "network-request-too-large",
111
+ "network-response-too-large",
112
+ "network-rate-limited",
113
+ "network-redirect-denied",
114
+ "network-credential-unavailable",
115
+ "network-scope-invalid",
116
+ "network-dns-denied",
117
+ "network-error"
118
+ ]);
119
+ /** 文件字节协议检查;权限和持久化仍由 Host 执行。 */
120
+ function createWidgetFileClient(dependencies) {
121
+ function preflight(operation, options) {
122
+ if (options?.signal?.aborted) throw new Error("file request aborted before dispatch");
123
+ const state = dependencies.state();
124
+ if (state === "disposed") throw new Error("client is disposed");
125
+ if (state !== "ready") throw new Error("Widget files are not ready");
126
+ if (!dependencies.granted(operation)) return {
127
+ ok: false,
128
+ reason: "permission-denied",
129
+ message: `${operation} is not granted`,
130
+ retryable: false
131
+ };
132
+ }
133
+ return Object.freeze({
134
+ async read(input, options) {
135
+ const denied = preflight("files.read", options);
136
+ if (denied) return denied;
137
+ if (!validReadInput(input)) invalid("read input");
138
+ const snapshot = {
139
+ ...input,
140
+ file: { ...input.file }
141
+ };
142
+ const result = await dependencies.request({
143
+ operation: "files.read",
144
+ input: snapshot
145
+ }, options);
146
+ if (isFailure(result)) return result;
147
+ if (!exactKeys(result, ["ok", "data"]) || result["ok"] !== true || !validReadResult(result["data"], snapshot)) invalid("read result");
148
+ return {
149
+ ok: true,
150
+ data: result["data"]
151
+ };
152
+ },
153
+ async write(input, options) {
154
+ const denied = preflight("files.write", options);
155
+ if (denied) return denied;
156
+ if (!validWriteInput(input)) invalid("write input");
157
+ const snapshot = {
158
+ ...input,
159
+ file: { ...input.file }
160
+ };
161
+ const bytes = snapshot.encoding === "utf8" ? new TextEncoder().encode(snapshot.content).byteLength : base64Bytes(snapshot.content);
162
+ if (bytes === null) invalid("write input encoding");
163
+ try {
164
+ const result = await dependencies.request({
165
+ operation: "files.write",
166
+ input: snapshot
167
+ }, options);
168
+ if (isFailure(result)) return result;
169
+ if (!exactKeys(result, ["ok", "data"]) || result["ok"] !== true || !validWriteResult(result["data"], snapshot, bytes)) invalid("write result");
170
+ return {
171
+ ok: true,
172
+ data: result["data"]
173
+ };
174
+ } catch {
175
+ return {
176
+ ok: false,
177
+ reason: "write-outcome-unknown",
178
+ message: "File write outcome is unknown; reconcile the file revision before retrying with the same mutationId",
179
+ retryable: false
180
+ };
181
+ }
182
+ }
183
+ });
184
+ }
185
+ function validReadInput(value) {
186
+ return exactKeys(value, [
187
+ "file",
188
+ "offset",
189
+ "length",
190
+ "encoding"
191
+ ], ["expectedRevision"]) && isFile(value["file"]) && nonnegative(value["offset"]) && nonnegative(value["length"]) && value["length"] > 0 && encoding(value["encoding"]) && (!Object.hasOwn(value, "expectedRevision") || revision(value["expectedRevision"]));
192
+ }
193
+ function validWriteInput(value) {
194
+ return exactKeys(value, [
195
+ "file",
196
+ "content",
197
+ "encoding",
198
+ "expectedRevision",
199
+ "mutationId"
200
+ ]) && isFile(value["file"]) && typeof value["content"] === "string" && encoding(value["encoding"]) && (value["encoding"] !== "utf8" || !/[\uD800-\uDFFF]/u.test(value["content"])) && revision(value["expectedRevision"]) && typeof value["mutationId"] === "string" && value["mutationId"].length <= 128 && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/u.test(value["mutationId"]);
201
+ }
202
+ function validReadResult(value, input) {
203
+ if (!exactKeys(value, ["content", "encoding"], [
204
+ "offset",
205
+ "bytesRead",
206
+ "nextOffset",
207
+ "eof",
208
+ "truncated",
209
+ "revision"
210
+ ]) || typeof value["content"] !== "string" || value["encoding"] !== input.encoding || Object.hasOwn(value, "offset") && value["offset"] !== input.offset || Object.hasOwn(value, "bytesRead") && (!nonnegative(value["bytesRead"]) || value["bytesRead"] > input.length || value["bytesRead"] === 0 && value["content"] !== "") || Object.hasOwn(value, "nextOffset") && (!nonnegative(value["nextOffset"]) || value["nextOffset"] < input.offset || value["nextOffset"] - input.offset > input.length || Object.hasOwn(value, "bytesRead") && value["nextOffset"] !== input.offset + value["bytesRead"]) || Object.hasOwn(value, "eof") && typeof value["eof"] !== "boolean" || Object.hasOwn(value, "truncated") && typeof value["truncated"] !== "boolean" || value["eof"] === false && value["bytesRead"] === 0 || Object.hasOwn(value, "revision") && !revision(value["revision"]) || input.expectedRevision !== void 0 && value["revision"] !== input.expectedRevision) return false;
211
+ if (input.encoding !== "base64") return true;
212
+ const bytes = base64Bytes(value["content"]);
213
+ return bytes !== null && bytes <= input.length && (!Object.hasOwn(value, "bytesRead") || value["bytesRead"] === bytes) && (!Object.hasOwn(value, "nextOffset") || value["nextOffset"] === input.offset + bytes);
214
+ }
215
+ function validWriteResult(value, input, bytes) {
216
+ return exactKeys(value, [
217
+ "file",
218
+ "revision",
219
+ "bytesWritten",
220
+ "mutationId"
221
+ ]) && isFile(value["file"]) && value["file"].id === input.file.id && revision(value["revision"]) && (value["file"].revision === void 0 || value["file"].revision === value["revision"]) && (value["file"].size === void 0 || value["file"].size === bytes) && nonnegative(value["bytesWritten"]) && value["bytesWritten"] === bytes && value["mutationId"] === input.mutationId;
222
+ }
223
+ function isFailure(value) {
224
+ return exactKeys(value, [
225
+ "ok",
226
+ "reason",
227
+ "message",
228
+ "retryable"
229
+ ]) && value["ok"] === false && typeof value["reason"] === "string" && failureReasons.has(value["reason"]) && typeof value["message"] === "string" && value["message"].length > 0 && typeof value["retryable"] === "boolean";
230
+ }
231
+ function isFile(value) {
232
+ return exactKeys(value, [
233
+ "id",
234
+ "kind",
235
+ "name"
236
+ ], [
237
+ "extension",
238
+ "mimeType",
239
+ "size",
240
+ "revision"
241
+ ]) && typeof value["id"] === "string" && value["id"].length > 0 && value["kind"] === "file" && typeof value["name"] === "string" && value["name"].length > 0 && (!Object.hasOwn(value, "extension") || typeof value["extension"] === "string" && value["extension"].length <= 32 && /^[a-z0-9][a-z0-9.-]*$/u.test(value["extension"])) && (!Object.hasOwn(value, "mimeType") || typeof value["mimeType"] === "string" && value["mimeType"].length > 0 && Array.from(value["mimeType"]).length <= 255) && (!Object.hasOwn(value, "size") || nonnegative(value["size"])) && (!Object.hasOwn(value, "revision") || revision(value["revision"]));
242
+ }
243
+ function revision(value) {
244
+ return typeof value === "string" && value.length > 0 && Array.from(value).length <= 256;
245
+ }
246
+ function nonnegative(value) {
247
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
248
+ }
249
+ function encoding(value) {
250
+ return value === "utf8" || value === "base64";
251
+ }
252
+ function base64Bytes(value) {
253
+ try {
254
+ const binary = atob(value);
255
+ return btoa(binary) === value ? binary.length : null;
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+ function exactKeys(value, required, optional = []) {
261
+ return value !== null && typeof value === "object" && !Array.isArray(value) && required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
262
+ }
263
+ function invalid(part) {
264
+ throw new Error(`file protocol error: invalid ${part}`);
265
+ }
266
+ //#endregion
90
267
  //#region src/widget.ts
91
268
  /**
92
269
  * Generated Widget Host 的唯一运行入口。Widget 作者只导出 activate/deactivate,
@@ -111,6 +288,13 @@ function runWidgetHost(widget, options = {}) {
111
288
  };
112
289
  }
113
290
  function createContext(client, payload, renderHandlers, lifetime) {
291
+ const subscriptions = { add(registration) {
292
+ lifetime.add(registration);
293
+ } };
294
+ const events = { on(handlerOrName, maybeHandler) {
295
+ const unsubscribe = typeof handlerOrName === "string" ? client.onEvent(handlerOrName, maybeHandler) : client.onEvent(handlerOrName);
296
+ return lifetime.add(unsubscribe);
297
+ } };
114
298
  return {
115
299
  instanceId: payload.instanceId,
116
300
  locale: payload.locale,
@@ -120,10 +304,17 @@ function createContext(client, payload, renderHandlers, lifetime) {
120
304
  granted: payload.capabilities,
121
305
  request: (request, options) => client.requestCapability(request, options)
122
306
  },
123
- files: { download: (input, options) => client.requestCapability({
124
- operation: "files.download",
125
- input
126
- }, options) },
307
+ files: {
308
+ ...createWidgetFileClient({
309
+ state: () => client.currentState,
310
+ granted: (operation) => payload.capabilities.some((grant) => grant.name === operation),
311
+ request: (request, requestOptions) => client.requestCapability(request, requestOptions)
312
+ }),
313
+ download: (input, options) => client.requestCapability({
314
+ operation: "files.download",
315
+ input
316
+ }, options)
317
+ },
127
318
  tasks: { watch: (input, options) => client.requestCapability({
128
319
  operation: "tasks.watch",
129
320
  input
@@ -157,13 +348,8 @@ function createContext(client, payload, renderHandlers, lifetime) {
157
348
  close: (options) => client.closePresentation(options),
158
349
  changeSurface: (surface, options) => client.changePresentationSurface(surface, options)
159
350
  },
160
- events: { on(handlerOrName, maybeHandler) {
161
- const unsubscribe = typeof handlerOrName === "string" ? client.onEvent(handlerOrName, maybeHandler) : client.onEvent(handlerOrName);
162
- return lifetime.add(unsubscribe);
163
- } },
164
- subscriptions: { add(registration) {
165
- lifetime.add(registration);
166
- } },
351
+ events,
352
+ subscriptions,
167
353
  render(handler) {
168
354
  renderHandlers.add(handler);
169
355
  return lifetime.add({ dispose: () => {
@@ -0,0 +1,24 @@
1
+ import type { CapabilityRequest, CapabilityResult, FileReadInput, FileReadResult, FileWriteInput, FileWriteResult } from "@dp-bohrium/widget-contracts";
2
+ import type { WidgetCapabilityRequestOptions, WidgetClientState } from "./client.js";
3
+ type Failure = Extract<CapabilityResult, {
4
+ ok: false;
5
+ }>;
6
+ export type FileReadCapabilityResult = {
7
+ readonly ok: true;
8
+ readonly data: FileReadResult;
9
+ } | Failure;
10
+ export type FileWriteCapabilityResult = {
11
+ readonly ok: true;
12
+ readonly data: FileWriteResult;
13
+ } | Failure;
14
+ interface Dependencies {
15
+ readonly state: () => WidgetClientState;
16
+ readonly granted: (operation: "files.read" | "files.write") => boolean;
17
+ readonly request: (request: CapabilityRequest, options?: WidgetCapabilityRequestOptions) => Promise<CapabilityResult>;
18
+ }
19
+ /** 文件字节协议检查;权限和持久化仍由 Host 执行。 */
20
+ export declare function createWidgetFileClient(dependencies: Dependencies): Readonly<{
21
+ read(input: FileReadInput, options?: WidgetCapabilityRequestOptions): Promise<FileReadCapabilityResult>;
22
+ write(input: FileWriteInput, options?: WidgetCapabilityRequestOptions): Promise<FileWriteCapabilityResult>;
23
+ }>;
24
+ export {};
@@ -1,3 +1,3 @@
1
- import { a as isWidgetHostConnectMessage, c as WidgetClient, i as createWidgetClient, l as WIDGET_SDK_VERSION, n as WIDGET_HOST_CONNECT, o as PRESENTATION_CONFIRMATION_TIMEOUT_MS, r as WidgetRuntimeClient, s as REGION_CONFIRMATION_TIMEOUT_MS, t as WIDGET_BOOTSTRAP_READY } from "../runtime-Drr96Awx.js";
1
+ import { a as isWidgetHostConnectMessage, c as WidgetClient, i as createWidgetClient, l as WIDGET_SDK_VERSION, n as WIDGET_HOST_CONNECT, o as PRESENTATION_CONFIRMATION_TIMEOUT_MS, r as WidgetRuntimeClient, s as REGION_CONFIRMATION_TIMEOUT_MS, t as WIDGET_BOOTSTRAP_READY } from "../runtime-RyAEhkyn.js";
2
2
  import { i as isWidgetProtocolMessage, r as MessagePortTransport } from "../protocol-endpoint-CDz-YDGT.js";
3
3
  export { MessagePortTransport, PRESENTATION_CONFIRMATION_TIMEOUT_MS, REGION_CONFIRMATION_TIMEOUT_MS, WIDGET_BOOTSTRAP_READY, WIDGET_HOST_CONNECT, WIDGET_SDK_VERSION, WidgetClient, WidgetRuntimeClient, createWidgetClient, isWidgetHostConnectMessage, isWidgetProtocolMessage };
@@ -2,7 +2,7 @@ import { r as MessagePortTransport, t as ProtocolEndpoint } from "./protocol-end
2
2
  //#endregion
3
3
  //#region src/runtime/version.ts
4
4
  /** 从当前安装的 SDK package.json 读取版本,插件作者不需要重复填写。 */
5
- const WIDGET_SDK_VERSION = "0.3.0";
5
+ const WIDGET_SDK_VERSION = "0.4.1";
6
6
  //#endregion
7
7
  //#region src/runtime/network.ts
8
8
  const failureReasons = /* @__PURE__ */ new Set([
@@ -95,6 +95,9 @@ const capabilityFailureReasons = {
95
95
  "invalid-reference": true,
96
96
  timeout: true,
97
97
  "internal-error": true,
98
+ "revision-conflict": true,
99
+ "mutation-conflict": true,
100
+ "write-outcome-unknown": true,
98
101
  "network-consent-denied": true,
99
102
  "network-policy-denied": true,
100
103
  "network-transport-unavailable": true,
package/dist/widget.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import type { CapabilityRequest, CapabilityResult, FileDownloadResult, FileReference, GrantedCapability, RegionCapabilityResult, RenderRequest, TaskObservationBatch, Theme, UiStateResult, UiStateWriteResult, WidgetEventName, WidgetLiveEvent } from "@dp-bohrium/widget-contracts";
1
+ import type { CapabilityRequest, CapabilityResult, FileDownloadResult, FileReadInput, FileReference, FileWriteInput, GrantedCapability, RegionCapabilityResult, RenderRequest, TaskObservationBatch, Theme, UiStateResult, UiStateWriteResult, WidgetEventName, WidgetLiveEvent } from "@dp-bohrium/widget-contracts";
2
+ import { type FileReadCapabilityResult, type FileWriteCapabilityResult } from "./runtime/files.js";
2
3
  import { type CreateWidgetClientOptions, type RegionName, type WidgetClientFactoryOptions, type WidgetClientState, type WidgetLiveEventContext, type WidgetLiveEventHandler } from "./runtime/index.js";
3
4
  import type { WidgetNetworkClient } from "./runtime/network.js";
4
5
  import type { WidgetHostUserClient, WidgetServiceClient } from "./runtime/service.js";
6
+ export type { FileReadCapabilityResult, FileWriteCapabilityResult, } from "./runtime/files.js";
5
7
  export interface WidgetDisposable {
6
8
  dispose(): void | Promise<void>;
7
9
  }
@@ -25,6 +27,16 @@ export interface WidgetCapabilityApi {
25
27
  }): Promise<CapabilityResult>;
26
28
  }
27
29
  export interface WidgetFileApi {
30
+ /** 读取指定版本的一个字节区间;二进制文件使用 base64。 */
31
+ read(input: FileReadInput, options?: {
32
+ readonly timeoutMs?: number;
33
+ readonly signal?: AbortSignal;
34
+ }): Promise<FileReadCapabilityResult>;
35
+ /** 原子替换当前文件;未知结果须核对版本并保留 mutationId,不自动重试。 */
36
+ write(input: FileWriteInput, options?: {
37
+ readonly timeoutMs?: number;
38
+ readonly signal?: AbortSignal;
39
+ }): Promise<FileWriteCapabilityResult>;
28
40
  /** 请求 Host 在用户确认后通过宿主 Download Adapter 保存文件。 */
29
41
  download(input: {
30
42
  readonly file: FileReference;
package/llms.txt CHANGED
@@ -21,4 +21,16 @@
21
21
  - 先在 Manifest 声明 capability,再等待 Host grant。
22
22
  - 用 `WidgetError` 的 `code/phase/retryable/correlationId` 诊断启动、渲染和能力失败。
23
23
  - `ok=false` 的能力结果是可处理状态,不要直接重试或绕过 Host。
24
+ - 文件读写使用 `context.files.read(input, options)` / `write(input, options)`;Manifest
25
+ 分别申请 `files.read` / `files.write`。file-preview 的引用来自当前文件;Panel 只有获得
26
+ `files.write` grant 时才能保存 `data.root` / `files.list` 取得的已有文件。SDK 只传字节,不负责 ASE
27
+ 或其他格式序列化。完整保存示例见 README「读取和保存当前文件」。
28
+ - read 兼容仅含 content/encoding 的旧响应,可选 offset/bytesRead/nextOffset/eof/truncated/revision
29
+ 只验证已提供值并原样返回,不补造版本或游标。无 revision 仍可展示,但不能保存。
30
+ - 请求 expectedRevision 必须返回有效且匹配的 revision;版本必须绑定同次读取的内容,不能另调
31
+ metadata/stat 补取。分块若有首块 revision,后续携带对应 expectedRevision;base64 解码后拼接
32
+ 原始字节,避免 utf8 区间切断多字节字符。文件 revision 不是 RenderRequest.revision。
33
+ - 保存必填 expectedRevision 和稳定 mutationId。发出后超时/中止/断线返回
34
+ write-outcome-unknown,不自动重试;保留内容和同一 mutationId 核对结果,成功回执后才标记已保存。
35
+ - revision-conflict 需重读并合并;mutation-conflict 表示同一 mutationId 被用于不同内容。
24
36
  - SDK 本地通过不等于 iframe 已真实渲染、Bundle 已发布或 Agent 已绑定。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dp-bohrium/widget-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Bohrium Widget 开发者 SDK:在 Widget Bundle 内与 Host 建立 Bridge,并暴露生命周期与能力协议。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "runtime"
19
19
  ],
20
20
  "author": "Bohrium",
21
- "homepage": "https://www.npmjs.com/package/@dp-bohrium/widget-sdk",
21
+ "homepage": "https://dev.dp.tech/bohrium-widget-sdk/",
22
22
  "types": "./dist/index.d.ts",
23
23
  "exports": {
24
24
  ".": {
@@ -48,7 +48,7 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@dp-bohrium/widget-contracts": "0.3.0"
51
+ "@dp-bohrium/widget-contracts": "0.4.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@biomejs/biome": "^2.2.0",