@ai-agent-tools/picgen 0.1.0-alpha.10 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -41
- package/dist/cli.js +54 -1036
- package/docs/release-alpha.md +3 -37
- package/package.json +1 -1
- package/skills/picgen/SKILL.md +5 -146
- package/docs/agent-install.md +0 -155
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import "dotenv/config";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/commands/create.ts
|
|
@@ -217,12 +218,12 @@ function padMilliseconds(value) {
|
|
|
217
218
|
function slug(value) {
|
|
218
219
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
219
220
|
}
|
|
220
|
-
function redactProviderImageData(value,
|
|
221
|
+
function redactProviderImageData(value, key) {
|
|
221
222
|
if (Array.isArray(value)) {
|
|
222
223
|
return value.map((item) => redactProviderImageData(item));
|
|
223
224
|
}
|
|
224
225
|
if (!value || typeof value !== "object") {
|
|
225
|
-
return shouldRedactImageDataKey(
|
|
226
|
+
return shouldRedactImageDataKey(key) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
|
|
226
227
|
}
|
|
227
228
|
return Object.fromEntries(
|
|
228
229
|
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
@@ -231,8 +232,8 @@ function redactProviderImageData(value, key2) {
|
|
|
231
232
|
])
|
|
232
233
|
);
|
|
233
234
|
}
|
|
234
|
-
function shouldRedactImageDataKey(
|
|
235
|
-
return
|
|
235
|
+
function shouldRedactImageDataKey(key) {
|
|
236
|
+
return key === "b64_json" || key === "data" || key === "thoughtSignature" || key === "thought_signature";
|
|
236
237
|
}
|
|
237
238
|
function redactedProviderDataPlaceholder(value) {
|
|
238
239
|
return `[redacted provider data: ${value.length} chars]`;
|
|
@@ -274,9 +275,9 @@ function mimeTypeFromPath(path) {
|
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
// src/config/store.ts
|
|
277
|
-
import { mkdir as
|
|
278
|
-
import { dirname
|
|
279
|
-
import { homedir
|
|
278
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
279
|
+
import { dirname, join as join2 } from "path";
|
|
280
|
+
import { homedir } from "os";
|
|
280
281
|
import YAML from "yaml";
|
|
281
282
|
|
|
282
283
|
// src/config/defaults.ts
|
|
@@ -362,164 +363,6 @@ var defaultConfig = {
|
|
|
362
363
|
}
|
|
363
364
|
};
|
|
364
365
|
|
|
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
|
-
|
|
523
366
|
// src/config/schema.ts
|
|
524
367
|
import { z } from "zod";
|
|
525
368
|
var providerSchema = z.object({
|
|
@@ -586,19 +429,14 @@ function defaultCapabilitiesForProtocol(protocol) {
|
|
|
586
429
|
|
|
587
430
|
// src/config/store.ts
|
|
588
431
|
function getConfigPath() {
|
|
589
|
-
return process.env.PICGEN_CONFIG ??
|
|
432
|
+
return process.env.PICGEN_CONFIG ?? join2(homedir(), ".picgen", "config.yaml");
|
|
590
433
|
}
|
|
591
434
|
async function loadConfig() {
|
|
592
435
|
const path = getConfigPath();
|
|
593
436
|
try {
|
|
594
|
-
const raw = await
|
|
437
|
+
const raw = await readFile(path, "utf8");
|
|
595
438
|
const parsed = YAML.parse(raw);
|
|
596
|
-
|
|
597
|
-
const migrated = await migrateConfig(config);
|
|
598
|
-
if (migrated.changed) {
|
|
599
|
-
await writeConfigFile(path, migrated.config);
|
|
600
|
-
}
|
|
601
|
-
return migrated.config;
|
|
439
|
+
return picgenConfigSchema.parse(parsed);
|
|
602
440
|
} catch (error) {
|
|
603
441
|
if (error.code === "ENOENT") {
|
|
604
442
|
return structuredClone(defaultConfig);
|
|
@@ -609,11 +447,8 @@ async function loadConfig() {
|
|
|
609
447
|
async function saveConfig(config) {
|
|
610
448
|
const parsed = picgenConfigSchema.parse(config);
|
|
611
449
|
const path = getConfigPath();
|
|
612
|
-
await
|
|
613
|
-
|
|
614
|
-
async function writeConfigFile(path, config) {
|
|
615
|
-
await mkdir3(dirname2(path), { recursive: true });
|
|
616
|
-
await writeFile3(path, YAML.stringify(config), "utf8");
|
|
450
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
451
|
+
await writeFile2(path, YAML.stringify(parsed), "utf8");
|
|
617
452
|
}
|
|
618
453
|
async function ensureConfig() {
|
|
619
454
|
const config = await loadConfig();
|
|
@@ -622,54 +457,7 @@ async function ensureConfig() {
|
|
|
622
457
|
}
|
|
623
458
|
|
|
624
459
|
// src/providers/gemini.ts
|
|
625
|
-
import { readFile as
|
|
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
|
-
}
|
|
460
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
673
461
|
|
|
674
462
|
// src/providers/urls.ts
|
|
675
463
|
function normalizeProviderBaseUrl(baseUrl) {
|
|
@@ -707,20 +495,15 @@ var GeminiAdapter = class {
|
|
|
707
495
|
const providerImages = [];
|
|
708
496
|
const requestCount = Math.max(1, plan.preset.n);
|
|
709
497
|
const referenceParts = await readReferenceImageParts(plan);
|
|
710
|
-
const timeoutMs = resolveProviderTimeoutMs(plan);
|
|
711
498
|
for (let index = 0; index < requestCount; index += 1) {
|
|
712
|
-
const response = await
|
|
713
|
-
|
|
714
|
-
{
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
"x-goog-api-key": apiKey,
|
|
718
|
-
"Content-Type": "application/json"
|
|
719
|
-
},
|
|
720
|
-
body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
|
|
499
|
+
const response = await fetch(buildGeminiGenerateContentUrl(plan.provider.base_url, plan.model), {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: {
|
|
502
|
+
"x-goog-api-key": apiKey,
|
|
503
|
+
"Content-Type": "application/json"
|
|
721
504
|
},
|
|
722
|
-
|
|
723
|
-
);
|
|
505
|
+
body: JSON.stringify(buildGeminiGenerateContentRequest(plan, referenceParts))
|
|
506
|
+
});
|
|
724
507
|
const raw = await readJsonResponse(response);
|
|
725
508
|
if (!response.ok) {
|
|
726
509
|
throw new Error(formatGeminiError(response.status, response.statusText, raw));
|
|
@@ -762,7 +545,7 @@ async function readReferenceImageParts(plan) {
|
|
|
762
545
|
plan.referenceImages.map(async (image) => ({
|
|
763
546
|
inlineData: {
|
|
764
547
|
mimeType: image.mime_type,
|
|
765
|
-
data: (await
|
|
548
|
+
data: (await readFile2(image.path)).toString("base64")
|
|
766
549
|
}
|
|
767
550
|
}))
|
|
768
551
|
);
|
|
@@ -841,18 +624,14 @@ var OpenAIImagesAdapter = class {
|
|
|
841
624
|
if (!apiKey) {
|
|
842
625
|
throw new Error(`Missing API key environment variable: ${plan.provider.api_key_env}`);
|
|
843
626
|
}
|
|
844
|
-
const response = await
|
|
845
|
-
|
|
846
|
-
{
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
Authorization: `Bearer ${apiKey}`,
|
|
850
|
-
"Content-Type": "application/json"
|
|
851
|
-
},
|
|
852
|
-
body: JSON.stringify(buildOpenAIImagesRequest(plan))
|
|
627
|
+
const response = await fetch(buildOpenAIImagesUrl(plan.provider.base_url), {
|
|
628
|
+
method: "POST",
|
|
629
|
+
headers: {
|
|
630
|
+
Authorization: `Bearer ${apiKey}`,
|
|
631
|
+
"Content-Type": "application/json"
|
|
853
632
|
},
|
|
854
|
-
|
|
855
|
-
);
|
|
633
|
+
body: JSON.stringify(buildOpenAIImagesRequest(plan))
|
|
634
|
+
});
|
|
856
635
|
const raw = await readJsonResponse2(response);
|
|
857
636
|
if (!response.ok) {
|
|
858
637
|
throw new Error(formatOpenAIImagesError(response.status, response.statusText, raw));
|
|
@@ -979,7 +758,7 @@ function getAdapter(protocol) {
|
|
|
979
758
|
}
|
|
980
759
|
|
|
981
760
|
// src/routing/resolve.ts
|
|
982
|
-
import { join as
|
|
761
|
+
import { join as join3 } from "path";
|
|
983
762
|
import { cwd } from "process";
|
|
984
763
|
function resolveGenerationPlan(config, options) {
|
|
985
764
|
const presetName = options.presetName ?? config.default_preset;
|
|
@@ -1013,7 +792,7 @@ function resolveGenerationPlan(config, options) {
|
|
|
1013
792
|
presetName,
|
|
1014
793
|
preset,
|
|
1015
794
|
modeName,
|
|
1016
|
-
outputDirectory: options.outputDirectory ??
|
|
795
|
+
outputDirectory: options.outputDirectory ?? join3(cwd(), "outputs", "picgen"),
|
|
1017
796
|
referenceImages: options.referenceImages ?? []
|
|
1018
797
|
};
|
|
1019
798
|
}
|
|
@@ -1214,13 +993,13 @@ function inspectProviders(config) {
|
|
|
1214
993
|
}
|
|
1215
994
|
|
|
1216
995
|
// src/commands/update.ts
|
|
1217
|
-
import { mkdir as
|
|
1218
|
-
import { dirname as
|
|
1219
|
-
import { homedir as
|
|
996
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
997
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
998
|
+
import { homedir as homedir2 } from "os";
|
|
1220
999
|
|
|
1221
1000
|
// src/version.ts
|
|
1222
1001
|
var PACKAGE_NAME = "@ai-agent-tools/picgen";
|
|
1223
|
-
var VERSION = "0.1.0-alpha.
|
|
1002
|
+
var VERSION = "0.1.0-alpha.2";
|
|
1224
1003
|
|
|
1225
1004
|
// src/commands/update.ts
|
|
1226
1005
|
var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1315,7 +1094,7 @@ async function fetchLatestVersion() {
|
|
|
1315
1094
|
}
|
|
1316
1095
|
async function readFreshCache() {
|
|
1317
1096
|
try {
|
|
1318
|
-
const cache = JSON.parse(await
|
|
1097
|
+
const cache = JSON.parse(await readFile3(getUpdateCachePath(), "utf8"));
|
|
1319
1098
|
const checkedAt = Date.parse(cache.checked_at);
|
|
1320
1099
|
if (!Number.isFinite(checkedAt)) return void 0;
|
|
1321
1100
|
if (Date.now() - checkedAt > UPDATE_CACHE_TTL_MS) return void 0;
|
|
@@ -1327,12 +1106,12 @@ async function readFreshCache() {
|
|
|
1327
1106
|
}
|
|
1328
1107
|
async function writeCache(cache) {
|
|
1329
1108
|
const path = getUpdateCachePath();
|
|
1330
|
-
await
|
|
1331
|
-
await
|
|
1109
|
+
await mkdir3(dirname2(path), { recursive: true });
|
|
1110
|
+
await writeFile3(path, JSON.stringify(cache, null, 2), "utf8");
|
|
1332
1111
|
}
|
|
1333
1112
|
function getUpdateCachePath() {
|
|
1334
1113
|
if (process.env.PICGEN_UPDATE_CACHE_PATH) return process.env.PICGEN_UPDATE_CACHE_PATH;
|
|
1335
|
-
return
|
|
1114
|
+
return join4(homedir2(), ".picgen", "update-check.json");
|
|
1336
1115
|
}
|
|
1337
1116
|
function parseVersion(version) {
|
|
1338
1117
|
const [core, prerelease] = version.split("-", 2);
|
|
@@ -1391,84 +1170,6 @@ async function runDoctor(options) {
|
|
|
1391
1170
|
await maybePrintUpdateHint();
|
|
1392
1171
|
}
|
|
1393
1172
|
|
|
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
|
-
|
|
1472
1173
|
// src/commands/provider.ts
|
|
1473
1174
|
import { input, select } from "@inquirer/prompts";
|
|
1474
1175
|
|
|
@@ -1614,32 +1315,6 @@ async function addProvider() {
|
|
|
1614
1315
|
await saveConfig(config);
|
|
1615
1316
|
console.log(`Added provider: ${provider2.name}`);
|
|
1616
1317
|
}
|
|
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
|
-
}
|
|
1643
1318
|
function addProviderToConfig(config, name, provider2) {
|
|
1644
1319
|
config.providers[name] = provider2;
|
|
1645
1320
|
const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
|
|
@@ -1736,11 +1411,7 @@ async function promptProvider(config, existingName, existing) {
|
|
|
1736
1411
|
});
|
|
1737
1412
|
const apiKeyEnv = await input({
|
|
1738
1413
|
message: "API key environment variable",
|
|
1739
|
-
default: existing?.api_key_env ??
|
|
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
|
-
)
|
|
1414
|
+
default: existing?.api_key_env ?? (protocol === "openai-images" ? channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY" : channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY")
|
|
1744
1415
|
});
|
|
1745
1416
|
const defaultModels = protocol === "openai-images" ? "gpt-image-2" : "gemini-3.1-flash-image-preview,gemini-3-pro-image-preview";
|
|
1746
1417
|
const modelsRaw = await input({
|
|
@@ -1770,562 +1441,6 @@ function nextAvailableProviderName(config, baseName, existingName) {
|
|
|
1770
1441
|
function defaultCapabilitiesForProtocol2(protocol) {
|
|
1771
1442
|
return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
|
|
1772
1443
|
}
|
|
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>`;
|
|
2329
1444
|
|
|
2330
1445
|
// src/commands/preferences.ts
|
|
2331
1446
|
async function preferMode(name) {
|
|
@@ -2351,39 +1466,26 @@ function formatQuickstart() {
|
|
|
2351
1466
|
"PicGen quickstart",
|
|
2352
1467
|
"",
|
|
2353
1468
|
"Install:",
|
|
2354
|
-
" node -v",
|
|
2355
|
-
" npm -v",
|
|
2356
1469
|
" 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",
|
|
2362
1470
|
"",
|
|
2363
1471
|
"Configure:",
|
|
2364
|
-
" picgen setup
|
|
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",
|
|
1472
|
+
" picgen setup",
|
|
2369
1473
|
" picgen doctor --json",
|
|
2370
1474
|
"",
|
|
2371
1475
|
"Preview before spending quota:",
|
|
2372
|
-
' picgen create --dry-run
|
|
1476
|
+
' picgen create --dry-run "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
|
|
2373
1477
|
"",
|
|
2374
1478
|
"Generate after confirmation:",
|
|
2375
|
-
' picgen create --yes
|
|
1479
|
+
' picgen create --yes "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
|
|
2376
1480
|
"",
|
|
2377
1481
|
"Use a reference image:",
|
|
2378
1482
|
' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
|
|
2379
1483
|
"",
|
|
2380
1484
|
"Agent prompt:",
|
|
2381
|
-
" \u8BF7\
|
|
1485
|
+
" \u8BF7\u5B89\u88C5\u5E76\u4F53\u9A8C @ai-agent-tools/picgen\uFF1A\u5168\u5C40\u5B89\u88C5 npm install -g @ai-agent-tools/picgen\uFF0C\u8FD0\u884C picgen setup \u914D\u7F6E\uFF0C\u7136\u540E\u5148 dry-run \u9884\u89C8\uFF0C\u518D\u786E\u8BA4\u751F\u6210\u4E00\u5F20\u6D4B\u8BD5\u56FE\u3002\u5982\u679C\u6211\u8981\u7528\u53C2\u8003\u56FE\uFF0C\u8BF7\u4F7F\u7528 --reference <\u56FE\u7247\u8DEF\u5F84>\u3002",
|
|
2382
1486
|
"",
|
|
2383
1487
|
"Notes:",
|
|
2384
1488
|
" - 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.",
|
|
2387
1489
|
" - Agent workflows should dry-run before real generation.",
|
|
2388
1490
|
" - Generated images are saved locally; do not paste base64 into chat.",
|
|
2389
1491
|
" - First-user rollout checklist: docs/release-alpha.md"
|
|
@@ -2391,7 +1493,7 @@ function formatQuickstart() {
|
|
|
2391
1493
|
}
|
|
2392
1494
|
|
|
2393
1495
|
// src/commands/setup.ts
|
|
2394
|
-
import { confirm as confirm2, input as input2,
|
|
1496
|
+
import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
|
|
2395
1497
|
async function runSetup() {
|
|
2396
1498
|
await ensureConfig();
|
|
2397
1499
|
console.log(`PicGen config: ${getConfigPath()}`);
|
|
@@ -2406,20 +1508,17 @@ async function runSetup() {
|
|
|
2406
1508
|
{ name: "Quick add a common provider/channel", value: "quick-add" },
|
|
2407
1509
|
{ name: "Choose default provider/channel", value: "provider" },
|
|
2408
1510
|
{ name: "Choose generation preference", value: "mode" },
|
|
2409
|
-
{ name: "Configure API key", value: "key" },
|
|
2410
1511
|
{ name: "Test a provider", value: "test" },
|
|
2411
1512
|
{ name: "Advanced: add a custom provider/channel", value: "add" },
|
|
2412
1513
|
{ name: "Finish setup", value: "done" }
|
|
2413
1514
|
]
|
|
2414
1515
|
});
|
|
2415
1516
|
if (action === "quick-add") {
|
|
2416
|
-
await
|
|
1517
|
+
await quickAddProvider();
|
|
2417
1518
|
} else if (action === "provider") {
|
|
2418
1519
|
await chooseDefaultProvider();
|
|
2419
1520
|
} else if (action === "mode") {
|
|
2420
1521
|
await chooseDefaultMode();
|
|
2421
|
-
} else if (action === "key") {
|
|
2422
|
-
await chooseProviderKeyToConfigure();
|
|
2423
1522
|
} else if (action === "test") {
|
|
2424
1523
|
await chooseProviderToTest();
|
|
2425
1524
|
} else if (action === "add") {
|
|
@@ -2438,7 +1537,7 @@ async function printSetupSummary() {
|
|
|
2438
1537
|
for (const [providerName, provider2] of Object.entries(config.providers)) {
|
|
2439
1538
|
const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
|
|
2440
1539
|
console.log(
|
|
2441
|
-
`- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)},
|
|
1540
|
+
`- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
|
|
2442
1541
|
);
|
|
2443
1542
|
}
|
|
2444
1543
|
}
|
|
@@ -2486,19 +1585,7 @@ async function chooseProviderToTest() {
|
|
|
2486
1585
|
if (result.model) console.log(`Model: ${result.model}`);
|
|
2487
1586
|
if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
|
|
2488
1587
|
}
|
|
2489
|
-
async function
|
|
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() {
|
|
1588
|
+
async function quickAddProvider() {
|
|
2502
1589
|
const config = await loadConfig();
|
|
2503
1590
|
const template = await select2({
|
|
2504
1591
|
message: "Choose the provider/channel you want to add",
|
|
@@ -2532,7 +1619,7 @@ async function quickAddProvider2() {
|
|
|
2532
1619
|
});
|
|
2533
1620
|
const apiKeyEnv = await input2({
|
|
2534
1621
|
message: "API key environment variable",
|
|
2535
|
-
default:
|
|
1622
|
+
default: defaults.api_key_env
|
|
2536
1623
|
});
|
|
2537
1624
|
const modelsRaw = await input2({
|
|
2538
1625
|
message: "Models (comma separated, press Enter for recommended defaults)",
|
|
@@ -2544,7 +1631,7 @@ async function quickAddProvider2() {
|
|
|
2544
1631
|
channel: defaults.channel,
|
|
2545
1632
|
base_url: normalizeProviderBaseUrl(baseUrl),
|
|
2546
1633
|
api_key_env: apiKeyEnv,
|
|
2547
|
-
models:
|
|
1634
|
+
models: parseModels(modelsRaw),
|
|
2548
1635
|
capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
|
|
2549
1636
|
};
|
|
2550
1637
|
addProviderToConfig(config, name, provider2);
|
|
@@ -2557,27 +1644,7 @@ async function quickAddProvider2() {
|
|
|
2557
1644
|
}
|
|
2558
1645
|
await saveConfig(config);
|
|
2559
1646
|
console.log(`Added provider: ${name}`);
|
|
2560
|
-
|
|
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.`);
|
|
1647
|
+
console.log(`Set ${apiKeyEnv} in your shell or .env before testing this provider.`);
|
|
2581
1648
|
}
|
|
2582
1649
|
function quickProviderDefaults(template) {
|
|
2583
1650
|
switch (template) {
|
|
@@ -2587,7 +1654,7 @@ function quickProviderDefaults(template) {
|
|
|
2587
1654
|
protocol: "openai-images",
|
|
2588
1655
|
channel: "third_party",
|
|
2589
1656
|
base_url: "https://www.pandai.vip",
|
|
2590
|
-
api_key_env: "
|
|
1657
|
+
api_key_env: "OPENAI_API_KEY",
|
|
2591
1658
|
models: ["gpt-image-2"]
|
|
2592
1659
|
};
|
|
2593
1660
|
case "gemini_proxy":
|
|
@@ -2596,7 +1663,7 @@ function quickProviderDefaults(template) {
|
|
|
2596
1663
|
protocol: "gemini",
|
|
2597
1664
|
channel: "third_party",
|
|
2598
1665
|
base_url: "https://www.pandai.vip",
|
|
2599
|
-
api_key_env: "
|
|
1666
|
+
api_key_env: "GEMINI_API_KEY",
|
|
2600
1667
|
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
2601
1668
|
};
|
|
2602
1669
|
case "openai_official":
|
|
@@ -2619,7 +1686,7 @@ function quickProviderDefaults(template) {
|
|
|
2619
1686
|
};
|
|
2620
1687
|
}
|
|
2621
1688
|
}
|
|
2622
|
-
function
|
|
1689
|
+
function parseModels(raw) {
|
|
2623
1690
|
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
2624
1691
|
}
|
|
2625
1692
|
function providerLabel(provider2) {
|
|
@@ -2641,54 +1708,11 @@ function modeLabel(modeName) {
|
|
|
2641
1708
|
}
|
|
2642
1709
|
}
|
|
2643
1710
|
|
|
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
|
-
|
|
2685
1711
|
// src/cli.ts
|
|
2686
|
-
await loadPicgenEnv();
|
|
2687
1712
|
var program = new Command();
|
|
2688
1713
|
program.name("picgen").description("Lightweight image generation connector for AI agents.").version(VERSION);
|
|
2689
1714
|
program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
|
|
2690
1715
|
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);
|
|
2692
1716
|
program.command("doctor").description("Inspect PicGen configuration and provider readiness.").option("--json", "Print machine-readable JSON.").action(runDoctor);
|
|
2693
1717
|
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(
|
|
2694
1718
|
"--reference <path>",
|
|
@@ -2699,18 +1723,12 @@ program.command("create").description("Create an image generation plan or genera
|
|
|
2699
1723
|
var provider = program.command("provider").description("Manage providers/channels.");
|
|
2700
1724
|
provider.command("list").description("List providers.").action(listProviders);
|
|
2701
1725
|
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);
|
|
2703
1726
|
provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
|
|
2704
1727
|
provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
|
|
2705
1728
|
provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
|
|
2706
1729
|
provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
|
|
2707
1730
|
provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
|
|
2708
1731
|
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);
|
|
2714
1732
|
program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
|
|
2715
1733
|
program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
|
|
2716
1734
|
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);
|