瀏覽代碼

fmode-image v2.0: ESM纯JS重写,6种模式(--app/--arch/--scene/--slide/--explode/--detail),零依赖fetch

FmodeAgent 13 小時之前
父節點
當前提交
77b5fe0f76
共有 7 個文件被更改,包括 273 次插入200 次删除
  1. 0 24
      bin/fmode-image.js
  2. 32 0
      bin/fmode-image.mjs
  3. 164 0
      lib/fmode-image.mjs
  4. 28 19
      package.json
  5. 0 92
      scripts/gen.py
  6. 10 5
      skill-package-manifest.json
  7. 39 60
      skills/fmode-image/SKILL.md

+ 0 - 24
bin/fmode-image.js

@@ -1,24 +0,0 @@
-#!/usr/bin/env node
-const path = require('path');
-const { spawnSync } = require('child_process');
-const GEN = path.resolve(__dirname, '..', 'scripts', 'gen.py');
-const args = process.argv.slice(2);
-if (!args.length || args[0]==='--help') {
-  console.log(`fmode-image — AI架构图和场景插图生成器
-
-用法:
-  npx fmode-image --arch    "prompt" [name]    架构图 ¥0.5
-  npx fmode-image --app     "prompt" [name]    应用界面 ¥0.5
-  npx fmode-image --explode "prompt" [name]    爆炸图 ¥0.5
-  npx fmode-image --scene   "prompt" [name]    场景插图 ¥0.3
-  npx fmode-image --detail  "prompt" [name]    细节图 ¥0.3
-  npx fmode-image --slide   "prompt" [name]    整页PPT ¥0.5
-
-自动读取 FMODE_API_KEY (env/config.json/.env)
-输出目录: SKILL_IMAGE_OUTPUT 环境变量控制(默认当前目录)
-`);
-  process.exit(0);
-}
-const python = process.platform==='win32'?'python':'python3';
-const r = spawnSync(python, [GEN, ...args], { stdio: 'inherit', shell: false });
-process.exit(r.status??0);

+ 32 - 0
bin/fmode-image.mjs

