dsh-packycode-api 1.2.4 → 1.2.6
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 +13 -0
- package/README.en.md +4 -2
- package/README.md +4 -2
- package/lib/client.js +295 -14
- package/lib/index.js +213 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.6
|
|
4
|
+
|
|
5
|
+
- Clicking black space around a large image now closes the preview, including the canvas inside the full-size dialog. The image hit area fits its actual displayed dimensions instead of covering letterboxing; image clicks, download controls and error notices keep the viewer open.
|
|
6
|
+
- Added a regression for canvas/dialog whitespace and nested button icons. Browser verification reproduced the old no-op on the black image margin, then confirmed the same click dismisses the updated viewer and restores focus, while clicking the image keeps it open.
|
|
7
|
+
- Validation, 2026-10-15: 412 tests, both rebuilt bundles and the versioned tarball. No paid image request or DSH restart was needed.
|
|
8
|
+
|
|
9
|
+
## 1.2.5
|
|
10
|
+
|
|
11
|
+
- Fixed generated-image clicks doing nothing in DSH desktop. The optional `nativeImageViewer` service is absent in the current desktop build, and its popup policy denies the previous `window.open(blob:...)` fallback. PackyCode now owns an in-app large-image overlay shared by main-chat and tool-detail results, with Escape/close, focus restoration and viewport fitting.
|
|
12
|
+
- Added **Download original** immediately to the left of the overlay's Close button. It opens a save-location picker and transfers the preserved PNG rather than DSH's normalized chat preview. Desktop builds without File System Access use Electron's normal download/save-dialog routine without a preset destination. Cancellation does not become an error or start another download.
|
|
13
|
+
- Original reads resolve native/PTC/Claude image evidence from a read-only session observation, accept no client-supplied paths and stay inside the plugin's original-image directory. The bounded chunk transfer checks the complete SHA-256 before writing; closing the viewer cancels pending reads, and missing/corrupt originals remain retryable without substituting the preview.
|
|
14
|
+
- Validation, 2026-10-15: 411 offline tests, both bundles and the tarball; real-browser verification of the built client confirmed large-image opening, uncropped fitting, Download original left of Close, Escape and focus restoration. Save-picker invocation, cancellation, exact original-byte writes and interrupted/corrupt transfers are covered by automated regressions. Native save-dialog interaction remains a manual DSH check because the current computer-use interface cannot control the Codex app's native chooser. No paid image request or DSH restart was performed.
|
|
15
|
+
|
|
3
16
|
## 1.2.4
|
|
4
17
|
|
|
5
18
|
- Added the current image key's remaining quota immediately after the Create image shortcut. Hovering or focusing it shows the key label, account name, remaining/used/total quota and update time. It shares the shortcut's visibility, refreshes every minute while mounted and after image-setting/key changes, and marks stale or failed reads without confusing them with zero credit. Real React lifecycle regressions cover provider switches, image availability, disabled/missing keys and late responses.
|
package/README.en.md
CHANGED
|
@@ -51,7 +51,7 @@ Open the desktop application's **Plugins** page, click **Add plugin**, and enter
|
|
|
51
51
|
dsh-packycode-api
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.2.
|
|
54
|
+
Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.2.6`.
|
|
55
55
|
|
|
56
56
|
A **first install requires no DSH restart**: open **Settings → PackyCode** and add a key immediately. When updating an already loaded older version, follow the restart guidance in the **Plugin version** card.
|
|
57
57
|
|
|
@@ -70,7 +70,7 @@ Choose a model from the `PackyCode` group. `deepseek-flash`, `gpt-6.1-sol`, `gpt
|
|
|
70
70
|
|
|
71
71
|
The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
|
|
72
72
|
|
|
73
|
-
**For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.
|
|
73
|
+
**For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.6`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
|
|
74
74
|
|
|
75
75
|
For a local checkout, keep the `link:` prefix: `dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`. **Do not pass a bare path or `file:`** — that install copies the whole checkout, `node_modules` included, into the profile, where it then shadows the packages the DSH installation itself provides and the plugin looks uninstalled; see the FAQ. If you already installed that way, uninstall first and reinstall as above.
|
|
76
76
|
|
|
@@ -156,6 +156,8 @@ Tool callers can set `model` and `quality` to `settings` to follow the saved pre
|
|
|
156
156
|
|
|
157
157
|
Image 2 accepts `auto / low / medium / high`; both 2.5 models also accept `xhigh / max`. These are Images API `quality` values, separate from text reasoning effort. There is no automatic model substitution, quality downgrade or paid retry. Each call returns one PNG; original bytes are saved under `~/.dsh/packycode-api/images/` (following `DSH_HOME` when set), alongside the chat preview. References and optional PNG masks must belong to the current session; file paths cannot replace attachment references.
|
|
158
158
|
|
|
159
|
+
Click a generated image to open the in-app large preview. **Download original**, immediately left of Close, opens a save-location picker for the complete preserved PNG. Cancelling is harmless; a moved or deleted original reports a retryable error instead of downloading the chat thumbnail. Saved conversations also support this as long as their original file is still on this host.
|
|
160
|
+
|
|
159
161
|
After adding the image key, `node scripts/image-check.mjs --all --edit` checks all three engines and one edit. Use `--model <id> --edit` for generation and editing on a specific engine, adding `--quality xhigh` or `--quality max` to check a quality. This consumes image quota, saves outputs under `.artifacts/`, and prints only a masked key tail plus the gateway-reported model/quality.
|
|
160
162
|
|
|
161
163
|
Live checks on 2026-10-15 passed generation and editing at `low` on all three engines. Sunburst also passed both operations at `xhigh` and `max`, with matching quality values in the responses. All 10 requests succeeded without retries and returned 1024×1024 PNGs; visual inspection confirmed the requested color edits with composition preserved. Generate/edit times were about 43/48 seconds at xhigh and 84/91 seconds at max. These are built-client gateway checks; no desktop conversation was created to verify the full chat flow in this run. See the [protocol research and gateway results](docs/research/image-api.md) for scope and evidence.
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
dsh-packycode-api
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.2.
|
|
54
|
+
在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.2.6`。
|
|
55
55
|
|
|
56
56
|
首次安装后**无需重启 DSH**,可直接打开**设置 → PackyCode** 添加 Key。更新已加载的旧版时,按「插件版本」卡片的重启提示操作。
|
|
57
57
|
|
|
@@ -70,7 +70,7 @@ dsh-packycode-api
|
|
|
70
70
|
|
|
71
71
|
桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
|
|
72
72
|
|
|
73
|
-
**仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.
|
|
73
|
+
**仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.6`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
|
|
74
74
|
|
|
75
75
|
本地开发要加 `link:` 前缀:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。**不要直接写路径(或 `file:`)** —— 那样的安装会把整个仓库目录(连同 `node_modules`)复制进 profile,之后它会在 profile 内部挡住 DSH 自带的包,插件看起来像没装,见「常见问题」。已经那样装过的话,先卸载再按上面的方式重装。
|
|
76
76
|
|
|
@@ -156,6 +156,8 @@ Claude Code CLI → 本机临时 MCP web_search → 插件选定的 DeepSeek/GPT
|
|
|
156
156
|
|
|
157
157
|
`gpt-image-2` 的质量为 `auto / low / medium / high`;两款 2.5 另支持 `xhigh / max`。质量按 Images API 的 `quality` 原样发送,不作为文字推理强度;不会自动换型号、降档或重试付费请求。输出先采用 PNG,每次一张,聊天预览之外的原图保存在 `~/.dsh/packycode-api/images/`(设置了 `DSH_HOME` 时跟随该目录)。参考图与可选 PNG mask 必须来自当前会话;不能用路径替代附件引用。
|
|
158
158
|
|
|
159
|
+
点击生成图可在应用内查看大图,右上角关闭按钮左侧的「下载原图」可选择保存位置,另存本机保留的完整 PNG。取消保存不会报错;若原图已被移走或删除,会提示失败,不会改为下载聊天缩略图。已有会话也可使用此功能,前提是记录对应的本机原图仍存在。
|
|
160
|
+
|
|
159
161
|
用户填入图片 Key 后,可运行 `node scripts/image-check.mjs --all --edit` 验证三个型号与一次编辑;用 `--model <型号> --edit` 单独验证某个型号的生图和编辑,搭配 `--quality xhigh` 或 `--quality max` 验证质量。测试会消耗图片额度,输出写入 `.artifacts/`,日志只显示 Key 的掩码尾号,并记录网关回报的型号/质量。
|
|
160
162
|
|
|
161
163
|
2026-10-15 实测:三个型号均通过 `low` 生图与编辑;Sunburst 额外通过 `xhigh`、`max` 生图与编辑,两档响应的 quality 均与请求一致。合计 10 次成功请求,无重试,全部返回 1024×1024 PNG;编辑正确改色并保留构图。xhigh 生成/编辑约 43/48 秒,max 约 84/91 秒。这是已构建图片 client 的网关验证;本轮未新建 DSH 对话验证完整聊天流程。完整范围和记录见[协议核查与网关实测](docs/research/image-api.md)。
|
package/lib/client.js
CHANGED
|
@@ -74,6 +74,11 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
imagesOpen: "打开图片",
|
|
75
75
|
imagesRetryPreview: "预览加载失败,点击重试",
|
|
76
76
|
imagesLoadingPreview: "正在加载图片…",
|
|
77
|
+
imagesViewerTitle: "图片预览",
|
|
78
|
+
imagesClosePreview: "关闭图片预览",
|
|
79
|
+
imagesDownloadOriginal: "下载原图",
|
|
80
|
+
imagesDownloadPreparing: "正在准备原图…",
|
|
81
|
+
imagesDownloadFailed: "原图保存失败。请确认本机原图仍存在,然后点击“下载原图”重试。",
|
|
77
82
|
refresh: "刷新",
|
|
78
83
|
refreshing: "刷新中…",
|
|
79
84
|
totalRemaining: "合计剩余",
|
|
@@ -251,6 +256,11 @@ window.__ModuleLoader__.load({
|
|
|
251
256
|
imagesOpen: "Open image",
|
|
252
257
|
imagesRetryPreview: "Preview failed; click to retry",
|
|
253
258
|
imagesLoadingPreview: "Loading image…",
|
|
259
|
+
imagesViewerTitle: "Image preview",
|
|
260
|
+
imagesClosePreview: "Close image preview",
|
|
261
|
+
imagesDownloadOriginal: "Download original",
|
|
262
|
+
imagesDownloadPreparing: "Preparing original…",
|
|
263
|
+
imagesDownloadFailed: "Could not save the original. Check that the original file still exists on this device, then try Download original again.",
|
|
254
264
|
refresh: "Refresh",
|
|
255
265
|
refreshing: "Refreshing…",
|
|
256
266
|
totalRemaining: "Total remaining",
|
|
@@ -421,6 +431,18 @@ window.__ModuleLoader__.load({
|
|
|
421
431
|
.pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
|
|
422
432
|
.pkc-generated-image{display:grid;place-items:center;align-self:flex-start;max-width:100%;width:320px;min-height:80px;padding:0;overflow:hidden;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:zoom-in}
|
|
423
433
|
.pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
|
|
434
|
+
.pkc-image-backdrop{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;padding:20px;background:rgba(0,0,0,.84);box-sizing:border-box}
|
|
435
|
+
.pkc-image-viewer{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;min-height:0;color:#f5f5f5;font:13px/1.5 var(--ds-font-family,system-ui,sans-serif)}
|
|
436
|
+
.pkc-image-viewer-toolbar{display:flex;align-items:center;gap:8px;flex:0 0 auto;min-height:40px;margin-bottom:12px}
|
|
437
|
+
.pkc-image-viewer-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgba(255,255,255,.75)}
|
|
438
|
+
.pkc-image-viewer-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;box-sizing:border-box;flex:none;height:36px;padding:0 12px;border:1px solid rgba(255,255,255,.18);border-radius:10px;background:rgba(255,255,255,.08);color:#f5f5f5;font:inherit;cursor:pointer;white-space:nowrap}
|
|
439
|
+
.pkc-image-viewer-button:hover:not(:disabled){background:rgba(255,255,255,.18)}
|
|
440
|
+
.pkc-image-viewer-button:focus-visible{outline:2px solid #a7c3ff;outline-offset:2px}
|
|
441
|
+
.pkc-image-viewer-button:disabled{opacity:.55;cursor:wait}
|
|
442
|
+
.pkc-image-viewer-close{width:36px;padding:0;border-color:transparent;background:transparent}
|
|
443
|
+
.pkc-image-viewer-canvas{display:flex;align-items:center;justify-content:center;flex:1;min-height:0;overflow:hidden}
|
|
444
|
+
.pkc-image-viewer-canvas img{display:block;flex:none;width:auto;height:auto;max-width:100%;max-height:100%;min-width:0;min-height:0;object-fit:contain}
|
|
445
|
+
.pkc-image-viewer-error{align-self:center;flex:none;margin:12px 0 0;padding:8px 12px;border-radius:8px;background:#4d2222;color:#ffd3d3;text-align:center}
|
|
424
446
|
.pkc-form{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
425
447
|
|
|
426
448
|
.pkc-chip{flex:none;padding:0 8px;border-radius:999px;font-size:11px;font-weight:500;line-height:18px;white-space:nowrap}
|
|
@@ -840,6 +862,260 @@ window.__ModuleLoader__.load({
|
|
|
840
862
|
});
|
|
841
863
|
}
|
|
842
864
|
//#endregion
|
|
865
|
+
//#region src/client-image-download.js
|
|
866
|
+
/** Original PNG transfer. The chat preview is never used as the download source. */
|
|
867
|
+
const MAX_BYTES = 48 * 1024 * 1024;
|
|
868
|
+
const CHUNK_BYTES = 256 * 1024;
|
|
869
|
+
function imageDownloadName(attachment) {
|
|
870
|
+
const name = (typeof attachment?.name === "string" ? attachment.name : "packycode-generated.png").replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/gu, "-").replace(/[. ]+$/u, "").trim().slice(0, 160);
|
|
871
|
+
return name === "" ? "packycode-generated.png" : name.replace(/\.(png|jpe?g|webp)$/iu, "") + ".png";
|
|
872
|
+
}
|
|
873
|
+
async function readOriginalImage(call, sessionId, attachmentId, { signal, onProgress } = {}) {
|
|
874
|
+
signal?.throwIfAborted();
|
|
875
|
+
const ref = await call("images/original", {
|
|
876
|
+
sessionId,
|
|
877
|
+
attachmentId
|
|
878
|
+
}, signal);
|
|
879
|
+
if (ref?.attachmentId !== attachmentId || ref?.mediaType !== "image/png" || !Number.isSafeInteger(ref.bytes) || ref.bytes < 24 || ref.bytes > MAX_BYTES || !/^[0-9a-f]{64}$/u.test(ref.sha256) || typeof ref.assetId !== "string") throw new Error("Original image metadata is invalid");
|
|
880
|
+
signal?.throwIfAborted();
|
|
881
|
+
const data = new Uint8Array(ref.bytes);
|
|
882
|
+
let offset = 0;
|
|
883
|
+
while (offset < data.length) {
|
|
884
|
+
signal?.throwIfAborted();
|
|
885
|
+
const chunk = await call("images/original/chunk", {
|
|
886
|
+
sessionId,
|
|
887
|
+
attachmentId,
|
|
888
|
+
assetId: ref.assetId,
|
|
889
|
+
offset
|
|
890
|
+
}, signal);
|
|
891
|
+
signal?.throwIfAborted();
|
|
892
|
+
if (chunk?.attachmentId !== attachmentId || chunk.assetId !== ref.assetId || chunk.offset !== offset || chunk.totalBytes !== ref.bytes || typeof chunk.encoded !== "string" || chunk.encoded.length === 0 || chunk.encoded.length > Math.ceil(CHUNK_BYTES / 3) * 4 || typeof chunk.done !== "boolean") throw new Error("Original image chunk is invalid");
|
|
893
|
+
const decoded = atob(chunk.encoded);
|
|
894
|
+
if (decoded.length === 0 || decoded.length > CHUNK_BYTES || offset + decoded.length > data.length) throw new Error("Original image chunk is invalid");
|
|
895
|
+
for (let index = 0; index < decoded.length; index += 1) data[offset + index] = decoded.charCodeAt(index);
|
|
896
|
+
offset += decoded.length;
|
|
897
|
+
if (chunk.done !== (offset === data.length)) throw new Error("Original image transfer is incomplete");
|
|
898
|
+
onProgress?.(Math.floor(offset / data.length * 100));
|
|
899
|
+
}
|
|
900
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
901
|
+
signal?.throwIfAborted();
|
|
902
|
+
if ([...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("") !== ref.sha256) throw new Error("Original image integrity check failed");
|
|
903
|
+
return data;
|
|
904
|
+
}
|
|
905
|
+
async function saveOriginalImage(read, attachment, { signal, onProgress } = {}) {
|
|
906
|
+
const name = imageDownloadName(attachment);
|
|
907
|
+
let destination;
|
|
908
|
+
if (typeof window.showSaveFilePicker === "function") try {
|
|
909
|
+
destination = await window.showSaveFilePicker({
|
|
910
|
+
suggestedName: name,
|
|
911
|
+
excludeAcceptAllOption: true,
|
|
912
|
+
types: [{
|
|
913
|
+
description: "PNG",
|
|
914
|
+
accept: { "image/png": [".png"] }
|
|
915
|
+
}]
|
|
916
|
+
});
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (error?.name !== "NotSupportedError") throw error;
|
|
919
|
+
}
|
|
920
|
+
signal?.throwIfAborted();
|
|
921
|
+
const data = await read({
|
|
922
|
+
signal,
|
|
923
|
+
onProgress
|
|
924
|
+
});
|
|
925
|
+
signal?.throwIfAborted();
|
|
926
|
+
if (destination !== void 0) {
|
|
927
|
+
const stream = await destination.createWritable();
|
|
928
|
+
try {
|
|
929
|
+
signal?.throwIfAborted();
|
|
930
|
+
await stream.write(data);
|
|
931
|
+
signal?.throwIfAborted();
|
|
932
|
+
await stream.close();
|
|
933
|
+
} catch (error) {
|
|
934
|
+
await stream.abort().catch(() => void 0);
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const url = URL.createObjectURL(new Blob([data], { type: "image/png" }));
|
|
940
|
+
const anchor = document.createElement("a");
|
|
941
|
+
anchor.href = url;
|
|
942
|
+
anchor.download = name;
|
|
943
|
+
document.body.append(anchor);
|
|
944
|
+
try {
|
|
945
|
+
anchor.click();
|
|
946
|
+
} finally {
|
|
947
|
+
anchor.remove();
|
|
948
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 6e4);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region src/client-image-viewer.jsx
|
|
953
|
+
/** One plugin-owned overlay; independent of optional host image viewers. */
|
|
954
|
+
function createImageViewer() {
|
|
955
|
+
let image;
|
|
956
|
+
let revision = 0;
|
|
957
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
958
|
+
const publish = (value) => {
|
|
959
|
+
image = value;
|
|
960
|
+
for (const listener of [...listeners]) listener();
|
|
961
|
+
};
|
|
962
|
+
return {
|
|
963
|
+
getSnapshot: () => image,
|
|
964
|
+
subscribe(listener) {
|
|
965
|
+
listeners.add(listener);
|
|
966
|
+
return () => listeners.delete(listener);
|
|
967
|
+
},
|
|
968
|
+
open: (request) => publish({
|
|
969
|
+
...request,
|
|
970
|
+
revision: ++revision
|
|
971
|
+
}),
|
|
972
|
+
close: () => publish(void 0),
|
|
973
|
+
dispose() {
|
|
974
|
+
publish(void 0);
|
|
975
|
+
listeners.clear();
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function ImageDialog({ image, viewer, t }) {
|
|
980
|
+
const dialog = react.useRef(null);
|
|
981
|
+
const close = react.useRef(null);
|
|
982
|
+
const download = react.useRef();
|
|
983
|
+
const [busy, setBusy] = react.useState(false);
|
|
984
|
+
const [progress, setProgress] = react.useState(0);
|
|
985
|
+
const [error, setError] = react.useState(false);
|
|
986
|
+
react.useEffect(() => {
|
|
987
|
+
const overflow = document.body.style.overflow;
|
|
988
|
+
document.body.style.overflow = "hidden";
|
|
989
|
+
close.current?.focus();
|
|
990
|
+
const onKey = (event) => {
|
|
991
|
+
if (event.key === "Escape") {
|
|
992
|
+
event.preventDefault();
|
|
993
|
+
event.stopPropagation();
|
|
994
|
+
viewer.close();
|
|
995
|
+
}
|
|
996
|
+
if (event.key === "Tab") {
|
|
997
|
+
const buttons = [...dialog.current?.querySelectorAll("button:not(:disabled),[tabindex=\"0\"]") ?? []];
|
|
998
|
+
const first = buttons[0], last = buttons.at(-1);
|
|
999
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
1000
|
+
event.preventDefault();
|
|
1001
|
+
last?.focus();
|
|
1002
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
1003
|
+
event.preventDefault();
|
|
1004
|
+
first?.focus();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
document.addEventListener("keydown", onKey, true);
|
|
1009
|
+
return () => {
|
|
1010
|
+
download.current?.abort();
|
|
1011
|
+
document.body.style.overflow = overflow;
|
|
1012
|
+
document.removeEventListener("keydown", onKey, true);
|
|
1013
|
+
if (image.opener?.isConnected !== false) image.opener?.focus?.();
|
|
1014
|
+
};
|
|
1015
|
+
}, [image, viewer]);
|
|
1016
|
+
const save = async () => {
|
|
1017
|
+
if (download.current !== void 0) return;
|
|
1018
|
+
const controller = new AbortController();
|
|
1019
|
+
download.current = controller;
|
|
1020
|
+
setBusy(true);
|
|
1021
|
+
setError(false);
|
|
1022
|
+
setProgress(0);
|
|
1023
|
+
try {
|
|
1024
|
+
await saveOriginalImage(image.readOriginal, image.attachment, {
|
|
1025
|
+
signal: controller.signal,
|
|
1026
|
+
onProgress: (value) => {
|
|
1027
|
+
if (!controller.signal.aborted) setProgress(value);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
} catch (failure) {
|
|
1031
|
+
if (!controller.signal.aborted && failure?.name !== "AbortError") setError(true);
|
|
1032
|
+
} finally {
|
|
1033
|
+
if (!controller.signal.aborted) {
|
|
1034
|
+
download.current = void 0;
|
|
1035
|
+
setBusy(false);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1040
|
+
className: "pkc-image-backdrop",
|
|
1041
|
+
onClick: (event) => {
|
|
1042
|
+
if (event.target.closest("img, button, [role=\"alert\"]") === null) viewer.close();
|
|
1043
|
+
},
|
|
1044
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1045
|
+
className: "pkc-image-viewer",
|
|
1046
|
+
role: "dialog",
|
|
1047
|
+
"aria-modal": "true",
|
|
1048
|
+
"aria-label": t("imagesViewerTitle"),
|
|
1049
|
+
ref: dialog,
|
|
1050
|
+
children: [
|
|
1051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1052
|
+
className: "pkc-image-viewer-toolbar",
|
|
1053
|
+
children: [
|
|
1054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1055
|
+
className: "pkc-image-viewer-name",
|
|
1056
|
+
children: image.attachment.name ?? t("imagesTitle")
|
|
1057
|
+
}),
|
|
1058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1059
|
+
type: "button",
|
|
1060
|
+
className: "pkc-image-viewer-button",
|
|
1061
|
+
disabled: busy,
|
|
1062
|
+
onClick: save,
|
|
1063
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1064
|
+
width: "16",
|
|
1065
|
+
height: "16",
|
|
1066
|
+
viewBox: "0 0 24 24",
|
|
1067
|
+
fill: "none",
|
|
1068
|
+
stroke: "currentColor",
|
|
1069
|
+
strokeWidth: "1.8",
|
|
1070
|
+
"aria-hidden": "true",
|
|
1071
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5" })
|
|
1072
|
+
}), busy ? `${t("imagesDownloadPreparing")} ${progress}%` : t("imagesDownloadOriginal")]
|
|
1073
|
+
}),
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1075
|
+
ref: close,
|
|
1076
|
+
type: "button",
|
|
1077
|
+
className: "pkc-image-viewer-button pkc-image-viewer-close",
|
|
1078
|
+
title: t("imagesClosePreview"),
|
|
1079
|
+
"aria-label": t("imagesClosePreview"),
|
|
1080
|
+
onClick: () => viewer.close(),
|
|
1081
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1082
|
+
width: "20",
|
|
1083
|
+
height: "20",
|
|
1084
|
+
viewBox: "0 0 24 24",
|
|
1085
|
+
fill: "none",
|
|
1086
|
+
stroke: "currentColor",
|
|
1087
|
+
strokeWidth: "1.8",
|
|
1088
|
+
"aria-hidden": "true",
|
|
1089
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12M18 6 6 18" })
|
|
1090
|
+
})
|
|
1091
|
+
})
|
|
1092
|
+
]
|
|
1093
|
+
}),
|
|
1094
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1095
|
+
className: "pkc-image-viewer-canvas",
|
|
1096
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1097
|
+
src: image.src,
|
|
1098
|
+
alt: image.attachment.name ?? t("imagesTitle")
|
|
1099
|
+
})
|
|
1100
|
+
}),
|
|
1101
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1102
|
+
className: "pkc-image-viewer-error",
|
|
1103
|
+
role: "alert",
|
|
1104
|
+
children: t("imagesDownloadFailed")
|
|
1105
|
+
})
|
|
1106
|
+
]
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
function PackyCodeImageViewer({ viewer, t }) {
|
|
1111
|
+
const image = react.useSyncExternalStore(viewer.subscribe, viewer.getSnapshot);
|
|
1112
|
+
return image === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageDialog, {
|
|
1113
|
+
image,
|
|
1114
|
+
viewer,
|
|
1115
|
+
t
|
|
1116
|
+
}, image.revision);
|
|
1117
|
+
}
|
|
1118
|
+
//#endregion
|
|
843
1119
|
//#region src/client-images.jsx
|
|
844
1120
|
const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
|
|
845
1121
|
/** Only projections come back from the host; the password draft is cleared on success. */
|
|
@@ -1276,26 +1552,31 @@ window.__ModuleLoader__.load({
|
|
|
1276
1552
|
}, block.toolCallId))
|
|
1277
1553
|
});
|
|
1278
1554
|
}
|
|
1279
|
-
function installImagePresentation(ctx, t) {
|
|
1555
|
+
function installImagePresentation(ctx, t, call) {
|
|
1280
1556
|
ctx.inject(["uiConversation"], (scope) => {
|
|
1281
1557
|
const conversation = scope.get("uiConversation");
|
|
1282
1558
|
if (typeof conversation?.events?.register !== "function" || typeof conversation?.imageUrl !== "function") return;
|
|
1559
|
+
const viewer = createImageViewer();
|
|
1560
|
+
scope.effect(() => () => viewer.dispose(), "packycode-api: image viewer");
|
|
1561
|
+
scope.slots.inject("shell.overlay", () => scope.slots.register({
|
|
1562
|
+
name: "shell.overlay",
|
|
1563
|
+
id: "packycode-image-viewer",
|
|
1564
|
+
order: 21,
|
|
1565
|
+
inject: () => ({
|
|
1566
|
+
viewer,
|
|
1567
|
+
t
|
|
1568
|
+
})
|
|
1569
|
+
}, PackyCodeImageViewer));
|
|
1283
1570
|
const imageProps = (sessionId) => ({
|
|
1284
1571
|
t,
|
|
1285
1572
|
loadImage: (attachment) => conversation.imageUrl(sessionId, attachment),
|
|
1286
1573
|
openImage: (src, attachment, opener) => {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
src,
|
|
1291
|
-
name: attachment.name ?? "packycode-generated.png",
|
|
1292
|
-
width: attachment.width,
|
|
1293
|
-
height: attachment.height
|
|
1294
|
-
}],
|
|
1574
|
+
viewer.open({
|
|
1575
|
+
src,
|
|
1576
|
+
attachment,
|
|
1295
1577
|
opener,
|
|
1296
|
-
|
|
1297
|
-
})
|
|
1298
|
-
window.open(src, "_blank", "noopener,noreferrer");
|
|
1578
|
+
readOriginal: (options) => readOriginalImage(call, sessionId, attachment.attachmentId, options)
|
|
1579
|
+
});
|
|
1299
1580
|
}
|
|
1300
1581
|
});
|
|
1301
1582
|
scope.effect(() => conversation.events.register(imageConversationNode), "packycode-api: image results in chat");
|
|
@@ -1355,7 +1636,7 @@ window.__ModuleLoader__.load({
|
|
|
1355
1636
|
* call carries an object.
|
|
1356
1637
|
*/
|
|
1357
1638
|
function channelCaller(rpc) {
|
|
1358
|
-
return (endpoint, payload = {}) => rpc.call(CHANNEL, endpoint, payload).then((response) => {
|
|
1639
|
+
return (endpoint, payload = {}, signal) => rpc.call(CHANNEL, endpoint, payload, signal).then((response) => {
|
|
1359
1640
|
if (!response?.ok) throw new Error(response?.error?.message ?? "PackyCode RPC failed");
|
|
1360
1641
|
return response.value;
|
|
1361
1642
|
});
|
|
@@ -2407,8 +2688,8 @@ window.__ModuleLoader__.load({
|
|
|
2407
2688
|
}, "packycode-api: style");
|
|
2408
2689
|
const connection = ctx.get("connection");
|
|
2409
2690
|
const t = ctx.locale.bind(NS);
|
|
2410
|
-
installImagePresentation(ctx, t);
|
|
2411
2691
|
const call = channelCaller(connection.rpc);
|
|
2692
|
+
installImagePresentation(ctx, t, call);
|
|
2412
2693
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2413
2694
|
name: "settings.section",
|
|
2414
2695
|
id: "packycode-api",
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute, join, posix, resolve, sep, win32 } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, posix, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
8
|
-
import { mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
11
|
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
@@ -4544,7 +4544,7 @@ const EDIT_MEDIA_TYPES = [
|
|
|
4544
4544
|
"image/webp"
|
|
4545
4545
|
];
|
|
4546
4546
|
const SIGNATURE = "89504e470d0a1a0a";
|
|
4547
|
-
const ATTACHMENT_ID$
|
|
4547
|
+
const ATTACHMENT_ID$2 = /^sha256:[0-9a-f]{64}$/u;
|
|
4548
4548
|
var ImageRequestError = class extends Error {};
|
|
4549
4549
|
const nonempty = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
4550
4550
|
/** Explicit absence, including zero-valued placeholders filled by some models. */
|
|
@@ -4746,13 +4746,13 @@ async function readReferences(values, attachments, messages, signal) {
|
|
|
4746
4746
|
const available = /* @__PURE__ */ new Map();
|
|
4747
4747
|
const visit = (content) => {
|
|
4748
4748
|
for (const block of content ?? []) {
|
|
4749
|
-
if (block?.type === "image" && ATTACHMENT_ID$
|
|
4749
|
+
if (block?.type === "image" && ATTACHMENT_ID$2.test(block.attachment?.attachmentId)) available.set(block.attachment.attachmentId, block.attachment);
|
|
4750
4750
|
if (block?.type === "tool-result" && Array.isArray(block.content)) visit(block.content);
|
|
4751
4751
|
}
|
|
4752
4752
|
};
|
|
4753
4753
|
for (const message of messages ?? []) if (Array.isArray(message?.content)) visit(message.content);
|
|
4754
4754
|
const ids = values.map((value) => value?.attachmentId);
|
|
4755
|
-
if (ids.some((id) => typeof id !== "string" || !ATTACHMENT_ID$
|
|
4755
|
+
if (ids.some((id) => typeof id !== "string" || !ATTACHMENT_ID$2.test(id) || !available.has(id))) throw new ImageRequestError("PackyCode image reference is missing from this session; use read_image and copy its complete attachment reference");
|
|
4756
4756
|
if (new Set(ids).size !== ids.length) throw new ImageRequestError("PackyCode image references must not contain duplicates");
|
|
4757
4757
|
const images = [];
|
|
4758
4758
|
let total = 0;
|
|
@@ -4970,7 +4970,7 @@ const MAX_CLI_IMAGE_CALLS = 4;
|
|
|
4970
4970
|
const MAX_SESSION_REFERENCES = 20;
|
|
4971
4971
|
const PREVIEW_BYTES = 1024 * 1024;
|
|
4972
4972
|
const PREVIEW_PIXELS = 1024 * 1024;
|
|
4973
|
-
const ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
|
|
4973
|
+
const ATTACHMENT_ID$1 = /^sha256:[0-9a-f]{64}$/u;
|
|
4974
4974
|
const FIELDS = [
|
|
4975
4975
|
"prompt",
|
|
4976
4976
|
"operation",
|
|
@@ -5050,7 +5050,7 @@ function createClaudeImageService({ sessionId, messages, client, attachments, ge
|
|
|
5050
5050
|
const visit = (content) => {
|
|
5051
5051
|
for (const block of content ?? []) {
|
|
5052
5052
|
const ref = block?.type === "image" ? block.attachment : void 0;
|
|
5053
|
-
if (ATTACHMENT_ID.test(ref?.attachmentId)) {
|
|
5053
|
+
if (ATTACHMENT_ID$1.test(ref?.attachmentId)) {
|
|
5054
5054
|
collected.delete(ref.attachmentId);
|
|
5055
5055
|
collected.set(ref.attachmentId, Object.fromEntries([
|
|
5056
5056
|
"attachmentId",
|
|
@@ -5644,6 +5644,185 @@ function createImageController({ ring, balance, readPreference, writePreference,
|
|
|
5644
5644
|
};
|
|
5645
5645
|
}
|
|
5646
5646
|
//#endregion
|
|
5647
|
+
//#region src/image-conversation.js
|
|
5648
|
+
/** Project this plugin's durable image results into chat without duplicating context. */
|
|
5649
|
+
const IMAGE_CHAT_KIND = "packycode-image-output";
|
|
5650
|
+
const IMAGE_TOOLS = /* @__PURE__ */ new Set(["packycode_image_generate", "claude_code"]);
|
|
5651
|
+
const hasImage = (content) => Array.isArray(content) && content.some((part) => part.type === "image" && part.attachment);
|
|
5652
|
+
function successfulImages(event) {
|
|
5653
|
+
if (event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image") {
|
|
5654
|
+
const message = event.data.message;
|
|
5655
|
+
const toolCallId = message?.source?.callId;
|
|
5656
|
+
if (message?.source?.kind !== "tool" || typeof toolCallId !== "string" || toolCallId === "" || message.isError === true || !hasImage(message.content)) return [];
|
|
5657
|
+
return [{
|
|
5658
|
+
type: "tool-result",
|
|
5659
|
+
toolCallId,
|
|
5660
|
+
content: message.content,
|
|
5661
|
+
isError: false,
|
|
5662
|
+
meta: event.data.meta
|
|
5663
|
+
}];
|
|
5664
|
+
}
|
|
5665
|
+
if (event?.type === "tool/ptc-dispatch" && IMAGE_TOOLS.has(event.data?.name) && event.data.isError === false && typeof event.data.subCallId === "string" && hasImage(event.data.content)) return [{
|
|
5666
|
+
type: "tool-result",
|
|
5667
|
+
toolCallId: event.data.subCallId,
|
|
5668
|
+
content: event.data.content,
|
|
5669
|
+
meta: { kind: "packycode-image" }
|
|
5670
|
+
}];
|
|
5671
|
+
const source = event?.data?.message?.source;
|
|
5672
|
+
if (event?.type !== "assistant/message" || source?.kind !== "model" || source.provider !== "packycode" || !/^claude-/u.test(source.model ?? "")) return [];
|
|
5673
|
+
return (event.data.message.content ?? []).flatMap((block, index) => block.type === "image" && block.attachment ? [{
|
|
5674
|
+
type: "tool-result",
|
|
5675
|
+
toolCallId: `packycode-cli-image-${event.seq}-${index}`,
|
|
5676
|
+
content: [block]
|
|
5677
|
+
}] : []);
|
|
5678
|
+
}
|
|
5679
|
+
const imageConversationNode = {
|
|
5680
|
+
kind: IMAGE_CHAT_KIND,
|
|
5681
|
+
target: "chat",
|
|
5682
|
+
match(event) {
|
|
5683
|
+
const images = successfulImages(event);
|
|
5684
|
+
if (images.length === 0) return null;
|
|
5685
|
+
const id = event.type === "assistant/message" ? `cli:${event.seq}` : event.data?.subCallId ?? event.data?.message?.source?.callId ?? images[0].toolCallId ?? `event:${event.seq}`;
|
|
5686
|
+
return {
|
|
5687
|
+
id: String(id),
|
|
5688
|
+
role: "update"
|
|
5689
|
+
};
|
|
5690
|
+
},
|
|
5691
|
+
start: () => ({}),
|
|
5692
|
+
update: (context) => context.state ?? {},
|
|
5693
|
+
buildViewNode(context) {
|
|
5694
|
+
const results = context.matches.filter((match) => successfulImages(match.event).length > 0);
|
|
5695
|
+
if (results.length === 0) return null;
|
|
5696
|
+
const turn = (results.find((match) => match.location?.kind === "turn" || match.location?.kind === "step")?.location)?.turn;
|
|
5697
|
+
const finished = turn?.end;
|
|
5698
|
+
const blocks = [...new Map(results.flatMap(({ event }) => successfulImages(event).map((block) => [block.toolCallId, {
|
|
5699
|
+
...block,
|
|
5700
|
+
kind: "tool-result"
|
|
5701
|
+
}]))).values()];
|
|
5702
|
+
const answerSeq = turn?.steps?.at(-1)?.data.get("assistant-step")?.finalNode?.seq;
|
|
5703
|
+
const lastImageSeq = Math.max(...results.map((match) => match.event.seq));
|
|
5704
|
+
return {
|
|
5705
|
+
key: context.key,
|
|
5706
|
+
id: context.id,
|
|
5707
|
+
kind: IMAGE_CHAT_KIND,
|
|
5708
|
+
target: "chat",
|
|
5709
|
+
location: { kind: "session" },
|
|
5710
|
+
visibility: "visible",
|
|
5711
|
+
anchorSeq: answerSeq > lastImageSeq ? answerSeq + .025 : finished === void 0 ? lastImageSeq + .025 : finished.seq - .025,
|
|
5712
|
+
data: {
|
|
5713
|
+
...turn === void 0 ? {} : { turn: turn.turn },
|
|
5714
|
+
blocks
|
|
5715
|
+
}
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
};
|
|
5719
|
+
//#endregion
|
|
5720
|
+
//#region src/image-originals.js
|
|
5721
|
+
/** Session-authorized access to PNGs preserved before DSH normalizes previews. */
|
|
5722
|
+
const ORIGINAL_CHUNK_BYTES = 256 * 1024;
|
|
5723
|
+
const ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
|
|
5724
|
+
const FILE_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.png$/u;
|
|
5725
|
+
const unavailable = () => /* @__PURE__ */ new Error("PackyCode original image is unavailable in this session");
|
|
5726
|
+
/** Read paths only from successful, host-recorded image events, never RPC input. */
|
|
5727
|
+
function originalFiles(events, attachmentId, directory) {
|
|
5728
|
+
const files = [];
|
|
5729
|
+
for (const event of events) {
|
|
5730
|
+
if (imageConversationNode.match(event) === null) continue;
|
|
5731
|
+
const content = event.type === "tool/ptc-dispatch" ? event.data.content : event.data.message.content;
|
|
5732
|
+
let candidate;
|
|
5733
|
+
for (const part of content) {
|
|
5734
|
+
if (part.type === "text" || part.type === "thinking") for (const match of String(part.text ?? "").matchAll(/(?:^|\n)(?:Original PNG on the DSH host: |原图:)([^\r\n]+)/gu)) {
|
|
5735
|
+
const path = match[1];
|
|
5736
|
+
candidate = dirname(path) === directory && FILE_NAME.test(basename(path)) ? basename(path) : void 0;
|
|
5737
|
+
}
|
|
5738
|
+
if (part.type === "image") {
|
|
5739
|
+
if (part.attachment?.attachmentId === attachmentId && candidate !== void 0) files.push(candidate);
|
|
5740
|
+
candidate = void 0;
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
return files;
|
|
5745
|
+
}
|
|
5746
|
+
function createImageOriginalReader({ imageDirectory, getSessionEvents }) {
|
|
5747
|
+
const directory = resolve(imageDirectory);
|
|
5748
|
+
async function withFile(payload, signal, read, chunk = false) {
|
|
5749
|
+
let file;
|
|
5750
|
+
try {
|
|
5751
|
+
signal?.throwIfAborted();
|
|
5752
|
+
const allowed = chunk ? [
|
|
5753
|
+
"sessionId",
|
|
5754
|
+
"attachmentId",
|
|
5755
|
+
"assetId",
|
|
5756
|
+
"offset"
|
|
5757
|
+
] : ["sessionId", "attachmentId"];
|
|
5758
|
+
if (!payload || typeof payload !== "object" || Object.keys(payload).some((key) => !allowed.includes(key)) || typeof payload.sessionId !== "string" || payload.sessionId.length === 0 || payload.sessionId.length > 200 || !ATTACHMENT_ID.test(payload.attachmentId) || chunk && (!FILE_NAME.test(payload.assetId) || !Number.isSafeInteger(payload.offset) || payload.offset < 0)) throw unavailable();
|
|
5759
|
+
const events = await getSessionEvents(payload.sessionId, signal);
|
|
5760
|
+
if (!Array.isArray(events)) throw unavailable();
|
|
5761
|
+
const candidates = originalFiles(events, payload.attachmentId, directory);
|
|
5762
|
+
const name = chunk ? candidates.find((value) => value === payload.assetId) : candidates.at(-1);
|
|
5763
|
+
if (name === void 0) throw unavailable();
|
|
5764
|
+
const path = join(directory, name);
|
|
5765
|
+
if ((await lstat(path)).isSymbolicLink()) throw unavailable();
|
|
5766
|
+
file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
5767
|
+
const stat = await file.stat();
|
|
5768
|
+
if (!stat.isFile() || stat.size < 24 || stat.size > 50331648) throw unavailable();
|
|
5769
|
+
const header = Buffer.alloc(24);
|
|
5770
|
+
await file.read(header, 0, header.length, 0);
|
|
5771
|
+
const dimensions = pngDimensions(header);
|
|
5772
|
+
const result = await read(file, name, stat, dimensions);
|
|
5773
|
+
signal?.throwIfAborted();
|
|
5774
|
+
const after = await file.stat();
|
|
5775
|
+
if (after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) throw unavailable();
|
|
5776
|
+
return result;
|
|
5777
|
+
} catch {
|
|
5778
|
+
throw unavailable();
|
|
5779
|
+
} finally {
|
|
5780
|
+
await file?.close().catch(() => void 0);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
return {
|
|
5784
|
+
info(payload, signal) {
|
|
5785
|
+
return withFile(payload, signal, async (file, assetId, stat, dimensions) => {
|
|
5786
|
+
const hash = createHash("sha256");
|
|
5787
|
+
const buffer = Buffer.alloc(Math.min(ORIGINAL_CHUNK_BYTES, stat.size));
|
|
5788
|
+
let offset = 0;
|
|
5789
|
+
while (offset < stat.size) {
|
|
5790
|
+
signal?.throwIfAborted();
|
|
5791
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, stat.size - offset), offset);
|
|
5792
|
+
if (bytesRead === 0) throw unavailable();
|
|
5793
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
5794
|
+
offset += bytesRead;
|
|
5795
|
+
}
|
|
5796
|
+
return {
|
|
5797
|
+
assetId,
|
|
5798
|
+
attachmentId: payload.attachmentId,
|
|
5799
|
+
name: `packycode-${assetId}`,
|
|
5800
|
+
mediaType: "image/png",
|
|
5801
|
+
bytes: stat.size,
|
|
5802
|
+
...dimensions,
|
|
5803
|
+
sha256: hash.digest("hex")
|
|
5804
|
+
};
|
|
5805
|
+
});
|
|
5806
|
+
},
|
|
5807
|
+
chunk(payload, signal) {
|
|
5808
|
+
return withFile(payload, signal, async (file, assetId, stat) => {
|
|
5809
|
+
if (payload.offset >= stat.size) throw unavailable();
|
|
5810
|
+
const buffer = Buffer.alloc(Math.min(ORIGINAL_CHUNK_BYTES, stat.size - payload.offset));
|
|
5811
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, payload.offset);
|
|
5812
|
+
if (bytesRead === 0) throw unavailable();
|
|
5813
|
+
return {
|
|
5814
|
+
assetId,
|
|
5815
|
+
attachmentId: payload.attachmentId,
|
|
5816
|
+
offset: payload.offset,
|
|
5817
|
+
totalBytes: stat.size,
|
|
5818
|
+
encoded: buffer.subarray(0, bytesRead).toString("base64"),
|
|
5819
|
+
done: payload.offset + bytesRead === stat.size
|
|
5820
|
+
};
|
|
5821
|
+
}, true);
|
|
5822
|
+
}
|
|
5823
|
+
};
|
|
5824
|
+
}
|
|
5825
|
+
//#endregion
|
|
5647
5826
|
//#region src/key-chooser.js
|
|
5648
5827
|
/**
|
|
5649
5828
|
* Which stored key a request runs with, and what happens when the gateway
|
|
@@ -6782,7 +6961,8 @@ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments,
|
|
|
6782
6961
|
* Every reply carries only what this plugin owns: key labels and state, the
|
|
6783
6962
|
* balance and group projections the host built, host overrides, whether this
|
|
6784
6963
|
* device has the Claude Code CLI, the plugin version, and the web search
|
|
6785
|
-
* preference and model visibility
|
|
6964
|
+
* preference and model visibility, or bounded session-authorized original PNG
|
|
6965
|
+
* chunks. Key material never crosses this boundary.
|
|
6786
6966
|
*
|
|
6787
6967
|
* @module dsh-packycode-api/rpc
|
|
6788
6968
|
*/
|
|
@@ -6808,7 +6988,7 @@ const keyId = (payload) => String(payload?.id ?? "");
|
|
|
6808
6988
|
* web search controller (without one, its endpoints refuse).
|
|
6809
6989
|
* @returns a handler for `connection.rpc.handle(CHANNEL, handler)`.
|
|
6810
6990
|
*/
|
|
6811
|
-
function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection, images }) {
|
|
6991
|
+
function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection, images, imageOriginals }) {
|
|
6812
6992
|
const need = (service, message) => {
|
|
6813
6993
|
if (service === void 0) throw new Error(message);
|
|
6814
6994
|
return service;
|
|
@@ -6819,6 +6999,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
6819
6999
|
const groupReader = () => need(groups, "PackyCode key groups are unavailable in this runtime");
|
|
6820
7000
|
const models = () => need(modelSelection, "PackyCode model selection is unavailable in this runtime");
|
|
6821
7001
|
const imageSupport = () => need(images, "PackyCode images are unavailable in this runtime");
|
|
7002
|
+
const originals = () => need(imageOriginals, "PackyCode original images are unavailable in this runtime");
|
|
6822
7003
|
const status = async () => ({
|
|
6823
7004
|
...await ring.list(),
|
|
6824
7005
|
hosts: hosts(),
|
|
@@ -6892,6 +7073,8 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
6892
7073
|
force: payload?.force === true,
|
|
6893
7074
|
signal
|
|
6894
7075
|
}),
|
|
7076
|
+
"images/original": (payload, signal) => originals().info(payload, signal),
|
|
7077
|
+
"images/original/chunk": (payload, signal) => originals().chunk(payload, signal),
|
|
6895
7078
|
"images/update": (payload) => imageSupport().update(payload),
|
|
6896
7079
|
"images/keys/add": (payload) => imageSupport().mutate("add", {
|
|
6897
7080
|
key: payload?.key,
|
|
@@ -8105,7 +8288,26 @@ function apply(ctx, config) {
|
|
|
8105
8288
|
pluginManager: createPackyCodePluginManager(),
|
|
8106
8289
|
search,
|
|
8107
8290
|
modelSelection,
|
|
8108
|
-
images
|
|
8291
|
+
images,
|
|
8292
|
+
imageOriginals: createImageOriginalReader({
|
|
8293
|
+
imageDirectory: join(dshHome, "packycode-api", "images"),
|
|
8294
|
+
getSessionEvents: async (sessionId, signal) => {
|
|
8295
|
+
const query = ctx.get?.("sessionQuery");
|
|
8296
|
+
if (typeof query?.observeSession === "function") {
|
|
8297
|
+
const observation = await query.observeSession(sessionId, {
|
|
8298
|
+
signal,
|
|
8299
|
+
projectionMode: "none"
|
|
8300
|
+
});
|
|
8301
|
+
try {
|
|
8302
|
+
return [...observation.events];
|
|
8303
|
+
} finally {
|
|
8304
|
+
observation[Symbol.dispose]();
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8307
|
+
if (typeof query?.readSession === "function") return (await query.readSession(sessionId)).events;
|
|
8308
|
+
return [];
|
|
8309
|
+
}
|
|
8310
|
+
})
|
|
8109
8311
|
});
|
|
8110
8312
|
/**
|
|
8111
8313
|
* The shipped Connection getter retains its own service context, whose row
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-packycode-api",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Use PackyCode API keys in DeepSeek Harness with multi-protocol models, image generation and editing, separate key rings, and balance display",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|