| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- // fmode-image v2.0 — 纯ESM零依赖版
- // 架构图/应用界面/场景插图/整页PPT生成器
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
- import { homedir } from 'node:os';
- import { join, dirname } from 'node:path';
- import { fileURLToPath } from 'node:url';
- const __dirname = dirname(fileURLToPath(import.meta.url));
- // ===== 模式注册表 =====
- export const MODES = {
- '--arch': {
- prefix: '纯白背景扁平风格架构图。',
- suffix: '简洁扁平商务风格,纯白背景适合PPT。',
- size: '1792x1024', cost: 0.5, desc: '架构/分层/闭环图',
- },
- '--app': {
- prefix: '现代SaaS应用界面截图,浅色主题。',
- suffix: '界面细节清晰锐利,中文界面文字,像真实在运行的产品。',
- size: '1792x1024', cost: 0.5, desc: '产品界面(表现力最强)',
- },
- '--explode': {
- prefix: '技术爆炸图,内部分层剖开并标注数据流向。',
- suffix: '扁平科技风格,纯白背景,层级与流向清晰。',
- size: '1792x1024', cost: 0.5, desc: '内部运转/拆解图',
- },
- '--product': {
- prefix: '产品设计效果图,纯白背景,多角度组合渲染。',
- suffix: '工业设计质感,材质细腻,白底适合PPT,写实渲染。',
- size: '1792x1024', cost: 0.5, desc: '产品设计图(硬件/工业)',
- },
- '--scene': {
- prefix: '扁平插画场景。',
- suffix: '清新明亮风格。',
- size: '1024x1024', cost: 0.3, desc: '场景/人物/痛点插图',
- },
- '--detail': {
- prefix: '精密细节特写图。',
- suffix: '写实质感,清晰锐利。',
- size: '1024x1024', cost: 0.3, desc: '零件/材质/实物特写',
- },
- '--slide': {
- prefix: '一页16:9演示幻灯片。',
- suffix: '留白充足,字号层级分明,商务演示风格。',
- size: '1920x1080', cost: 0.5, desc: '整页PPT直接使用',
- },
- };
- // ===== 凭据自动获取 =====
- export function getApiKey() {
- let key = process.env.FMODE_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || '';
- if (key && !key.includes('***') && key.length > 10) return key;
- try {
- const p = join(homedir(), '.fmode', 'config.json');
- if (existsSync(p)) {
- const c = JSON.parse(readFileSync(p, 'utf-8'));
- for (const k of ['api_key', 'FMODE_API_KEY', 'fmodeApiToken']) if (c[k]) return c[k];
- }
- } catch {}
- try {
- const p = join(homedir(), '.fmode', 'config.yaml');
- if (existsSync(p)) {
- for (const l of readFileSync(p, 'utf-8').split('\n')) {
- for (const kw of ['api_key', 'FMODE_API_KEY', 'fmodeApiToken']) {
- if (l.includes(kw)) {
- const v = l.split(':').slice(1).join(':').trim().replace(/["']/g, '');
- if (v && v !== '${FMODE_API_KEY}') return v;
- }
- }
- }
- }
- } catch {}
- for (const p of ['/opt/data/.env', '.env']) {
- try {
- if (existsSync(p)) {
- for (const l of readFileSync(p, 'utf-8').split('\n')) {
- if (l.includes('FMODE_API_KEY')) {
- const v = l.split('=').slice(1).join('=').trim().replace(/["']/g, '');
- if (v && !v.includes('***') && v.length > 10) return v;
- }
- }
- }
- } catch {}
- }
- throw new Error('No API Key found. Set FMODE_API_KEY env or ~/.fmode/config.json');
- }
- // ===== API调用(纯fetch,零依赖) =====
- export async function callImageAPI(prompt, size) {
- const key = getApiKey();
- const resp = await fetch('https://api.fmode.cn/v1/images/generations', {
- method: 'POST',
- headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
- body: JSON.stringify({ model: 'gpt-image-2.5-sunburst', prompt, n: 1, size }),
- signal: AbortSignal.timeout(300000),
- });
- if (!resp.ok) throw new Error(`API ${resp.status}: ${(await resp.text().catch(()=>'')).slice(0,200)}`);
- const data = await resp.json();
- if (data.data?.[0]?.b64_json) return Buffer.from(data.data[0].b64_json, 'base64');
- if (data.data?.[0]?.url) {
- const r = await fetch(data.data[0].url, { signal: AbortSignal.timeout(60000) });
- if (!r.ok) throw new Error(`Download failed: ${r.status}`);
- return Buffer.from(await r.arrayBuffer());
- }
- throw new Error('Response has no image data');
- }
- // ===== 统一生成 =====
- export async function gen(mode, prompt, name = 'output') {
- const cfg = MODES[mode];
- if (!cfg) throw new Error(`Unknown mode: ${mode}`);
- const fullPrompt = `${cfg.prefix}${prompt} ${cfg.suffix}`;
- const outdir = process.env.SKILL_IMAGE_OUTPUT || '.';
- mkdirSync(outdir, { recursive: true });
- const outPath = join(outdir, `${name}.png`);
- const imgData = await callImageAPI(fullPrompt, cfg.size);
- writeFileSync(outPath, imgData);
- console.log(` OK ${cfg.desc} | ${Math.round(imgData.length/1024)}KB | ${cfg.size} | ${outPath}`);
- return outPath;
- }
- export const genArch = (p, n) => gen('--arch', p, n);
- export const genApp = (p, n) => gen('--app', p, n);
- export const genExplode = (p, n) => gen('--explode', p, n);
- export const genProduct = (p, n) => gen('--product', p, n);
- export const genScene = (p, n) => gen('--scene', p, n);
- export const genDetail = (p, n) => gen('--detail', p, n);
- export const genSlide = (p, n) => gen('--slide', p, n);
- export async function batch(tasks) {
- let total = 0;
- for (const [mode, name, prompt] of tasks) {
- process.stdout.write(`${name}: `);
- try { await gen(mode, prompt, name); total += MODES[mode].cost; }
- catch (e) { console.error(` FAIL: ${e.message}`); }
- }
- console.log(`\nTotal cost: ¥${total.toFixed(1)}`);
- }
- async function main() {
- const args = process.argv.slice(2);
- if (!args.length || args[0] === '--help' || args[0] === '-h') {
- console.log(`
- fmode-image v2.0 — AI架构图和场景插图生成器
- 用法:
- npx fmode-image --arch "prompt" [name] 架构图 ¥0.5
- npx fmode-image --app "prompt" [name] 应用界面 ¥0.5
- npx fmode-image --scene "prompt" [name] 场景插图 ¥0.3
- npx fmode-image --slide "prompt" [name] 整页PPT ¥0.5
- npx fmode-image --explode "prompt" [name] 爆炸图 ¥0.5
- npx fmode-image --detail "prompt" [name] 细节图 ¥0.3
- `);
- process.exit(0);
- }
- const mode = args[0];
- if (!MODES[mode]) { console.error(`Unknown mode: ${mode}`); process.exit(1); }
- const prompt = args.slice(1, -1).join(' ') || args[1] || '';
- const name = args.length > 2 ? args[args.length - 1] : 'output';
- if (!prompt) { console.error('Error: prompt required'); process.exit(1); }
- try { await gen(mode, prompt, name); }
- catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
- }
- if (process.argv[1] === fileURLToPath(import.meta.url)) main();
|