dsh-packycode-api 1.2.1 → 1.2.3
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 +14 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/research/image-api.md +2 -0
- package/lib/client.js +25 -14
- package/lib/index.js +18 -11
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.3
|
|
4
|
+
|
|
5
|
+
- Fixed ordinary reference-only image edits failing with `PackyCode image mask must be a PNG smaller than 4 MB` when a model also fills the optional mask field with the same JPEG/WebP reference. Native `maskImage` and CLI `maskId` now explicitly accept `null`; the field descriptions distinguish sketch refinement from using an actual mask. Only an identical reference already resolved by the host as non-PNG is treated as a redundant non-mask. Reference bytes, edit endpoint, model and quality stay intact; real PNG masks and all distinct mask references still undergo format, size and dimension checks. Invalid mask-format errors now explain how to request editing without a mask. Added native and authenticated MCP regressions without paid calls.
|
|
6
|
+
- Fixed native image results still missing from main chat in 1.2.2. DSH persists `tool/result.data.message` as a flat `ToolMessage`: the call identity is in `source.callId`, image/text parts are directly in `content`, and failure status is on the message. The gallery incorrectly looked for nested SDK `tool-result` blocks and discarded every successful native result. It now projects the actual message into the existing gallery view and retains the plugin metadata and durable image references.
|
|
7
|
+
- Replaced the incorrect native fixtures with the real message structure and added a sanitized stored-session regression that fails against the published 1.2.2 parser. Added a built-client integration through real Cordis 4.0.4, DSH 0.2.0-rc.2 event/view/group and Slot registries, bound session feed and image cache, including plugin unload/reinstall and history reload. Native, nested PTC and Claude CLI image presentation each exercise the built bundle.
|
|
8
|
+
- Browser comparison of the published 1.2.2 client and the corrected client with the same native ToolMessage: the old client renders zero main-chat images while the tool detail PNG loads; the corrected client renders one image with details collapsed and after history reload, with no console errors. `scripts/image-preview-check.mjs` supports `?protocol=native` or `?protocol=ptc` for focused replay. This uses a public PNG fixture without making paid API requests or changing existing sessions; it does not replace a physical Intel Mac retest.
|
|
9
|
+
- Validation, 2026-10-15: 390 offline tests, both rebuilt bundles and the 1.2.3 tarball. The built host tool also passed a real local HTTP multipart replay for null and repeated non-PNG mask arguments: both requests used `/v1/images/edits`, retained the original JPEG and saved Sunburst `max` setting, and omitted the unused mask. No paid generation, existing-session rewrite or DSH restart was performed for these fixes.
|
|
10
|
+
|
|
11
|
+
## 1.2.2
|
|
12
|
+
|
|
13
|
+
- Fixed the remaining missing-gallery bug in 1.2.1. Real native `tool/result` and nested `tool/ptc-dispatch` records do not have `data.turn`; keying image contexts by that missing field separated image evidence from the actual `turn/end` context. Image contexts now use the tool call ID (or CLI message sequence) and obtain the owning turn and completion boundary from DSH's engine-provided Match Location. Results appear as soon as available and stay visible when history later supplies missing boundaries.
|
|
14
|
+
- Removed fabricated turn/step fields from the runtime regression fixture. Added the real DSH 0.2 Chat snapshot builder and grouping to the built-client path, plus multi-turn, interleaved-session and older-page-prepend regressions. A React/ReactDOM browser replay (`scripts/image-preview-check.mjs`) reproduces the published 1.2.1 bundle showing zero main-chat images while tool details can load the PNG; the corrected build displays both native/PTC image cards with details collapsed and retains them after history reload. No new paid generation or existing-session modification is needed.
|
|
15
|
+
- Validation, 2026-10-15: 381 offline tests, both bundles and the 1.2.2 tarball. Browser comparison used the published 1.2.1 client and the corrected client against the same raw native/PTC events and real DSH 0.2.0-rc.2 Chat assembly/grouping: the old client rendered zero main-chat images with a loadable tool-detail image; the new client rendered both PNGs with details collapsed and after history reload. The final browser replay uses React, ReactDOM and the JSX runtime with no console errors. The colleague's physical Intel Mac still needs an update-and-retest; no DSH process was restarted and no paid image request was made for this verification.
|
|
16
|
+
|
|
3
17
|
## 1.2.1
|
|
4
18
|
|
|
5
19
|
- **Generated image visible only inside tool details.** DSH 0.2 treats custom Turn/Step nodes as foldable process content, so the chat gallery disappeared when completed work was collapsed. PTC also logs successful child images as `tool/ptc-dispatch` without root presentation metadata, which the gallery previously ignored. The gallery now recognizes this plugin's successful native/PTC/CLI results and renders a Session-level card anchored beside its owning answer, independent of process folding. Stored conversations replay the same image references; no log rewrite or paid regeneration is needed. Duplicate results for the same call are projected once. Tool instructions also tell models to use automatic image presentation instead of inventing Markdown image links to host paths or attachment IDs.
|
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.3`.
|
|
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.3`. 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
|
|
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.3`。
|
|
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.3`。不要对 `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
|
|
|
@@ -32,6 +32,8 @@ Multipart 使用 `image`(单图)或重复的 `image[]`(多图),最多
|
|
|
32
32
|
|
|
33
33
|
官方资料存在一个明确差异:`CreateImageEditRequest.mask` 要求 **小于 4 MB**;指南的 Mask requirements 则写图与 mask 格式、尺寸一致且小于 50 MB。实现建议采用更严格的 PNG mask <4 MB,并在需要 mask 时使用相同尺寸的 PNG 输入,直到实际网关验证更宽限制;不要把指南的 50 MB 直接当作 mask 已确认上限。[OpenAPI mask schema](https://github.com/openai/openai-openapi/blob/main/openapi.json)、[指南 Mask requirements](https://developers.openai.com/api/docs/guides/image-generation#mask-requirements)
|
|
34
34
|
|
|
35
|
+
2026-10-15 再核对:[官方编辑 API 参考](https://developers.openai.com/api/reference/python/resources/images/methods/edit)仍将 `mask` 列为可选 PNG、与输入同尺寸且 <4 MB。用户随后报告“完善草图”触发同样的 mask 校验,并确认仅上传原图,没有单独遮罩。当前本机没有该报错会话的调用参数;离线回放可用“JPEG/WebP 参考图又被模型重复填到 mask 字段”复现完全相同的报错,不能据此声称已取得同事设备的原始参数。工具现支持 `maskImage: null` / CLI `maskId: null` 明确表示无遮罩,并只兼容宿主已验证的同一个非 PNG 参考附件被重复填入 mask 的情况。真实 PNG mask 和不同附件的 mask 保留原有检查;没有放宽 4 MB 限制,没有把编辑换成生图,也没有新增付费重试。
|
|
36
|
+
|
|
35
37
|
JSON 编辑的字段为 `images: [{ image_url: "…" }]` 或 `images: [{ file_id: "…" }]`,每项二选一;`image_url` 可为完整 URL 或 data URL。JSON 的可选 mask 同样为图片引用对象。这是官方额外能力,不能据此假定 PackyCode 也支持它。[官方 OpenAPI:EditImageBodyJsonParam / ImageRefParam](https://github.com/openai/openai-openapi/blob/main/openapi.json)
|
|
36
38
|
|
|
37
39
|
`gpt-image-2` 必须省略 `input_fidelity`,其图片输入始终按高保真处理。当前通用 schema 只明确为 GPT Image 1 / 1.5 声明可调 high/low,没有明确说明 2.5 两款可调;本次集成建议三款均省略此字段,不推断 2.5 的未记载行为。[官方高保真说明](https://developers.openai.com/api/docs/guides/image-generation#image-input-fidelity)、[官方 OpenAPI:InputFidelity](https://github.com/openai/openai-openapi/blob/main/openapi.json)
|
package/lib/client.js
CHANGED
|
@@ -496,10 +496,18 @@ window.__ModuleLoader__.load({
|
|
|
496
496
|
const IMAGE_TOOLS = /* @__PURE__ */ new Set(["packycode_image_generate", "claude_code"]);
|
|
497
497
|
const hasImage = (content) => Array.isArray(content) && content.some((part) => part.type === "image" && part.attachment);
|
|
498
498
|
function successfulImages(event) {
|
|
499
|
-
if (event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image")
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
499
|
+
if (event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image") {
|
|
500
|
+
const message = event.data.message;
|
|
501
|
+
const toolCallId = message?.source?.callId;
|
|
502
|
+
if (message?.source?.kind !== "tool" || typeof toolCallId !== "string" || toolCallId === "" || message.isError === true || !hasImage(message.content)) return [];
|
|
503
|
+
return [{
|
|
504
|
+
type: "tool-result",
|
|
505
|
+
toolCallId,
|
|
506
|
+
content: message.content,
|
|
507
|
+
isError: false,
|
|
508
|
+
meta: event.data.meta
|
|
509
|
+
}];
|
|
510
|
+
}
|
|
503
511
|
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 [{
|
|
504
512
|
type: "tool-result",
|
|
505
513
|
toolCallId: event.data.subCallId,
|
|
@@ -518,23 +526,26 @@ window.__ModuleLoader__.load({
|
|
|
518
526
|
kind: IMAGE_CHAT_KIND,
|
|
519
527
|
target: "chat",
|
|
520
528
|
match(event) {
|
|
521
|
-
|
|
522
|
-
|
|
529
|
+
const images = successfulImages(event);
|
|
530
|
+
if (images.length === 0) return null;
|
|
531
|
+
const id = event.type === "assistant/message" ? `cli:${event.seq}` : event.data?.subCallId ?? event.data?.message?.source?.callId ?? images[0].toolCallId ?? `event:${event.seq}`;
|
|
532
|
+
return {
|
|
533
|
+
id: String(id),
|
|
523
534
|
role: "update"
|
|
524
|
-
}
|
|
535
|
+
};
|
|
525
536
|
},
|
|
526
|
-
start: () =>
|
|
527
|
-
update: (context) => context.state,
|
|
537
|
+
start: () => ({}),
|
|
538
|
+
update: (context) => context.state ?? {},
|
|
528
539
|
buildViewNode(context) {
|
|
529
|
-
const finished = context.matches.find((match) => match.event.type === "turn/end");
|
|
530
|
-
if (finished === void 0) return null;
|
|
531
540
|
const results = context.matches.filter((match) => successfulImages(match.event).length > 0);
|
|
532
541
|
if (results.length === 0) return null;
|
|
542
|
+
const turn = (results.find((match) => match.location?.kind === "turn" || match.location?.kind === "step")?.location)?.turn;
|
|
543
|
+
const finished = turn?.end;
|
|
533
544
|
const blocks = [...new Map(results.flatMap(({ event }) => successfulImages(event).map((block) => [block.toolCallId, {
|
|
534
545
|
...block,
|
|
535
546
|
kind: "tool-result"
|
|
536
547
|
}]))).values()];
|
|
537
|
-
const answerSeq =
|
|
548
|
+
const answerSeq = turn?.steps?.at(-1)?.data.get("assistant-step")?.finalNode?.seq;
|
|
538
549
|
const lastImageSeq = Math.max(...results.map((match) => match.event.seq));
|
|
539
550
|
return {
|
|
540
551
|
key: context.key,
|
|
@@ -543,9 +554,9 @@ window.__ModuleLoader__.load({
|
|
|
543
554
|
target: "chat",
|
|
544
555
|
location: { kind: "session" },
|
|
545
556
|
visibility: "visible",
|
|
546
|
-
anchorSeq: answerSeq > lastImageSeq ? answerSeq + .025 : finished.
|
|
557
|
+
anchorSeq: answerSeq > lastImageSeq ? answerSeq + .025 : finished === void 0 ? lastImageSeq + .025 : finished.seq - .025,
|
|
547
558
|
data: {
|
|
548
|
-
turn:
|
|
559
|
+
...turn === void 0 ? {} : { turn: turn.turn },
|
|
549
560
|
blocks
|
|
550
561
|
}
|
|
551
562
|
};
|
package/lib/index.js
CHANGED
|
@@ -4547,9 +4547,10 @@ const SIGNATURE = "89504e470d0a1a0a";
|
|
|
4547
4547
|
const ATTACHMENT_ID$1 = /^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. */
|
|
4551
4551
|
function emptyMaskReference(value) {
|
|
4552
|
-
|
|
4552
|
+
if (value === null) return true;
|
|
4553
|
+
return value !== void 0 && typeof value === "object" && (value.attachmentId === "" || value.attachmentId === `sha256:${"0".repeat(64)}`) && value.bytes === 0 && value.width === 0 && value.height === 0 && (value.name === void 0 || value.name === "") && (value.originalDimensions === void 0 || value.originalDimensions.width === 0 && value.originalDimensions.height === 0);
|
|
4553
4554
|
}
|
|
4554
4555
|
/** A bounded reader applies to both successful payloads and error envelopes. */
|
|
4555
4556
|
async function readJson(response, maximum, signal) {
|
|
@@ -4657,7 +4658,8 @@ function createPackyCodeImageClient({ resolveKey, getHost, fetchImpl = ambientFe
|
|
|
4657
4658
|
body.append("image[]", new Blob([image.data], { type: image.mediaType }), `reference-${index + 1}.${extension}`);
|
|
4658
4659
|
}
|
|
4659
4660
|
if (mask !== void 0) {
|
|
4660
|
-
if (mask.mediaType !== "image/png"
|
|
4661
|
+
if (mask.mediaType !== "image/png") throw new ImageRequestError("PackyCode maskImage must be a separate PNG mask, not an ordinary reference image; omit maskImage or set it to null for edits without a mask");
|
|
4662
|
+
if (!(mask.data instanceof Uint8Array) || mask.data.byteLength === 0 || mask.data.byteLength >= 4 * 1024 * 1024) throw new ImageRequestError("PackyCode image mask must be a PNG smaller than 4 MB");
|
|
4661
4663
|
const dimensions = pngDimensions(mask.data);
|
|
4662
4664
|
if (dimensions.width !== images[0].width || dimensions.height !== images[0].height) throw new ImageRequestError("PackyCode image mask must match the first reference image dimensions");
|
|
4663
4665
|
body.append("mask", new Blob([mask.data], { type: "image/png" }), "mask.png");
|
|
@@ -4872,8 +4874,8 @@ function createPackyCodeImageTool({ client, attachments, getPreference, getSessi
|
|
|
4872
4874
|
description: "1-16 complete attachment references for editing. Omit or use [] for a new image. Paths and unrelated images are not accepted; never drop real references after an edit fails."
|
|
4873
4875
|
},
|
|
4874
4876
|
maskImage: {
|
|
4875
|
-
|
|
4876
|
-
description: "Omit for new images and edits
|
|
4877
|
+
oneOf: [{ type: "null" }, referenceSchema],
|
|
4878
|
+
description: "Omit or use null for new images and ordinary reference-image edits, including refining a sketch. Only supply an actual PNG mask provided for masking, matching the first reference and under 4 MB; alpha=0 marks the area to edit. Do not copy an ordinary reference into this field or invent a mask."
|
|
4877
4879
|
}
|
|
4878
4880
|
},
|
|
4879
4881
|
output: {
|
|
@@ -4906,7 +4908,9 @@ function createPackyCodeImageTool({ client, attachments, getPreference, getSessi
|
|
|
4906
4908
|
throw new ImageRequestError("PackyCode could not read the current session image references");
|
|
4907
4909
|
}
|
|
4908
4910
|
const images = editing ? await readReferences(args.referenceImages, attachments, messages, signal) : void 0;
|
|
4909
|
-
const
|
|
4911
|
+
const repeatedIndex = maskImage === void 0 ? -1 : args.referenceImages.findIndex((ref) => ref.attachmentId === maskImage.attachmentId);
|
|
4912
|
+
const repeatedNonPng = repeatedIndex >= 0 && images[repeatedIndex].mediaType !== "image/png";
|
|
4913
|
+
const masks = maskImage === void 0 || repeatedNonPng ? void 0 : await readReferences([maskImage], attachments, messages, signal);
|
|
4910
4914
|
if (!getPreference().imageEnabled) throw new ImageRequestError("PackyCode images are disabled in settings");
|
|
4911
4915
|
const result = await client.request({
|
|
4912
4916
|
...args,
|
|
@@ -5026,9 +5030,11 @@ const MCP_IMAGE_SCHEMA = Object.freeze({
|
|
|
5026
5030
|
description: "For edits use image handles from the session image catalog or a previous successful result in this run. Omit or [] for generation. Never invent handles or substitute files/URLs."
|
|
5027
5031
|
},
|
|
5028
5032
|
maskId: {
|
|
5029
|
-
type: "
|
|
5030
|
-
|
|
5031
|
-
|
|
5033
|
+
oneOf: [{ type: "null" }, {
|
|
5034
|
+
type: "string",
|
|
5035
|
+
pattern: "^image_[1-9][0-9]*$"
|
|
5036
|
+
}],
|
|
5037
|
+
description: "Omit or use null for ordinary reference-image editing, including sketch refinement. Only use an actual PNG mask provided for masking, matching the first reference; never reuse an ordinary reference as a mask."
|
|
5032
5038
|
}
|
|
5033
5039
|
},
|
|
5034
5040
|
required: ["prompt"]
|
|
@@ -5086,7 +5092,8 @@ function createClaudeImageService({ sessionId, messages, client, attachments, ge
|
|
|
5086
5092
|
if (!Array.isArray(ids) || ids.length > 16 || new Set(ids).size !== ids.length || ids.some((id) => !references.has(id))) invalid("Use 1-16 distinct image handles from this run’s session catalog for an edit.");
|
|
5087
5093
|
if (value.operation === "edit" && ids.length === 0) invalid("Image edits require referenceIds; an edit will not be replaced with generation.");
|
|
5088
5094
|
if (value.operation === "generate" && ids.length > 0) invalid("Image generation cannot include referenceIds; use edit.");
|
|
5089
|
-
|
|
5095
|
+
const maskId = value.maskId ?? void 0;
|
|
5096
|
+
if (maskId !== void 0 && (!references.has(maskId) || ids.length === 0)) invalid("A mask requires references and a valid session image handle.");
|
|
5090
5097
|
const preference = getPreference();
|
|
5091
5098
|
const model = value.model === void 0 || value.model === "settings" ? preference.imageModel : value.model;
|
|
5092
5099
|
const quality = value.quality === void 0 || value.quality === "settings" ? preference.imageQuality : value.quality;
|
|
@@ -5099,7 +5106,7 @@ function createClaudeImageService({ sessionId, messages, client, attachments, ge
|
|
|
5099
5106
|
} catch (error) {
|
|
5100
5107
|
invalid(error.message);
|
|
5101
5108
|
}
|
|
5102
|
-
const { referenceIds: _ids, maskId, ...args } = value;
|
|
5109
|
+
const { referenceIds: _ids, maskId: _maskId, ...args } = value;
|
|
5103
5110
|
return {
|
|
5104
5111
|
...args,
|
|
5105
5112
|
model,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-packycode-api",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
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",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"@earendil-works/pi-ai": "0.85.1",
|
|
100
100
|
"packycode-host-v020-fixture": "file:tests/fixtures/host-v020",
|
|
101
101
|
"react": "18.3.1",
|
|
102
|
+
"react-dom": "18.3.1",
|
|
102
103
|
"tsdown": "0.22.2",
|
|
103
104
|
"yaml": "2.9.1"
|
|
104
105
|
},
|