@dp-bohrium/widget-sdk 0.3.0 → 0.4.0
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 +10 -0
- package/README.md +67 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +192 -12
- package/dist/runtime/files.d.ts +24 -0
- package/dist/runtime/index.js +1 -1
- package/dist/{runtime-Drr96Awx.js → runtime-8ppcE14g.js} +4 -1
- package/dist/widget.d.ts +13 -1
- package/llms.txt +11 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
本文件只记录影响使用者的变更:运行时 API、事件订阅、Manifest 要求。
|
|
4
4
|
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
**1. 增加文件读取与条件保存接口**
|
|
10
|
+
|
|
11
|
+
- 新增 context.files.read 与 context.files.write。保存使用 expectedRevision 和稳定 mutationId,由 Host 执行权限、预算、原子替换与去重。
|
|
12
|
+
- 旧 Host 只返回 content 与 encoding 时,读取仍然成功。游标、截断标记和版本是可选扩展,SDK 不补造这些字段。
|
|
13
|
+
- 发出保存后若超时、中止、断线或回执无法确认,返回 write-outcome-unknown,不自动重试。
|
|
14
|
+
|
|
5
15
|
## 0.3.0
|
|
6
16
|
|
|
7
17
|
### 破坏性变更
|
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`
|
|
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
|
-
-
|
|
208
|
-
dispose
|
|
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,67 @@ 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` 对当前 `file-preview` 绑定的已有文件作整文件条件替换;引用来自
|
|
240
|
+
`request.content.file`,不能传路径、URL、别的文件 id 或指定另存位置。Host 校验引用、作用域、
|
|
241
|
+
写权限和实际字节预算,成功回执表示执行端已确认持久化。SDK 不负责 ASE、晶格或文件格式转换,
|
|
242
|
+
插件应使用自己的序列化器生成最终字节。
|
|
243
|
+
|
|
244
|
+
`FileReadInput` / `FileReadResult`、`FileWriteInput` / `FileWriteResult`、`FileRevision`、
|
|
245
|
+
`FileMutationId` 与两个 `*CapabilityResult` 类型均可从 SDK 根入口导入。`revision` 是文件内容的
|
|
246
|
+
不透明版本,与渲染快照的 `RenderRequest.revision` 无关。下面的 `file` 来自当前文件预览:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const loaded = await context.files.read({
|
|
250
|
+
file, offset: 0, length: 64_000, encoding: "utf8",
|
|
251
|
+
...(file.revision ? { expectedRevision: file.revision } : {}),
|
|
252
|
+
});
|
|
253
|
+
if (!loaded.ok) return showReadFailure(loaded.reason);
|
|
254
|
+
if (!loaded.data.revision) return showReadOnlyFile(loaded.data.content);
|
|
255
|
+
if (loaded.data.eof === false || loaded.data.truncated === true) {
|
|
256
|
+
return showFileNeedsChunkedReading();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const expectedRevision = loaded.data.revision;
|
|
260
|
+
const content = serializeEditedModel(loaded.data.content);
|
|
261
|
+
// 一次逻辑保存只生成一次;核对或重试同一内容时复用。
|
|
262
|
+
const mutationId = crypto.randomUUID();
|
|
263
|
+
const saved = await context.files.write({
|
|
264
|
+
file, content, encoding: "utf8", expectedRevision, mutationId,
|
|
265
|
+
}, { signal: saveController.signal, timeoutMs: 15_000 });
|
|
266
|
+
if (saved.ok) {
|
|
267
|
+
markSaved(saved.data.file, saved.data.revision);
|
|
268
|
+
} else {
|
|
269
|
+
keepUnsavedChanges(saved.reason, mutationId);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`read` 兼容旧 Host 的 `{ content, encoding }` 结果;`offset`、`bytesRead`、`nextOffset`、`eof`、
|
|
274
|
+
`truncated` 和 `revision` 均为可选扩展。SDK 原样返回结果,只验证已提供字段,不补造游标或版本。
|
|
275
|
+
没有 `revision` 时内容可展示,但不能据此保存;带 `expectedRevision` 的读取必须返回有效且匹配的
|
|
276
|
+
版本,不能降级为无版本读取。Host 必须从同次读取的内容快照签发版本,不能另用 metadata/stat 补取。
|
|
277
|
+
|
|
278
|
+
有游标的 `read` 按原始字节计数:使用 `nextOffset` 继续;若首块带版本,后续块携带对应
|
|
279
|
+
`expectedRevision`,防止混合版本。`utf8` 字节区间可能切断多字节字符,不能用重新编码后的文本长度推断 `bytesRead`。
|
|
280
|
+
二进制文件或需完整解码的分块文本使用 `base64`,先将每块解码为字节并拼接,再解码文本。
|
|
281
|
+
`base64` 使用标准、带正确 padding 的编码;空文件和空字符串替换均合法。
|
|
282
|
+
上面的整文件保存示例以 Host 返回完整内容为前提;缺少完整性标记时,插件需依据 Host 的读取约定
|
|
283
|
+
确认内容完整,不能把一个未完成的分块直接替换为整个文件。
|
|
284
|
+
|
|
285
|
+
`write` 必填读取时的 `expectedRevision` 和一次逻辑保存的 `mutationId`(1–128 个字符,只含
|
|
286
|
+
字母、数字、`_ . : -` 且以字母或数字开头)。`revision-conflict` 表示当前文件已变化,应重新读取
|
|
287
|
+
并由用户合并;相同 mutationId 携带不同内容会返回 `mutation-conflict`。保存成功后更新本地文件引用
|
|
288
|
+
和基础版本;未修改内容的保存可以返回相同版本。
|
|
289
|
+
游标元信息不是保存的前提;只有读取内容所绑定的有效版本才能用作 `expectedRevision`。
|
|
290
|
+
|
|
291
|
+
SDK 不自动重试保存。调用前已中止、客户端未就绪或输入非法会 reject,且不发送请求;缺少 grant
|
|
292
|
+
返回 `permission-denied`。发出后超时、中止、断线或畸形回执都不能证明未写入,返回
|
|
293
|
+
`{ ok: false, reason: "write-outcome-unknown", retryable: false }`。此时保留未保存内容、原基础版本
|
|
294
|
+
和 mutationId,通过 Host 核对文件版本或用**同一 mutationId、同一内容**确认结果;不要直接生成新 id
|
|
295
|
+
重试,也不要提前标记已保存。新的编辑内容属于新的逻辑保存,应使用新的 mutationId。
|
|
296
|
+
|
|
233
297
|
## 测试
|
|
234
298
|
|
|
235
299
|
```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-
|
|
1
|
+
import { i as createWidgetClient, l as WIDGET_SDK_VERSION } from "./runtime-8ppcE14g.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) {
|
|
@@ -87,6 +87,177 @@ function createWidgetLifetime(onError) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
//#endregion
|
|
90
|
+
//#region src/runtime/files.ts
|
|
91
|
+
const failureReasons = /* @__PURE__ */ new Set([
|
|
92
|
+
"unsupported",
|
|
93
|
+
"permission-denied",
|
|
94
|
+
"resource-limit",
|
|
95
|
+
"invalid-reference",
|
|
96
|
+
"timeout",
|
|
97
|
+
"internal-error",
|
|
98
|
+
"revision-conflict",
|
|
99
|
+
"mutation-conflict",
|
|
100
|
+
"write-outcome-unknown",
|
|
101
|
+
"network-consent-denied",
|
|
102
|
+
"network-policy-denied",
|
|
103
|
+
"network-transport-unavailable",
|
|
104
|
+
"network-request-too-large",
|
|
105
|
+
"network-response-too-large",
|
|
106
|
+
"network-rate-limited",
|
|
107
|
+
"network-redirect-denied",
|
|
108
|
+
"network-credential-unavailable",
|
|
109
|
+
"network-scope-invalid",
|
|
110
|
+
"network-dns-denied",
|
|
111
|
+
"network-error"
|
|
112
|
+
]);
|
|
113
|
+
/** 文件字节协议检查;权限和持久化仍由 Host 执行。 */
|
|
114
|
+
function createWidgetFileClient(dependencies) {
|
|
115
|
+
function preflight(operation, options) {
|
|
116
|
+
if (options?.signal?.aborted) throw new Error("file request aborted before dispatch");
|
|
117
|
+
const state = dependencies.state();
|
|
118
|
+
if (state === "disposed") throw new Error("client is disposed");
|
|
119
|
+
if (state !== "ready") throw new Error("Widget files are not ready");
|
|
120
|
+
if (!dependencies.granted(operation)) return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: "permission-denied",
|
|
123
|
+
message: `${operation} is not granted`,
|
|
124
|
+
retryable: false
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
async read(input, options) {
|
|
129
|
+
const denied = preflight("files.read", options);
|
|
130
|
+
if (denied) return denied;
|
|
131
|
+
if (!validReadInput(input)) invalid("read input");
|
|
132
|
+
const snapshot = {
|
|
133
|
+
...input,
|
|
134
|
+
file: { ...input.file }
|
|
135
|
+
};
|
|
136
|
+
const result = await dependencies.request({
|
|
137
|
+
operation: "files.read",
|
|
138
|
+
input: snapshot
|
|
139
|
+
}, options);
|
|
140
|
+
if (isFailure(result)) return result;
|
|
141
|
+
if (!exactKeys(result, ["ok", "data"]) || result["ok"] !== true || !validReadResult(result["data"], snapshot)) invalid("read result");
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
data: result["data"]
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
async write(input, options) {
|
|
148
|
+
const denied = preflight("files.write", options);
|
|
149
|
+
if (denied) return denied;
|
|
150
|
+
if (!validWriteInput(input)) invalid("write input");
|
|
151
|
+
const snapshot = {
|
|
152
|
+
...input,
|
|
153
|
+
file: { ...input.file }
|
|
154
|
+
};
|
|
155
|
+
const bytes = snapshot.encoding === "utf8" ? new TextEncoder().encode(snapshot.content).byteLength : base64Bytes(snapshot.content);
|
|
156
|
+
if (bytes === null) invalid("write input encoding");
|
|
157
|
+
try {
|
|
158
|
+
const result = await dependencies.request({
|
|
159
|
+
operation: "files.write",
|
|
160
|
+
input: snapshot
|
|
161
|
+
}, options);
|
|
162
|
+
if (isFailure(result)) return result;
|
|
163
|
+
if (!exactKeys(result, ["ok", "data"]) || result["ok"] !== true || !validWriteResult(result["data"], snapshot, bytes)) invalid("write result");
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
data: result["data"]
|
|
167
|
+
};
|
|
168
|
+
} catch {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason: "write-outcome-unknown",
|
|
172
|
+
message: "File write outcome is unknown; reconcile the file revision before retrying with the same mutationId",
|
|
173
|
+
retryable: false
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function validReadInput(value) {
|
|
180
|
+
return exactKeys(value, [
|
|
181
|
+
"file",
|
|
182
|
+
"offset",
|
|
183
|
+
"length",
|
|
184
|
+
"encoding"
|
|
185
|
+
], ["expectedRevision"]) && isFile(value["file"]) && nonnegative(value["offset"]) && nonnegative(value["length"]) && value["length"] > 0 && encoding(value["encoding"]) && (!Object.hasOwn(value, "expectedRevision") || revision(value["expectedRevision"]));
|
|
186
|
+
}
|
|
187
|
+
function validWriteInput(value) {
|
|
188
|
+
return exactKeys(value, [
|
|
189
|
+
"file",
|
|
190
|
+
"content",
|
|
191
|
+
"encoding",
|
|
192
|
+
"expectedRevision",
|
|
193
|
+
"mutationId"
|
|
194
|
+
]) && 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"]);
|
|
195
|
+
}
|
|
196
|
+
function validReadResult(value, input) {
|
|
197
|
+
if (!exactKeys(value, ["content", "encoding"], [
|
|
198
|
+
"offset",
|
|
199
|
+
"bytesRead",
|
|
200
|
+
"nextOffset",
|
|
201
|
+
"eof",
|
|
202
|
+
"truncated",
|
|
203
|
+
"revision"
|
|
204
|
+
]) || 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;
|
|
205
|
+
if (input.encoding !== "base64") return true;
|
|
206
|
+
const bytes = base64Bytes(value["content"]);
|
|
207
|
+
return bytes !== null && bytes <= input.length && (!Object.hasOwn(value, "bytesRead") || value["bytesRead"] === bytes) && (!Object.hasOwn(value, "nextOffset") || value["nextOffset"] === input.offset + bytes);
|
|
208
|
+
}
|
|
209
|
+
function validWriteResult(value, input, bytes) {
|
|
210
|
+
return exactKeys(value, [
|
|
211
|
+
"file",
|
|
212
|
+
"revision",
|
|
213
|
+
"bytesWritten",
|
|
214
|
+
"mutationId"
|
|
215
|
+
]) && 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;
|
|
216
|
+
}
|
|
217
|
+
function isFailure(value) {
|
|
218
|
+
return exactKeys(value, [
|
|
219
|
+
"ok",
|
|
220
|
+
"reason",
|
|
221
|
+
"message",
|
|
222
|
+
"retryable"
|
|
223
|
+
]) && value["ok"] === false && typeof value["reason"] === "string" && failureReasons.has(value["reason"]) && typeof value["message"] === "string" && value["message"].length > 0 && typeof value["retryable"] === "boolean";
|
|
224
|
+
}
|
|
225
|
+
function isFile(value) {
|
|
226
|
+
return exactKeys(value, [
|
|
227
|
+
"id",
|
|
228
|
+
"kind",
|
|
229
|
+
"name"
|
|
230
|
+
], [
|
|
231
|
+
"extension",
|
|
232
|
+
"mimeType",
|
|
233
|
+
"size",
|
|
234
|
+
"revision"
|
|
235
|
+
]) && 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"]));
|
|
236
|
+
}
|
|
237
|
+
function revision(value) {
|
|
238
|
+
return typeof value === "string" && value.length > 0 && Array.from(value).length <= 256;
|
|
239
|
+
}
|
|
240
|
+
function nonnegative(value) {
|
|
241
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
242
|
+
}
|
|
243
|
+
function encoding(value) {
|
|
244
|
+
return value === "utf8" || value === "base64";
|
|
245
|
+
}
|
|
246
|
+
function base64Bytes(value) {
|
|
247
|
+
try {
|
|
248
|
+
const binary = atob(value);
|
|
249
|
+
return btoa(binary) === value ? binary.length : null;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function exactKeys(value, required, optional = []) {
|
|
255
|
+
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));
|
|
256
|
+
}
|
|
257
|
+
function invalid(part) {
|
|
258
|
+
throw new Error(`file protocol error: invalid ${part}`);
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
90
261
|
//#region src/widget.ts
|
|
91
262
|
/**
|
|
92
263
|
* Generated Widget Host 的唯一运行入口。Widget 作者只导出 activate/deactivate,
|
|
@@ -111,6 +282,13 @@ function runWidgetHost(widget, options = {}) {
|
|
|
111
282
|
};
|
|
112
283
|
}
|
|
113
284
|
function createContext(client, payload, renderHandlers, lifetime) {
|
|
285
|
+
const subscriptions = { add(registration) {
|
|
286
|
+
lifetime.add(registration);
|
|
287
|
+
} };
|
|
288
|
+
const events = { on(handlerOrName, maybeHandler) {
|
|
289
|
+
const unsubscribe = typeof handlerOrName === "string" ? client.onEvent(handlerOrName, maybeHandler) : client.onEvent(handlerOrName);
|
|
290
|
+
return lifetime.add(unsubscribe);
|
|
291
|
+
} };
|
|
114
292
|
return {
|
|
115
293
|
instanceId: payload.instanceId,
|
|
116
294
|
locale: payload.locale,
|
|
@@ -120,10 +298,17 @@ function createContext(client, payload, renderHandlers, lifetime) {
|
|
|
120
298
|
granted: payload.capabilities,
|
|
121
299
|
request: (request, options) => client.requestCapability(request, options)
|
|
122
300
|
},
|
|
123
|
-
files: {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
301
|
+
files: {
|
|
302
|
+
...createWidgetFileClient({
|
|
303
|
+
state: () => client.currentState,
|
|
304
|
+
granted: (operation) => payload.capabilities.some((grant) => grant.name === operation),
|
|
305
|
+
request: (request, requestOptions) => client.requestCapability(request, requestOptions)
|
|
306
|
+
}),
|
|
307
|
+
download: (input, options) => client.requestCapability({
|
|
308
|
+
operation: "files.download",
|
|
309
|
+
input
|
|
310
|
+
}, options)
|
|
311
|
+
},
|
|
127
312
|
tasks: { watch: (input, options) => client.requestCapability({
|
|
128
313
|
operation: "tasks.watch",
|
|
129
314
|
input
|
|
@@ -157,13 +342,8 @@ function createContext(client, payload, renderHandlers, lifetime) {
|
|
|
157
342
|
close: (options) => client.closePresentation(options),
|
|
158
343
|
changeSurface: (surface, options) => client.changePresentationSurface(surface, options)
|
|
159
344
|
},
|
|
160
|
-
events
|
|
161
|
-
|
|
162
|
-
return lifetime.add(unsubscribe);
|
|
163
|
-
} },
|
|
164
|
-
subscriptions: { add(registration) {
|
|
165
|
-
lifetime.add(registration);
|
|
166
|
-
} },
|
|
345
|
+
events,
|
|
346
|
+
subscriptions,
|
|
167
347
|
render(handler) {
|
|
168
348
|
renderHandlers.add(handler);
|
|
169
349
|
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 {};
|
package/dist/runtime/index.js
CHANGED
|
@@ -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-
|
|
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-8ppcE14g.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.
|
|
5
|
+
const WIDGET_SDK_VERSION = "0.4.0";
|
|
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,15 @@
|
|
|
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。SDK 只传字节,不负责 ASE
|
|
26
|
+
或其他格式序列化。完整保存示例见 README「读取和保存当前文件」。
|
|
27
|
+
- read 兼容仅含 content/encoding 的旧响应,可选 offset/bytesRead/nextOffset/eof/truncated/revision
|
|
28
|
+
只验证已提供值并原样返回,不补造版本或游标。无 revision 仍可展示,但不能保存。
|
|
29
|
+
- 请求 expectedRevision 必须返回有效且匹配的 revision;版本必须绑定同次读取的内容,不能另调
|
|
30
|
+
metadata/stat 补取。分块若有首块 revision,后续携带对应 expectedRevision;base64 解码后拼接
|
|
31
|
+
原始字节,避免 utf8 区间切断多字节字符。文件 revision 不是 RenderRequest.revision。
|
|
32
|
+
- 保存必填 expectedRevision 和稳定 mutationId。发出后超时/中止/断线返回
|
|
33
|
+
write-outcome-unknown,不自动重试;保留内容和同一 mutationId 核对结果,成功回执后才标记已保存。
|
|
34
|
+
- revision-conflict 需重读并合并;mutation-conflict 表示同一 mutationId 被用于不同内容。
|
|
24
35
|
- 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
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Bohrium Widget 开发者 SDK:在 Widget Bundle 内与 Host 建立 Bridge,并暴露生命周期与能力协议。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@dp-bohrium/widget-contracts": "0.
|
|
51
|
+
"@dp-bohrium/widget-contracts": "0.4.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@biomejs/biome": "^2.2.0",
|