@dickpy/dsh-imagegen 1.0.0 → 1.0.2
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/README.md +36 -22
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/plugin-settings.png +0 -0
- package/lib/client.js +187 -112
- package/lib/client.js.map +1 -1
- package/lib/index.js +267 -52
- package/package.json +3 -2
- package/src/client/ImageGenPanel.tsx +51 -19
- package/src/client/api.ts +25 -14
- package/src/client/locales.ts +14 -1
- package/src/client/panel.module.css +50 -0
- package/src/history-store.ts +62 -41
- package/src/index.ts +2 -1
- package/src/protocol.ts +21 -0
- package/src/routes.ts +80 -7
- package/src/updater.ts +116 -0
package/lib/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { SettingsConflictError, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
2
|
import z from "schemastery";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { promises } from "node:fs";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
6
8
|
//#region src/protocol.ts
|
|
7
9
|
/**
|
|
8
10
|
* Wire contract shared by the host and client halves of dsh-imagegen: the
|
|
@@ -18,6 +20,11 @@ const SETTINGS_API = {
|
|
|
18
20
|
};
|
|
19
21
|
/** The image-generation proxy route. */
|
|
20
22
|
const GENERATE_API = "/api/dsh-imagegen/generate";
|
|
23
|
+
/** Host-mediated GitHub Release update routes. */
|
|
24
|
+
const UPDATE_API = {
|
|
25
|
+
check: "/api/dsh-imagegen/update/check",
|
|
26
|
+
apply: "/api/dsh-imagegen/update/apply"
|
|
27
|
+
};
|
|
21
28
|
/**
|
|
22
29
|
* Same-origin route family for the host-persisted generation history. Images
|
|
23
30
|
* live as files under ~/.dsh/dsh-imagegen/images/ and are served back through
|
|
@@ -260,6 +267,12 @@ function extensionOf$1(mime) {
|
|
|
260
267
|
const HISTORY_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
261
268
|
const INDEX_PATH = path.join(HISTORY_DIR, "index.json");
|
|
262
269
|
const IMAGES_DIR = path.join(HISTORY_DIR, "images");
|
|
270
|
+
let pendingMutation = Promise.resolve();
|
|
271
|
+
function mutateHistory(operation) {
|
|
272
|
+
const next = pendingMutation.then(operation, operation);
|
|
273
|
+
pendingMutation = next.then(() => void 0, () => void 0);
|
|
274
|
+
return next;
|
|
275
|
+
}
|
|
263
276
|
/** File extension for a MIME type (image file names). */
|
|
264
277
|
function extensionOf(mime) {
|
|
265
278
|
switch (mime.split(";")[0].trim()) {
|
|
@@ -351,52 +364,63 @@ async function listHistory() {
|
|
|
351
364
|
}
|
|
352
365
|
/** Append one generation, evicting the oldest beyond HISTORY_MAX. */
|
|
353
366
|
async function appendHistory(input) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
367
|
+
return mutateHistory(async () => {
|
|
368
|
+
await ensureDirs();
|
|
369
|
+
const prefix = safeId(input.id);
|
|
370
|
+
const storedImages = [];
|
|
371
|
+
try {
|
|
372
|
+
for (let index = 0; index < input.images.length; index++) {
|
|
373
|
+
const image = input.images[index];
|
|
374
|
+
const file = `${prefix}-${index}.${extensionOf(image.mime)}`;
|
|
375
|
+
await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
|
|
376
|
+
storedImages.push({
|
|
377
|
+
file,
|
|
378
|
+
mime: image.mime,
|
|
379
|
+
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
await removeEntryFiles({ images: storedImages });
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
const merged = [{
|
|
387
|
+
id: input.id,
|
|
388
|
+
createdAt: input.createdAt,
|
|
389
|
+
mode: input.mode,
|
|
390
|
+
model: input.model,
|
|
391
|
+
prompt: input.prompt,
|
|
392
|
+
size: input.size,
|
|
393
|
+
quality: input.quality,
|
|
394
|
+
detail: input.detail,
|
|
395
|
+
n: input.n,
|
|
396
|
+
images: storedImages,
|
|
397
|
+
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
398
|
+
}, ...await readIndex()];
|
|
399
|
+
const kept = merged.slice(0, 50);
|
|
400
|
+
for (const dropped of merged.slice(50)) await removeEntryFiles(dropped);
|
|
401
|
+
await writeIndex(kept);
|
|
402
|
+
return kept.map(toWire);
|
|
403
|
+
});
|
|
384
404
|
}
|
|
385
405
|
/** Remove one entry (and its image files). */
|
|
386
406
|
async function removeHistory(id) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
407
|
+
return mutateHistory(async () => {
|
|
408
|
+
const previous = await readIndex();
|
|
409
|
+
const target = previous.find((entry) => entry.id === id);
|
|
410
|
+
if (target !== void 0) await removeEntryFiles(target);
|
|
411
|
+
const kept = previous.filter((entry) => entry.id !== id);
|
|
412
|
+
await writeIndex(kept);
|
|
413
|
+
return kept.map(toWire);
|
|
414
|
+
});
|
|
393
415
|
}
|
|
394
416
|
/** Remove every entry (and all image files). */
|
|
395
417
|
async function clearHistory() {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
418
|
+
return mutateHistory(async () => {
|
|
419
|
+
const previous = await readIndex();
|
|
420
|
+
for (const entry of previous) await removeEntryFiles(entry);
|
|
421
|
+
await writeIndex([]);
|
|
422
|
+
return [];
|
|
423
|
+
});
|
|
400
424
|
}
|
|
401
425
|
/** Read one stored image file by its (validated) file name. */
|
|
402
426
|
async function readHistoryImage(file) {
|
|
@@ -411,6 +435,106 @@ async function readHistoryImage(file) {
|
|
|
411
435
|
}
|
|
412
436
|
}
|
|
413
437
|
//#endregion
|
|
438
|
+
//#region src/updater.ts
|
|
439
|
+
/** GitHub Release discovery and explicit, user-triggered plugin updates. */
|
|
440
|
+
/** Keep this in sync with package.json for each published release. */
|
|
441
|
+
const CURRENT_VERSION = "1.0.2";
|
|
442
|
+
const PACKAGE_NAME = "@dickpy/dsh-imagegen";
|
|
443
|
+
const RELEASES_URL = "https://api.github.com/repos/dickpy/dsh-imagegen/releases/latest";
|
|
444
|
+
const CHECK_TIMEOUT_MS = 1e4;
|
|
445
|
+
const CACHE_TTL_MS = 15 * 6e4;
|
|
446
|
+
let cached;
|
|
447
|
+
/** Compare stable semver triples; returns positive when `left` is newer. */
|
|
448
|
+
function compareVersions(left, right) {
|
|
449
|
+
const parse = (value) => {
|
|
450
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
|
|
451
|
+
if (match === null) return [
|
|
452
|
+
0,
|
|
453
|
+
0,
|
|
454
|
+
0
|
|
455
|
+
];
|
|
456
|
+
return [
|
|
457
|
+
Number(match[1]),
|
|
458
|
+
Number(match[2]),
|
|
459
|
+
Number(match[3])
|
|
460
|
+
];
|
|
461
|
+
};
|
|
462
|
+
const a = parse(left);
|
|
463
|
+
const b = parse(right);
|
|
464
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
465
|
+
}
|
|
466
|
+
function normalizedReleaseVersion(tag) {
|
|
467
|
+
if (typeof tag !== "string" || !/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(tag.trim())) return void 0;
|
|
468
|
+
return tag.trim().replace(/^v/, "");
|
|
469
|
+
}
|
|
470
|
+
/** Read the latest stable GitHub Release, with a short host-side cache. */
|
|
471
|
+
async function checkForUpdate(fetchFn = fetch, now = Date.now()) {
|
|
472
|
+
if (cached !== void 0 && cached.expiresAt > now) return cached.value;
|
|
473
|
+
const response = await fetchFn(RELEASES_URL, {
|
|
474
|
+
headers: {
|
|
475
|
+
accept: "application/vnd.github+json",
|
|
476
|
+
"user-agent": "dsh-imagegen-update-check"
|
|
477
|
+
},
|
|
478
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS)
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) throw new Error(`GitHub Releases returned HTTP ${response.status}`);
|
|
481
|
+
const payload = await response.json();
|
|
482
|
+
if (payload === null || typeof payload !== "object") throw new Error("GitHub Releases returned malformed JSON");
|
|
483
|
+
const release = payload;
|
|
484
|
+
if (release.draft === true || release.prerelease === true) throw new Error("latest GitHub Release is not stable");
|
|
485
|
+
const latestVersion = normalizedReleaseVersion(release.tag_name);
|
|
486
|
+
if (latestVersion === void 0) throw new Error("latest GitHub Release has an invalid version tag");
|
|
487
|
+
const releaseUrl = typeof release.html_url === "string" ? release.html_url : "https://github.com/dickpy/dsh-imagegen/releases";
|
|
488
|
+
const value = {
|
|
489
|
+
currentVersion: CURRENT_VERSION,
|
|
490
|
+
latestVersion,
|
|
491
|
+
updateAvailable: compareVersions(latestVersion, CURRENT_VERSION) > 0,
|
|
492
|
+
releaseUrl,
|
|
493
|
+
...typeof release.published_at === "string" ? { publishedAt: release.published_at } : {}
|
|
494
|
+
};
|
|
495
|
+
cached = {
|
|
496
|
+
expiresAt: now + CACHE_TTL_MS,
|
|
497
|
+
value
|
|
498
|
+
};
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
/** Resolve the profile that launched the current DSH process. */
|
|
502
|
+
function profileFromProcess(argv = process.argv, env = process.env) {
|
|
503
|
+
const envProfile = env.DSH_PROFILE?.trim();
|
|
504
|
+
if (envProfile !== void 0 && /^[a-zA-Z0-9_-]+$/.test(envProfile)) return envProfile;
|
|
505
|
+
const profileIndex = argv.indexOf("--profile");
|
|
506
|
+
const explicit = profileIndex >= 0 ? argv[profileIndex + 1]?.trim() : void 0;
|
|
507
|
+
if (explicit !== void 0 && /^[a-zA-Z0-9_-]+$/.test(explicit)) return explicit;
|
|
508
|
+
if (argv.includes("web")) return "web";
|
|
509
|
+
return "web";
|
|
510
|
+
}
|
|
511
|
+
/** Run the same official command documented for plugin installation. */
|
|
512
|
+
function installUpdate(version, spawnFn = spawn, argv = process.argv, env = process.env) {
|
|
513
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) return Promise.reject(/* @__PURE__ */ new Error("invalid update version"));
|
|
514
|
+
const profile = profileFromProcess(argv, env);
|
|
515
|
+
const child = spawnFn(process.platform === "win32" ? "dsh.cmd" : "dsh", [
|
|
516
|
+
"plugin",
|
|
517
|
+
"--profile",
|
|
518
|
+
profile,
|
|
519
|
+
"add",
|
|
520
|
+
`${PACKAGE_NAME}@${version}`
|
|
521
|
+
], {
|
|
522
|
+
shell: process.platform === "win32",
|
|
523
|
+
stdio: "ignore"
|
|
524
|
+
});
|
|
525
|
+
return new Promise((resolve, reject) => {
|
|
526
|
+
child.once("error", reject);
|
|
527
|
+
child.once("exit", (code, signal) => {
|
|
528
|
+
if (code === 0) resolve();
|
|
529
|
+
else reject(/* @__PURE__ */ new Error(signal === null ? `plugin update exited with code ${code ?? "unknown"}` : `plugin update terminated by ${signal}`));
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** Test helper: clear the host-side Release cache. */
|
|
534
|
+
function clearUpdateCache() {
|
|
535
|
+
cached = void 0;
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
414
538
|
//#region src/routes.ts
|
|
415
539
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
416
540
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024;
|
|
@@ -550,6 +674,13 @@ function failureOf(error) {
|
|
|
550
674
|
* @returns the route registrations.
|
|
551
675
|
*/
|
|
552
676
|
function makeRoutes(deps) {
|
|
677
|
+
const history = deps.history ?? {
|
|
678
|
+
list: listHistory,
|
|
679
|
+
append: appendHistory,
|
|
680
|
+
remove: removeHistory,
|
|
681
|
+
clear: clearHistory,
|
|
682
|
+
readImage: readHistoryImage
|
|
683
|
+
};
|
|
553
684
|
const guard = (req, res, method) => {
|
|
554
685
|
if (!isLoopbackRequest(req)) {
|
|
555
686
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -661,13 +792,37 @@ function makeRoutes(deps) {
|
|
|
661
792
|
quality: typeof body.quality === "string" ? body.quality : "auto",
|
|
662
793
|
n: typeof body.n === "number" ? body.n : 1,
|
|
663
794
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
664
|
-
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {}
|
|
795
|
+
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
796
|
+
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {}
|
|
665
797
|
};
|
|
666
798
|
try {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
799
|
+
const result = await generateImage(deps.resolve(), request);
|
|
800
|
+
try {
|
|
801
|
+
const entries = await history.append({
|
|
802
|
+
id: randomUUID(),
|
|
803
|
+
createdAt: Date.now(),
|
|
804
|
+
mode: request.mode,
|
|
805
|
+
model: request.model,
|
|
806
|
+
prompt: request.prompt,
|
|
807
|
+
size: request.size,
|
|
808
|
+
quality: request.quality,
|
|
809
|
+
detail: request.detail,
|
|
810
|
+
n: request.n,
|
|
811
|
+
images: result.images,
|
|
812
|
+
...request.refName === void 0 ? {} : { refName: request.refName }
|
|
813
|
+
});
|
|
814
|
+
writeJson(res, 200, {
|
|
815
|
+
ok: true,
|
|
816
|
+
...result,
|
|
817
|
+
history: entries
|
|
818
|
+
});
|
|
819
|
+
} catch (error) {
|
|
820
|
+
writeJson(res, 200, {
|
|
821
|
+
ok: true,
|
|
822
|
+
...result,
|
|
823
|
+
historyError: messageOf(error)
|
|
824
|
+
});
|
|
825
|
+
}
|
|
671
826
|
} catch (error) {
|
|
672
827
|
const message = error instanceof Error ? error.message : String(error);
|
|
673
828
|
writeJson(res, 200, {
|
|
@@ -678,6 +833,66 @@ function makeRoutes(deps) {
|
|
|
678
833
|
}
|
|
679
834
|
}
|
|
680
835
|
},
|
|
836
|
+
{
|
|
837
|
+
kind: "exact",
|
|
838
|
+
path: UPDATE_API.check,
|
|
839
|
+
handler: async (req, res) => {
|
|
840
|
+
if (!guard(req, res, "POST")) return;
|
|
841
|
+
try {
|
|
842
|
+
writeJson(res, 200, {
|
|
843
|
+
ok: true,
|
|
844
|
+
update: await checkForUpdate()
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
writeJson(res, 200, {
|
|
848
|
+
ok: false,
|
|
849
|
+
code: "update-check-failed",
|
|
850
|
+
message: messageOf(error)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
kind: "exact",
|
|
857
|
+
path: UPDATE_API.apply,
|
|
858
|
+
handler: async (req, res) => {
|
|
859
|
+
if (!guard(req, res, "POST")) return;
|
|
860
|
+
const body = await readJsonBody(req);
|
|
861
|
+
const version = body !== void 0 && typeof body.version === "string" ? body.version.trim() : "";
|
|
862
|
+
if (version === "") {
|
|
863
|
+
writeJson(res, 200, {
|
|
864
|
+
ok: false,
|
|
865
|
+
code: "bad-request",
|
|
866
|
+
message: "update version is required"
|
|
867
|
+
});
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
const latest = await checkForUpdate();
|
|
872
|
+
if (!latest.updateAvailable || latest.latestVersion !== version) {
|
|
873
|
+
writeJson(res, 200, {
|
|
874
|
+
ok: false,
|
|
875
|
+
code: "update-not-available",
|
|
876
|
+
message: `version ${version} is not the latest available release`
|
|
877
|
+
});
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
await installUpdate(version);
|
|
881
|
+
writeJson(res, 200, {
|
|
882
|
+
ok: true,
|
|
883
|
+
currentVersion: CURRENT_VERSION,
|
|
884
|
+
updatedVersion: version,
|
|
885
|
+
restartRequired: true
|
|
886
|
+
});
|
|
887
|
+
} catch (error) {
|
|
888
|
+
writeJson(res, 200, {
|
|
889
|
+
ok: false,
|
|
890
|
+
code: "update-failed",
|
|
891
|
+
message: messageOf(error)
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
},
|
|
681
896
|
{
|
|
682
897
|
kind: "exact",
|
|
683
898
|
path: HISTORY_API.list,
|
|
@@ -686,7 +901,7 @@ function makeRoutes(deps) {
|
|
|
686
901
|
try {
|
|
687
902
|
writeJson(res, 200, {
|
|
688
903
|
ok: true,
|
|
689
|
-
entries: await
|
|
904
|
+
entries: await history.list()
|
|
690
905
|
});
|
|
691
906
|
} catch (error) {
|
|
692
907
|
writeJson(res, 200, {
|
|
@@ -723,7 +938,7 @@ function makeRoutes(deps) {
|
|
|
723
938
|
try {
|
|
724
939
|
writeJson(res, 200, {
|
|
725
940
|
ok: true,
|
|
726
|
-
entries: await
|
|
941
|
+
entries: await history.append(entry)
|
|
727
942
|
});
|
|
728
943
|
} catch (error) {
|
|
729
944
|
writeJson(res, 200, {
|
|
@@ -752,7 +967,7 @@ function makeRoutes(deps) {
|
|
|
752
967
|
try {
|
|
753
968
|
writeJson(res, 200, {
|
|
754
969
|
ok: true,
|
|
755
|
-
entries: await
|
|
970
|
+
entries: await history.remove(id)
|
|
756
971
|
});
|
|
757
972
|
} catch (error) {
|
|
758
973
|
writeJson(res, 200, {
|
|
@@ -771,7 +986,7 @@ function makeRoutes(deps) {
|
|
|
771
986
|
try {
|
|
772
987
|
writeJson(res, 200, {
|
|
773
988
|
ok: true,
|
|
774
|
-
entries: await
|
|
989
|
+
entries: await history.clear()
|
|
775
990
|
});
|
|
776
991
|
} catch (error) {
|
|
777
992
|
writeJson(res, 200, {
|
|
@@ -799,7 +1014,7 @@ function makeRoutes(deps) {
|
|
|
799
1014
|
writeJson(res, 404, { error: "not found" });
|
|
800
1015
|
return;
|
|
801
1016
|
}
|
|
802
|
-
const found = await
|
|
1017
|
+
const found = await history.readImage(file);
|
|
803
1018
|
if (found === void 0) {
|
|
804
1019
|
writeJson(res, 404, { error: "not found" });
|
|
805
1020
|
return;
|
|
@@ -834,7 +1049,7 @@ const DEFAULT_ANNOUNCE = true;
|
|
|
834
1049
|
/** Order of the announcement section within the tool-guidance band. */
|
|
835
1050
|
const SECTION_ORDER = 150;
|
|
836
1051
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
837
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI
|
|
1052
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / 文生图 / 图生图」时即指本插件,请据此协作。";
|
|
838
1053
|
/**
|
|
839
1054
|
* Mount the settings section, routes, and announcement.
|
|
840
1055
|
* @param ctx - host plugin context carrying webServer/systemPrompt.
|
|
@@ -893,4 +1108,4 @@ function apply(ctx, config) {
|
|
|
893
1108
|
sync();
|
|
894
1109
|
}
|
|
895
1110
|
//#endregion
|
|
896
|
-
export { Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, apply, generateImage, inject, makeRoutes, name };
|
|
1111
|
+
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, apply, checkForUpdate, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, makeRoutes, name, profileFromProcess };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI 生图 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / gpt-image-1 / dall-e-3), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"lib",
|
|
52
52
|
"src",
|
|
53
53
|
"cordis.patch.yml",
|
|
54
|
+
"docs",
|
|
54
55
|
"README.md"
|
|
55
56
|
],
|
|
56
57
|
"license": "Apache-2.0",
|
|
@@ -63,4 +64,4 @@
|
|
|
63
64
|
"watch": "tsdown --watch",
|
|
64
65
|
"typecheck": "tsc --noEmit"
|
|
65
66
|
}
|
|
66
|
-
}
|
|
67
|
+
}
|
|
@@ -13,7 +13,7 @@ import { createPortal } from 'react-dom'
|
|
|
13
13
|
import { Button, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
14
14
|
import type { ImageGenApi } from './api.ts'
|
|
15
15
|
import { errorMessage, tt } from './helpers.ts'
|
|
16
|
-
import type { GeneratedImage, GenerateMode, GenerateRequest, HistoryEntry,
|
|
16
|
+
import type { GeneratedImage, GenerateMode, GenerateRequest, HistoryEntry, HistoryImageRef, UpdateInfo } from '../protocol.ts'
|
|
17
17
|
import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
|
|
18
18
|
import css from './panel.module.css'
|
|
19
19
|
|
|
@@ -124,6 +124,10 @@ export function ImageGenPanel(props: {
|
|
|
124
124
|
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
125
125
|
const [viewingHistoryId, setViewingHistoryId] = useState<string | null>(null)
|
|
126
126
|
const [preview, setPreview] = useState<{ images: GeneratedImage[]; index: number } | null>(null)
|
|
127
|
+
const [update, setUpdate] = useState<UpdateInfo | null>(null)
|
|
128
|
+
const [updating, setUpdating] = useState(false)
|
|
129
|
+
const [updateMessage, setUpdateMessage] = useState<string | null>(null)
|
|
130
|
+
const [updateResult, setUpdateResult] = useState<'success' | 'failed' | null>(null)
|
|
127
131
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
128
132
|
const elapsed = useElapsed(generating, startedAt)
|
|
129
133
|
|
|
@@ -137,6 +141,35 @@ export function ImageGenPanel(props: {
|
|
|
137
141
|
return () => { disposed = true }
|
|
138
142
|
}, [api])
|
|
139
143
|
|
|
144
|
+
// Release checks are host-mediated and intentionally best-effort: a GitHub
|
|
145
|
+
// outage must never make the image-generation studio unavailable.
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
let disposed = false
|
|
148
|
+
api.updateCheck()
|
|
149
|
+
.then(info => {
|
|
150
|
+
if (!disposed && info.updateAvailable) setUpdate(info)
|
|
151
|
+
})
|
|
152
|
+
.catch(() => { /* update discovery is optional */ })
|
|
153
|
+
return () => { disposed = true }
|
|
154
|
+
}, [api])
|
|
155
|
+
|
|
156
|
+
const applyUpdate = async (): Promise<void> => {
|
|
157
|
+
if (update === null || updating) return
|
|
158
|
+
setUpdating(true)
|
|
159
|
+
setUpdateMessage(null)
|
|
160
|
+
setUpdateResult(null)
|
|
161
|
+
try {
|
|
162
|
+
const result = await api.updateApply(update.latestVersion)
|
|
163
|
+
setUpdateMessage(tt('update.success', { version: result.updatedVersion }))
|
|
164
|
+
setUpdateResult('success')
|
|
165
|
+
} catch {
|
|
166
|
+
setUpdateMessage(tt('update.failed'))
|
|
167
|
+
setUpdateResult('failed')
|
|
168
|
+
} finally {
|
|
169
|
+
setUpdating(false)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
140
173
|
/** Read an uploaded reference image into a data URL. */
|
|
141
174
|
const acceptFile = (file: File | undefined): void => {
|
|
142
175
|
if (file === undefined) return
|
|
@@ -177,6 +210,7 @@ export function ImageGenPanel(props: {
|
|
|
177
210
|
n: count,
|
|
178
211
|
detail,
|
|
179
212
|
...mode === 'edit' && refImage !== null ? { image: refImage.dataUrl } : {},
|
|
213
|
+
...mode === 'edit' && refImage !== null ? { refName: refImage.name } : {},
|
|
180
214
|
}
|
|
181
215
|
setGenerating(true)
|
|
182
216
|
setError(null)
|
|
@@ -186,24 +220,8 @@ export function ImageGenPanel(props: {
|
|
|
186
220
|
const result = await api.generate(request)
|
|
187
221
|
setImages(result.images)
|
|
188
222
|
setViewingHistoryId(null)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
createdAt: Date.now(),
|
|
192
|
-
mode,
|
|
193
|
-
model,
|
|
194
|
-
prompt: promptText,
|
|
195
|
-
size,
|
|
196
|
-
quality,
|
|
197
|
-
detail,
|
|
198
|
-
n: count,
|
|
199
|
-
images: result.images,
|
|
200
|
-
...mode === 'edit' && refImage !== null ? { refName: refImage.name } : {},
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
setHistory(await api.historyAppend(entry))
|
|
204
|
-
} catch {
|
|
205
|
-
// Persisting history is best-effort; the images stay on the canvas.
|
|
206
|
-
}
|
|
223
|
+
if (result.history !== undefined) setHistory(result.history)
|
|
224
|
+
if (result.historyError !== undefined) setError(result.historyError)
|
|
207
225
|
} catch (caught) {
|
|
208
226
|
setError(errorMessage(caught))
|
|
209
227
|
} finally {
|
|
@@ -308,6 +326,20 @@ export function ImageGenPanel(props: {
|
|
|
308
326
|
? <div className={css.banner} data-kind="warn">{tt('config.missing')}</div>
|
|
309
327
|
: <div className={css.banner} data-kind="ok">{tt('config.configured', { url: apiUrl })}</div>}
|
|
310
328
|
|
|
329
|
+
{update !== null ? (
|
|
330
|
+
<div className={css.updateBanner} data-kind={updateResult === 'success' ? 'ok' : 'warn'}>
|
|
331
|
+
<span className={css.updateText}>
|
|
332
|
+
{updateMessage ?? tt('update.available', { version: update.latestVersion })}
|
|
333
|
+
</span>
|
|
334
|
+
<span className={css.updateActions}>
|
|
335
|
+
<a className={css.updateRelease} href={update.releaseUrl} target="_blank" rel="noreferrer">{tt('update.release')}</a>
|
|
336
|
+
<Button variant="primary" size="sm" disabled={updating || updateMessage !== null} onClick={() => { void applyUpdate() }}>
|
|
337
|
+
{updating ? tt('update.installing') : tt('update.install')}
|
|
338
|
+
</Button>
|
|
339
|
+
</span>
|
|
340
|
+
</div>
|
|
341
|
+
) : null}
|
|
342
|
+
|
|
311
343
|
<div className={css.studio}>
|
|
312
344
|
{/* ---------------------------------------------------- config sidebar */}
|
|
313
345
|
<aside className={css.config}>
|
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { GENERATE_API, HISTORY_API, type GenerateRequest, type GenerateResult, type HistoryEntry, type
|
|
6
|
+
import { GENERATE_API, HISTORY_API, UPDATE_API, type GenerateRequest, type GenerateResult, type HistoryEntry, type UpdateInfo } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -40,6 +40,24 @@ async function readEnvelope<T>(response: Response): Promise<T> {
|
|
|
40
40
|
|
|
41
41
|
/** The browser half's data entry point. */
|
|
42
42
|
export class ImageGenApi {
|
|
43
|
+
/** Ask the host to check the latest stable GitHub Release. */
|
|
44
|
+
async updateCheck(): Promise<UpdateInfo> {
|
|
45
|
+
const response = await fetch(UPDATE_API.check, { method: 'POST' })
|
|
46
|
+
const body = await readEnvelope<{ ok: true; update: UpdateInfo }>(response)
|
|
47
|
+
return body.update
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Ask the host to install a previously discovered Release. */
|
|
51
|
+
async updateApply(version: string): Promise<{ updatedVersion: string; restartRequired: boolean }> {
|
|
52
|
+
const response = await fetch(UPDATE_API.apply, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'content-type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ version }),
|
|
56
|
+
})
|
|
57
|
+
const body = await readEnvelope<{ ok: true; updatedVersion: string; restartRequired: boolean }>(response)
|
|
58
|
+
return { updatedVersion: body.updatedVersion, restartRequired: body.restartRequired }
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/** Forward one generate request to the host proxy. */
|
|
44
62
|
async generate(request: GenerateRequest): Promise<GenerateResult> {
|
|
45
63
|
const response = await fetch(GENERATE_API, {
|
|
@@ -47,8 +65,12 @@ export class ImageGenApi {
|
|
|
47
65
|
headers: { 'content-type': 'application/json' },
|
|
48
66
|
body: JSON.stringify(request),
|
|
49
67
|
})
|
|
50
|
-
const body = await readEnvelope<{ ok: true; images: GenerateResult['images'] }>(response)
|
|
51
|
-
return {
|
|
68
|
+
const body = await readEnvelope<{ ok: true; images: GenerateResult['images']; history?: HistoryEntry[]; historyError?: string }>(response)
|
|
69
|
+
return {
|
|
70
|
+
images: body.images,
|
|
71
|
+
...body.history === undefined ? {} : { history: body.history },
|
|
72
|
+
...body.historyError === undefined ? {} : { historyError: body.historyError },
|
|
73
|
+
}
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/** List the host-persisted history (newest first). */
|
|
@@ -58,17 +80,6 @@ export class ImageGenApi {
|
|
|
58
80
|
return body.entries
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
/** Append one generation to the host-persisted history. */
|
|
62
|
-
async historyAppend(entry: HistoryEntryInput): Promise<HistoryEntry[]> {
|
|
63
|
-
const response = await fetch(HISTORY_API.append, {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: { 'content-type': 'application/json' },
|
|
66
|
-
body: JSON.stringify({ entry }),
|
|
67
|
-
})
|
|
68
|
-
const body = await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)
|
|
69
|
-
return body.entries
|
|
70
|
-
}
|
|
71
|
-
|
|
72
83
|
/** Remove one history entry by id. */
|
|
73
84
|
async historyRemove(id: string): Promise<HistoryEntry[]> {
|
|
74
85
|
const response = await fetch(HISTORY_API.remove, {
|
package/src/client/locales.ts
CHANGED
|
@@ -76,6 +76,13 @@ export const zh = {
|
|
|
76
76
|
'config.missing': '尚未配置 API:请前往「设置 → 插件 → 可配置」为 AI 生图填写 api_url 与 api_key。',
|
|
77
77
|
'config.configured': '已连接 {url}',
|
|
78
78
|
'config.disabled': '插件已停用,请在设置中重新启用。',
|
|
79
|
+
// plugin update
|
|
80
|
+
'update.available': '检测到新版本:{version}',
|
|
81
|
+
'update.install': '在线更新',
|
|
82
|
+
'update.installing': '更新中…',
|
|
83
|
+
'update.success': '已更新到 {version},请重启 DSH',
|
|
84
|
+
'update.failed': '更新失败,请重试',
|
|
85
|
+
'update.release': '查看 Release',
|
|
79
86
|
// settings card
|
|
80
87
|
'settings.title': 'AI 生图(dsh-imagegen)',
|
|
81
88
|
'settings.description': '配置图像生成 API 地址与密钥',
|
|
@@ -168,9 +175,15 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
168
175
|
'preview.prev': 'Previous',
|
|
169
176
|
'preview.next': 'Next',
|
|
170
177
|
'preview.index': '{index} / {total}',
|
|
171
|
-
'config.missing': 'API not configured: open "Settings →
|
|
178
|
+
'config.missing': 'API not configured: open "Settings → Plugins → Configurable" and fill in api_url and api_key for AI Image.',
|
|
172
179
|
'config.configured': 'Connected to {url}',
|
|
173
180
|
'config.disabled': 'The plugin is disabled — re-enable it in Settings.',
|
|
181
|
+
'update.available': 'A new version is available: {version}',
|
|
182
|
+
'update.install': 'Update online',
|
|
183
|
+
'update.installing': 'Updating…',
|
|
184
|
+
'update.success': 'Updated to {version}; restart DSH to load it',
|
|
185
|
+
'update.failed': 'Update failed; please try again',
|
|
186
|
+
'update.release': 'View Release',
|
|
174
187
|
'settings.title': 'AI Image (dsh-imagegen)',
|
|
175
188
|
'settings.description': 'Configure the image generation API endpoint and key',
|
|
176
189
|
'settings.apiUrl': 'API URL (api_url)',
|