@ai-agent-tools/picgen 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +56 -4
- package/dist/cli.js +1169 -48
- package/docs/agent-install.md +155 -0
- package/docs/release-alpha.md +49 -3
- package/package.json +5 -3
- package/skills/picgen/SKILL.md +146 -5
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import "dotenv/config";
|
|
5
4
|
import { Command } from "commander";
|
|
6
5
|
|
|
7
6
|
// src/commands/create.ts
|
|
@@ -218,12 +217,12 @@ function padMilliseconds(value) {
|
|
|
218
217
|
function slug(value) {
|
|
219
218
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
220
219
|
}
|
|
221
|
-
function redactProviderImageData(value,
|
|
220
|
+
function redactProviderImageData(value, key2) {
|
|
222
221
|
if (Array.isArray(value)) {
|
|
223
222
|
return value.map((item) => redactProviderImageData(item));
|
|
224
223
|
}
|
|
225
224
|
if (!value || typeof value !== "object") {
|
|
226
|
-
return shouldRedactImageDataKey(
|
|
225
|
+
return shouldRedactImageDataKey(key2) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
|
|
227
226
|
}
|
|
228
227
|
return Object.fromEntries(
|
|
229
228
|
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
@@ -232,8 +231,8 @@ function redactProviderImageData(value, key) {
|
|
|
232
231
|
])
|
|
233
232
|
);
|
|
234
233
|
}
|
|
235
|
-
function shouldRedactImageDataKey(
|
|
236
|
-
return
|
|
234
|
+
function shouldRedactImageDataKey(key2) {
|
|
235
|
+
return key2 === "b64_json" || key2 === "data" || key2 === "thoughtSignature" || key2 === "thought_signature";
|
|
237
236
|
}
|
|
238
237
|
function redactedProviderDataPlaceholder(value) {
|
|
239
238
|
return `[redacted provider data: ${value.length} chars]`;
|
|
@@ -275,9 +274,9 @@ function mimeTypeFromPath(path) {
|
|
|
275
274
|
}
|
|
276
275
|
|
|
277
276
|
// src/config/store.ts
|
|
278
|
-
import { mkdir as
|
|
279
|
-
import { dirname, join as
|
|
280
|
-
import { homedir } from "os";
|
|
277
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
278
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
279
|
+
import { homedir as homedir2 } from "os";
|
|
281
280
|
import YAML from "yaml";
|
|
282
281
|
|
|
283
282
|
// src/config/defaults.ts
|
|
@@ -363,6 +362,164 @@ var defaultConfig = {
|
|
|
363
362
|
}
|
|
364
363
|
};
|
|
365
364
|
|
|
365
|
+
// src/config/env.ts
|
|
366
|
+
import { chmod, mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
367
|
+
import { existsSync } from "fs";
|
|
368
|
+
import { createHash } from "crypto";
|
|
369
|
+
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
370
|
+
import { homedir } from "os";
|
|
371
|
+
import { parse } from "dotenv";
|
|
372
|
+
var loadedEnvSources = /* @__PURE__ */ new Map();
|
|
373
|
+
function getManagedEnvPath() {
|
|
374
|
+
return process.env.PICGEN_ENV_PATH ?? join2(homedir(), ".picgen", ".env");
|
|
375
|
+
}
|
|
376
|
+
async function loadPicgenEnv() {
|
|
377
|
+
const shellEnv = new Set(Object.keys(process.env));
|
|
378
|
+
await loadEnvFile(getManagedEnvPath(), shellEnv, false, "managed");
|
|
379
|
+
await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true, "project");
|
|
380
|
+
}
|
|
381
|
+
async function saveManagedEnvVar(name, value) {
|
|
382
|
+
const path = getManagedEnvPath();
|
|
383
|
+
const current = await readManagedEnvFile(path);
|
|
384
|
+
const next = {
|
|
385
|
+
...current,
|
|
386
|
+
[name]: value
|
|
387
|
+
};
|
|
388
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
389
|
+
await writeFile2(path, stringifyEnv(next), "utf8");
|
|
390
|
+
await chmod(path, 384);
|
|
391
|
+
process.env[name] = value;
|
|
392
|
+
loadedEnvSources.set(name, { source: "managed", path });
|
|
393
|
+
return path;
|
|
394
|
+
}
|
|
395
|
+
async function inspectEnvVar(name) {
|
|
396
|
+
const shellValue = process.env[name];
|
|
397
|
+
if (shellValue !== void 0) {
|
|
398
|
+
const loadedSource = loadedEnvSources.get(name);
|
|
399
|
+
return describeEnvValue(
|
|
400
|
+
name,
|
|
401
|
+
shellValue,
|
|
402
|
+
loadedSource?.source ?? "shell",
|
|
403
|
+
loadedSource?.path
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const projectPath = resolve2(process.cwd(), ".env");
|
|
407
|
+
const project = await readEnvFile(projectPath);
|
|
408
|
+
if (project[name] !== void 0) {
|
|
409
|
+
return describeEnvValue(name, project[name], "project", projectPath);
|
|
410
|
+
}
|
|
411
|
+
const managedPath = getManagedEnvPath();
|
|
412
|
+
const managed = await readEnvFile(managedPath);
|
|
413
|
+
if (managed[name] !== void 0) {
|
|
414
|
+
return describeEnvValue(name, managed[name], "managed", managedPath);
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
name,
|
|
418
|
+
set: false
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async function inspectEnvVars(names) {
|
|
422
|
+
const uniqueNames = [...new Set(names)];
|
|
423
|
+
return Promise.all(uniqueNames.map((name) => inspectEnvVar(name)));
|
|
424
|
+
}
|
|
425
|
+
async function readEnvVarValue(name) {
|
|
426
|
+
if (process.env[name] !== void 0) return process.env[name];
|
|
427
|
+
const project = await readEnvFile(resolve2(process.cwd(), ".env"));
|
|
428
|
+
if (project[name] !== void 0) return project[name];
|
|
429
|
+
const managed = await readEnvFile(getManagedEnvPath());
|
|
430
|
+
return managed[name];
|
|
431
|
+
}
|
|
432
|
+
async function loadEnvFile(path, shellEnv, overrideManagedValues, source) {
|
|
433
|
+
if (!existsSync(path)) return;
|
|
434
|
+
const parsed = parse(await readFile(path, "utf8"));
|
|
435
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
436
|
+
if (shellEnv.has(name)) continue;
|
|
437
|
+
if (!overrideManagedValues && process.env[name] !== void 0) continue;
|
|
438
|
+
process.env[name] = value;
|
|
439
|
+
loadedEnvSources.set(name, { source, path });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function readManagedEnvFile(path) {
|
|
443
|
+
return readEnvFile(path);
|
|
444
|
+
}
|
|
445
|
+
async function readEnvFile(path) {
|
|
446
|
+
try {
|
|
447
|
+
return parse(await readFile(path, "utf8"));
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error.code === "ENOENT") return {};
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function stringifyEnv(values) {
|
|
454
|
+
return `${Object.entries(values).map(([name, value]) => `${name}=${quoteEnvValue(value)}`).join("\n")}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
function quoteEnvValue(value) {
|
|
458
|
+
if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
|
|
459
|
+
return JSON.stringify(value);
|
|
460
|
+
}
|
|
461
|
+
function describeEnvValue(name, value, source, path) {
|
|
462
|
+
return {
|
|
463
|
+
name,
|
|
464
|
+
set: true,
|
|
465
|
+
source,
|
|
466
|
+
path,
|
|
467
|
+
length: value.length,
|
|
468
|
+
preview: maskSecret(value),
|
|
469
|
+
fingerprint: createHash("sha256").update(value).digest("hex").slice(0, 12)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function maskSecret(value) {
|
|
473
|
+
if (value.length <= 11) return "*".repeat(value.length);
|
|
474
|
+
return `${value.slice(0, 7)}...${value.slice(-4)}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/config/providerKeys.ts
|
|
478
|
+
function nextAvailableProviderApiKeyEnv(config, baseEnv, providerName, existingEnv) {
|
|
479
|
+
if (existingEnv) return existingEnv;
|
|
480
|
+
const usedEnvs = new Set(Object.values(config.providers).map((provider2) => provider2.api_key_env));
|
|
481
|
+
if (!usedEnvs.has(baseEnv)) return baseEnv;
|
|
482
|
+
const providerEnv = providerNameToApiKeyEnv(providerName);
|
|
483
|
+
if (!usedEnvs.has(providerEnv)) return providerEnv;
|
|
484
|
+
let index = 2;
|
|
485
|
+
while (usedEnvs.has(`${providerEnv}_${index}`)) index += 1;
|
|
486
|
+
return `${providerEnv}_${index}`;
|
|
487
|
+
}
|
|
488
|
+
function providerNameToApiKeyEnv(providerName) {
|
|
489
|
+
const safeName = providerName.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
490
|
+
return `PICGEN_${safeName || "PROVIDER"}_KEY`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/config/migrations.ts
|
|
494
|
+
async function migrateConfig(config) {
|
|
495
|
+
const migrated = structuredClone(config);
|
|
496
|
+
let changed = false;
|
|
497
|
+
const usedEnvs = /* @__PURE__ */ new Set();
|
|
498
|
+
for (const [providerName, provider2] of Object.entries(migrated.providers)) {
|
|
499
|
+
const currentEnv = provider2.api_key_env;
|
|
500
|
+
if (!usedEnvs.has(currentEnv)) {
|
|
501
|
+
usedEnvs.add(currentEnv);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const nextEnv = nextUniqueProviderEnv(usedEnvs, providerName);
|
|
505
|
+
const currentValue = await readEnvVarValue(currentEnv);
|
|
506
|
+
provider2.api_key_env = nextEnv;
|
|
507
|
+
usedEnvs.add(nextEnv);
|
|
508
|
+
changed = true;
|
|
509
|
+
if (currentValue && !await readEnvVarValue(nextEnv)) {
|
|
510
|
+
await saveManagedEnvVar(nextEnv, currentValue);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { config: migrated, changed };
|
|
514
|
+
}
|
|
515
|
+
function nextUniqueProviderEnv(usedEnvs, providerName) {
|
|
516
|
+
const baseEnv = providerNameToApiKeyEnv(providerName);
|
|
517
|
+
if (!usedEnvs.has(baseEnv)) return baseEnv;
|
|
518
|
+
let index = 2;
|
|
519
|
+
while (usedEnvs.has(`${baseEnv}_${index}`)) index += 1;
|
|
520
|
+
return `${baseEnv}_${index}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
366
523
|
// src/config/schema.ts
|
|
367
524
|
import { z } from "zod";
|
|
368
525
|
var providerSchema = z.object({
|
|
@@ -429,14 +586,19 @@ function defaultCapabilitiesForProtocol(protocol) {
|
|
|
429
586
|
|
|
430
587
|
// src/config/store.ts
|
|
431
588
|
function getConfigPath() {
|
|
432
|
-
return process.env.PICGEN_CONFIG ??
|
|
589
|
+
return process.env.PICGEN_CONFIG ?? join3(homedir2(), ".picgen", "config.yaml");
|
|
433
590
|
}
|
|
434
591
|
async function loadConfig() {
|
|
435
592
|
const path = getConfigPath();
|
|
436
593
|
try {
|
|
437
|
-
const raw = await
|
|
594
|
+
const raw = await readFile2(path, "utf8");
|
|
438
595
|
const parsed = YAML.parse(raw);
|
|
439
|
-
|
|
596
|
+
const config = picgenConfigSchema.parse(parsed);
|
|
597
|
+
const migrated = await migrateConfig(config);
|
|
598
|
+
if (migrated.changed) {
|
|
599
|
+
await writeConfigFile(path, migrated.config);
|
|
600
|
+
}
|
|
601
|
+
return migrated.config;
|
|
440
602
|
} catch (error) {
|
|
441
603
|
if (error.code === "ENOENT") {
|
|
442
604
|
return structuredClone(defaultConfig);
|
|
@@ -447,8 +609,11 @@ async function loadConfig() {
|
|
|
447
609
|
async function saveConfig(config) {
|
|
448
610
|
const parsed = picgenConfigSchema.parse(config);
|
|
449
611
|
const path = getConfigPath();
|
|
450
|
-
await
|
|
451
|
-
|
|
612
|
+
await writeConfigFile(path, parsed);
|
|
613
|
+
}
|
|
614
|
+
async function writeConfigFile(path, config) {
|
|
615
|
+
await mkdir3(dirname2(path), { recursive: true });
|
|
616
|
+
await writeFile3(path, YAML.stringify(config), "utf8");
|
|
452
617
|
}
|
|
453
618
|
async function ensureConfig() {
|
|
454
619
|
const config = await loadConfig();
|
|
@@ -457,7 +622,54 @@ async function ensureConfig() {
|
|
|
457
622
|
}
|
|
458
623
|
|
|
459
624
|
// src/providers/gemini.ts
|
|
460
|
-
import { readFile as
|
|
625
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
626
|
+
|
|
627
|
+
// src/providers/timeout.ts
|
|
628
|
+
var FAST_PROVIDER_TIMEOUT_MS = 12e4;
|
|
629
|
+
var DEFAULT_PROVIDER_TIMEOUT_MS = 18e4;
|
|
630
|
+
var SLOW_PROVIDER_TIMEOUT_MS = 3e5;
|
|
631
|
+
function resolveProviderTimeoutMs(plan) {
|
|
632
|
+
const override = parseTimeoutOverride(process.env.PICGEN_PROVIDER_TIMEOUT_MS);
|
|
633
|
+
if (override !== void 0) return override;
|
|
634
|
+
if (plan.presetName === "fast-draft" || plan.modeName === "fast" || plan.preset.quality === "low") {
|
|
635
|
+
return FAST_PROVIDER_TIMEOUT_MS;
|
|
636
|
+
}
|
|
637
|
+
if (plan.modeName === "premium" || plan.preset.size === "large" || plan.preset.quality === "high") {
|
|
638
|
+
return SLOW_PROVIDER_TIMEOUT_MS;
|
|
639
|
+
}
|
|
640
|
+
return DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
641
|
+
}
|
|
642
|
+
async function fetchWithProviderTimeout(input3, init, timeoutMs) {
|
|
643
|
+
const controller = new AbortController();
|
|
644
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
645
|
+
try {
|
|
646
|
+
return await fetch(input3, {
|
|
647
|
+
...init,
|
|
648
|
+
signal: controller.signal
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (isAbortError(error)) {
|
|
652
|
+
throw new Error(formatProviderTimeoutError(timeoutMs));
|
|
653
|
+
}
|
|
654
|
+
throw error;
|
|
655
|
+
} finally {
|
|
656
|
+
clearTimeout(timeout);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function formatProviderTimeoutError(timeoutMs) {
|
|
660
|
+
return `Provider request timed out after ${Math.ceil(
|
|
661
|
+
timeoutMs / 1e3
|
|
662
|
+
)}s. The provider may still be processing or temporarily unavailable. Try again, use a faster preset, or increase PICGEN_PROVIDER_TIMEOUT_MS.`;
|
|
663
|
+
}
|
|
664
|
+
function parseTimeoutOverride(value) {
|
|
665
|
+
if (!value) return void 0;
|
|
666
|
+
const timeoutMs = Number(value);
|
|
667
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) return void 0;
|
|
668
|
+
return timeoutMs;
|
|
669
|
+
}
|
|
670
|
+
function isAbortError(error) {
|
|
671
|
+
return error instanceof Error && error.name === "AbortError";
|
|
672
|
+
}
|
|
461
673
|
|
|
462
674
|
// src/providers/urls.ts
|
|
463
675
|
function normalizeProviderBaseUrl(baseUrl) {
|
|
@@ -495,15 +707,20 @@ var GeminiAdapter = class {
|
|
|
495
707
|
const providerImages = [];
|
|
496
708
|
const requestCount = Math.max(1, plan.preset.n);
|
|
497
709
|
const referenceParts = await readReferenceImageParts(plan);
|
|
710
|
+
const timeoutMs = resolveProviderTimeoutMs(plan);
|
|
498
711
|
for (let index = 0; index < requestCount; index += 1) {
|
|
499
|
-
const response = await
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"
|
|
503
|
-
|
|
712
|
+
const response = await fetchWithProviderTimeout(
|
|
713
|
+
buildGeminiGenerateContentUrl(plan.provider.base_url, plan.model),
|
|
714
|
+
{
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: {
|
|
717
|
+
"x-goog-api-key": apiKey,
|
|
718
|
+
"Content-Type": "application/json"
|
|
719
|
+
},
|
|
720
|
+
body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
|
|
504
721
|
},
|
|
505
|
-
|
|
506
|
-
|
|
722
|
+
timeoutMs
|
|
723
|
+
);
|
|
507
724
|
const raw = await readJsonResponse(response);
|
|
508
725
|
if (!response.ok) {
|
|
509
726
|
throw new Error(formatGeminiError(response.status, response.statusText, raw));
|
|
@@ -545,7 +762,7 @@ async function readReferenceImageParts(plan) {
|
|
|
545
762
|
plan.referenceImages.map(async (image) => ({
|
|
546
763
|
inlineData: {
|
|
547
764
|
mimeType: image.mime_type,
|
|
548
|
-
data: (await
|
|
765
|
+
data: (await readFile3(image.path)).toString("base64")
|
|
549
766
|
}
|
|
550
767
|
}))
|
|
551
768
|
);
|
|
@@ -624,14 +841,18 @@ var OpenAIImagesAdapter = class {
|
|
|
624
841
|
if (!apiKey) {
|
|
625
842
|
throw new Error(`Missing API key environment variable: ${plan.provider.api_key_env}`);
|
|
626
843
|
}
|
|
627
|
-
const response = await
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
844
|
+
const response = await fetchWithProviderTimeout(
|
|
845
|
+
buildOpenAIImagesUrl(plan.provider.base_url),
|
|
846
|
+
{
|
|
847
|
+
method: "POST",
|
|
848
|
+
headers: {
|
|
849
|
+
Authorization: `Bearer ${apiKey}`,
|
|
850
|
+
"Content-Type": "application/json"
|
|
851
|
+
},
|
|
852
|
+
body: JSON.stringify(buildOpenAIImagesRequest(plan))
|
|
632
853
|
},
|
|
633
|
-
|
|
634
|
-
|
|
854
|
+
resolveProviderTimeoutMs(plan)
|
|
855
|
+
);
|
|
635
856
|
const raw = await readJsonResponse2(response);
|
|
636
857
|
if (!response.ok) {
|
|
637
858
|
throw new Error(formatOpenAIImagesError(response.status, response.statusText, raw));
|
|
@@ -758,7 +979,7 @@ function getAdapter(protocol) {
|
|
|
758
979
|
}
|
|
759
980
|
|
|
760
981
|
// src/routing/resolve.ts
|
|
761
|
-
import { join as
|
|
982
|
+
import { join as join4 } from "path";
|
|
762
983
|
import { cwd } from "process";
|
|
763
984
|
function resolveGenerationPlan(config, options) {
|
|
764
985
|
const presetName = options.presetName ?? config.default_preset;
|
|
@@ -792,7 +1013,7 @@ function resolveGenerationPlan(config, options) {
|
|
|
792
1013
|
presetName,
|
|
793
1014
|
preset,
|
|
794
1015
|
modeName,
|
|
795
|
-
outputDirectory: options.outputDirectory ??
|
|
1016
|
+
outputDirectory: options.outputDirectory ?? join4(cwd(), "outputs", "picgen"),
|
|
796
1017
|
referenceImages: options.referenceImages ?? []
|
|
797
1018
|
};
|
|
798
1019
|
}
|
|
@@ -992,6 +1213,140 @@ function inspectProviders(config) {
|
|
|
992
1213
|
});
|
|
993
1214
|
}
|
|
994
1215
|
|
|
1216
|
+
// src/commands/update.ts
|
|
1217
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1218
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1219
|
+
import { homedir as homedir3 } from "os";
|
|
1220
|
+
|
|
1221
|
+
// src/version.ts
|
|
1222
|
+
var PACKAGE_NAME = "@ai-agent-tools/picgen";
|
|
1223
|
+
var VERSION = "0.1.0-alpha.10";
|
|
1224
|
+
|
|
1225
|
+
// src/commands/update.ts
|
|
1226
|
+
var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1227
|
+
async function runUpdateCheck(options = {}) {
|
|
1228
|
+
const result = await checkForUpdate({ force: true });
|
|
1229
|
+
if (options.json) {
|
|
1230
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const message = formatUpdateMessage(result);
|
|
1234
|
+
console.log(message ?? "PicGen is up to date.");
|
|
1235
|
+
}
|
|
1236
|
+
async function maybePrintUpdateHint() {
|
|
1237
|
+
const result = await checkForUpdate({ force: false });
|
|
1238
|
+
const message = formatUpdateMessage(result);
|
|
1239
|
+
if (message) {
|
|
1240
|
+
console.log("");
|
|
1241
|
+
console.log(message);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async function checkForUpdate(options = {}) {
|
|
1245
|
+
if (process.env.PICGEN_DISABLE_UPDATE_CHECK === "1") {
|
|
1246
|
+
return {
|
|
1247
|
+
checked: false,
|
|
1248
|
+
disabled: true,
|
|
1249
|
+
current_version: VERSION,
|
|
1250
|
+
package_name: PACKAGE_NAME
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
const cached = options.force ? void 0 : await readFreshCache();
|
|
1255
|
+
const latestVersion = cached?.latest_version ?? await fetchLatestVersion();
|
|
1256
|
+
const checkedAt = cached?.checked_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1257
|
+
if (!cached) {
|
|
1258
|
+
await writeCache({
|
|
1259
|
+
checked_at: checkedAt,
|
|
1260
|
+
latest_version: latestVersion
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
checked: true,
|
|
1265
|
+
current_version: VERSION,
|
|
1266
|
+
latest_version: latestVersion,
|
|
1267
|
+
update_available: isNewerVersion(latestVersion, VERSION),
|
|
1268
|
+
package_name: PACKAGE_NAME,
|
|
1269
|
+
checked_at: checkedAt
|
|
1270
|
+
};
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
return {
|
|
1273
|
+
checked: false,
|
|
1274
|
+
current_version: VERSION,
|
|
1275
|
+
package_name: PACKAGE_NAME,
|
|
1276
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function formatUpdateMessage(result) {
|
|
1281
|
+
if (!result.checked || !result.update_available || !result.latest_version) {
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
return [
|
|
1285
|
+
`PicGen update available: ${result.current_version} -> ${result.latest_version}`,
|
|
1286
|
+
"Upgrade with:",
|
|
1287
|
+
` npm install -g ${result.package_name}@latest`
|
|
1288
|
+
].join("\n");
|
|
1289
|
+
}
|
|
1290
|
+
function isNewerVersion(candidate, current) {
|
|
1291
|
+
const candidateParts = parseVersion(candidate);
|
|
1292
|
+
const currentParts = parseVersion(current);
|
|
1293
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1294
|
+
if (candidateParts.numbers[index] > currentParts.numbers[index]) return true;
|
|
1295
|
+
if (candidateParts.numbers[index] < currentParts.numbers[index]) return false;
|
|
1296
|
+
}
|
|
1297
|
+
if (!candidateParts.prerelease && currentParts.prerelease) return true;
|
|
1298
|
+
if (candidateParts.prerelease && !currentParts.prerelease) return false;
|
|
1299
|
+
if (candidateParts.prerelease && currentParts.prerelease) {
|
|
1300
|
+
return candidateParts.prerelease.localeCompare(currentParts.prerelease) > 0;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
async function fetchLatestVersion() {
|
|
1305
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`);
|
|
1306
|
+
if (!response.ok) {
|
|
1307
|
+
throw new Error(`npm registry returned ${response.status} ${response.statusText}`.trim());
|
|
1308
|
+
}
|
|
1309
|
+
const body = await response.json();
|
|
1310
|
+
const latest = body["dist-tags"]?.latest;
|
|
1311
|
+
if (typeof latest !== "string" || !latest) {
|
|
1312
|
+
throw new Error("npm registry response did not include dist-tags.latest.");
|
|
1313
|
+
}
|
|
1314
|
+
return latest;
|
|
1315
|
+
}
|
|
1316
|
+
async function readFreshCache() {
|
|
1317
|
+
try {
|
|
1318
|
+
const cache = JSON.parse(await readFile4(getUpdateCachePath(), "utf8"));
|
|
1319
|
+
const checkedAt = Date.parse(cache.checked_at);
|
|
1320
|
+
if (!Number.isFinite(checkedAt)) return void 0;
|
|
1321
|
+
if (Date.now() - checkedAt > UPDATE_CACHE_TTL_MS) return void 0;
|
|
1322
|
+
return cache;
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (error.code === "ENOENT") return void 0;
|
|
1325
|
+
return void 0;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async function writeCache(cache) {
|
|
1329
|
+
const path = getUpdateCachePath();
|
|
1330
|
+
await mkdir4(dirname3(path), { recursive: true });
|
|
1331
|
+
await writeFile4(path, JSON.stringify(cache, null, 2), "utf8");
|
|
1332
|
+
}
|
|
1333
|
+
function getUpdateCachePath() {
|
|
1334
|
+
if (process.env.PICGEN_UPDATE_CACHE_PATH) return process.env.PICGEN_UPDATE_CACHE_PATH;
|
|
1335
|
+
return join5(homedir3(), ".picgen", "update-check.json");
|
|
1336
|
+
}
|
|
1337
|
+
function parseVersion(version) {
|
|
1338
|
+
const [core, prerelease] = version.split("-", 2);
|
|
1339
|
+
const numbers = core.split(".").map((part) => Number.parseInt(part, 10));
|
|
1340
|
+
return {
|
|
1341
|
+
numbers: [
|
|
1342
|
+
Number.isFinite(numbers[0]) ? numbers[0] : 0,
|
|
1343
|
+
Number.isFinite(numbers[1]) ? numbers[1] : 0,
|
|
1344
|
+
Number.isFinite(numbers[2]) ? numbers[2] : 0
|
|
1345
|
+
],
|
|
1346
|
+
prerelease
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
995
1350
|
// src/commands/doctor.ts
|
|
996
1351
|
async function runDoctor(options) {
|
|
997
1352
|
const config = await loadConfig();
|
|
@@ -1002,6 +1357,7 @@ async function runDoctor(options) {
|
|
|
1002
1357
|
JSON.stringify(
|
|
1003
1358
|
{
|
|
1004
1359
|
configured,
|
|
1360
|
+
version: VERSION,
|
|
1005
1361
|
config_path: getConfigPath(),
|
|
1006
1362
|
default_preset: config.default_preset,
|
|
1007
1363
|
default_mode: config.routing.default_mode,
|
|
@@ -1016,6 +1372,7 @@ async function runDoctor(options) {
|
|
|
1016
1372
|
return;
|
|
1017
1373
|
}
|
|
1018
1374
|
console.log(`Config: ${getConfigPath()}`);
|
|
1375
|
+
console.log(`Version: ${VERSION}`);
|
|
1019
1376
|
console.log(`Default preset: ${config.default_preset}`);
|
|
1020
1377
|
console.log(`Default mode: ${config.routing.default_mode}`);
|
|
1021
1378
|
console.log(`Default provider: ${config.routing.default_provider}`);
|
|
@@ -1031,8 +1388,87 @@ async function runDoctor(options) {
|
|
|
1031
1388
|
console.log("");
|
|
1032
1389
|
console.log("No usable provider found. Run `picgen setup` or set the API key env vars above.");
|
|
1033
1390
|
}
|
|
1391
|
+
await maybePrintUpdateHint();
|
|
1034
1392
|
}
|
|
1035
1393
|
|
|
1394
|
+
// src/commands/key.ts
|
|
1395
|
+
import { execFile } from "child_process";
|
|
1396
|
+
import { promisify } from "util";
|
|
1397
|
+
import { password } from "@inquirer/prompts";
|
|
1398
|
+
var execFileAsync = promisify(execFile);
|
|
1399
|
+
async function setApiKey(name, options) {
|
|
1400
|
+
validateEnvName(name);
|
|
1401
|
+
const value = options.clipboard ? await readClipboard() : options.stdin ? await readStdin() : options.value ? options.value : await password({
|
|
1402
|
+
message: `Paste API key for ${name}`,
|
|
1403
|
+
mask: "*"
|
|
1404
|
+
});
|
|
1405
|
+
if (!value.trim()) {
|
|
1406
|
+
throw new Error("API key is empty.");
|
|
1407
|
+
}
|
|
1408
|
+
const path = await saveManagedEnvVar(name, value.trim());
|
|
1409
|
+
console.log(`Saved ${name} to ${path}`);
|
|
1410
|
+
}
|
|
1411
|
+
async function listApiKeys(options) {
|
|
1412
|
+
const config = await loadConfig();
|
|
1413
|
+
const names = Object.values(config.providers).map((provider2) => provider2.api_key_env);
|
|
1414
|
+
const inspections = await inspectEnvVars(names);
|
|
1415
|
+
if (options.json) {
|
|
1416
|
+
console.log(JSON.stringify(inspections, null, 2));
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
for (const inspection of inspections) {
|
|
1420
|
+
printInspection(inspection);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async function showApiKey(name, options) {
|
|
1424
|
+
validateEnvName(name);
|
|
1425
|
+
const inspection = await inspectEnvVar(name);
|
|
1426
|
+
if (options.json) {
|
|
1427
|
+
console.log(JSON.stringify(inspection, null, 2));
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
printInspection(inspection);
|
|
1431
|
+
}
|
|
1432
|
+
function validateEnvName(name) {
|
|
1433
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
1434
|
+
throw new Error(`Invalid environment variable name: ${name}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
async function readStdin() {
|
|
1438
|
+
const chunks = [];
|
|
1439
|
+
for await (const chunk of process.stdin) {
|
|
1440
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1441
|
+
}
|
|
1442
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1443
|
+
}
|
|
1444
|
+
async function readClipboard() {
|
|
1445
|
+
try {
|
|
1446
|
+
const { stdout } = await execFileAsync("pbpaste");
|
|
1447
|
+
return stdout;
|
|
1448
|
+
} catch {
|
|
1449
|
+
throw new Error("Could not read clipboard. Use --stdin or run picgen key set without flags.");
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function printInspection(inspection) {
|
|
1453
|
+
if (!inspection.set) {
|
|
1454
|
+
console.log(`${inspection.name}: missing`);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const location = inspection.path ? ` ${inspection.path}` : "";
|
|
1458
|
+
console.log(
|
|
1459
|
+
`${inspection.name}: set source=${inspection.source}${location} length=${inspection.length} preview=${inspection.preview} fingerprint=${inspection.fingerprint}`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/commands/open.ts
|
|
1464
|
+
import { createServer } from "http";
|
|
1465
|
+
import { randomBytes } from "crypto";
|
|
1466
|
+
import { execFile as execFile2 } from "child_process";
|
|
1467
|
+
import { readdir, readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
1468
|
+
import { createReadStream } from "fs";
|
|
1469
|
+
import { extname as extname3, join as join6, resolve as resolve3 } from "path";
|
|
1470
|
+
import { cwd as cwd2 } from "process";
|
|
1471
|
+
|
|
1036
1472
|
// src/commands/provider.ts
|
|
1037
1473
|
import { input, select } from "@inquirer/prompts";
|
|
1038
1474
|
|
|
@@ -1178,6 +1614,32 @@ async function addProvider() {
|
|
|
1178
1614
|
await saveConfig(config);
|
|
1179
1615
|
console.log(`Added provider: ${provider2.name}`);
|
|
1180
1616
|
}
|
|
1617
|
+
async function quickAddProvider(templateName, options) {
|
|
1618
|
+
const config = await loadConfig();
|
|
1619
|
+
const template = quickProviderTemplate(templateName);
|
|
1620
|
+
const name = options.name ?? nextAvailableProviderName(config, template.name);
|
|
1621
|
+
const apiKeyEnv = options.keyEnv ?? nextAvailableProviderApiKeyEnv(config, template.api_key_env, name);
|
|
1622
|
+
const provider2 = {
|
|
1623
|
+
enabled: true,
|
|
1624
|
+
protocol: template.protocol,
|
|
1625
|
+
channel: template.channel,
|
|
1626
|
+
base_url: normalizeProviderBaseUrl(options.host ?? template.base_url),
|
|
1627
|
+
api_key_env: apiKeyEnv,
|
|
1628
|
+
models: parseModels(options.models ?? template.models.join(",")),
|
|
1629
|
+
capabilities: defaultCapabilitiesForProtocol2(template.protocol)
|
|
1630
|
+
};
|
|
1631
|
+
addProviderToConfig(config, name, provider2);
|
|
1632
|
+
if (options.prefer) {
|
|
1633
|
+
setPreferredProvider(config, name);
|
|
1634
|
+
}
|
|
1635
|
+
await saveConfig(config);
|
|
1636
|
+
console.log(`Added provider: ${name}`);
|
|
1637
|
+
console.log(`Protocol: ${provider2.protocol}`);
|
|
1638
|
+
console.log(`Host: ${provider2.base_url}`);
|
|
1639
|
+
console.log(`API key env: ${provider2.api_key_env}`);
|
|
1640
|
+
console.log(`Models: ${provider2.models.join(",")}`);
|
|
1641
|
+
if (options.prefer) console.log(`Preferred provider: ${name}`);
|
|
1642
|
+
}
|
|
1181
1643
|
function addProviderToConfig(config, name, provider2) {
|
|
1182
1644
|
config.providers[name] = provider2;
|
|
1183
1645
|
const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
|
|
@@ -1274,7 +1736,11 @@ async function promptProvider(config, existingName, existing) {
|
|
|
1274
1736
|
});
|
|
1275
1737
|
const apiKeyEnv = await input({
|
|
1276
1738
|
message: "API key environment variable",
|
|
1277
|
-
default: existing?.api_key_env ?? (
|
|
1739
|
+
default: existing?.api_key_env ?? nextAvailableProviderApiKeyEnv(
|
|
1740
|
+
config,
|
|
1741
|
+
protocol === "openai-images" ? channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY" : channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY",
|
|
1742
|
+
name
|
|
1743
|
+
)
|
|
1278
1744
|
});
|
|
1279
1745
|
const defaultModels = protocol === "openai-images" ? "gpt-image-2" : "gemini-3.1-flash-image-preview,gemini-3-pro-image-preview";
|
|
1280
1746
|
const modelsRaw = await input({
|
|
@@ -1304,6 +1770,562 @@ function nextAvailableProviderName(config, baseName, existingName) {
|
|
|
1304
1770
|
function defaultCapabilitiesForProtocol2(protocol) {
|
|
1305
1771
|
return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
|
|
1306
1772
|
}
|
|
1773
|
+
function quickProviderTemplate(templateName) {
|
|
1774
|
+
switch (templateName.replaceAll("_", "-")) {
|
|
1775
|
+
case "openai-proxy":
|
|
1776
|
+
return {
|
|
1777
|
+
name: "openai_proxy",
|
|
1778
|
+
protocol: "openai-images",
|
|
1779
|
+
channel: "third_party",
|
|
1780
|
+
base_url: "https://www.pandai.vip",
|
|
1781
|
+
api_key_env: "PICGEN_OPENAI_PROXY_KEY",
|
|
1782
|
+
models: ["gpt-image-2"]
|
|
1783
|
+
};
|
|
1784
|
+
case "gemini-proxy":
|
|
1785
|
+
return {
|
|
1786
|
+
name: "gemini_proxy",
|
|
1787
|
+
protocol: "gemini",
|
|
1788
|
+
channel: "third_party",
|
|
1789
|
+
base_url: "https://www.pandai.vip",
|
|
1790
|
+
api_key_env: "PICGEN_GEMINI_PROXY_KEY",
|
|
1791
|
+
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1792
|
+
};
|
|
1793
|
+
case "openai-official":
|
|
1794
|
+
return {
|
|
1795
|
+
name: "openai_official",
|
|
1796
|
+
protocol: "openai-images",
|
|
1797
|
+
channel: "official",
|
|
1798
|
+
base_url: defaultProviderBaseUrl("openai-images"),
|
|
1799
|
+
api_key_env: "OPENAI_API_KEY",
|
|
1800
|
+
models: ["gpt-image-2"]
|
|
1801
|
+
};
|
|
1802
|
+
case "gemini-official":
|
|
1803
|
+
return {
|
|
1804
|
+
name: "gemini_official",
|
|
1805
|
+
protocol: "gemini",
|
|
1806
|
+
channel: "official",
|
|
1807
|
+
base_url: defaultProviderBaseUrl("gemini"),
|
|
1808
|
+
api_key_env: "GEMINI_API_KEY",
|
|
1809
|
+
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1810
|
+
};
|
|
1811
|
+
default:
|
|
1812
|
+
throw new Error(
|
|
1813
|
+
`Unknown provider template: ${templateName}. Use openai-proxy, gemini-proxy, openai-official, or gemini-official.`
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function parseModels(raw) {
|
|
1818
|
+
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/commands/open.ts
|
|
1822
|
+
var DEFAULT_PORT = 8188;
|
|
1823
|
+
var MAX_PORT_ATTEMPTS = 30;
|
|
1824
|
+
async function runOpen(options) {
|
|
1825
|
+
const token = randomBytes(24).toString("hex");
|
|
1826
|
+
const port = await listenOnAvailablePort(Number(options.port ?? DEFAULT_PORT), token);
|
|
1827
|
+
const url = `http://127.0.0.1:${port}/?token=${token}`;
|
|
1828
|
+
console.log(`PicGen is open: ${url}`);
|
|
1829
|
+
console.log("Keep this terminal running while using PicGen. Press Ctrl+C to close.");
|
|
1830
|
+
if (options.open !== false) {
|
|
1831
|
+
await openBrowser(url);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
async function listenOnAvailablePort(startPort, token) {
|
|
1835
|
+
for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset += 1) {
|
|
1836
|
+
const port = startPort + offset;
|
|
1837
|
+
const server = createServer((request, response) => {
|
|
1838
|
+
handleRequest(request, response, token).catch((error) => {
|
|
1839
|
+
sendJson(response, 500, {
|
|
1840
|
+
ok: false,
|
|
1841
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
});
|
|
1845
|
+
const result = await tryListen(server, port);
|
|
1846
|
+
if (result) return port;
|
|
1847
|
+
}
|
|
1848
|
+
throw new Error(`Could not find an available port starting at ${startPort}.`);
|
|
1849
|
+
}
|
|
1850
|
+
function tryListen(server, port) {
|
|
1851
|
+
return new Promise((resolveListen, reject) => {
|
|
1852
|
+
server.once("error", (error) => {
|
|
1853
|
+
if (error.code === "EADDRINUSE") {
|
|
1854
|
+
resolveListen(false);
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
reject(error);
|
|
1858
|
+
});
|
|
1859
|
+
server.listen(port, "127.0.0.1", () => resolveListen(true));
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
async function handleRequest(request, response, token) {
|
|
1863
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
1864
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1865
|
+
if (url.searchParams.get("token") !== token) {
|
|
1866
|
+
sendText(response, 403, "Invalid PicGen session token.");
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
sendHtml(response, appHtml);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (!isAuthorized(request, url, token)) {
|
|
1873
|
+
sendJson(response, 403, { ok: false, error: "Invalid PicGen session token." });
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (request.method === "GET" && url.pathname === "/api/state") {
|
|
1877
|
+
sendJson(response, 200, await buildState());
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (request.method === "POST" && url.pathname === "/api/providers") {
|
|
1881
|
+
sendJson(response, 200, await addProviderFromBody(await readJson(request)));
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const providerTestMatch = /^\/api\/providers\/([^/]+)\/test$/.exec(url.pathname);
|
|
1885
|
+
if (request.method === "POST" && providerTestMatch) {
|
|
1886
|
+
const config = await loadConfig();
|
|
1887
|
+
const name = decodeURIComponent(providerTestMatch[1]);
|
|
1888
|
+
const provider2 = config.providers[name];
|
|
1889
|
+
if (!provider2) throw new Error(`Unknown provider: ${name}`);
|
|
1890
|
+
sendJson(response, 200, await testProvider(name, provider2));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
const providerDefaultMatch = /^\/api\/providers\/([^/]+)\/default$/.exec(url.pathname);
|
|
1894
|
+
if (request.method === "POST" && providerDefaultMatch) {
|
|
1895
|
+
const config = await loadConfig();
|
|
1896
|
+
const name = decodeURIComponent(providerDefaultMatch[1]);
|
|
1897
|
+
setPreferredProvider(config, name);
|
|
1898
|
+
await saveConfig(config);
|
|
1899
|
+
sendJson(response, 200, await buildState());
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const providerMatch = /^\/api\/providers\/([^/]+)$/.exec(url.pathname);
|
|
1903
|
+
if (providerMatch) {
|
|
1904
|
+
const name = decodeURIComponent(providerMatch[1]);
|
|
1905
|
+
if (request.method === "PATCH") {
|
|
1906
|
+
sendJson(response, 200, await patchProvider(name, await readJson(request)));
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (request.method === "DELETE") {
|
|
1910
|
+
sendJson(response, 200, await deleteProvider(name));
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (request.method === "POST" && url.pathname === "/api/key") {
|
|
1915
|
+
const body = await readJson(request);
|
|
1916
|
+
if (typeof body.name !== "string" || typeof body.value !== "string") {
|
|
1917
|
+
throw new Error("Key name and value are required.");
|
|
1918
|
+
}
|
|
1919
|
+
await saveManagedEnvVar(body.name, body.value);
|
|
1920
|
+
sendJson(response, 200, { ok: true, state: await buildState() });
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
const keyMatch = /^\/api\/key\/([^/]+)$/.exec(url.pathname);
|
|
1924
|
+
if (request.method === "GET" && keyMatch) {
|
|
1925
|
+
const name = decodeURIComponent(keyMatch[1]);
|
|
1926
|
+
sendJson(response, 200, { ok: true, value: await readEnvVarValue(name) });
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (request.method === "POST" && url.pathname === "/api/plan") {
|
|
1930
|
+
sendJson(response, 200, await planGeneration(await readJson(request)));
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
if (request.method === "POST" && url.pathname === "/api/generate") {
|
|
1934
|
+
sendJson(response, 200, await generate(await readJson(request)));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (request.method === "GET" && url.pathname === "/api/history") {
|
|
1938
|
+
sendJson(response, 200, { ok: true, runs: await listHistory() });
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (request.method === "GET" && url.pathname === "/api/file") {
|
|
1942
|
+
await sendOutputFile(response, url.searchParams.get("path"));
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
sendJson(response, 404, { ok: false, error: "Not found." });
|
|
1946
|
+
}
|
|
1947
|
+
function isAuthorized(request, url, token) {
|
|
1948
|
+
return request.headers["x-picgen-token"] === token || url.searchParams.get("token") === token;
|
|
1949
|
+
}
|
|
1950
|
+
async function buildState() {
|
|
1951
|
+
const config = await loadConfig();
|
|
1952
|
+
const keyInspections = await inspectEnvVars(
|
|
1953
|
+
Object.values(config.providers).map((provider2) => provider2.api_key_env)
|
|
1954
|
+
);
|
|
1955
|
+
const keys = Object.fromEntries(keyInspections.map((key2) => [key2.name, key2]));
|
|
1956
|
+
return {
|
|
1957
|
+
ok: true,
|
|
1958
|
+
config_path: getConfigPath(),
|
|
1959
|
+
key_file_path: getManagedEnvPath(),
|
|
1960
|
+
default_provider: config.routing.default_provider,
|
|
1961
|
+
fallback_providers: config.routing.fallback_providers,
|
|
1962
|
+
default_preset: config.default_preset,
|
|
1963
|
+
default_mode: config.routing.default_mode,
|
|
1964
|
+
providers: Object.entries(config.providers).map(([name, provider2]) => ({
|
|
1965
|
+
name,
|
|
1966
|
+
...provider2,
|
|
1967
|
+
key: keys[provider2.api_key_env],
|
|
1968
|
+
preference: name === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(name) ? "fallback" : "manual"
|
|
1969
|
+
})),
|
|
1970
|
+
presets: config.presets,
|
|
1971
|
+
modes: config.modes
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
async function addProviderFromBody(body) {
|
|
1975
|
+
const config = await loadConfig();
|
|
1976
|
+
const protocol = body.protocol === "gemini" ? "gemini" : "openai-images";
|
|
1977
|
+
const channel = body.channel === "official" ? "official" : "third_party";
|
|
1978
|
+
const template = defaultProviderTemplate(protocol, channel);
|
|
1979
|
+
const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : nextAvailableProviderName(config, template.name);
|
|
1980
|
+
const apiKeyEnv = typeof body.api_key_env === "string" && body.api_key_env.trim() ? body.api_key_env.trim() : nextAvailableProviderApiKeyEnv(config, template.api_key_env, name);
|
|
1981
|
+
const models = typeof body.models === "string" && body.models.trim() ? parseModels2(body.models) : template.models;
|
|
1982
|
+
const provider2 = {
|
|
1983
|
+
enabled: true,
|
|
1984
|
+
protocol,
|
|
1985
|
+
channel,
|
|
1986
|
+
base_url: normalizeProviderBaseUrl(String(body.base_url ?? template.base_url)),
|
|
1987
|
+
api_key_env: apiKeyEnv,
|
|
1988
|
+
models,
|
|
1989
|
+
capabilities: defaultCapabilitiesForProtocol2(protocol)
|
|
1990
|
+
};
|
|
1991
|
+
addProviderToConfig(config, name, provider2);
|
|
1992
|
+
if (body.prefer === true || !config.providers[config.routing.default_provider]) {
|
|
1993
|
+
setPreferredProvider(config, name);
|
|
1994
|
+
}
|
|
1995
|
+
await saveConfig(config);
|
|
1996
|
+
if (typeof body.api_key === "string" && body.api_key.trim()) {
|
|
1997
|
+
await saveManagedEnvVar(apiKeyEnv, body.api_key.trim());
|
|
1998
|
+
}
|
|
1999
|
+
return { ok: true, state: await buildState() };
|
|
2000
|
+
}
|
|
2001
|
+
async function patchProvider(name, body) {
|
|
2002
|
+
const config = await loadConfig();
|
|
2003
|
+
const provider2 = config.providers[name];
|
|
2004
|
+
if (!provider2) throw new Error(`Unknown provider: ${name}`);
|
|
2005
|
+
if (typeof body.enabled === "boolean") provider2.enabled = body.enabled;
|
|
2006
|
+
if (typeof body.base_url === "string") provider2.base_url = normalizeProviderBaseUrl(body.base_url);
|
|
2007
|
+
if (typeof body.models === "string") provider2.models = parseModels2(body.models);
|
|
2008
|
+
if (typeof body.api_key === "string" && body.api_key.trim()) {
|
|
2009
|
+
await saveManagedEnvVar(provider2.api_key_env, body.api_key.trim());
|
|
2010
|
+
}
|
|
2011
|
+
await saveConfig(config);
|
|
2012
|
+
return { ok: true, state: await buildState() };
|
|
2013
|
+
}
|
|
2014
|
+
async function deleteProvider(name) {
|
|
2015
|
+
const config = await loadConfig();
|
|
2016
|
+
if (!config.providers[name]) throw new Error(`Unknown provider: ${name}`);
|
|
2017
|
+
delete config.providers[name];
|
|
2018
|
+
config.routing.fallback_providers = config.routing.fallback_providers.filter((item) => item !== name);
|
|
2019
|
+
if (config.routing.default_provider === name) {
|
|
2020
|
+
const nextDefault = config.routing.fallback_providers[0] ?? Object.keys(config.providers)[0];
|
|
2021
|
+
if (nextDefault) {
|
|
2022
|
+
config.routing.default_provider = nextDefault;
|
|
2023
|
+
config.routing.fallback_providers = config.routing.fallback_providers.filter(
|
|
2024
|
+
(item) => item !== nextDefault
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
await saveConfig(config);
|
|
2029
|
+
return { ok: true, state: await buildState() };
|
|
2030
|
+
}
|
|
2031
|
+
async function planGeneration(body) {
|
|
2032
|
+
const config = await loadConfig();
|
|
2033
|
+
const plan = resolveGenerationPlan(config, {
|
|
2034
|
+
prompt: String(body.prompt ?? "").trim(),
|
|
2035
|
+
presetName: asOptionalString(body.preset),
|
|
2036
|
+
providerName: asOptionalString(body.provider),
|
|
2037
|
+
modeName: asOptionalString(body.mode),
|
|
2038
|
+
model: asOptionalString(body.model),
|
|
2039
|
+
outputDirectory: asOptionalString(body.output_directory)
|
|
2040
|
+
});
|
|
2041
|
+
return {
|
|
2042
|
+
ok: true,
|
|
2043
|
+
dry_run: true,
|
|
2044
|
+
provider_called: false,
|
|
2045
|
+
plan: toPlanOutput(plan)
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
async function generate(body) {
|
|
2049
|
+
const config = await loadConfig();
|
|
2050
|
+
const plan = resolveGenerationPlan(config, {
|
|
2051
|
+
prompt: String(body.prompt ?? "").trim(),
|
|
2052
|
+
presetName: asOptionalString(body.preset),
|
|
2053
|
+
providerName: asOptionalString(body.provider),
|
|
2054
|
+
modeName: asOptionalString(body.mode),
|
|
2055
|
+
model: asOptionalString(body.model),
|
|
2056
|
+
outputDirectory: asOptionalString(body.output_directory)
|
|
2057
|
+
});
|
|
2058
|
+
const run = await createGenerationRun(plan);
|
|
2059
|
+
const runtimePlan = { ...plan, outputDirectory: run.outputDirectory };
|
|
2060
|
+
const runtimePlanOutput = toPlanOutput(runtimePlan);
|
|
2061
|
+
await writeGenerationMetadata(run, {
|
|
2062
|
+
plan: runtimePlanOutput,
|
|
2063
|
+
run: {
|
|
2064
|
+
id: run.id,
|
|
2065
|
+
output_directory: run.outputDirectory,
|
|
2066
|
+
metadata_path: run.metadataPath,
|
|
2067
|
+
prompt_path: run.promptPath
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
const adapter = getAdapter(plan.provider.protocol);
|
|
2071
|
+
try {
|
|
2072
|
+
const result = await adapter.generate(runtimePlan, run);
|
|
2073
|
+
await writeGenerationMetadata(run, {
|
|
2074
|
+
plan: runtimePlanOutput,
|
|
2075
|
+
run: {
|
|
2076
|
+
id: run.id,
|
|
2077
|
+
output_directory: run.outputDirectory,
|
|
2078
|
+
metadata_path: run.metadataPath,
|
|
2079
|
+
prompt_path: run.promptPath
|
|
2080
|
+
},
|
|
2081
|
+
provider_response: result.provider_response,
|
|
2082
|
+
images: result.images
|
|
2083
|
+
});
|
|
2084
|
+
return {
|
|
2085
|
+
ok: true,
|
|
2086
|
+
output_dir: run.outputDirectory,
|
|
2087
|
+
metadata_path: run.metadataPath,
|
|
2088
|
+
images: result.images
|
|
2089
|
+
};
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
await writeGenerationMetadata(run, {
|
|
2092
|
+
plan: runtimePlanOutput,
|
|
2093
|
+
run: {
|
|
2094
|
+
id: run.id,
|
|
2095
|
+
output_directory: run.outputDirectory,
|
|
2096
|
+
metadata_path: run.metadataPath,
|
|
2097
|
+
prompt_path: run.promptPath
|
|
2098
|
+
},
|
|
2099
|
+
error: {
|
|
2100
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2101
|
+
name: error instanceof Error ? error.name : void 0
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
throw error;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
async function listHistory() {
|
|
2108
|
+
const baseDir = resolve3(cwd2(), "outputs", "picgen");
|
|
2109
|
+
const dates = await safeReaddir(baseDir);
|
|
2110
|
+
const runs = [];
|
|
2111
|
+
for (const date of dates) {
|
|
2112
|
+
const datePath = join6(baseDir, date);
|
|
2113
|
+
if (!await isDirectory(datePath)) continue;
|
|
2114
|
+
for (const runName of await safeReaddir(datePath)) {
|
|
2115
|
+
const runPath = join6(datePath, runName);
|
|
2116
|
+
if (!await isDirectory(runPath)) continue;
|
|
2117
|
+
const metadataPath = join6(runPath, "metadata.json");
|
|
2118
|
+
const promptPath = join6(runPath, "prompt.txt");
|
|
2119
|
+
const metadata = await readJsonFile(metadataPath);
|
|
2120
|
+
const prompt = await readTextFile(promptPath);
|
|
2121
|
+
const images = (metadata?.images ?? []).filter((image) => image.path).map((image) => ({
|
|
2122
|
+
...image,
|
|
2123
|
+
url: `/api/file?path=${encodeURIComponent(image.path)}`
|
|
2124
|
+
}));
|
|
2125
|
+
const info = await stat2(runPath);
|
|
2126
|
+
runs.push({
|
|
2127
|
+
id: runName,
|
|
2128
|
+
date,
|
|
2129
|
+
created_at: info.mtime.toISOString(),
|
|
2130
|
+
output_dir: runPath,
|
|
2131
|
+
metadata_path: metadataPath,
|
|
2132
|
+
prompt,
|
|
2133
|
+
plan: metadata?.plan,
|
|
2134
|
+
images
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return runs.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))).slice(0, 60);
|
|
2139
|
+
}
|
|
2140
|
+
async function sendOutputFile(response, path) {
|
|
2141
|
+
if (!path) {
|
|
2142
|
+
sendJson(response, 400, { ok: false, error: "Missing file path." });
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
const resolved = resolve3(path);
|
|
2146
|
+
const allowedRoot = resolve3(cwd2(), "outputs", "picgen");
|
|
2147
|
+
if (!resolved.startsWith(`${allowedRoot}/`)) {
|
|
2148
|
+
sendJson(response, 403, { ok: false, error: "File is outside PicGen outputs." });
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const extension = extname3(resolved).toLowerCase();
|
|
2152
|
+
const mimeType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".webp" ? "image/webp" : extension === ".png" ? "image/png" : "application/octet-stream";
|
|
2153
|
+
response.writeHead(200, {
|
|
2154
|
+
"Content-Type": mimeType,
|
|
2155
|
+
"Cache-Control": "no-store"
|
|
2156
|
+
});
|
|
2157
|
+
createReadStream(resolved).pipe(response);
|
|
2158
|
+
}
|
|
2159
|
+
function defaultProviderTemplate(protocol, channel) {
|
|
2160
|
+
if (protocol === "gemini") {
|
|
2161
|
+
return {
|
|
2162
|
+
name: channel === "official" ? "gemini_official" : "gemini_proxy",
|
|
2163
|
+
base_url: channel === "official" ? "https://generativelanguage.googleapis.com" : "https://www.pandai.vip",
|
|
2164
|
+
api_key_env: channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY",
|
|
2165
|
+
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
name: channel === "official" ? "openai_official" : "openai_proxy",
|
|
2170
|
+
base_url: channel === "official" ? "https://api.openai.com" : "https://www.pandai.vip",
|
|
2171
|
+
api_key_env: channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY",
|
|
2172
|
+
models: ["gpt-image-2"]
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function parseModels2(raw) {
|
|
2176
|
+
return raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2177
|
+
}
|
|
2178
|
+
function asOptionalString(value) {
|
|
2179
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2180
|
+
}
|
|
2181
|
+
async function readJson(request) {
|
|
2182
|
+
const chunks = [];
|
|
2183
|
+
for await (const chunk of request) {
|
|
2184
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2185
|
+
}
|
|
2186
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
2187
|
+
if (!text.trim()) return {};
|
|
2188
|
+
return JSON.parse(text);
|
|
2189
|
+
}
|
|
2190
|
+
async function safeReaddir(path) {
|
|
2191
|
+
try {
|
|
2192
|
+
return await readdir(path);
|
|
2193
|
+
} catch {
|
|
2194
|
+
return [];
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async function isDirectory(path) {
|
|
2198
|
+
try {
|
|
2199
|
+
return (await stat2(path)).isDirectory();
|
|
2200
|
+
} catch {
|
|
2201
|
+
return false;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async function readJsonFile(path) {
|
|
2205
|
+
try {
|
|
2206
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
2207
|
+
} catch {
|
|
2208
|
+
return void 0;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
async function readTextFile(path) {
|
|
2212
|
+
try {
|
|
2213
|
+
return await readFile5(path, "utf8");
|
|
2214
|
+
} catch {
|
|
2215
|
+
return void 0;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
function sendJson(response, status, value) {
|
|
2219
|
+
response.writeHead(status, {
|
|
2220
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
2221
|
+
"Cache-Control": "no-store"
|
|
2222
|
+
});
|
|
2223
|
+
response.end(JSON.stringify(value, null, 2));
|
|
2224
|
+
}
|
|
2225
|
+
function sendHtml(response, value) {
|
|
2226
|
+
response.writeHead(200, {
|
|
2227
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2228
|
+
"Cache-Control": "no-store"
|
|
2229
|
+
});
|
|
2230
|
+
response.end(value);
|
|
2231
|
+
}
|
|
2232
|
+
function sendText(response, status, value) {
|
|
2233
|
+
response.writeHead(status, {
|
|
2234
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
2235
|
+
"Cache-Control": "no-store"
|
|
2236
|
+
});
|
|
2237
|
+
response.end(value);
|
|
2238
|
+
}
|
|
2239
|
+
async function openBrowser(url) {
|
|
2240
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2241
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2242
|
+
await new Promise((resolveOpen) => {
|
|
2243
|
+
execFile2(command, args, () => resolveOpen());
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
var appHtml = `<!doctype html>
|
|
2247
|
+
<html lang="zh-CN">
|
|
2248
|
+
<head>
|
|
2249
|
+
<meta charset="utf-8">
|
|
2250
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2251
|
+
<title>PicGen</title>
|
|
2252
|
+
<style>
|
|
2253
|
+
:root{--bg:#f6f7f9;--panel:#fff;--text:#17202a;--muted:#667085;--border:#d9dee7;--soft:#eef2f6;--accent:#2563eb;--accent2:#0f766e;--danger:#b42318;--ok:#087443;--warn:#b54708;--radius:8px}
|
|
2254
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,select,textarea{font:inherit}button{border:1px solid var(--border);background:#fff;color:var(--text);border-radius:7px;padding:7px 10px;cursor:pointer}button.primary{background:var(--accent);border-color:var(--accent);color:#fff}button.ghost{background:transparent}button.danger{color:var(--danger)}button:disabled{opacity:.55;cursor:not-allowed}input,select,textarea{width:100%;border:1px solid var(--border);border-radius:7px;padding:8px 9px;background:#fff;color:var(--text)}textarea{min-height:120px;resize:vertical}.wrap{max-width:1120px;margin:0 auto;padding:22px}.top{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.brand h1{font-size:22px;margin:0 0 3px}.brand p,.muted{color:var(--muted);margin:0}.notice{border:1px solid #bfdbfe;background:#eff6ff;color:#1e3a8a;border-radius:var(--radius);padding:10px 12px;margin-bottom:16px}.tabs{display:flex;gap:6px;margin-bottom:16px;border-bottom:1px solid var(--border)}.tab{border:0;border-radius:7px 7px 0 0;background:transparent;padding:9px 12px}.tab.active{background:#fff;border:1px solid var(--border);border-bottom-color:#fff;margin-bottom:-1px}.grid{display:grid;gap:14px}.cols{display:grid;grid-template-columns:1fr 360px;gap:14px}.panel{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px}.panel h2{font-size:15px;margin:0 0 12px}.row{display:grid;grid-template-columns:150px 1fr;gap:10px;align-items:center;margin:9px 0}.actions{display:flex;gap:8px;flex-wrap:wrap}.provider{display:grid;grid-template-columns:1fr auto;gap:12px;border:1px solid var(--border);border-radius:var(--radius);padding:12px;background:#fff}.provider+.provider{margin-top:10px}.title{font-weight:650}.badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.badge{display:inline-flex;align-items:center;border-radius:999px;background:var(--soft);color:#344054;font-size:12px;padding:2px 8px}.badge.ok{background:#dcfae6;color:var(--ok)}.badge.warn{background:#fef0c7;color:var(--warn)}.badge.default{background:#dbeafe;color:#1d4ed8}.keyline{margin-top:8px;color:var(--muted)}.keyline code,.path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f2f4f7;border-radius:5px;padding:2px 5px}.source{display:inline-flex;align-items:center;border-radius:5px;padding:2px 6px;background:#ecfdf3;color:#067647;font-size:12px;margin-left:4px}.source.project{background:#fff7ed;color:#9a3412}.source.shell{background:#eef4ff;color:#3538cd}.source.missing{background:#fef3f2;color:#b42318}.hint{margin-top:8px;color:var(--muted);font-size:12px}.warning{margin-top:8px;color:var(--warn);font-size:12px}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:31px;height:31px;padding:0;vertical-align:middle}.icon-btn svg{width:16px;height:16px;stroke:currentColor}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.full{grid-column:1/-1}.plan{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius);padding:10px}.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}.run img,.result img{width:100%;height:150px;object-fit:contain;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius)}.run{background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:10px}.hidden{display:none}.toast{position:fixed;right:18px;bottom:18px;background:#111827;color:#fff;border-radius:8px;padding:10px 12px;max-width:360px}.small{font-size:12px}.split{display:flex;justify-content:space-between;gap:8px;align-items:center}@media(max-width:860px){.cols{grid-template-columns:1fr}.row{grid-template-columns:1fr}.formgrid{grid-template-columns:1fr}.provider{grid-template-columns:1fr}.top{display:block}.wrap{padding:14px}}
|
|
2255
|
+
</style>
|
|
2256
|
+
</head>
|
|
2257
|
+
<body>
|
|
2258
|
+
<main class="wrap">
|
|
2259
|
+
<div class="top">
|
|
2260
|
+
<div class="brand"><h1>PicGen</h1><p>\u672C\u5730\u751F\u56FE\u5DE5\u4F5C\u53F0</p></div>
|
|
2261
|
+
<div class="actions"><button id="refresh">\u5237\u65B0</button></div>
|
|
2262
|
+
</div>
|
|
2263
|
+
<div class="notice">\u8FD9\u662F\u672C\u673A\u4E34\u65F6\u9875\u9762\uFF0C\u53EA\u7ED1\u5B9A 127.0.0.1\u3002\u4F7F\u7528\u5B8C\u53EF\u4EE5\u5173\u95ED\u9875\u9762\uFF0C\u5E76\u5728\u542F\u52A8 PicGen \u7684\u7EC8\u7AEF\u6309 Ctrl+C \u9000\u51FA\u670D\u52A1\u3002</div>
|
|
2264
|
+
<nav class="tabs">
|
|
2265
|
+
<button class="tab active" data-tab="settings">\u914D\u7F6E</button>
|
|
2266
|
+
<button class="tab" data-tab="generate">\u751F\u6210</button>
|
|
2267
|
+
<button class="tab" data-tab="history">\u5386\u53F2</button>
|
|
2268
|
+
</nav>
|
|
2269
|
+
<section id="settings" class="grid"></section>
|
|
2270
|
+
<section id="generate" class="hidden"></section>
|
|
2271
|
+
<section id="history" class="hidden grid"></section>
|
|
2272
|
+
</main>
|
|
2273
|
+
<div id="toast" class="toast hidden"></div>
|
|
2274
|
+
<script>
|
|
2275
|
+
const token = new URLSearchParams(location.search).get('token');
|
|
2276
|
+
const headers = {'Content-Type':'application/json','x-picgen-token':token};
|
|
2277
|
+
const state = {data:null,plan:null};
|
|
2278
|
+
const $ = (s,p=document)=>p.querySelector(s);
|
|
2279
|
+
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
2280
|
+
function toast(msg){const el=$('#toast');el.textContent=msg;el.classList.remove('hidden');setTimeout(()=>el.classList.add('hidden'),3500)}
|
|
2281
|
+
async function api(path, opts={}){const res=await fetch(path,{...opts,headers:{...headers,...opts.headers}});const data=await res.json().catch(()=>({ok:false,error:'Invalid JSON'}));if(!res.ok||data.ok===false)throw new Error(data.error||data.message||res.statusText);return data}
|
|
2282
|
+
async function load(){state.data=await api('/api/state');renderSettings();renderGenerate();await renderHistory()}
|
|
2283
|
+
function tab(name){document.querySelectorAll('.tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===name));['settings','generate','history'].forEach(id=>$('#'+id).classList.toggle('hidden',id!==name))}
|
|
2284
|
+
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>tab(b.dataset.tab));$('#refresh').onclick=()=>load().then(()=>toast('\u5DF2\u5237\u65B0')).catch(e=>toast(e.message));
|
|
2285
|
+
const labels={default:'\u9ED8\u8BA4',fallback:'\u5907\u7528',manual:'\u624B\u52A8',official:'\u5B98\u65B9',third_party:'\u7B2C\u4E09\u65B9','openai-images':'OpenAI \u517C\u5BB9',gemini:'Gemini',enabled:'\u5DF2\u542F\u7528',disabled:'\u5DF2\u505C\u7528',shell:'\u7EC8\u7AEF\u73AF\u5883\u53D8\u91CF',project:'\u5F53\u524D\u9879\u76EE .env',managed:'PicGen \u7BA1\u7406\u6587\u4EF6',missing:'\u672A\u914D\u7F6E'};
|
|
2286
|
+
const eyeIcon='<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
2287
|
+
const eyeOffIcon='<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="m2 2 20 20"/><path d="M10.6 10.6A3 3 0 0 0 13.4 13.4"/><path d="M9.9 4.2A10.4 10.4 0 0 1 12 4.0c6.5 0 10 8 10 8a17.8 17.8 0 0 1-2.3 3.5"/><path d="M6.6 6.6C3.6 8.7 2 12 2 12s3.5 8 10 8c1.6 0 3-.3 4.2-.9"/></svg>';
|
|
2288
|
+
function label(v){return labels[v]||v}
|
|
2289
|
+
function keySource(key){return key?.set?label(key.source):'\u672A\u914D\u7F6E'}
|
|
2290
|
+
function templateDefaults(template){return {gemini_proxy:{protocol:'gemini',channel:'third_party',name:'gemini_proxy',host:'https://www.pandai.vip',models:'gemini-3.1-flash-image-preview, gemini-3-pro-image-preview'},openai_proxy:{protocol:'openai-images',channel:'third_party',name:'openai_proxy',host:'https://www.pandai.vip',models:'gpt-image-2'},gemini_official:{protocol:'gemini',channel:'official',name:'gemini_official',host:'https://generativelanguage.googleapis.com',models:'gemini-3.1-flash-image-preview, gemini-3-pro-image-preview'},openai_official:{protocol:'openai-images',channel:'official',name:'openai_official',host:'https://api.openai.com',models:'gpt-image-2'}}[template]}
|
|
2291
|
+
function renderSettings(){const s=state.data;$('#settings').innerHTML=\`
|
|
2292
|
+
<div class="panel"><h2>\u8DEF\u5F84</h2><div class="row"><div>\u914D\u7F6E\u6587\u4EF6</div><div class="path">\${esc(s.config_path)}</div></div><div class="row"><div>PicGen \u5BC6\u94A5\u6587\u4EF6</div><div class="path">\${esc(s.key_file_path)}</div></div><p class="hint">key \u8BFB\u53D6\u4F18\u5148\u7EA7\uFF1A\u7EC8\u7AEF\u73AF\u5883\u53D8\u91CF > \u5F53\u524D\u9879\u76EE .env > PicGen \u7BA1\u7406\u6587\u4EF6\u3002</p></div>
|
|
2293
|
+
<div class="panel"><div class="split"><h2>\u6E20\u9053</h2><button class="primary" id="addProviderBtn">\u6DFB\u52A0\u6E20\u9053</button></div>
|
|
2294
|
+
<div class="hidden" id="providerForm" style="margin:12px 0 14px"><h2>\u6DFB\u52A0\u6E20\u9053</h2>
|
|
2295
|
+
<div class="formgrid">
|
|
2296
|
+
<label class="full">\u6E20\u9053\u7C7B\u578B<select id="newTemplate"><option value="gemini_proxy">\u7B2C\u4E09\u65B9 Gemini</option><option value="openai_proxy">\u7B2C\u4E09\u65B9 OpenAI \u517C\u5BB9</option><option value="gemini_official">\u5B98\u65B9 Gemini</option><option value="openai_official">\u5B98\u65B9 OpenAI</option></select></label>
|
|
2297
|
+
<label>\u540D\u79F0<input id="newName" placeholder="\u81EA\u52A8"></label>
|
|
2298
|
+
<label>Host<input id="newHost" value="https://www.pandai.vip"></label>
|
|
2299
|
+
<label class="full">API key<input id="newKey" type="password" placeholder="\u4FDD\u5B58\u5728\u672C\u673A\uFF0C\u4E0D\u5199\u5165\u804A\u5929"></label>
|
|
2300
|
+
<p class="hint full">\u6BCF\u4E2A\u65B0\u6E20\u9053\u4F1A\u81EA\u52A8\u5206\u914D\u72EC\u7ACB\u7684 key \u540D\u79F0\uFF0C\u907F\u514D\u591A\u4E2A\u6E20\u9053\u4E92\u76F8\u8986\u76D6\u3002</p>
|
|
2301
|
+
<label class="full">\u6A21\u578B\u5217\u8868<input id="newModels" placeholder="\u4F7F\u7528\u63A8\u8350\u9ED8\u8BA4\u503C"></label>
|
|
2302
|
+
<label><input id="newPrefer" type="checkbox" checked style="width:auto"> \u8BBE\u4E3A\u9ED8\u8BA4\u6E20\u9053</label>
|
|
2303
|
+
<div class="actions"><button class="primary" id="saveProvider">\u4FDD\u5B58\u6E20\u9053</button><button id="cancelProvider">\u53D6\u6D88</button></div>
|
|
2304
|
+
</div>
|
|
2305
|
+
</div><div id="providers"></div></div>\`;
|
|
2306
|
+
$('#addProviderBtn').onclick=()=>{const form=$('#providerForm');form.classList.remove('hidden');form.scrollIntoView({block:'nearest'});};$('#cancelProvider').onclick=()=>$('#providerForm').classList.add('hidden');$('#saveProvider').onclick=saveProvider;$('#newTemplate').onchange=applyProviderTemplate;applyProviderTemplate();
|
|
2307
|
+
$('#providers').innerHTML=s.providers.map(providerCard).join('') || '<p class="muted">\u8FD8\u6CA1\u6709\u914D\u7F6E\u6E20\u9053\u3002</p>';
|
|
2308
|
+
bindProviderActions();
|
|
2309
|
+
}
|
|
2310
|
+
function providerCard(p){const key=p.key||{set:false};const source=key.set?\`<span class="source \${esc(key.source)}">\${esc(keySource(key))}</span>\`:'<span class="source missing">\u672A\u914D\u7F6E</span>';const sourcePath=key.path?\`<div class="keyline small">\u6765\u6E90\u8DEF\u5F84 <span class="path">\${esc(key.path)}</span></div>\`:'';const officialHint=p.channel==='official'?'<div class="warning">\u5B98\u65B9\u6E20\u9053\u901A\u5E38\u9700\u8981\u5B98\u65B9 API key\uFF1B\u5982\u679C\u8FD9\u91CC\u653E\u7684\u662F\u7B2C\u4E09\u65B9\u6E20\u9053 key\uFF0C\u6D4B\u8BD5\u53EF\u80FD\u5931\u8D25\u3002</div>':'';return \`<div class="provider"><div><div class="title">\${esc(p.name)}</div><div class="badges"><span class="badge \${p.preference==='default'?'default':''}">\${esc(label(p.preference))}</span><span class="badge">\${esc(label(p.protocol))}</span><span class="badge">\${esc(label(p.channel))}</span><span class="badge \${p.enabled?'ok':'warn'}">\${p.enabled?'\u5DF2\u542F\u7528':'\u5DF2\u505C\u7528'}</span></div><div class="keyline">\${esc(p.base_url)}</div><div class="keyline">Key <code>\${esc(p.api_key_env)}</code>: \${key.set?\`<code data-key-preview="\${esc(p.api_key_env)}">\${esc(key.preview)}</code> <button class="icon-btn" title="\u663E\u793A\u5B8C\u6574 key" aria-label="\u663E\u793A\u5B8C\u6574 key" data-reveal="\${esc(p.api_key_env)}" data-visible="false">\${eyeIcon}</button>\`:'\u672A\u914D\u7F6E'} \${source} \${key.fingerprint?'<span class="small">fingerprint '+esc(key.fingerprint)+'</span>':''}</div>\${sourcePath}\${officialHint}</div><div class="actions"><button data-test="\${esc(p.name)}">\u6D4B\u8BD5</button><button data-default="\${esc(p.name)}">\u8BBE\u4E3A\u9ED8\u8BA4</button><button data-toggle="\${esc(p.name)}">\${p.enabled?'\u505C\u7528':'\u542F\u7528'}</button><button class="danger" data-delete="\${esc(p.name)}">\u79FB\u9664</button></div></div>\`}
|
|
2311
|
+
function bindProviderActions(){document.querySelectorAll('[data-test]').forEach(b=>b.onclick=()=>testProvider(b.dataset.test));document.querySelectorAll('[data-default]').forEach(b=>b.onclick=()=>post('/api/providers/'+encodeURIComponent(b.dataset.default)+'/default',{}));document.querySelectorAll('[data-toggle]').forEach(b=>{const p=state.data.providers.find(x=>x.name===b.dataset.toggle);b.onclick=()=>patch('/api/providers/'+encodeURIComponent(p.name),{enabled:!p.enabled})});document.querySelectorAll('[data-delete]').forEach(b=>b.onclick=()=>confirm('\u786E\u8BA4\u79FB\u9664\u8FD9\u4E2A\u6E20\u9053\uFF1F')&&del('/api/providers/'+encodeURIComponent(b.dataset.delete)));document.querySelectorAll('[data-reveal]').forEach(b=>b.onclick=()=>toggleKey(b.dataset.reveal,b))}
|
|
2312
|
+
function applyProviderTemplate(){const t=templateDefaults($('#newTemplate').value);$('#newName').placeholder=t.name;$('#newHost').value=t.host;$('#newModels').placeholder=t.models}
|
|
2313
|
+
async function saveProvider(){const t=templateDefaults($('#newTemplate').value);await post('/api/providers',{protocol:t.protocol,channel:t.channel,name:$('#newName').value,base_url:$('#newHost').value,api_key:$('#newKey').value,models:$('#newModels').value,prefer:$('#newPrefer').checked});$('#providerForm').classList.add('hidden');toast('\u6E20\u9053\u5DF2\u4FDD\u5B58')}
|
|
2314
|
+
async function testProvider(name){const r=await api('/api/providers/'+encodeURIComponent(name)+'/test',{method:'POST'});toast((r.ok?'\u6D4B\u8BD5\u901A\u8FC7\uFF1A':'\u6D4B\u8BD5\u5931\u8D25\uFF1A')+r.message)}
|
|
2315
|
+
async function toggleKey(name,btn){const slot=document.querySelector('[data-key-preview="'+CSS.escape(name)+'"]');if(btn.dataset.visible==='true'){const key=state.data.providers.find(p=>p.api_key_env===name)?.key;slot.textContent=key?.preview||'';btn.dataset.visible='false';btn.title='\u663E\u793A\u5B8C\u6574 key';btn.setAttribute('aria-label','\u663E\u793A\u5B8C\u6574 key');btn.innerHTML=eyeIcon;return}const r=await api('/api/key/'+encodeURIComponent(name));slot.textContent=r.value||'';btn.dataset.visible='true';btn.title='\u9690\u85CF key';btn.setAttribute('aria-label','\u9690\u85CF key');btn.innerHTML=eyeOffIcon}
|
|
2316
|
+
async function post(path,body){await api(path,{method:'POST',body:JSON.stringify(body)});await load()}
|
|
2317
|
+
async function patch(path,body){await api(path,{method:'PATCH',body:JSON.stringify(body)});await load()}
|
|
2318
|
+
async function del(path){await api(path,{method:'DELETE'});await load()}
|
|
2319
|
+
function renderGenerate(){const s=state.data;const prefs=JSON.parse(localStorage.getItem('picgen:prefs')||'{}');$('#generate').innerHTML=\`<div class="cols"><div class="panel"><h2>\u751F\u6210\u56FE\u7247</h2><label>\u63D0\u793A\u8BCD<textarea id="prompt">\${esc(prefs.prompt||'\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE\uFF0C\u767D\u8272\u80CC\u666F\uFF0C\u5C11\u91CF\u84DD\u7EFF\u8272\u79D1\u6280\u611F\u70B9\u7F00')}</textarea></label><div class="formgrid"><label>\u6E20\u9053<select id="genProvider"><option value="">\u81EA\u52A8\u9009\u62E9</option>\${s.providers.map(p=>'<option value="'+esc(p.name)+'" '+(prefs.provider===p.name?'selected':'')+'>'+esc(p.name)+'</option>').join('')}</select></label><label>\u9884\u8BBE<select id="genPreset">\${Object.keys(s.presets).map(p=>'<option '+((prefs.preset||'fast-draft')===p?'selected':'')+'>'+esc(p)+'</option>').join('')}</select></label><label>\u6A21\u578B<input id="genModel" value="\${esc(prefs.model||'')}" placeholder="\u53EF\u9009"></label><label>\u6A21\u5F0F<input id="genMode" value="\${esc(prefs.mode||'')}" placeholder="\u53EF\u9009"></label></div><div class="actions" style="margin-top:12px"><button id="preview" class="primary">\u9884\u89C8\u65B9\u6848</button><button id="generateBtn" disabled>\u5F00\u59CB\u751F\u6210</button></div></div><div class="panel"><h2>\u65B9\u6848 / \u7ED3\u679C</h2><div id="plan" class="plan">\u8FD8\u6CA1\u6709\u9884\u89C8\u3002</div><div id="result" class="result" style="margin-top:12px"></div></div></div>\`;$('#preview').onclick=preview;$('#generateBtn').onclick=generateNow}
|
|
2320
|
+
function genBody(){const body={prompt:$('#prompt').value,preset:$('#genPreset').value,provider:$('#genProvider').value,model:$('#genModel').value,mode:$('#genMode').value};localStorage.setItem('picgen:prefs',JSON.stringify(body));return body}
|
|
2321
|
+
async function preview(){const r=await api('/api/plan',{method:'POST',body:JSON.stringify(genBody())});state.plan=r.plan;$('#plan').textContent=JSON.stringify(r.plan,null,2);$('#generateBtn').disabled=false}
|
|
2322
|
+
async function generateNow(){if(!state.plan&&!confirm('\u8FD8\u6CA1\u6709\u9884\u89C8\u65B9\u6848\uFF0C\u786E\u5B9A\u76F4\u63A5\u751F\u6210\uFF1F'))return;$('#generateBtn').disabled=true;$('#result').innerHTML='<p class="muted">\u6B63\u5728\u751F\u6210...</p>';try{const r=await api('/api/generate',{method:'POST',body:JSON.stringify(genBody())});$('#result').innerHTML='<p>\u5DF2\u4FDD\u5B58\u5230 <span class="path">'+esc(r.output_dir)+'</span></p>'+r.images.map(img=>'<img src="/api/file?path='+encodeURIComponent(img.path)+'&token='+token+'"><div class="path">'+esc(img.path)+'</div>').join('');await renderHistory()}catch(e){$('#result').innerHTML='<p style="color:var(--danger)">'+esc(e.message)+'</p>'}finally{$('#generateBtn').disabled=false}}
|
|
2323
|
+
async function renderHistory(){const h=$('#history');if(!state.data)return;const r=await api('/api/history');h.innerHTML='<div class="panel"><h2>\u5386\u53F2\u8BB0\u5F55</h2><p class="muted">\u627E\u56DE\u6700\u8FD1\u751F\u6210\u7684\u56FE\u7247\u548C\u672C\u5730\u4FDD\u5B58\u8DEF\u5F84\u3002</p></div><div class="gallery">'+r.runs.map(runCard).join('')+'</div>'}
|
|
2324
|
+
function runCard(r){const img=(r.images&&r.images[0])?'<img src="'+r.images[0].url+'&token='+token+'">':'';const plan=r.plan||{};return '<div class="run">'+img+'<div class="title">'+esc((r.prompt||'\u672A\u547D\u540D').slice(0,60))+'</div><div class="muted small">'+esc(new Date(r.created_at).toLocaleString())+' \xB7 '+esc(plan.provider||'')+' \xB7 '+esc(plan.preset||'')+'</div><div class="path small">'+esc(r.output_dir)+'</div><div class="actions" style="margin-top:8px"><button onclick="navigator.clipboard.writeText(\\''+esc(String(r.output_dir)).replaceAll("'","\\\\'")+'\\')">\u590D\u5236\u6587\u4EF6\u5939</button></div></div>'}
|
|
2325
|
+
load().catch(e=>toast(e.message));
|
|
2326
|
+
</script>
|
|
2327
|
+
</body>
|
|
2328
|
+
</html>`;
|
|
1307
2329
|
|
|
1308
2330
|
// src/commands/preferences.ts
|
|
1309
2331
|
async function preferMode(name) {
|
|
@@ -1320,34 +2342,48 @@ async function preferPreset(name) {
|
|
|
1320
2342
|
}
|
|
1321
2343
|
|
|
1322
2344
|
// src/commands/quickstart.ts
|
|
1323
|
-
function runQuickstart() {
|
|
2345
|
+
async function runQuickstart() {
|
|
1324
2346
|
console.log(formatQuickstart());
|
|
2347
|
+
await maybePrintUpdateHint();
|
|
1325
2348
|
}
|
|
1326
2349
|
function formatQuickstart() {
|
|
1327
2350
|
return [
|
|
1328
2351
|
"PicGen quickstart",
|
|
1329
2352
|
"",
|
|
1330
2353
|
"Install:",
|
|
2354
|
+
" node -v",
|
|
2355
|
+
" npm -v",
|
|
1331
2356
|
" npm install -g @ai-agent-tools/picgen",
|
|
2357
|
+
" npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy",
|
|
2358
|
+
" picgen skill install codex",
|
|
2359
|
+
"",
|
|
2360
|
+
"Open local web UI:",
|
|
2361
|
+
" picgen open",
|
|
1332
2362
|
"",
|
|
1333
2363
|
"Configure:",
|
|
1334
|
-
" picgen setup",
|
|
2364
|
+
" picgen setup # can save provider API keys for you",
|
|
2365
|
+
" picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer",
|
|
2366
|
+
" picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard",
|
|
2367
|
+
" picgen key set PICGEN_GEMINI_PROXY_KEY --stdin",
|
|
2368
|
+
" picgen key list --json",
|
|
1335
2369
|
" picgen doctor --json",
|
|
1336
2370
|
"",
|
|
1337
2371
|
"Preview before spending quota:",
|
|
1338
|
-
' picgen create --dry-run "\u4E00\u5F20\
|
|
2372
|
+
' picgen create --dry-run --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
|
|
1339
2373
|
"",
|
|
1340
2374
|
"Generate after confirmation:",
|
|
1341
|
-
' picgen create --yes "\u4E00\u5F20\
|
|
2375
|
+
' picgen create --yes --preset fast-draft "\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE"',
|
|
1342
2376
|
"",
|
|
1343
2377
|
"Use a reference image:",
|
|
1344
2378
|
' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
|
|
1345
2379
|
"",
|
|
1346
2380
|
"Agent prompt:",
|
|
1347
|
-
" \u8BF7\u5B89\u88C5\u5E76\
|
|
2381
|
+
" \u8BF7\u5E2E\u6211\u5B89\u88C5\u5E76\u914D\u7F6E PicGen \u751F\u56FE\u5DE5\u5177\u3002\u8BF7\u5148\u9605\u8BFB\u5E76\u6309\u8FD9\u4E2A\u6307\u5357\u6267\u884C\uFF1Ahttps://raw.githubusercontent.com/ai-agent-tools/picgen/refs/heads/main/docs/agent-install.md \u3002\u4F60\u8D1F\u8D23\u5224\u65AD\u662F\u5426\u5728\u672C\u673A\u6301\u4E45\u73AF\u5883\u3001\u5B89\u88C5 CLI \u548C skill\u3001\u5F15\u5BFC\u6211\u914D\u7F6E provider/API key\uFF0C\u5E76\u5148\u9884\u89C8\u751F\u6210\u65B9\u6848\uFF0C\u7B49\u6211\u786E\u8BA4\u540E\u518D\u751F\u6210\u6D4B\u8BD5\u56FE\u3002\u4E0D\u8981\u8BA9\u6211\u7406\u89E3\u547D\u4EE4\u7EC6\u8282\uFF0C\u4E5F\u4E0D\u8981\u8BA9\u6211\u628A API key \u53D1\u5230\u804A\u5929\u91CC\u3002",
|
|
1348
2382
|
"",
|
|
1349
2383
|
"Notes:",
|
|
1350
2384
|
" - Provider host URLs should not include /v1 or /v1beta.",
|
|
2385
|
+
" - picgen setup can store API keys in ~/.picgen/.env.",
|
|
2386
|
+
" - picgen open starts a foreground local web UI on 127.0.0.1:8188.",
|
|
1351
2387
|
" - Agent workflows should dry-run before real generation.",
|
|
1352
2388
|
" - Generated images are saved locally; do not paste base64 into chat.",
|
|
1353
2389
|
" - First-user rollout checklist: docs/release-alpha.md"
|
|
@@ -1355,7 +2391,7 @@ function formatQuickstart() {
|
|
|
1355
2391
|
}
|
|
1356
2392
|
|
|
1357
2393
|
// src/commands/setup.ts
|
|
1358
|
-
import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
|
|
2394
|
+
import { confirm as confirm2, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
1359
2395
|
async function runSetup() {
|
|
1360
2396
|
await ensureConfig();
|
|
1361
2397
|
console.log(`PicGen config: ${getConfigPath()}`);
|
|
@@ -1370,17 +2406,20 @@ async function runSetup() {
|
|
|
1370
2406
|
{ name: "Quick add a common provider/channel", value: "quick-add" },
|
|
1371
2407
|
{ name: "Choose default provider/channel", value: "provider" },
|
|
1372
2408
|
{ name: "Choose generation preference", value: "mode" },
|
|
2409
|
+
{ name: "Configure API key", value: "key" },
|
|
1373
2410
|
{ name: "Test a provider", value: "test" },
|
|
1374
2411
|
{ name: "Advanced: add a custom provider/channel", value: "add" },
|
|
1375
2412
|
{ name: "Finish setup", value: "done" }
|
|
1376
2413
|
]
|
|
1377
2414
|
});
|
|
1378
2415
|
if (action === "quick-add") {
|
|
1379
|
-
await
|
|
2416
|
+
await quickAddProvider2();
|
|
1380
2417
|
} else if (action === "provider") {
|
|
1381
2418
|
await chooseDefaultProvider();
|
|
1382
2419
|
} else if (action === "mode") {
|
|
1383
2420
|
await chooseDefaultMode();
|
|
2421
|
+
} else if (action === "key") {
|
|
2422
|
+
await chooseProviderKeyToConfigure();
|
|
1384
2423
|
} else if (action === "test") {
|
|
1385
2424
|
await chooseProviderToTest();
|
|
1386
2425
|
} else if (action === "add") {
|
|
@@ -1399,7 +2438,7 @@ async function printSetupSummary() {
|
|
|
1399
2438
|
for (const [providerName, provider2] of Object.entries(config.providers)) {
|
|
1400
2439
|
const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
|
|
1401
2440
|
console.log(
|
|
1402
|
-
`- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
|
|
2441
|
+
`- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, key=${provider2.api_key_env}${process.env[provider2.api_key_env] ? " set" : " missing"}, capabilities=${provider2.capabilities.join(",")}`
|
|
1403
2442
|
);
|
|
1404
2443
|
}
|
|
1405
2444
|
}
|
|
@@ -1447,7 +2486,19 @@ async function chooseProviderToTest() {
|
|
|
1447
2486
|
if (result.model) console.log(`Model: ${result.model}`);
|
|
1448
2487
|
if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
|
|
1449
2488
|
}
|
|
1450
|
-
async function
|
|
2489
|
+
async function chooseProviderKeyToConfigure() {
|
|
2490
|
+
const config = await loadConfig();
|
|
2491
|
+
const name = await select2({
|
|
2492
|
+
message: "Choose the provider key to configure",
|
|
2493
|
+
default: config.routing.default_provider,
|
|
2494
|
+
choices: Object.entries(config.providers).map(([providerName, provider2]) => ({
|
|
2495
|
+
name: `${providerName} (${provider2.api_key_env}${process.env[provider2.api_key_env] ? ", currently set" : ", missing"})`,
|
|
2496
|
+
value: providerName
|
|
2497
|
+
}))
|
|
2498
|
+
});
|
|
2499
|
+
await configureProviderApiKey(config.providers[name]);
|
|
2500
|
+
}
|
|
2501
|
+
async function quickAddProvider2() {
|
|
1451
2502
|
const config = await loadConfig();
|
|
1452
2503
|
const template = await select2({
|
|
1453
2504
|
message: "Choose the provider/channel you want to add",
|
|
@@ -1481,7 +2532,7 @@ async function quickAddProvider() {
|
|
|
1481
2532
|
});
|
|
1482
2533
|
const apiKeyEnv = await input2({
|
|
1483
2534
|
message: "API key environment variable",
|
|
1484
|
-
default: defaults.api_key_env
|
|
2535
|
+
default: nextAvailableProviderApiKeyEnv(config, defaults.api_key_env, name)
|
|
1485
2536
|
});
|
|
1486
2537
|
const modelsRaw = await input2({
|
|
1487
2538
|
message: "Models (comma separated, press Enter for recommended defaults)",
|
|
@@ -1493,7 +2544,7 @@ async function quickAddProvider() {
|
|
|
1493
2544
|
channel: defaults.channel,
|
|
1494
2545
|
base_url: normalizeProviderBaseUrl(baseUrl),
|
|
1495
2546
|
api_key_env: apiKeyEnv,
|
|
1496
|
-
models:
|
|
2547
|
+
models: parseModels3(modelsRaw),
|
|
1497
2548
|
capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
|
|
1498
2549
|
};
|
|
1499
2550
|
addProviderToConfig(config, name, provider2);
|
|
@@ -1506,7 +2557,27 @@ async function quickAddProvider() {
|
|
|
1506
2557
|
}
|
|
1507
2558
|
await saveConfig(config);
|
|
1508
2559
|
console.log(`Added provider: ${name}`);
|
|
1509
|
-
|
|
2560
|
+
await configureProviderApiKey(provider2);
|
|
2561
|
+
}
|
|
2562
|
+
async function configureProviderApiKey(provider2) {
|
|
2563
|
+
if (process.env[provider2.api_key_env]) {
|
|
2564
|
+
const replace = await confirm2({
|
|
2565
|
+
message: `${provider2.api_key_env} is already available. Replace the saved PicGen key?`,
|
|
2566
|
+
default: false
|
|
2567
|
+
});
|
|
2568
|
+
if (!replace) return;
|
|
2569
|
+
}
|
|
2570
|
+
const value = await password2({
|
|
2571
|
+
message: `Paste API key for ${provider2.api_key_env} (leave empty to skip)`,
|
|
2572
|
+
mask: "*"
|
|
2573
|
+
});
|
|
2574
|
+
if (!value.trim()) {
|
|
2575
|
+
console.log(`Skipped API key. You can configure it later with picgen setup.`);
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
const path = await saveManagedEnvVar(provider2.api_key_env, value.trim());
|
|
2579
|
+
console.log(`Saved ${provider2.api_key_env} to ${path}`);
|
|
2580
|
+
console.log(`PicGen loads this file automatically. Advanced users can override it with shell env vars or a project .env.`);
|
|
1510
2581
|
}
|
|
1511
2582
|
function quickProviderDefaults(template) {
|
|
1512
2583
|
switch (template) {
|
|
@@ -1516,7 +2587,7 @@ function quickProviderDefaults(template) {
|
|
|
1516
2587
|
protocol: "openai-images",
|
|
1517
2588
|
channel: "third_party",
|
|
1518
2589
|
base_url: "https://www.pandai.vip",
|
|
1519
|
-
api_key_env: "
|
|
2590
|
+
api_key_env: "PICGEN_OPENAI_PROXY_KEY",
|
|
1520
2591
|
models: ["gpt-image-2"]
|
|
1521
2592
|
};
|
|
1522
2593
|
case "gemini_proxy":
|
|
@@ -1525,7 +2596,7 @@ function quickProviderDefaults(template) {
|
|
|
1525
2596
|
protocol: "gemini",
|
|
1526
2597
|
channel: "third_party",
|
|
1527
2598
|
base_url: "https://www.pandai.vip",
|
|
1528
|
-
api_key_env: "
|
|
2599
|
+
api_key_env: "PICGEN_GEMINI_PROXY_KEY",
|
|
1529
2600
|
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1530
2601
|
};
|
|
1531
2602
|
case "openai_official":
|
|
@@ -1548,7 +2619,7 @@ function quickProviderDefaults(template) {
|
|
|
1548
2619
|
};
|
|
1549
2620
|
}
|
|
1550
2621
|
}
|
|
1551
|
-
function
|
|
2622
|
+
function parseModels3(raw) {
|
|
1552
2623
|
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
1553
2624
|
}
|
|
1554
2625
|
function providerLabel(provider2) {
|
|
@@ -1570,11 +2641,54 @@ function modeLabel(modeName) {
|
|
|
1570
2641
|
}
|
|
1571
2642
|
}
|
|
1572
2643
|
|
|
2644
|
+
// src/commands/skill.ts
|
|
2645
|
+
import { access, cp, mkdir as mkdir5 } from "fs/promises";
|
|
2646
|
+
import { constants } from "fs";
|
|
2647
|
+
import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
|
|
2648
|
+
import { homedir as homedir4 } from "os";
|
|
2649
|
+
import { fileURLToPath } from "url";
|
|
2650
|
+
async function installSkill(target, options) {
|
|
2651
|
+
if (target !== "codex") {
|
|
2652
|
+
throw new Error(`Unsupported skill target: ${target}. Supported target: codex.`);
|
|
2653
|
+
}
|
|
2654
|
+
const source = await findBundledPicgenSkill();
|
|
2655
|
+
const destination = join7(getCodexHome(), "skills", "picgen");
|
|
2656
|
+
await mkdir5(dirname4(destination), { recursive: true });
|
|
2657
|
+
await cp(source, destination, {
|
|
2658
|
+
recursive: true,
|
|
2659
|
+
force: options.force ?? false,
|
|
2660
|
+
errorOnExist: !(options.force ?? false)
|
|
2661
|
+
});
|
|
2662
|
+
console.log(`Installed PicGen skill for Codex: ${destination}`);
|
|
2663
|
+
console.log("Restart Codex or start a new Codex session if the skill is not visible yet.");
|
|
2664
|
+
}
|
|
2665
|
+
function getCodexHome() {
|
|
2666
|
+
return process.env.CODEX_HOME ?? join7(homedir4(), ".codex");
|
|
2667
|
+
}
|
|
2668
|
+
async function findBundledPicgenSkill() {
|
|
2669
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
2670
|
+
const candidates = [
|
|
2671
|
+
resolve4(here, "../skills/picgen"),
|
|
2672
|
+
resolve4(here, "../../skills/picgen"),
|
|
2673
|
+
resolve4(process.cwd(), "skills/picgen")
|
|
2674
|
+
];
|
|
2675
|
+
for (const candidate of candidates) {
|
|
2676
|
+
try {
|
|
2677
|
+
await access(join7(candidate, "SKILL.md"), constants.R_OK);
|
|
2678
|
+
return candidate;
|
|
2679
|
+
} catch {
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
throw new Error("Bundled PicGen skill not found. Reinstall @ai-agent-tools/picgen and try again.");
|
|
2683
|
+
}
|
|
2684
|
+
|
|
1573
2685
|
// src/cli.ts
|
|
2686
|
+
await loadPicgenEnv();
|
|
1574
2687
|
var program = new Command();
|
|
1575
|
-
program.name("picgen").description("Lightweight image generation connector for AI agents.").version(
|
|
2688
|
+
program.name("picgen").description("Lightweight image generation connector for AI agents.").version(VERSION);
|
|
1576
2689
|
program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
|
|
1577
2690
|
program.command("quickstart").description("Print install and first-run guidance.").action(runQuickstart);
|
|
2691
|
+
program.command("open").description("Open the local PicGen web interface.").option("--port <port>", "Preferred local port. Defaults to 8188.").option("--no-open", "Print the URL without opening the browser.").action(runOpen);
|
|
1578
2692
|
program.command("doctor").description("Inspect PicGen configuration and provider readiness.").option("--json", "Print machine-readable JSON.").action(runDoctor);
|
|
1579
2693
|
program.command("create").description("Create an image generation plan or generate images.").argument("<prompt...>", "Prompt text.").option("--dry-run", "Plan generation without calling a provider.").option("--preset <name>", "Preset name.").option("--provider <name>", "Provider name.").option("--mode <name>", "Mode name.").option("--model <name>", "Model name.").option("--out-dir <path>", "Output directory.").option(
|
|
1580
2694
|
"--reference <path>",
|
|
@@ -1585,14 +2699,21 @@ program.command("create").description("Create an image generation plan or genera
|
|
|
1585
2699
|
var provider = program.command("provider").description("Manage providers/channels.");
|
|
1586
2700
|
provider.command("list").description("List providers.").action(listProviders);
|
|
1587
2701
|
provider.command("add").description("Add a provider.").action(addProvider);
|
|
2702
|
+
provider.command("quick-add").argument("<template>", "openai-proxy, gemini-proxy, openai-official, or gemini-official").description("Add a common provider/channel without interactive prompts.").option("--name <name>", "Provider name.").option("--host <url>", "Provider host URL. Do not include /v1 or /v1beta.").option("--key-env <name>", "API key environment variable.").option("--models <models>", "Comma-separated model list.").option("--prefer", "Use this provider as the default.").action(quickAddProvider);
|
|
1588
2703
|
provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
|
|
1589
2704
|
provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
|
|
1590
2705
|
provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
|
|
1591
2706
|
provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
|
|
1592
2707
|
provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
|
|
1593
2708
|
provider.command("remove").argument("<name>").description("Remove a provider.").action(removeProvider);
|
|
2709
|
+
var key = program.command("key").description("Manage PicGen API keys.");
|
|
2710
|
+
key.command("set").argument("<env-name>", "Environment variable name, such as PICGEN_GEMINI_PROXY_KEY.").description("Save an API key to PicGen's managed env file.").option("--stdin", "Read the key value from stdin.").option("--clipboard", "Read the key value from the macOS clipboard.").option("--value <value>", "Set the key value directly. Prefer --stdin for agent workflows.").action(setApiKey);
|
|
2711
|
+
key.command("list").description("List configured provider keys without revealing secret values.").option("--json", "Print machine-readable JSON.").action(listApiKeys);
|
|
2712
|
+
key.command("show").argument("<env-name>", "Environment variable name.").description("Show one configured key without revealing the secret value.").option("--json", "Print machine-readable JSON.").action(showApiKey);
|
|
2713
|
+
program.command("skill").description("Install PicGen agent skills.").command("install").argument("<target>", "Skill target. Currently supported: codex.").description("Install the bundled PicGen skill into an agent skill directory.").option("--force", "Overwrite an existing installed skill.").action(installSkill);
|
|
1594
2714
|
program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
|
|
1595
2715
|
program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
|
|
2716
|
+
program.command("update").description("Manage PicGen updates.").command("check").description("Check whether a newer PicGen version is available.").option("--json", "Print machine-readable JSON.").action(runUpdateCheck);
|
|
1596
2717
|
program.parseAsync().catch((error) => {
|
|
1597
2718
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1598
2719
|
process.exitCode = 1;
|