@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+import { spawn } from 'node:child_process';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const libPath = join(__dirname, '..', 'lib', 'fmode-image.mjs');
+
+const args = process.argv.slice(2);
+if (!args.length || args[0] === '--help' || args[0] === '-h') {
+  const { MODES } = await import(libPath);
+  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
+
+可用模式:`);
+  for (const [m, c] of Object.entries(MODES)) {
+    console.log(`  ${m.padEnd(12)} ${c.size.padEnd(14)} ¥${c.cost.toFixed(1)}  ${c.desc}`);
+  }
+  console.log(`\n环境变量: FMODE_API_KEY(必设), SKILL_IMAGE_OUTPUT(默认当前目录)`);
+  process.exit(0);
+}
+
+const child = spawn(process.execPath, [libPath, ...args], { stdio: 'inherit', shell: false });
+child.on('exit', (code) => process.exit(code ?? 0));

+ 164 - 0
lib/fmode-image.mjs

@@ -0,0 +1,164 @@
+// 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: '内部运转/拆解图',
+  },
+  '--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 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();

+ 28 - 19
package.json

@@ -1,29 +1,38 @@
 {
   "name": "fmode-image",
   "version": "2.0.0",
-  "description": "Fmode Image Generator: \u67b6\u6784\u56fe\u548c\u573a\u666f\u63d2\u56fe\u751f\u6210\u5668\u3002\u81ea\u52a8\u8bfb\u53d6API Key\uff0c\u652f\u6301AI\u67b6\u6784\u56fe(1792x1024)\u548c\u573a\u666f\u63d2\u56fe(1024x1024)\u53cc\u6a21\u5f0f\uff0c\u7eaf\u767d\u5e95PNG\u76f4\u63a5\u7528\u4e8ePPT\u3002",
-  "type": "commonjs",
+  "description": "AI架构图和场景插图生成器。6种模式(--app/--arch/--scene/--slide),自动读API Key,零依赖纯ESM。",
+  "type": "module",
+  "main": "./lib/fmode-image.mjs",
+  "exports": {
+    ".": {
+      "import": "./lib/fmode-image.mjs",
+      "default": "./lib/fmode-image.mjs"
+    }
+  },
   "bin": {
-    "fmode-image": "bin/fmode-image.js"
+    "fmode-image": "./bin/fmode-image.mjs"
   },
   "files": [
-    ".claude-plugin/",
+    "lib/",
     "bin/",
+    "skills/",
     "scripts/",
     "README.md",
-    "skill-package-manifest.json",
-    "skills/"
-  ],
-  "keywords": [
-    "claude-code",
-    "claude-skill",
-    "fmode",
-    "image",
-    "architecture",
-    "diagram",
-    "illustration",
-    "harness",
-    "ppt"
+    "LICENSE",
+    "skill-package-manifest.json"
   ],
-  "license": "MIT"
-}
+  "keywords": ["fmode", "image", "ai", "architecture", "diagram", "scene", "presentation"],
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/fmodecn/fmode-image.git"
+  },
+  "bugs": {
+    "url": "https://github.com/fmodecn/fmode-image/issues"
+  },
+  "homepage": "https://github.com/fmodecn/fmode-image#readme",
+  "engines": {
+    "node": ">=18"
+  }
+}

+ 0 - 92
scripts/gen.py

@@ -1,92 +0,0 @@
-#!/usr/bin/env python3
-"""
-fmode-image v2.0 — 架构图/应用界面/场景插图/整页PPT生成器
-
-用法:
-  python3 gen.py --arch    "prompt" [name]    架构图 1792x1024 ¥0.5
-  python3 gen.py --app     "prompt" [name]    应用界面 1792x1024 ¥0.5
-  python3 gen.py --explode "prompt" [name]    爆炸图 1792x1024 ¥0.5
-  python3 gen.py --scene   "prompt" [name]    场景插图 1024x1024 ¥0.3
-  python3 gen.py --detail  "prompt" [name]    细节图 1024x1024 ¥0.3
-  python3 gen.py --slide   "prompt" [name]    整页PPT 1920x1080 ¥0.5
-
-凭据: 自动读取 FMODE_API_KEY (env/config.json/.env)
-输出: 白底PNG,SKILL_IMAGE_OUTPUT 控制输出目录
-"""
-import json, base64, urllib.request, os, sys
-sys.stdout.reconfigure(encoding='utf-8', errors='replace')
-
-API = 'https://api.fmode.cn/v1/images/generations'
-
-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':'内部运转/拆解图'},
-    '--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直接使用'},
-}
-
-def _get_key():
-    for v in [os.environ.get('FMODE_API_KEY',''), os.environ.get('ANTHROPIC_AUTH_TOKEN','')]:
-        if v and '***' not in v and len(v) > 10: return v
-    try:
-        with open(os.path.expanduser('~/.fmode/config.json'),encoding='utf-8') as f:
-            c = json.load(f)
-            for k in ('api_key','FMODE_API_KEY','fmodeApiToken'):
-                if c.get(k): return c[k]
-    except: pass
-    try:
-        with open(os.path.expanduser('~/.fmode/config.yaml'),encoding='utf-8') as f:
-            for l in f:
-                for kw in ('api_key','FMODE_API_KEY','fmodeApiToken'):
-                    if kw in l:
-                        v = l.split(':',1)[1].strip().strip('"\'')
-                        if v and v != '${FMODE_API_KEY}': return v
-    except: pass
-    for p in ['/opt/data/.env','.env']:
-        try:
-            with open(p,encoding='utf-8') as f:
-                for l in f:
-                    if 'FMODE_API_KEY' in l:
-                        v = l.split('=',1)[1].strip().strip('"\'')
-                        if v and '***' not in v and len(v)>10: return v
-        except: pass
-    print('No API Key found. Set FMODE_API_KEY env or ~/.fmode/config.json')
-    sys.exit(1)
-
-def _call_api(prompt, size):
-    body = json.dumps({"model":"gpt-image-2.5-sunburst","prompt":prompt,"n":1,"size":size}).encode()
-    req = urllib.request.Request(API,data=body,
-        headers={"Authorization":f"Bearer {_get_key()}","Content-Type":"application/json"},method='POST')
-    resp = urllib.request.urlopen(req,timeout=300)
-    data = json.loads(resp.read())
-    if 'b64_json' in data['data'][0]: return base64.b64decode(data['data'][0]['b64_json'])
-    if 'url' in data['data'][0]: return urllib.request.urlopen(data['data'][0]['url'],timeout=60).read()
-    raise Exception('No image data')
-
-def gen(mode, prompt, name='output'):
-    cfg = MODES.get(mode)
-    if not cfg: print(f'Unknown mode {mode}'); sys.exit(1)
-    full = f'{cfg["prefix"]}{prompt} {cfg["suffix"]}'
-    outdir = os.environ.get('SKILL_IMAGE_OUTPUT','.')
-    os.makedirs(outdir,exist_ok=True)
-    path = os.path.join(outdir,f'{name}.png')
-    data = _call_api(full,cfg['size'])
-    with open(path,'wb') as f: f.write(data)
-    sz = os.path.getsize(path)//1024
-    print(f'OK {cfg["desc"]} {sz}KB {cfg["size"]} {path}')
-    return path
-
-gen_arch=lambda p,n='arch':gen('--arch',p,n)
-gen_app=lambda p,n='app':gen('--app',p,n)
-gen_explode=lambda p,n='explode':gen('--explode',p,n)
-gen_scene=lambda p,n='scene':gen('--scene',p,n)
-gen_detail=lambda p,n='detail':gen('--detail',p,n)
-gen_slide=lambda p,n='slide':gen('--slide',p,n)
-
-if __name__=='__main__':
-    if len(sys.argv)<3: print(__doc__); sys.exit(1)
-    mode,content=sys.argv[1],sys.argv[2]
-    name=sys.argv[3] if len(sys.argv)>3 else 'output'
-    gen(mode,content,name)

+ 10 - 5
skill-package-manifest.json

@@ -1,9 +1,14 @@
 {
   "name": "fmode-image",
-  "version": "1.0.0",
-  "description": "Fmode Image Generator",
-  "install": {
-    "claude-code": "npx fmode-image",
-    "hermes": "skill_view(name='fmode-image')"
+  "version": "2.0.0",
+  "type": "native",
+  "entry": "./skills/fmode-image/SKILL.md",
+  "requires": ["node>=18"],
+  "description": "AI架构图和场景插图生成器。6种模式,零依赖纯ESM,自动读API Key。",
+  "commands": {
+    "fmode-image": {
+      "description": "生成AI图像",
+      "args": "<mode> <prompt> [name]"
+    }
   }
 }

+ 39 - 60
skills/fmode-image/SKILL.md

@@ -7,78 +7,57 @@ license: MIT
 platforms: [linux, macos, windows]
 ---
 
-# fmode-image v2 — AI架构图和场景插图生成器
+# fmode-image v2.0 — AI图像生成器
 
-6种模式 · 自动读API Key · 白底PNG直接放PPT
-
-## 凭据自动获取
-FMODE_API_KEY (env) → ~/.fmode/config.json → .env
+6种模式 · 零依赖纯ESM · 自动读API Key · 白底PNG直接放PPT
 
 ## 六种模式速查
 
-| 模式 | 尺寸 | 成本 | 用途 | 推荐度 |
-|------|------|------|------|--------|
-| `--app` | 1792×1024 | ¥0.5 | **应用界面**(表现力最强) | ⭐⭐⭐⭐⭐ |
-| `--arch` | 1792×1024 | ¥0.5 | 架构图/分层/闭环 | ⭐⭐⭐⭐ |
-| `--slide` | 1920×1080 | ¥0.5 | 整页PPT(封面/金句) | ⭐⭐⭐⭐ |
-| `--explode` | 1792×1024 | ¥0.5 | 爆炸图/内部运转 | ⭐⭐⭐ |
-| `--scene` | 1024×1024 | ¥0.3 | 场景插图/痛点 | ⭐⭐⭐⭐ |
-| `--detail` | 1024×1024 | ¥0.3 | 零件/材质特写 | ⭐⭐ |
-
-## 快速开始
+| 模式 | 尺寸 | 成本 | 用途 |
+|------|------|------|------|
+| `--app` | 1792×1024 | ¥0.5 | **应用界面**(投入产出比最高) |
+| `--arch` | 1792×1024 | ¥0.5 | 架构图/分层/闭环 |
+| `--slide` | 1920×1080 | ¥0.5 | 整页PPT(封面/金句页) |
+| `--explode` | 1792×1024 | ¥0.5 | 爆炸图/内部运转 |
+| `--scene` | 1024×1024 | ¥0.3 | 场景插图/痛点 |
+| `--detail` | 1024×1024 | ¥0.3 | 零件/材质特写 |
 
+## 安装
 ```bash
-# 安装
 npm install -g fmode-image
-
-# 生成应用界面(表现力最强,推荐!)
-npx fmode-image --app "标题"内容合规系统"左侧导航5项,顶部指标卡3张" my-app
-
-# 生成架构图
-npx fmode-image --arch "标题"Harness五层引擎"从上到下五层用箭头连接" my-arch
-
-# 生成场景插图
-npx fmode-image --scene "法务团队深夜加班堆满稿件的办公场景,人物表情焦虑" legal-scene
-
-# 生成整页PPT
-npx fmode-image --slide "主标题内容营销的超级预审Agent,副标题把合规从3天变成3分钟" cover
 ```
 
-## 最佳实践
-
-### --app 应用界面(投入产出比最高)
-- 结构:左导航 + 顶部指标卡 + 中间表格 + 右侧详情面板
-- 指标卡带同比(↑12%)瞬间有真实感
-- 表格行用彩色状态徽章、截断省略号
-- 所有数字必须与讲稿一致(模型会编造)
-
-### --arch 架构图
-- 右侧数据卡是灵魂——让架构图从说明书变成成果展示
-- 每层给名称 + 副标 + 三个要点
-- 数字必须来自讲稿且逐项核对
-- 层级名与讲稿逐字一致
+## 快速使用
+```bash
+# 应用界面(推荐!)
+npx fmode-image --app "标题"内容合规系统"左侧导航5项,顶部指标卡3张" app-demo
 
-### --scene 场景插图
-- 给情绪词(焦虑/惊喜/专注)比纯描述生动十倍
-- 环境元素承载信息:67%退审率大屏、深夜时钟
-- 对比场景(Before/After)冲击最大
-- 同案例内风格统一
+# 架构图
+npx fmode-image --arch "Harness五层引擎,从上到下数据流" harness
 
-### --slide 整页PPT
-- 封面页/金句页/章节过渡页最稳(文字少)
-- 用1920×1080严格16:9比例
-- 内容页文字多易糊,推荐素材图+HTML排版
+# 场景插图(省40%成本)
+npx fmode-image --scene "法务团队深夜加班堆满稿件" scene
 
-## Prompt技巧
-1. 数字要给全写死,模型会编造
-2. 单段文字≤12字,长句拆短标签
-3. 关键标题/数字在prompt里出现两次
-4. 方向描述要明确(从X提升到Y,不说"显著提升")
-5. 同案例多图固定四样:配色色值/风格词/版式/品牌落款
+# 整页PPT
+npx fmode-image --slide "封面:内容营销的超级预审Agent" cover
+```
 
 ## 输出控制
-- 环境变量 `SKILL_IMAGE_OUTPUT` 控制目录(默认当前目录)
-- 文件名语义化:`arch-harness-5layer.png`
+- `FMODE_API_KEY` 环境变量(必设)
+- `SKILL_IMAGE_OUTPUT` 输出目录(默认当前目录)
+
+## 最佳实践
+- **--app 界面图**:结构=左导航+顶部指标卡+中间表格+右侧详情,指标卡带同比↑瞬间真实
+- **--arch 架构图**:右侧数据卡是灵魂,所有数字必须与讲稿一致
+- **--scene 场景图**:给情绪词(焦虑/惊喜)比纯描述生动十倍
+- **--slide 整页PPT**:封面/金句/章节过渡页最稳(文字少),内容页素材图+HTML排版
+
+## 独立调用(ESM import)
+```js
+import { gen, genArch, genApp, genScene, batch } from 'fmode-image';
+await genApp('内容营销预审Agent界面', 'my-app');
+```
 
-## 独立脚本
-scripts/gen.py 可直接运行:`python3 gen.py --arch "..." out`
+## 链接
+- GitHub:https://github.com/fmodecn/fmode-image
+- npm:https://www.npmjs.com/package/fmode-image