@dickpy/dsh-imagegen 1.0.6 → 1.0.9
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 +9 -5
- package/lib/client.js +748 -105
- package/lib/client.js.map +1 -1
- package/lib/index.js +359 -5
- package/package.json +13 -12
- package/src/client/ImageGenPanel.tsx +25 -0
- package/src/client/TemplateLibrary.tsx +336 -0
- package/src/client/api.ts +21 -1
- package/src/client/index.ts +126 -127
- package/src/client/locales.ts +56 -0
- package/src/client/panel.module.css +36 -1
- package/src/client/templates.module.css +453 -0
- package/src/index.ts +2 -1
- package/src/protocol.ts +59 -1
- package/src/routes.ts +73 -1
- package/src/templates/cases.json +10196 -0
- package/src/templates-store.ts +278 -0
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { promises } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
9
|
//#region src/protocol.ts
|
|
9
10
|
/**
|
|
@@ -14,7 +15,7 @@ import { spawn } from "node:child_process";
|
|
|
14
15
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
15
16
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
16
17
|
/** Published package version shared by the host updater and the client UI. */
|
|
17
|
-
const PLUGIN_VERSION = "1.0.
|
|
18
|
+
const PLUGIN_VERSION = "1.0.9";
|
|
18
19
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
19
20
|
const SETTINGS_API = {
|
|
20
21
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -40,6 +41,17 @@ const HISTORY_API = {
|
|
|
40
41
|
clear: "/api/dsh-imagegen/history/clear",
|
|
41
42
|
image: "/api/dsh-imagegen/history/image"
|
|
42
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Same-origin route family for the bundled prompt-template library
|
|
46
|
+
* (awesome-gpt-image-2 mirror). The case list ships inside the package and is
|
|
47
|
+
* served by the host; reference images are proxied through the `image` prefix
|
|
48
|
+
* route and cached on disk so repeated views never hit the network again.
|
|
49
|
+
*/
|
|
50
|
+
const TEMPLATES_API = {
|
|
51
|
+
list: "/api/dsh-imagegen/templates/list",
|
|
52
|
+
refresh: "/api/dsh-imagegen/templates/refresh",
|
|
53
|
+
image: "/api/dsh-imagegen/templates/image"
|
|
54
|
+
};
|
|
43
55
|
//#endregion
|
|
44
56
|
//#region src/engine.ts
|
|
45
57
|
/** A generation failure with a user-presentable message. */
|
|
@@ -285,7 +297,7 @@ function extensionOf(mime) {
|
|
|
285
297
|
}
|
|
286
298
|
}
|
|
287
299
|
/** MIME type for a stored image file name (image route responses). */
|
|
288
|
-
function mimeOfFile(file) {
|
|
300
|
+
function mimeOfFile$1(file) {
|
|
289
301
|
switch (path.extname(file).toLowerCase()) {
|
|
290
302
|
case ".jpg":
|
|
291
303
|
case ".jpeg": return "image/jpeg";
|
|
@@ -430,11 +442,280 @@ async function readHistoryImage(file) {
|
|
|
430
442
|
try {
|
|
431
443
|
return {
|
|
432
444
|
data: await promises.readFile(path.join(IMAGES_DIR, file)),
|
|
433
|
-
mime: mimeOfFile(file)
|
|
445
|
+
mime: mimeOfFile$1(file)
|
|
446
|
+
};
|
|
447
|
+
} catch {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/templates-store.ts
|
|
453
|
+
/**
|
|
454
|
+
* Prompt-template library store (awesome-gpt-image-2 mirror).
|
|
455
|
+
*
|
|
456
|
+
* The case list ships as a bundled snapshot (src/templates/cases.json, inside
|
|
457
|
+
* the npm package) so the library works offline out of the box; a successful
|
|
458
|
+
* manual refresh writes a runtime copy under ~/.dsh/dsh-imagegen/templates/
|
|
459
|
+
* which then takes precedence. Reference images are not bundled (441 files,
|
|
460
|
+
* ≈100 MB) — they are fetched from the vibeui.top mirror on demand, cached on
|
|
461
|
+
* disk under ~/.dsh/dsh-imagegen/template-images/, and served from there on
|
|
462
|
+
* every later view.
|
|
463
|
+
*
|
|
464
|
+
* Framework-free (node:fs only) so the route layer and tests can drive it
|
|
465
|
+
* directly.
|
|
466
|
+
*/
|
|
467
|
+
/** Upstream mirror the library refreshes from (vibeui.top static mirror). */
|
|
468
|
+
const SOURCE_URL = "https://vibeui.top/extra/awesome-gpt-image-2/data/cases.json";
|
|
469
|
+
/** Remote image directory (file names come from the case list). */
|
|
470
|
+
const IMAGE_BASE_URL = "https://vibeui.top/extra/awesome-gpt-image-2/data/images/";
|
|
471
|
+
/** Category label map mirrored from vibeui.top's site.js (zh display names). */
|
|
472
|
+
const CATEGORY_ZH = {
|
|
473
|
+
"Architecture & Spaces": "建筑与空间",
|
|
474
|
+
"Brand & Logos": "品牌与标志",
|
|
475
|
+
"Characters & People": "人物与角色",
|
|
476
|
+
"Charts & Infographics": "图表与信息可视化",
|
|
477
|
+
"Documents & Publishing": "文档与出版物",
|
|
478
|
+
"History & Classical Themes": "历史与古风题材",
|
|
479
|
+
"Illustration & Art": "插画与艺术",
|
|
480
|
+
"Other Use Cases": "其他应用场景",
|
|
481
|
+
"Photography & Realism": "摄影与写实",
|
|
482
|
+
"Posters & Typography": "海报与排版",
|
|
483
|
+
"Products & E-commerce": "商品与电商",
|
|
484
|
+
"Scenes & Storytelling": "场景与叙事",
|
|
485
|
+
"UI & Interfaces": "UI 与界面",
|
|
486
|
+
"Portraits & Fashion": "人像与时尚",
|
|
487
|
+
"Celebrities & Sports": "名人与运动",
|
|
488
|
+
"Characters & IP": "角色与 IP",
|
|
489
|
+
"Food & Beverage": "美食与饮品",
|
|
490
|
+
"Brand & Icons": "品牌与图标",
|
|
491
|
+
"Social Media & Stickers": "社媒与表情包",
|
|
492
|
+
"Infographics & Diagrams": "信息图与图解",
|
|
493
|
+
"UI & App Screens": "UI 与应用界面",
|
|
494
|
+
"Architecture & Interiors": "建筑与室内",
|
|
495
|
+
"Cinematic & Storytelling": "影视与叙事",
|
|
496
|
+
"Illustration & Comics": "插画与漫画",
|
|
497
|
+
"Historical & Fantasy": "历史与幻想",
|
|
498
|
+
"Animals & Nature": "动物与自然",
|
|
499
|
+
"Other Creative Uses": "其他创意用途"
|
|
500
|
+
};
|
|
501
|
+
const DATA_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
502
|
+
const REFRESHED_CASES_PATH = path.join(DATA_DIR, "templates", "cases.json");
|
|
503
|
+
const IMAGE_CACHE_DIR = path.join(DATA_DIR, "template-images");
|
|
504
|
+
/**
|
|
505
|
+
* Bundled snapshot path. The host bundle emits to lib/index.js while this
|
|
506
|
+
* source file lives at src/templates-store.ts — both exactly one level below
|
|
507
|
+
* the package root — so `../src/templates/cases.json` resolves to the shipped
|
|
508
|
+
* snapshot in development and in the installed package alike.
|
|
509
|
+
*/
|
|
510
|
+
const BUNDLED_CASES_PATH = fileURLToPath(new URL("../src/templates/cases.json", import.meta.url));
|
|
511
|
+
/** Budget for one upstream fetch (list refresh or one image). */
|
|
512
|
+
const FETCH_TIMEOUT_MS = 6e4;
|
|
513
|
+
/** Refuse to cache implausibly large "images". */
|
|
514
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
515
|
+
/** Strict reference-image file names this store writes and serves. */
|
|
516
|
+
const IMAGE_FILE_PATTERN = /^case\d+\.(jpg|jpeg|png|webp|gif)$/i;
|
|
517
|
+
/** In-memory memo of the active list (avoid re-parsing on every request). */
|
|
518
|
+
let memo;
|
|
519
|
+
/** Per-file in-flight downloads, so a gallery scroll never double-fetches. */
|
|
520
|
+
const inflightImages = /* @__PURE__ */ new Map();
|
|
521
|
+
/** Validate + normalize one raw upstream case; undefined when unusable. */
|
|
522
|
+
function normalizeCase(raw) {
|
|
523
|
+
if (raw === null || typeof raw !== "object") return void 0;
|
|
524
|
+
const record = raw;
|
|
525
|
+
const id = Number(record.id);
|
|
526
|
+
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
527
|
+
const prompt = typeof record.prompt === "string" ? record.prompt.trim() : "";
|
|
528
|
+
if (!Number.isInteger(id) || title === "" || prompt === "") return void 0;
|
|
529
|
+
const category = typeof record.category === "string" ? record.category : "";
|
|
530
|
+
const image = imageFileOf(typeof record.image === "string" ? record.image : "");
|
|
531
|
+
return {
|
|
532
|
+
id,
|
|
533
|
+
title,
|
|
534
|
+
prompt,
|
|
535
|
+
category,
|
|
536
|
+
categoryZh: CATEGORY_ZH[category] ?? category,
|
|
537
|
+
styles: Array.isArray(record.styles) ? record.styles.map(String) : [],
|
|
538
|
+
scenes: Array.isArray(record.scenes) ? record.scenes.map(String) : [],
|
|
539
|
+
sourceLabel: typeof record.sourceLabel === "string" ? record.sourceLabel : "",
|
|
540
|
+
sourceUrl: typeof record.sourceUrl === "string" ? record.sourceUrl : "",
|
|
541
|
+
githubUrl: typeof record.githubUrl === "string" ? record.githubUrl : "",
|
|
542
|
+
image,
|
|
543
|
+
featured: record.featured === true
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** Extract the bare file name from an upstream image path. */
|
|
547
|
+
function imageFileOf(value) {
|
|
548
|
+
const name = value.replace(/^\/+/, "").split("/").pop() ?? "";
|
|
549
|
+
return IMAGE_FILE_PATTERN.test(name) ? name : "";
|
|
550
|
+
}
|
|
551
|
+
/** Parse a snapshot payload (bundled, refreshed cache, or fresh download). */
|
|
552
|
+
function parseSnapshot(payload) {
|
|
553
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
554
|
+
const snapshot = payload;
|
|
555
|
+
if (!Array.isArray(snapshot.cases)) return void 0;
|
|
556
|
+
const cases = [];
|
|
557
|
+
for (const raw of snapshot.cases) {
|
|
558
|
+
const normalized = normalizeCase(raw);
|
|
559
|
+
if (normalized !== void 0) cases.push(normalized);
|
|
560
|
+
}
|
|
561
|
+
if (cases.length === 0) return void 0;
|
|
562
|
+
cases.sort((a, b) => b.id - a.id);
|
|
563
|
+
return {
|
|
564
|
+
cases,
|
|
565
|
+
repository: typeof snapshot.repository === "string" && snapshot.repository !== "" ? snapshot.repository : "freestylefly/awesome-gpt-image-2",
|
|
566
|
+
fetchedAt: typeof snapshot.fetchedAt === "string" && snapshot.fetchedAt !== "" ? snapshot.fetchedAt : ""
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/** Read + parse a snapshot file; undefined when missing/corrupt. */
|
|
570
|
+
async function readSnapshotFile(file) {
|
|
571
|
+
try {
|
|
572
|
+
return parseSnapshot(JSON.parse(await promises.readFile(file, "utf8")));
|
|
573
|
+
} catch {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The active template list: the refreshed runtime copy wins, the bundled
|
|
579
|
+
* snapshot is the always-available fallback. Memoized; a successful refresh
|
|
580
|
+
* replaces the memo.
|
|
581
|
+
*/
|
|
582
|
+
async function listTemplates() {
|
|
583
|
+
if (memo !== void 0) return memo;
|
|
584
|
+
const refreshed = await readSnapshotFile(REFRESHED_CASES_PATH);
|
|
585
|
+
if (refreshed !== void 0) {
|
|
586
|
+
memo = {
|
|
587
|
+
...refreshed,
|
|
588
|
+
total: refreshed.cases.length,
|
|
589
|
+
origin: "refreshed"
|
|
590
|
+
};
|
|
591
|
+
return memo;
|
|
592
|
+
}
|
|
593
|
+
const bundled = await readSnapshotFile(BUNDLED_CASES_PATH);
|
|
594
|
+
if (bundled !== void 0) {
|
|
595
|
+
memo = {
|
|
596
|
+
...bundled,
|
|
597
|
+
total: bundled.cases.length,
|
|
598
|
+
origin: "bundled"
|
|
434
599
|
};
|
|
600
|
+
return memo;
|
|
601
|
+
}
|
|
602
|
+
memo = {
|
|
603
|
+
cases: [],
|
|
604
|
+
total: 0,
|
|
605
|
+
origin: "bundled",
|
|
606
|
+
repository: "freestylefly/awesome-gpt-image-2",
|
|
607
|
+
fetchedAt: ""
|
|
608
|
+
};
|
|
609
|
+
return memo;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Re-download the case list from the upstream mirror and persist it as the
|
|
613
|
+
* runtime copy. Throws with a user-presentable message on failure; the
|
|
614
|
+
* previous list (refreshed or bundled) stays active.
|
|
615
|
+
*/
|
|
616
|
+
async function refreshTemplates() {
|
|
617
|
+
let response;
|
|
618
|
+
try {
|
|
619
|
+
response = await fetch(SOURCE_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
620
|
+
} catch (error) {
|
|
621
|
+
throw new Error(`无法连接模板库源站:${error instanceof Error ? error.message : String(error)}`);
|
|
622
|
+
}
|
|
623
|
+
if (!response.ok) throw new Error(`模板库源站拒绝请求(HTTP ${response.status})`);
|
|
624
|
+
let payload;
|
|
625
|
+
try {
|
|
626
|
+
payload = await response.json();
|
|
627
|
+
} catch {
|
|
628
|
+
throw new Error("模板库源站返回了非 JSON 响应");
|
|
629
|
+
}
|
|
630
|
+
const parsed = parseSnapshot(payload);
|
|
631
|
+
if (parsed === void 0) throw new Error("模板库源站数据格式无效");
|
|
632
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
633
|
+
const snapshot = {
|
|
634
|
+
repository: parsed.repository,
|
|
635
|
+
sourceUrl: SOURCE_URL,
|
|
636
|
+
fetchedAt,
|
|
637
|
+
totalCases: parsed.cases.length,
|
|
638
|
+
cases: parsed.cases
|
|
639
|
+
};
|
|
640
|
+
await promises.mkdir(path.dirname(REFRESHED_CASES_PATH), { recursive: true });
|
|
641
|
+
const tmp = `${REFRESHED_CASES_PATH}.tmp-${process.pid}`;
|
|
642
|
+
await promises.writeFile(tmp, JSON.stringify(snapshot), "utf8");
|
|
643
|
+
await promises.rename(tmp, REFRESHED_CASES_PATH);
|
|
644
|
+
memo = {
|
|
645
|
+
cases: parsed.cases,
|
|
646
|
+
total: parsed.cases.length,
|
|
647
|
+
origin: "refreshed",
|
|
648
|
+
repository: parsed.repository,
|
|
649
|
+
fetchedAt
|
|
650
|
+
};
|
|
651
|
+
return {
|
|
652
|
+
total: parsed.cases.length,
|
|
653
|
+
fetchedAt
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/** MIME type for a cached reference-image file name. */
|
|
657
|
+
function mimeOfFile(file) {
|
|
658
|
+
switch (path.extname(file).toLowerCase()) {
|
|
659
|
+
case ".jpg":
|
|
660
|
+
case ".jpeg": return "image/jpeg";
|
|
661
|
+
case ".webp": return "image/webp";
|
|
662
|
+
case ".gif": return "image/gif";
|
|
663
|
+
default: return "image/png";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** Download one reference image into the disk cache; undefined on failure. */
|
|
667
|
+
async function fetchTemplateImage(file) {
|
|
668
|
+
let response;
|
|
669
|
+
try {
|
|
670
|
+
response = await fetch(`${IMAGE_BASE_URL}${encodeURIComponent(file)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
435
671
|
} catch {
|
|
436
672
|
return;
|
|
437
673
|
}
|
|
674
|
+
if (!response.ok) return void 0;
|
|
675
|
+
if (Number(response.headers.get("content-length") ?? 0) > MAX_IMAGE_BYTES) return void 0;
|
|
676
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
677
|
+
if (data.byteLength === 0 || data.byteLength > MAX_IMAGE_BYTES) return void 0;
|
|
678
|
+
const mime = mimeOfFile(file);
|
|
679
|
+
try {
|
|
680
|
+
await promises.mkdir(IMAGE_CACHE_DIR, { recursive: true });
|
|
681
|
+
const tmp = path.join(IMAGE_CACHE_DIR, `${file}.tmp-${process.pid}`);
|
|
682
|
+
await promises.writeFile(tmp, data);
|
|
683
|
+
await promises.rename(tmp, path.join(IMAGE_CACHE_DIR, file));
|
|
684
|
+
} catch {}
|
|
685
|
+
return {
|
|
686
|
+
data,
|
|
687
|
+
mime
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Read one reference image for the template library. Cache hit → disk; miss →
|
|
692
|
+
* fetch from the upstream mirror, cache, and serve. Only file names present in
|
|
693
|
+
* the active case list are served, so the route can never act as an open
|
|
694
|
+
* proxy. Undefined when the name is unknown or the fetch failed.
|
|
695
|
+
*/
|
|
696
|
+
async function readTemplateImage(file) {
|
|
697
|
+
if (!IMAGE_FILE_PATTERN.test(file) || file.includes("..")) return void 0;
|
|
698
|
+
if (!(await listTemplates()).cases.some((entry) => entry.image === file)) return void 0;
|
|
699
|
+
const cached = path.join(IMAGE_CACHE_DIR, file);
|
|
700
|
+
try {
|
|
701
|
+
return {
|
|
702
|
+
data: await promises.readFile(cached),
|
|
703
|
+
mime: mimeOfFile(file)
|
|
704
|
+
};
|
|
705
|
+
} catch {}
|
|
706
|
+
const inflight = inflightImages.get(file);
|
|
707
|
+
if (inflight !== void 0) return inflight;
|
|
708
|
+
const pending = fetchTemplateImage(file);
|
|
709
|
+
inflightImages.set(file, pending);
|
|
710
|
+
try {
|
|
711
|
+
return await pending;
|
|
712
|
+
} finally {
|
|
713
|
+
inflightImages.delete(file);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/** Drop the in-memory list memo (tests). */
|
|
717
|
+
function clearTemplateMemo() {
|
|
718
|
+
memo = void 0;
|
|
438
719
|
}
|
|
439
720
|
//#endregion
|
|
440
721
|
//#region src/updater.ts
|
|
@@ -683,6 +964,11 @@ function makeRoutes(deps) {
|
|
|
683
964
|
clear: clearHistory,
|
|
684
965
|
readImage: readHistoryImage
|
|
685
966
|
};
|
|
967
|
+
const templates = deps.templates ?? {
|
|
968
|
+
list: listTemplates,
|
|
969
|
+
refresh: refreshTemplates,
|
|
970
|
+
readImage: readTemplateImage
|
|
971
|
+
};
|
|
686
972
|
const guard = (req, res, method) => {
|
|
687
973
|
if (!isLoopbackRequest(req)) {
|
|
688
974
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -1028,6 +1314,74 @@ function makeRoutes(deps) {
|
|
|
1028
1314
|
});
|
|
1029
1315
|
res.end(found.data);
|
|
1030
1316
|
}
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
kind: "exact",
|
|
1320
|
+
path: TEMPLATES_API.list,
|
|
1321
|
+
handler: async (req, res) => {
|
|
1322
|
+
if (!guard(req, res, "POST")) return;
|
|
1323
|
+
try {
|
|
1324
|
+
writeJson(res, 200, {
|
|
1325
|
+
ok: true,
|
|
1326
|
+
...await templates.list()
|
|
1327
|
+
});
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
writeJson(res, 200, {
|
|
1330
|
+
ok: false,
|
|
1331
|
+
code: "templates-failed",
|
|
1332
|
+
message: messageOf(error)
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
kind: "exact",
|
|
1339
|
+
path: TEMPLATES_API.refresh,
|
|
1340
|
+
handler: async (req, res) => {
|
|
1341
|
+
if (!guard(req, res, "POST")) return;
|
|
1342
|
+
try {
|
|
1343
|
+
writeJson(res, 200, {
|
|
1344
|
+
ok: true,
|
|
1345
|
+
...await templates.refresh()
|
|
1346
|
+
});
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
writeJson(res, 200, {
|
|
1349
|
+
ok: false,
|
|
1350
|
+
code: "templates-refresh-failed",
|
|
1351
|
+
message: messageOf(error)
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
kind: "prefix",
|
|
1358
|
+
path: TEMPLATES_API.image,
|
|
1359
|
+
handler: async (req, res) => {
|
|
1360
|
+
if (!isLoopbackRequest(req)) {
|
|
1361
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (req.method !== "GET") {
|
|
1365
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const file = imageFileFrom(req.url, TEMPLATES_API.image);
|
|
1369
|
+
if (file === void 0) {
|
|
1370
|
+
writeJson(res, 404, { error: "not found" });
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const found = await templates.readImage(file);
|
|
1374
|
+
if (found === void 0) {
|
|
1375
|
+
writeJson(res, 404, { error: "not found" });
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
res.writeHead(200, {
|
|
1379
|
+
"content-type": found.mime,
|
|
1380
|
+
"content-length": found.data.length,
|
|
1381
|
+
"cache-control": "private, max-age=86400"
|
|
1382
|
+
});
|
|
1383
|
+
res.end(found.data);
|
|
1384
|
+
}
|
|
1031
1385
|
}
|
|
1032
1386
|
];
|
|
1033
1387
|
}
|
|
@@ -1051,7 +1405,7 @@ const DEFAULT_ANNOUNCE = true;
|
|
|
1051
1405
|
/** Order of the announcement section within the tool-guidance band. */
|
|
1052
1406
|
const SECTION_ORDER = 150;
|
|
1053
1407
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
1054
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64
|
|
1408
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条 gpt-image-2 提示词案例(含中文标题、分类、参考图,可搜索/按分类筛选),参考图经宿主代理按需缓存到本地;用户可一键把模板提示词填入提示词框再生成。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / 文生图 / 图生图 / 提示词模板」时即指本插件,请据此协作。";
|
|
1055
1409
|
/**
|
|
1056
1410
|
* Mount the settings section, routes, and announcement.
|
|
1057
1411
|
* @param ctx - host plugin context carrying webServer/systemPrompt.
|
|
@@ -1110,4 +1464,4 @@ function apply(ctx, config) {
|
|
|
1110
1464
|
sync();
|
|
1111
1465
|
}
|
|
1112
1466
|
//#endregion
|
|
1113
|
-
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, apply, checkForUpdate, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, makeRoutes, name, profileFromProcess };
|
|
1467
|
+
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, apply, checkForUpdate, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, listTemplates, makeRoutes, name, profileFromProcess, readTemplateImage, refreshTemplates };
|
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.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
30
|
-
"@deepseek-ai/dsh-client-connection": "
|
|
31
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
32
|
-
"@deepseek-ai/dsh-client-runtime": "
|
|
33
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
34
|
-
"@deepseek-ai/dsh-client-ui-settings": "
|
|
35
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
36
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
37
|
-
"@deepseek-ai/dsh-settings": "
|
|
38
|
-
"@deepseek-ai/dsh-system-prompt": "
|
|
30
|
+
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.7",
|
|
31
|
+
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.7",
|
|
32
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.7",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.7",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.7",
|
|
36
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
|
|
37
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.7",
|
|
38
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.7",
|
|
39
39
|
"@types/node": "^22.20.0",
|
|
40
40
|
"@types/react": "~18.3.1",
|
|
41
41
|
"@types/react-dom": "^18.3.5",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsdown",
|
|
64
64
|
"watch": "tsdown --watch",
|
|
65
|
-
"typecheck": "tsc --noEmit"
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"fetch-templates": "node scripts/fetch-templates.mjs"
|
|
66
67
|
}
|
|
67
|
-
}
|
|
68
|
+
}
|
|
@@ -13,6 +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 { TemplateLibrary } from './TemplateLibrary.tsx'
|
|
16
17
|
import type { GeneratedImage, GenerateMode, GenerateRequest, HistoryEntry, HistoryImageRef, UpdateInfo } from '../protocol.ts'
|
|
17
18
|
import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
|
|
18
19
|
import css from './panel.module.css'
|
|
@@ -146,6 +147,7 @@ export function ImageGenPanel(props: {
|
|
|
146
147
|
const [updating, setUpdating] = useState(false)
|
|
147
148
|
const [updateMessage, setUpdateMessage] = useState<string | null>(null)
|
|
148
149
|
const [updateResult, setUpdateResult] = useState<'success' | 'failed' | null>(null)
|
|
150
|
+
const [libraryOpen, setLibraryOpen] = useState(false)
|
|
149
151
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
150
152
|
const previewStage = useRef<HTMLDivElement>(null)
|
|
151
153
|
const elapsed = useElapsed(generating, startedAt)
|
|
@@ -508,6 +510,15 @@ export function ImageGenPanel(props: {
|
|
|
508
510
|
onChange={(event) => { setPrompt(event.target.value) }}
|
|
509
511
|
/>
|
|
510
512
|
<div className={css.promptFooter}>
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
className={css.templatesButton}
|
|
516
|
+
title={tt('templates.title')}
|
|
517
|
+
onClick={() => { setLibraryOpen(true) }}
|
|
518
|
+
>
|
|
519
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M2.5 3.5h11M2.5 8h11M2.5 12.5h7"/></svg>
|
|
520
|
+
{tt('templates.open')}
|
|
521
|
+
</button>
|
|
511
522
|
<span className={css.promptCount}>{tt('prompt.count', { count: prompt.length })}</span>
|
|
512
523
|
</div>
|
|
513
524
|
</section>
|
|
@@ -741,6 +752,20 @@ export function ImageGenPanel(props: {
|
|
|
741
752
|
</aside>
|
|
742
753
|
</div>
|
|
743
754
|
|
|
755
|
+
{/* ------------------------------------------------ template library */}
|
|
756
|
+
{libraryOpen ? (
|
|
757
|
+
<TemplateLibrary
|
|
758
|
+
api={api}
|
|
759
|
+
onClose={() => { setLibraryOpen(false) }}
|
|
760
|
+
onUse={(text) => {
|
|
761
|
+
setMode('text')
|
|
762
|
+
setPrompt(text)
|
|
763
|
+
setError(null)
|
|
764
|
+
setLibraryOpen(false)
|
|
765
|
+
}}
|
|
766
|
+
/>
|
|
767
|
+
) : null}
|
|
768
|
+
|
|
744
769
|
{/* -------------------------------------------------- preview overlay */}
|
|
745
770
|
{preview !== null && previewImage !== null
|
|
746
771
|
? createPortal(
|