Merge pull request #38 from kingdomad/feature/baoyu-image-gen-extend-config

add EXTEND.md configuration support
This commit is contained in:
Jim Liu 宝玉 2026-02-05 23:10:11 -06:00 committed by GitHub
commit 0faea4ecaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 188 additions and 14 deletions

View File

@ -41,7 +41,9 @@ test -f "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "user"
│ Not found │ Use defaults │ │ Not found │ Use defaults │
└───────────┴───────────────────────────────────────────────────────────────────────────┘ └───────────┴───────────────────────────────────────────────────────────────────────────┘
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio **EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models
Schema: `references/config/preferences-schema.md`
## Usage ## Usage
@ -99,7 +101,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image ou
| `GOOGLE_BASE_URL` | Custom Google endpoint | | `GOOGLE_BASE_URL` | Custom Google endpoint |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | | `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
**Load Priority**: CLI args > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env` **Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
## Provider Selection ## Provider Selection

View File

@ -0,0 +1,66 @@
---
name: preferences-schema
description: EXTEND.md YAML schema for baoyu-image-gen user preferences
---
# Preferences Schema
## Full Schema
```yaml
---
version: 1
default_provider: null # google|openai|dashscope|null (null = auto-detect)
default_quality: null # normal|2k|null (null = use default: 2k)
default_aspect_ratio: null # "16:9"|"1:1"|"4:3"|"3:4"|"2.35:1"|null
default_image_size: null # 1K|2K|4K|null (Google only, overrides quality)
default_model:
google: null # e.g., "gemini-3-pro-image-preview"
openai: null # e.g., "gpt-image-1.5"
dashscope: null # e.g., "z-image-turbo"
---
```
## Field Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | int | 1 | Schema version |
| `default_provider` | string\|null | null | Default provider (null = auto-detect) |
| `default_quality` | string\|null | null | Default quality (null = 2k) |
| `default_aspect_ratio` | string\|null | null | Default aspect ratio |
| `default_image_size` | string\|null | null | Google image size (overrides quality) |
| `default_model.google` | string\|null | null | Google default model |
| `default_model.openai` | string\|null | null | OpenAI default model |
| `default_model.dashscope` | string\|null | null | DashScope default model |
## Examples
**Minimal**:
```yaml
---
version: 1
default_provider: google
default_quality: 2k
---
```
**Full**:
```yaml
---
version: 1
default_provider: google
default_quality: 2k
default_aspect_ratio: "16:9"
default_image_size: 2K
default_model:
google: "gemini-3-pro-image-preview"
openai: "gpt-image-1.5"
dashscope: "z-image-turbo"
---
```

View File

