@senguoyun/dsh-arkme 0.1.55 → 0.1.56
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/docs/dsh-remote-liveness.md +22 -0
- package/lib/client.js +74 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +376 -29
- package/lib/types/client/ArkmeFileViewer.d.ts +1 -1
- package/lib/types/client/ArkmeFileViewer.d.ts.map +1 -1
- package/lib/types/dsh-remote/gateway-api.d.ts +28 -0
- package/lib/types/dsh-remote/gateway-api.d.ts.map +1 -0
- package/lib/types/host-api.d.ts +2 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -44,3 +44,25 @@ Host 的单一消费者在上批完成后,从当前缓冲取下一批。空闲
|
|
|
44
44
|
默认 `live_delivery_window` 每 10 秒最多一条,复用 Host poll 和现有日志出口。含 user_id/runtime_ref、batch_count/event_count/incomplete_batches/replayed_batches、payload_bytes、pending_entries_max/pending_bytes_max,以及 queue_ms、ownership_ms、capture_ms、channel_queue_ms、publish_ack_ms 的 sum/max。capture_ms 包含 outbox 队列等待及落盘调用;publish_ack_ms 包含分片、重试和等待确认,不是单次网络 RTT。replay 的 queue_ms 是原积压起点计算的保守年龄。incomplete_batches 表示未完整确认的批次;event_count 是尝试处理数量,不是已确认数量。指标使用单调时钟;不包含事件正文或凭据,不挤占连接故障的 32 条上下文,不新增 Sentry 连接事故。
|
|
45
45
|
|
|
46
46
|
受控性能回归:`pnpm exec vitest run tests/dsh-live-batching-benchmark.test.ts --disableConsoleIntercept`。它对比相同生成流和模拟归档/ACK,不能替代实际 Profile、设备和网络验收。
|
|
47
|
+
|
|
48
|
+
## DSH Gateway 兼容
|
|
49
|
+
|
|
50
|
+
旧版通过 `apiProxy` 创建远控 Host;新版通过 `typertGateway`、`sessionController`、`workspaceController` 和 `connection` 创建同一个 Host。两者仍由账号作用域持有,不改变 Realtime 协议、租约、心跳和重连策略。功能关闭或依赖缺失时,Host API 返回具体原因;普通业务请求不执行远控诊断探测。
|
|
51
|
+
|
|
52
|
+
新版在 `gateway-api.ts` 适配公共接口:
|
|
53
|
+
|
|
54
|
+
- `session.list` 使用 Gateway 的 `_request` 命名参数;发送把已有稳定 RPC ID 映射为 `requestId`,不另造去重标识。
|
|
55
|
+
- 工作区读取消费 `workspace.follow` 的首个 baseline 后立即取消、关闭迭代器;历史读取从 `session.follow` 获取确定水位,再以 `throughSeq` 调用 `session.page`。读取不恢复 Agent。
|
|
56
|
+
- 实时日志使用一条公开 `session/event` 订阅;`session.control` 提供基线和 goal 投影;Gateway `$events` 提供问题和审批,结果通过公开的进程内 Connection Fetch carrier 回传。不会对每个历史会话建立 follow 流。
|
|
57
|
+
- 交互必须匹配本代事件 ID 和 Session;过期事件不可回答。保留原有审批安全限制:上下文不完整时只允许拒绝,不能远程批准。
|
|
58
|
+
- 本地事件缓冲最多 1024 帧、8 MiB,待处理交互最多 256 个。超限使本代失败,交由已有订阅恢复逻辑重新建立基线,不新增重连 owner。取消时移除日志监听、取消两条流并清除本代交互。
|
|
59
|
+
|
|
60
|
+
该适配转发新版的已提交 canonical 事件。新版进程内 `assistant-stream` 的无序号临时片段不伪装为 canonical 日志;逐 token 展示不在本次协议适配的验收范围。
|
|
61
|
+
|
|
62
|
+
能力面保持原合同:Host 是唯一远控业务 owner;现有 SDK、UI 和 `arkme_current_dsh_session` 工具继续消费同一 Host,无新增业务入口。客户端和 DSH 源码无需修改,桌面生效仍需正式插件制品发布与客户端消费。
|
|
63
|
+
|
|
64
|
+
回归:`NODE_OPTIONS=--no-experimental-webstorage pnpm test`、`pnpm typecheck`、`pnpm build`。`dsh-remote-gateway-api.test.ts` 覆盖参数映射、分页水位、事件和交互、取消、积压上限;`dsh-remote-host-api-sdk.test.ts` 覆盖缺失依赖诊断。旧版适配器和 WebSocket 保活测试继续运行。
|
|
65
|
+
|
|
66
|
+
2026-09-16 在独立数据目录、独立凭据前缀、空闲端口的未修改 DSH `0.1.5-rc.2` 中安装本地 `.tgz`:Host 成功创建,未登录时返回登录提示;真实 Gateway 验证了工作区、创建/列举 Session、历史首屏和分页、发送参数进入业务校验,以及问题回答和审批拒绝的往返。审批探针走公共 scoped waterfall,不执行工具。该 CLI 探针没有使用生产账号发送提示词,未验证真实模型响应、生产 Realtime 注册/心跳、移动端消费或 Windows/Linux。
|
|
67
|
+
|
|
68
|
+
随后使用客户端官方 master 基线 `f5e0c00` 启动独立 App Data 的 macOS 开发实例,加载本次插件与 DSH `0.1.5-rc.2`。实例通过正常登录态读取完成生产 Realtime 注册,`remote.getStatus` 返回 `available/enabled/connected=true`。该证据证明连接和注册,实际远程操作由用户在此常驻实例回归;开发路径运行不等同于正式 Release Set 发布。
|
package/lib/client.js
CHANGED
|
@@ -5934,7 +5934,7 @@ window.__ModuleLoader__.load({
|
|
|
5934
5934
|
qrcode.stringToBytes;
|
|
5935
5935
|
//#endregion
|
|
5936
5936
|
//#region package.json
|
|
5937
|
-
var version$3 = "0.1.
|
|
5937
|
+
var version$3 = "0.1.56";
|
|
5938
5938
|
//#endregion
|
|
5939
5939
|
//#region src/client/arkme-billing.css?inline
|
|
5940
5940
|
var arkme_billing_default = ".arkme-billing-backdrop {\n z-index: 120;\n backdrop-filter: blur(3px);\n background: #181b223d;\n place-items: center;\n padding: 28px;\n display: grid;\n position: fixed;\n inset: 0;\n}\n\n.arkme-billing-recharge-dialog, .arkme-billing-payment-dialog {\n color: #20232c;\n background: #fff;\n border: 1px solid #dedfe3;\n border-radius: 20px;\n width: min(620px, 100%);\n max-height: min(720px, 100vh - 56px);\n overflow-y: auto;\n box-shadow: 0 24px 72px #191c232e;\n}\n\n.arkme-billing-payment-dialog {\n width: min(420px, 100%);\n}\n\n.arkme-billing-dialog-header {\n border-bottom: 1px solid #ececef;\n justify-content: space-between;\n align-items: flex-start;\n gap: 18px;\n min-height: 78px;\n padding: 20px 22px 16px;\n display: flex;\n}\n\n.arkme-billing-dialog-header h2 {\n margin: 0;\n font-size: 18px;\n font-weight: 600;\n line-height: 25px;\n}\n\n.arkme-billing-dialog-header p {\n overflow-wrap: anywhere;\n color: #8a8e96;\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 18px;\n}\n\n.arkme-billing-dialog-header > button {\n color: #777c85;\n width: 32px;\n height: 32px;\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 9px;\n flex: none;\n place-items: center;\n font-size: 22px;\n display: grid;\n}\n\n.arkme-billing-dialog-header > button:hover {\n color: #20232c;\n background: #f3f3f5;\n}\n\n.arkme-billing-dialog-body {\n padding: 18px 22px 22px;\n}\n\n.arkme-billing-dialog-balance {\n border-bottom: 1px solid #ececef;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n column-gap: 12px;\n min-height: 48px;\n padding: 0 2px 16px;\n display: grid;\n}\n\n.arkme-billing-dialog-balance > span {\n color: #8a8e96;\n grid-column: 1;\n font-size: 11px;\n line-height: 16px;\n}\n\n.arkme-billing-dialog-balance > strong {\n grid-column: 1;\n font-size: 22px;\n font-weight: 600;\n line-height: 28px;\n}\n\n.arkme-billing-dialog-balance > button, .arkme-billing-error > button {\n color: #626772;\n height: 30px;\n font: inherit;\n cursor: pointer;\n background: #fff;\n border: 1px solid #dfe1e5;\n border-radius: 9px;\n grid-area: 1 / 2 / span 2;\n padding: 0 12px;\n font-size: 11px;\n}\n\n.arkme-billing-products-section, .arkme-billing-payment-section {\n padding-top: 17px;\n}\n\n.arkme-billing-products-section h3, .arkme-billing-payment-section h3 {\n color: #747984;\n margin: 0 0 11px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n\n.arkme-billing-product-grid {\n width: 100%;\n}\n\n.arkme-billing-product-grid > button {\n color: #20232c;\n min-width: 0;\n min-height: 62px;\n font: inherit;\n text-align: left;\n cursor: pointer;\n background: #fff;\n border: 1px solid #dfe1e5;\n border-radius: 11px;\n align-content: center;\n gap: 4px;\n padding: 9px 11px;\n display: grid;\n}\n\n.arkme-billing-product-grid > button:hover {\n background: #f8f8f9;\n border-color: #cfd2d8;\n}\n\n.arkme-billing-product-grid > button.is-selected {\n border-color: #75839e;\n box-shadow: 0 0 0 1px #75839e;\n}\n\n.arkme-billing-product-grid > button > span {\n white-space: nowrap;\n text-overflow: ellipsis;\n font-size: 12px;\n font-weight: 500;\n line-height: 17px;\n overflow: hidden;\n}\n\n.arkme-billing-product-grid > button > strong {\n font-size: 14px;\n font-weight: 600;\n line-height: 19px;\n}\n\n.arkme-billing-payment-actions {\n flex-wrap: wrap;\n gap: 10px;\n display: flex;\n}\n\n.arkme-billing-payment-actions > button {\n color: #333842;\n min-width: 154px;\n height: 38px;\n font: inherit;\n cursor: pointer;\n background: #fff;\n border: 1px solid #dfe1e5;\n border-radius: 10px;\n justify-content: center;\n align-items: center;\n gap: 8px;\n padding: 0 14px;\n font-size: 12px;\n display: inline-flex;\n}\n\n.arkme-billing-payment-actions > button:hover {\n background: #f7f7f8;\n border-color: #cfd3da;\n}\n\n.arkme-billing-payment-actions > button:disabled, .arkme-billing-product-grid > button:disabled {\n opacity: .48;\n cursor: default;\n}\n\n.arkme-billing-platform-icon {\n flex: none;\n width: 18px;\n height: 18px;\n}\n\n.arkme-billing-platform-icon.is-alipay {\n color: #1677ff;\n}\n\n.arkme-billing-platform-icon.is-wechat {\n color: #07c160;\n}\n\n.arkme-billing-state {\n color: #858992;\n margin: 12px 0 2px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.arkme-billing-error {\n color: #b54841;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n margin-top: 10px;\n font-size: 12px;\n line-height: 18px;\n display: flex;\n}\n\n.arkme-billing-error.is-purchase {\n justify-content: flex-start;\n}\n\n.arkme-billing-payment-content {\n text-align: center;\n justify-items: center;\n gap: 10px;\n padding: 28px 24px 18px;\n display: grid;\n}\n\n.arkme-billing-payment-content > .arkme-billing-platform-icon {\n width: 38px;\n height: 38px;\n margin-bottom: 4px;\n}\n\n.arkme-billing-payment-content > img {\n border-radius: 10px;\n width: 210px;\n height: 210px;\n}\n\n.arkme-billing-payment-content > strong {\n font-size: 14px;\n font-weight: 600;\n line-height: 21px;\n}\n\n.arkme-billing-payment-content > p {\n color: #858992;\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n}\n\n.arkme-billing-payment-content.is-paid > strong {\n color: #16794f;\n font-size: 18px;\n line-height: 25px;\n}\n\n.arkme-billing-payment-content.is-paid > .arkme-billing-error {\n justify-content: center;\n}\n\n.arkme-billing-payment-content.is-crediting > strong {\n color: #335f9e;\n}\n\n.arkme-billing-dialog-actions {\n flex-wrap: wrap;\n justify-content: center;\n gap: 9px;\n padding: 0 22px 20px;\n display: flex;\n}\n\n.arkme-billing-dialog-actions > button, .arkme-billing-primary-action {\n color: #3c424d;\n height: 34px;\n font: inherit;\n cursor: pointer;\n background: #fff;\n border: 1px solid #dfe1e5;\n border-radius: 9px;\n padding: 0 14px;\n font-size: 12px;\n}\n\n.arkme-billing-dialog-actions > .arkme-billing-primary-action, .arkme-billing-payment-content > .arkme-billing-primary-action {\n color: #fff;\n background: #20232d;\n border-color: #20232d;\n}\n";
|
|
@@ -49976,10 +49976,23 @@ html body {
|
|
|
49976
49976
|
if (block.kind === "video") return "视频";
|
|
49977
49977
|
return "文件";
|
|
49978
49978
|
}
|
|
49979
|
-
function arkmeClipboardImageBlob(blob
|
|
49980
|
-
const
|
|
49981
|
-
const
|
|
49982
|
-
|
|
49979
|
+
async function arkmeClipboardImageBlob(blob) {
|
|
49980
|
+
const canvas = document.createElement("canvas");
|
|
49981
|
+
const bitmap = await createImageBitmap(blob);
|
|
49982
|
+
try {
|
|
49983
|
+
canvas.width = bitmap.width;
|
|
49984
|
+
canvas.height = bitmap.height;
|
|
49985
|
+
const context = canvas.getContext("2d");
|
|
49986
|
+
if (context === null) throw new Error("图片转换失败");
|
|
49987
|
+
context.drawImage(bitmap, 0, 0);
|
|
49988
|
+
return await new Promise((resolve, reject) => {
|
|
49989
|
+
canvas.toBlob((value) => value === null ? reject(/* @__PURE__ */ new Error("图片转换失败")) : resolve(value), "image/png");
|
|
49990
|
+
});
|
|
49991
|
+
} finally {
|
|
49992
|
+
bitmap.close();
|
|
49993
|
+
canvas.width = 0;
|
|
49994
|
+
canvas.height = 0;
|
|
49995
|
+
}
|
|
49983
49996
|
}
|
|
49984
49997
|
function FileReceptionProgress({ reception, fileName, noun = "文件" }) {
|
|
49985
49998
|
const percent = reception.totalBytes > 0 ? Math.max(0, Math.min(100, Math.round(reception.receivedBytes / reception.totalBytes * 100))) : void 0;
|
|
@@ -50114,21 +50127,23 @@ html body {
|
|
|
50114
50127
|
save
|
|
50115
50128
|
};
|
|
50116
50129
|
}
|
|
50117
|
-
function useArkmeImageCopy(block,
|
|
50130
|
+
function useArkmeImageCopy(block, sources, onNotice) {
|
|
50131
|
+
const { localOriginalRef, remoteOriginalRef, previewUrl } = sources;
|
|
50132
|
+
const imageIdentity = block.fileAssetUid ?? block.mediaRef;
|
|
50118
50133
|
const [copying, setCopying] = (0, react$1.useState)(false);
|
|
50119
50134
|
const copyController = (0, react$1.useRef)();
|
|
50120
|
-
(0, react$1.useEffect)(() =>
|
|
50121
|
-
|
|
50122
|
-
|
|
50123
|
-
|
|
50124
|
-
|
|
50125
|
-
|
|
50126
|
-
]);
|
|
50135
|
+
(0, react$1.useEffect)(() => {
|
|
50136
|
+
setCopying(false);
|
|
50137
|
+
return () => {
|
|
50138
|
+
copyController.current?.abort();
|
|
50139
|
+
copyController.current = void 0;
|
|
50140
|
+
};
|
|
50141
|
+
}, [imageIdentity]);
|
|
50127
50142
|
const copy = async () => {
|
|
50128
|
-
if (block.kind !== "image" ||
|
|
50143
|
+
if (block.kind !== "image" || copyController.current !== void 0) return;
|
|
50129
50144
|
const clipboardWrite = typeof navigator === "undefined" ? void 0 : navigator.clipboard?.write?.bind(navigator.clipboard);
|
|
50130
50145
|
const ClipboardItemConstructor = typeof ClipboardItem === "undefined" ? void 0 : ClipboardItem;
|
|
50131
|
-
if (clipboardWrite === void 0 || ClipboardItemConstructor === void 0 || typeof fetch === "undefined" ||
|
|
50146
|
+
if (clipboardWrite === void 0 || ClipboardItemConstructor === void 0 || typeof fetch === "undefined" || localOriginalRef === void 0 && remoteOriginalRef === void 0 && previewUrl === void 0) {
|
|
50132
50147
|
onNotice?.({
|
|
50133
50148
|
message: "复制失败",
|
|
50134
50149
|
kind: "error"
|
|
@@ -50143,11 +50158,30 @@ html body {
|
|
|
50143
50158
|
kind: "progress"
|
|
50144
50159
|
});
|
|
50145
50160
|
try {
|
|
50146
|
-
const
|
|
50147
|
-
|
|
50148
|
-
|
|
50149
|
-
|
|
50150
|
-
|
|
50161
|
+
const urls = [...new Set([
|
|
50162
|
+
localOriginalRef === void 0 ? void 0 : arkmeLocalFileUrl(localOriginalRef),
|
|
50163
|
+
remoteOriginalRef === void 0 ? void 0 : `/arkme-self/api/media?ref=${encodeURIComponent(remoteOriginalRef)}`,
|
|
50164
|
+
previewUrl
|
|
50165
|
+
].filter((url) => url !== void 0))];
|
|
50166
|
+
const prepare = async () => {
|
|
50167
|
+
for (const url of urls) try {
|
|
50168
|
+
controller.signal.throwIfAborted();
|
|
50169
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
50170
|
+
if (!response.ok) throw new Error("图片不可用");
|
|
50171
|
+
const blob = await response.blob();
|
|
50172
|
+
controller.signal.throwIfAborted();
|
|
50173
|
+
const image = await arkmeClipboardImageBlob(blob);
|
|
50174
|
+
controller.signal.throwIfAborted();
|
|
50175
|
+
return image;
|
|
50176
|
+
} catch (error) {
|
|
50177
|
+
controller.signal.throwIfAborted();
|
|
50178
|
+
if (url === urls.at(-1)) throw error;
|
|
50179
|
+
}
|
|
50180
|
+
throw new Error("图片不可用");
|
|
50181
|
+
};
|
|
50182
|
+
const image = prepare();
|
|
50183
|
+
image.catch(() => {});
|
|
50184
|
+
await clipboardWrite([new ClipboardItemConstructor({ "image/png": image })]);
|
|
50151
50185
|
controller.signal.throwIfAborted();
|
|
50152
50186
|
onNotice?.({
|
|
50153
50187
|
message: "已复制",
|
|
@@ -50159,7 +50193,11 @@ html body {
|
|
|
50159
50193
|
kind: "error"
|
|
50160
50194
|
});
|
|
50161
50195
|
} finally {
|
|
50162
|
-
|
|
50196
|
+
controller.abort();
|
|
50197
|
+
if (copyController.current === controller) {
|
|
50198
|
+
copyController.current = void 0;
|
|
50199
|
+
setCopying(false);
|
|
50200
|
+
}
|
|
50163
50201
|
}
|
|
50164
50202
|
};
|
|
50165
50203
|
return {
|
|
@@ -50321,10 +50359,10 @@ html body {
|
|
|
50321
50359
|
})
|
|
50322
50360
|
});
|
|
50323
50361
|
}
|
|
50324
|
-
function ImageCopyAction({ block,
|
|
50325
|
-
const { copying, copy } = useArkmeImageCopy(block,
|
|
50362
|
+
function ImageCopyAction({ block, sources, onNotice }) {
|
|
50363
|
+
const { copying, copy } = useArkmeImageCopy(block, sources, onNotice);
|
|
50326
50364
|
if (block.kind !== "image") return null;
|
|
50327
|
-
const unavailable =
|
|
50365
|
+
const unavailable = sources.localOriginalRef === void 0 && sources.remoteOriginalRef === void 0 && sources.previewUrl === void 0;
|
|
50328
50366
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
50329
50367
|
type: "button",
|
|
50330
50368
|
"aria-label": "复制图片",
|
|
@@ -50349,7 +50387,11 @@ html body {
|
|
|
50349
50387
|
}
|
|
50350
50388
|
function ArkmeFileActions({ block, original, copySourceUrl, onImageCopyNotice, showDownloadStatus = true, hideDownloadAfterSave = true, style }) {
|
|
50351
50389
|
const download = useArkmeFileDownload(block, original);
|
|
50352
|
-
const
|
|
50390
|
+
const sources = {
|
|
50391
|
+
localOriginalRef: original.localRef ?? block.localFileRef,
|
|
50392
|
+
remoteOriginalRef: block.originalRef,
|
|
50393
|
+
previewUrl: copySourceUrl ?? (block.mediaRef === "" || block.mediaRef === block.localFileRef ? void 0 : `/arkme-self/api/media?ref=${encodeURIComponent(block.mediaRef)}`)
|
|
50394
|
+
};
|
|
50353
50395
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
50354
50396
|
style: {
|
|
50355
50397
|
...fileActionGroupStyle,
|
|
@@ -50357,7 +50399,7 @@ html body {
|
|
|
50357
50399
|
},
|
|
50358
50400
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageCopyAction, {
|
|
50359
50401
|
block,
|
|
50360
|
-
|
|
50402
|
+
sources,
|
|
50361
50403
|
onNotice: onImageCopyNotice
|
|
50362
50404
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileDownloadAction, {
|
|
50363
50405
|
block,
|
|
@@ -50633,7 +50675,11 @@ html body {
|
|
|
50633
50675
|
}),
|
|
50634
50676
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageCopyAction, {
|
|
50635
50677
|
block,
|
|
50636
|
-
|
|
50678
|
+
sources: {
|
|
50679
|
+
localOriginalRef: original.localRef ?? block.localFileRef,
|
|
50680
|
+
remoteOriginalRef: block.originalRef,
|
|
50681
|
+
previewUrl: block.mediaRef === "" || block.mediaRef === block.localFileRef ? void 0 : `/arkme-self/api/media?ref=${encodeURIComponent(block.mediaRef)}`
|
|
50682
|
+
},
|
|
50637
50683
|
onNotice: showActionNotice
|
|
50638
50684
|
}),
|
|
50639
50685
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -89604,7 +89650,7 @@ yoga yokohama you youtube yt yun za zappos zara zero zip zm zone zuerich zw
|
|
|
89604
89650
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeFileActions, {
|
|
89605
89651
|
block: selected,
|
|
89606
89652
|
original,
|
|
89607
|
-
copySourceUrl:
|
|
89653
|
+
copySourceUrl: previewUrl ?? (selected.mediaRef === selected.localFileRef ? void 0 : `${mediaRoute$1}?ref=${encodeURIComponent(selected.mediaRef)}`),
|
|
89608
89654
|
onImageCopyNotice: showActionNotice,
|
|
89609
89655
|
showDownloadStatus: false,
|
|
89610
89656
|
hideDownloadAfterSave: false,
|