dsh-packycode-api 1.2.2 → 1.2.4
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 +15 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/research/image-api.md +2 -0
- package/lib/client.js +168 -40
- package/lib/index.js +18 -11
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.4
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Removed the redundant "Separate image key" badge from the image settings header.
|
|
7
|
+
- Image/account quota totals now use total remaining plus total used, as requested, rather than depending on the optional periodic `quota_limit` field. Non-resetting image keys therefore show their total even when no period cap is set. Per-key quota figures and progress bars use the same calculation. Missing balance components remain unknown, and unlimited keys retain their unlimited display.
|
|
8
|
+
- Validation, 2026-10-15: 397 tests, both rebuilt bundles and the versioned tarball. Browser verification with real React and DSH 0.2.0-rc.2 composer CSS confirmed the balance sits to the right of Create image, the hover/focus details show the current key's quota, and provider switches, the image setting and the host's hidden toolbar hide both controls together. A read-only live image-key quota projection also confirmed the no-period-cap shape; no image generation was needed for these UI changes.
|
|
9
|
+
|
|
10
|
+
## 1.2.3
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
3
18
|
## 1.2.2
|
|
4
19
|
|
|
5
20
|
- 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.
|
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.4`.
|
|
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.4`. 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.4`。
|
|
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.4`。不要对 `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
|
@@ -40,9 +40,11 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
intro: "填入 PackyCode(PackyAPI)的 API Key 即可使用内置模型路由。多个 Key 按顺序使用:额度用尽或失效时自动切到下一个;某个 Key 的分组用不了所选模型时,自动改用分组合适的 Key。",
|
|
41
41
|
accountsTitle: "账户与 Key",
|
|
42
42
|
imagesTitle: "图片生成与编辑",
|
|
43
|
-
imagesSeparateKey: "独立图片 Key",
|
|
44
43
|
imagesBalanceLoading: "正在读取图片 Key 余额…",
|
|
45
44
|
imagesBalanceFailed: "图片余额读取失败",
|
|
45
|
+
imagesComposerLabel: "图片剩余额度",
|
|
46
|
+
imagesBalanceKey: "当前图片 Key",
|
|
47
|
+
imagesBalanceReadAt: "更新于 {time}",
|
|
46
48
|
imagesBalanceHint: "仅汇总图片 Key 的额度,每分钟自动更新;也可手动刷新。与对话 Key 的余额分开统计。",
|
|
47
49
|
imagesIntro: "使用独立图片 Key 调用 PackyCode 图片接口。生成新图或上传图片后描述修改要求;图片模型独立于对话模型。原始 PNG 保存在本机,聊天中显示图片预览。",
|
|
48
50
|
imagesEnabled: "图片生成与编辑",
|
|
@@ -215,9 +217,11 @@ window.__ModuleLoader__.load({
|
|
|
215
217
|
intro: "Paste a PackyCode (PackyAPI) API key to use the built-in model route. Keys are used in order: an exhausted or rejected key hands over to the next one, and a key whose groups cannot reach the selected model is passed over for one whose groups can.",
|
|
216
218
|
accountsTitle: "Accounts and keys",
|
|
217
219
|
imagesTitle: "Image generation and editing",
|
|
218
|
-
imagesSeparateKey: "Separate image key",
|
|
219
220
|
imagesBalanceLoading: "Reading image key balances…",
|
|
220
221
|
imagesBalanceFailed: "Could not read image balances",
|
|
222
|
+
imagesComposerLabel: "Image balance",
|
|
223
|
+
imagesBalanceKey: "Current image key",
|
|
224
|
+
imagesBalanceReadAt: "Updated at {time}",
|
|
221
225
|
imagesBalanceHint: "Only image key quotas are counted here. Updates every minute, or use Refresh. Conversation key balances are separate.",
|
|
222
226
|
imagesIntro: "Use a dedicated image key with PackyCode’s Images API. Describe a new image or attach images with editing instructions. The image engine is independent of the conversation model. Original PNGs are saved locally and previews appear in chat.",
|
|
223
227
|
imagesEnabled: "Generate and edit images",
|
|
@@ -409,6 +413,11 @@ window.__ModuleLoader__.load({
|
|
|
409
413
|
.pkc-input:disabled{opacity:.5}
|
|
410
414
|
.pkc-image-choice{flex:none;width:100%;max-width:300px;cursor:pointer}
|
|
411
415
|
.pkc-image-shortcut{display:inline-flex;align-items:center;gap:8px}
|
|
416
|
+
.pkc-image-balance{position:relative;display:inline-flex;align-items:center}
|
|
417
|
+
.pkc-image-balance-value{cursor:help;border-radius:var(--dsw-radius-xs,4px)}
|
|
418
|
+
.pkc-image-balance-value:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
419
|
+
.pkc-image-balance-tooltip{position:absolute;bottom:calc(100% + 8px);left:0;z-index:100;box-sizing:border-box;width:max-content;max-width:min(320px,calc(100vw - 32px));padding:10px 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv1);font-size:12px;line-height:20px;white-space:pre-line;overflow-wrap:anywhere;visibility:hidden;opacity:0;pointer-events:none}
|
|
420
|
+
.pkc-image-balance:hover>.pkc-image-balance-tooltip,.pkc-image-balance:focus-within>.pkc-image-balance-tooltip{visibility:visible;opacity:1}
|
|
412
421
|
.pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
|
|
413
422
|
.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}
|
|
414
423
|
.pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
|
|
@@ -496,10 +505,18 @@ window.__ModuleLoader__.load({
|
|
|
496
505
|
const IMAGE_TOOLS = /* @__PURE__ */ new Set(["packycode_image_generate", "claude_code"]);
|
|
497
506
|
const hasImage = (content) => Array.isArray(content) && content.some((part) => part.type === "image" && part.attachment);
|
|
498
507
|
function successfulImages(event) {
|
|
499
|
-
if (event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image")
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
508
|
+
if (event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image") {
|
|
509
|
+
const message = event.data.message;
|
|
510
|
+
const toolCallId = message?.source?.callId;
|
|
511
|
+
if (message?.source?.kind !== "tool" || typeof toolCallId !== "string" || toolCallId === "" || message.isError === true || !hasImage(message.content)) return [];
|
|
512
|
+
return [{
|
|
513
|
+
type: "tool-result",
|
|
514
|
+
toolCallId,
|
|
515
|
+
content: message.content,
|
|
516
|
+
isError: false,
|
|
517
|
+
meta: event.data.meta
|
|
518
|
+
}];
|
|
519
|
+
}
|
|
503
520
|
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
521
|
type: "tool-result",
|
|
505
522
|
toolCallId: event.data.subCallId,
|
|
@@ -568,11 +585,19 @@ window.__ModuleLoader__.load({
|
|
|
568
585
|
* @module dsh-packycode-api/totals
|
|
569
586
|
*/
|
|
570
587
|
const amount = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
588
|
+
/** Displayed key quota is remaining plus used, independent of a period cap. */
|
|
589
|
+
function quotaTotalUsd(usage) {
|
|
590
|
+
if (usage?.unlimitedQuota === true) return void 0;
|
|
591
|
+
const remaining = amount(usage?.availableUsd);
|
|
592
|
+
const used = amount(usage?.usedUsd);
|
|
593
|
+
if (remaining === void 0 || used === void 0) return void 0;
|
|
594
|
+
return amount(Number((remaining + used).toFixed(6)));
|
|
595
|
+
}
|
|
571
596
|
/**
|
|
572
597
|
* Add up the balance rows the host returned.
|
|
573
598
|
* @param rows - the `keys` array of a balance projection.
|
|
574
599
|
* @returns summed amounts plus the conditions that make a sum incomplete:
|
|
575
|
-
* keys whose read failed, and keys with
|
|
600
|
+
* keys whose read failed, and keys with unlimited quota.
|
|
576
601
|
*/
|
|
577
602
|
function sumBalances(rows) {
|
|
578
603
|
const total = {
|
|
@@ -598,11 +623,10 @@ window.__ModuleLoader__.load({
|
|
|
598
623
|
}
|
|
599
624
|
const remaining = amount(usage.availableUsd);
|
|
600
625
|
const used = amount(usage.usedUsd);
|
|
601
|
-
const limit = amount(usage.limitUsd);
|
|
602
626
|
if (remaining !== void 0) total.remainingUsd += remaining;
|
|
603
627
|
if (used !== void 0) total.usedUsd += used;
|
|
604
|
-
if (limit !== void 0) total.limitUsd += limit;
|
|
605
628
|
}
|
|
629
|
+
total.limitUsd = Number((total.remainingUsd + total.usedUsd).toFixed(6));
|
|
606
630
|
return total;
|
|
607
631
|
}
|
|
608
632
|
//#endregion
|
|
@@ -639,13 +663,14 @@ window.__ModuleLoader__.load({
|
|
|
639
663
|
/** The same quota presentation for conversation and image credential records. */
|
|
640
664
|
function KeyBalance({ row, t }) {
|
|
641
665
|
const usage = row?.usage;
|
|
666
|
+
const totalUsd = quotaTotalUsd(usage);
|
|
642
667
|
const figures = [
|
|
643
668
|
usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
|
|
644
669
|
usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
|
|
645
|
-
|
|
670
|
+
totalUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(totalUsd)]
|
|
646
671
|
].filter(Boolean);
|
|
647
672
|
const meta = [usage?.name === void 0 ? t("keyNameUnknown") : `${t("keyName")} ${usage.name}`, RESET_KEYS[usage?.resetPeriod] === void 0 ? void 0 : t(RESET_KEYS[usage.resetPeriod])].filter(Boolean).join(" · ");
|
|
648
|
-
const share = usage?.unlimitedQuota !== true &&
|
|
673
|
+
const share = usage?.unlimitedQuota !== true && totalUsd > 0 && typeof usage.availableUsd === "number" ? Math.max(0, Math.min(1, usage.availableUsd / totalUsd)) : void 0;
|
|
649
674
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
650
675
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
651
676
|
className: "pkc-meta",
|
|
@@ -708,7 +733,7 @@ window.__ModuleLoader__.load({
|
|
|
708
733
|
children: t("totalLimit")
|
|
709
734
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
710
735
|
className: "pkc-stat-value",
|
|
711
|
-
children: totals.unlimited > 0 ? t("keyUnlimited") : known("
|
|
736
|
+
children: totals.unlimited > 0 ? t("keyUnlimited") : known("availableUsd") && known("usedUsd") ? formatUsd(totals.limitUsd) : "—"
|
|
712
737
|
})]
|
|
713
738
|
})
|
|
714
739
|
]
|
|
@@ -718,6 +743,103 @@ window.__ModuleLoader__.load({
|
|
|
718
743
|
})] });
|
|
719
744
|
}
|
|
720
745
|
//#endregion
|
|
746
|
+
//#region src/client-image-balance.jsx
|
|
747
|
+
/** Mirror the host's next-usable image key using only its public balance rows. */
|
|
748
|
+
function currentImageKey(balance) {
|
|
749
|
+
const keys = balance?.keys ?? [];
|
|
750
|
+
const start = Math.max(0, keys.findIndex((row) => row.id === balance.activeId));
|
|
751
|
+
for (let offset = 0; offset < keys.length; offset += 1) {
|
|
752
|
+
const row = keys[(start + offset) % keys.length];
|
|
753
|
+
if (row.disabled !== true) return row;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** Mounted inside the visible Create image shortcut, so its timer shares that lifetime. */
|
|
757
|
+
function ImageShortcutBalance({ call, t }) {
|
|
758
|
+
const [view, setView] = react.useState({ loading: true });
|
|
759
|
+
const refresh = react.useRef(() => {});
|
|
760
|
+
const tooltipId = react.useId();
|
|
761
|
+
react.useEffect(() => {
|
|
762
|
+
let live = true, generation = 0, pending = false;
|
|
763
|
+
const read = async (force = false, reset = false) => {
|
|
764
|
+
if (!live || pending && !force) return;
|
|
765
|
+
const current = ++generation;
|
|
766
|
+
pending = true;
|
|
767
|
+
setView((previous) => reset ? { loading: true } : {
|
|
768
|
+
...previous,
|
|
769
|
+
loading: true
|
|
770
|
+
});
|
|
771
|
+
try {
|
|
772
|
+
const balance = await call("images/balance", { force });
|
|
773
|
+
if (live && current === generation) setView({
|
|
774
|
+
balance,
|
|
775
|
+
loading: false
|
|
776
|
+
});
|
|
777
|
+
} catch {
|
|
778
|
+
if (live && current === generation) setView((previous) => ({
|
|
779
|
+
...previous,
|
|
780
|
+
loading: false,
|
|
781
|
+
failed: true
|
|
782
|
+
}));
|
|
783
|
+
} finally {
|
|
784
|
+
if (current === generation) pending = false;
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
refresh.current = read;
|
|
788
|
+
read();
|
|
789
|
+
const changed = () => {
|
|
790
|
+
read(true, true);
|
|
791
|
+
};
|
|
792
|
+
const timer = window.setInterval(() => {
|
|
793
|
+
read();
|
|
794
|
+
}, 6e4);
|
|
795
|
+
window.addEventListener("packycode-images-updated", changed);
|
|
796
|
+
return () => {
|
|
797
|
+
live = false;
|
|
798
|
+
generation += 1;
|
|
799
|
+
refresh.current = () => {};
|
|
800
|
+
window.clearInterval(timer);
|
|
801
|
+
window.removeEventListener("packycode-images-updated", changed);
|
|
802
|
+
};
|
|
803
|
+
}, [call]);
|
|
804
|
+
const row = currentImageKey(view.balance);
|
|
805
|
+
const usage = row?.usage;
|
|
806
|
+
const unlimited = usage?.unlimitedQuota === true;
|
|
807
|
+
const remaining = unlimited ? t("keyUnlimited") : formatUsd(usage?.availableUsd);
|
|
808
|
+
const stale = usage !== void 0 && (view.failed || row.stale !== void 0);
|
|
809
|
+
const display = remaining ?? (view.loading ? "…" : "—");
|
|
810
|
+
const details = [
|
|
811
|
+
t("imagesComposerLabel"),
|
|
812
|
+
row?.label ? `${t("imagesBalanceKey")}: ${row.label}` : void 0,
|
|
813
|
+
usage?.name ? `${t("keyName")}: ${usage.name}` : void 0,
|
|
814
|
+
`${t("keyRemaining")}: ${remaining ?? "—"}`,
|
|
815
|
+
`${t("keyUsed")}: ${formatUsd(usage?.usedUsd) ?? "—"}`,
|
|
816
|
+
`${t("accountLimit")}: ${unlimited ? t("keyUnlimited") : formatUsd(quotaTotalUsd(usage)) ?? "—"}`,
|
|
817
|
+
stale ? fill(t("composerStale"), { time: readTime(row.stale?.fetchedAt ?? view.balance?.fetchedAt) }) : view.failed || row?.error ? t("imagesBalanceFailed") : view.loading ? t("imagesBalanceLoading") : view.balance?.fetchedAt === void 0 ? void 0 : fill(t("imagesBalanceReadAt"), { time: readTime(view.balance.fetchedAt) })
|
|
818
|
+
].filter(Boolean).join("\n");
|
|
819
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
820
|
+
className: "pkc-image-balance",
|
|
821
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
822
|
+
className: "pkc-composer pkc-image-balance-value",
|
|
823
|
+
role: "status",
|
|
824
|
+
tabIndex: 0,
|
|
825
|
+
"aria-label": `${t("imagesComposerLabel")}: ${stale ? "≈" : ""}${display}`,
|
|
826
|
+
"aria-describedby": tooltipId,
|
|
827
|
+
onMouseEnter: () => {
|
|
828
|
+
refresh.current();
|
|
829
|
+
},
|
|
830
|
+
onFocus: () => {
|
|
831
|
+
refresh.current();
|
|
832
|
+
},
|
|
833
|
+
children: stale ? `≈${display}` : display
|
|
834
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
835
|
+
className: "pkc-image-balance-tooltip",
|
|
836
|
+
id: tooltipId,
|
|
837
|
+
role: "tooltip",
|
|
838
|
+
children: details
|
|
839
|
+
})]
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
721
843
|
//#region src/client-images.jsx
|
|
722
844
|
const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
|
|
723
845
|
/** Only projections come back from the host; the password draft is cleared on success. */
|
|
@@ -796,13 +918,9 @@ window.__ModuleLoader__.load({
|
|
|
796
918
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
797
919
|
className: "pkc-title",
|
|
798
920
|
children: t("imagesTitle")
|
|
799
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
921
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
800
922
|
className: "pkc-actions",
|
|
801
|
-
children:
|
|
802
|
-
className: "pkc-chip",
|
|
803
|
-
"data-tone": "neutral",
|
|
804
|
-
children: t("imagesSeparateKey")
|
|
805
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
923
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
806
924
|
className: "pkc-btn",
|
|
807
925
|
type: "button",
|
|
808
926
|
disabled: busy || balanceLoading,
|
|
@@ -810,7 +928,7 @@ window.__ModuleLoader__.load({
|
|
|
810
928
|
readBalance(true);
|
|
811
929
|
},
|
|
812
930
|
children: balanceLoading ? t("refreshing") : t("refresh")
|
|
813
|
-
})
|
|
931
|
+
})
|
|
814
932
|
})]
|
|
815
933
|
}),
|
|
816
934
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1020,11 +1138,14 @@ window.__ModuleLoader__.load({
|
|
|
1020
1138
|
const [error, setError] = react.useState();
|
|
1021
1139
|
react.useEffect(() => {
|
|
1022
1140
|
let live = true;
|
|
1141
|
+
let generation = 0;
|
|
1023
1142
|
const refresh = () => {
|
|
1024
|
-
if (active)
|
|
1025
|
-
|
|
1143
|
+
if (!active) return;
|
|
1144
|
+
const current = ++generation;
|
|
1145
|
+
call("images/status").then((value) => {
|
|
1146
|
+
if (live && current === generation) setReady(value.imageEnabled && value.available && value.keys.some((key) => !key.disabled));
|
|
1026
1147
|
}).catch(() => {
|
|
1027
|
-
if (live) setReady(false);
|
|
1148
|
+
if (live && current === generation) setReady(false);
|
|
1028
1149
|
});
|
|
1029
1150
|
};
|
|
1030
1151
|
setReady(false);
|
|
@@ -1038,24 +1159,31 @@ window.__ModuleLoader__.load({
|
|
|
1038
1159
|
if (!active || !ready) return null;
|
|
1039
1160
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1040
1161
|
className: "pkc-image-shortcut",
|
|
1041
|
-
children: [
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1162
|
+
children: [
|
|
1163
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1164
|
+
className: "pkc-btn",
|
|
1165
|
+
type: "button",
|
|
1166
|
+
title: t("imagesShortcutHint"),
|
|
1167
|
+
onClick: () => {
|
|
1168
|
+
try {
|
|
1169
|
+
prepare(t("imagesDraft"));
|
|
1170
|
+
setError(void 0);
|
|
1171
|
+
} catch {
|
|
1172
|
+
setError(t("imagesDraftBusy"));
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
children: t("imagesCreate")
|
|
1176
|
+
}),
|
|
1177
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageShortcutBalance, {
|
|
1178
|
+
call,
|
|
1179
|
+
t
|
|
1180
|
+
}),
|
|
1181
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1182
|
+
className: "pkc-error",
|
|
1183
|
+
role: "alert",
|
|
1184
|
+
children: error
|
|
1185
|
+
})
|
|
1186
|
+
]
|
|
1059
1187
|
});
|
|
1060
1188
|
}
|
|
1061
1189
|
function GeneratedImage({ attachment, loadImage, openImage, t }) {
|
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.4",
|
|
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",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"packycode-host-v020-fixture": "file:tests/fixtures/host-v020",
|
|
101
101
|
"react": "18.3.1",
|
|
102
102
|
"react-dom": "18.3.1",
|
|
103
|
+
"react-test-renderer": "18.3.1",
|
|
103
104
|
"tsdown": "0.22.2",
|
|
104
105
|
"yaml": "2.9.1"
|
|
105
106
|
},
|