@ -2,7 +2,7 @@ import path from "node:path";
import process from "node:process"; import process from "node:process";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import type { CliArgs, Provider } from "./types"; import type { CliArgs, Provider, ExtendConfig } from "./types";
function printUsage(): void { function printUsage(): void {
console.log(`Usage: console.log(`Usage:
@ -37,7 +37,7 @@ Environment variables:
GOOGLE_BASE_URL Custom Google endpoint GOOGLE_BASE_URL Custom Google endpoint
DASHSCOPE_BASE_URL Custom DashScope endpoint DASHSCOPE_BASE_URL Custom DashScope endpoint
Env file load order: CLI args > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`); Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
} }
function parseArgs(argv: string[]): CliArgs { function parseArgs(argv: string[]): CliArgs {
@ -49,7 +49,7 @@ function parseArgs(argv: string[]): CliArgs {
model: null, model: null,
aspectRatio: null, aspectRatio: null,
size: null, size: null,
quality: "2k", quality: null,
imageSize: null, imageSize: null,
referenceImages: [], referenceImages: [],
n: 1, n: 1,
@ -215,6 +215,87 @@ async function loadEnv(): Promise<void> {
} }
} }
function extractYamlFrontMatter(content: string): string | null {
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m);
return match ? match[1] : null;
}
function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
const config: Partial<ExtendConfig> = {};
const lines = yaml.split("\n");
let currentKey: string | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
if (trimmed.includes(":") && !trimmed.startsWith("-")) {
const colonIdx = trimmed.indexOf(":");
const key = trimmed.slice(0, colonIdx).trim();
let value = trimmed.slice(colonIdx + 1).trim();
if (value === "null" || value === "") {
value = "null";
}
if (key === "version") {
config.version = value === "null" ? 1 : parseInt(value, 10);
} else if (key === "default_provider") {
config.default_provider = value === "null" ? null : (value as Provider);
} else if (key === "default_quality") {
config.default_quality = value === "null" ? null : (value as "normal" | "2k");
} else if (key === "default_aspect_ratio") {
const cleaned = value.replace(/['"]/g, "");
config.default_aspect_ratio = cleaned === "null" ? null : cleaned;
} else if (key === "default_image_size") {
config.default_image_size = value === "null" ? null : (value as "1K" | "2K" | "4K");
} else if (key === "default_model") {
config.default_model = { google: null, openai: null, dashscope: null };
currentKey = "default_model";
} else if (currentKey === "default_model" && (key === "google" || key === "openai" || key === "dashscope")) {
const cleaned = value.replace(/['"]/g, "");
config.default_model![key] = cleaned === "null" ? null : cleaned;
}
}
}
return config;
}
async function loadExtendConfig(): Promise<Partial<ExtendConfig>> {
const home = homedir();
const cwd = process.cwd();
const paths = [
path.join(cwd, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
path.join(home, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
];
for (const p of paths) {
try {
const content = await readFile(p, "utf8");
const yaml = extractYamlFrontMatter(content);
if (!yaml) continue;
return parseSimpleYaml(yaml);
} catch {
continue;
}
}
return {};
}
function mergeConfig(args: CliArgs, extend: Partial<ExtendConfig>): CliArgs {
return {
...args,
provider: args.provider ?? extend.default_provider ?? null,
quality: args.quality ?? extend.default_quality ?? null,
aspectRatio: args.aspectRatio ?? extend.default_aspect_ratio ?? null,
imageSize: args.imageSize ?? extend.default_image_size ?? null,
};
}
async function readPromptFromFiles(files: string[]): Promise<string> { async function readPromptFromFiles(files: string[]): Promise<string> {
const parts: string[] = []; const parts: string[] = [];
for (const f of files) { for (const f of files) {
@ -283,9 +364,13 @@ async function main(): Promise<void> {
} }
await loadEnv(); await loadEnv();
const extendConfig = await loadExtendConfig();
const mergedArgs = mergeConfig(args, extendConfig);
let prompt: string | null = args.prompt; if (!mergedArgs.quality) mergedArgs.quality = "2k";
if (!prompt && args.promptFiles.length > 0) prompt = await readPromptFromFiles(args.promptFiles);
let prompt: string | null = mergedArgs.prompt;
if (!prompt && mergedArgs.promptFiles.length > 0) prompt = await readPromptFromFiles(mergedArgs.promptFiles);
if (!prompt) prompt = await readPromptFromStdin(); if (!prompt) prompt = await readPromptFromStdin();
if (!prompt) { if (!prompt) {
@ -295,24 +380,32 @@ async function main(): Promise<void> {
return; return;
} }
if (!args.imagePath) { if (!mergedArgs.imagePath) {
console.error("Error: --image is required"); console.error("Error: --image is required");
printUsage(); printUsage();
process.exitCode = 1; process.exitCode = 1;
return; return;
} }
const provider = detectProvider(args); const provider = detectProvider(mergedArgs);
const providerModule = await loadProviderModule(provider); const providerModule = await loadProviderModule(provider);
const model = args.model || providerModule.getDefaultModel();
const outputPath = normalizeOutputImagePath(args.imagePath); let model = mergedArgs.model;
if (!model && extendConfig.default_model) {
if (provider === "google") model = extendConfig.default_model.google ?? null;
if (provider === "openai") model = extendConfig.default_model.openai ?? null;
if (provider === "dashscope") model = extendConfig.default_model.dashscope ?? null;
}
model = model || providerModule.getDefaultModel();
const outputPath = normalizeOutputImagePath(mergedArgs.imagePath);
let imageData: Uint8Array; let imageData: Uint8Array;
let retried = false; let retried = false;
while (true) { while (true) {
try { try {
imageData = await providerModule.generateImage(prompt, model, args); imageData = await providerModule.generateImage(prompt, model, mergedArgs);
break; break;
} catch (e) { } catch (e) {
if (!retried) { if (!retried) {
@ -328,7 +421,7 @@ async function main(): Promise<void> {
await mkdir(dir, { recursive: true }); await mkdir(dir, { recursive: true });
await writeFile(outputPath, imageData); await writeFile(outputPath, imageData);
if (args.json) { if (mergedArgs.json) {
console.log( console.log(
JSON.stringify( JSON.stringify(
{ {

View File

@ -9,10 +9,23 @@ export type CliArgs = {
model: string | null; model: string | null;
aspectRatio: string | null; aspectRatio: string | null;
size: string | null; size: string | null;
quality: Quality; quality: Quality | null;
imageSize: string | null; imageSize: string | null;
referenceImages: string[]; referenceImages: string[];
n: number; n: number;
json: boolean; json: boolean;
help: boolean; help: boolean;
}; };
export type ExtendConfig = {
version: number;
default_provider: Provider | null;
default_quality: Quality | null;
default_aspect_ratio: string | null;
default_image_size: "1K" | "2K" | "4K" | null;
default_model: {
google: string | null;
openai: string | null;
dashscope: string | null;
};
};