fmode-image.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. // fmode-image v2.0 — 纯ESM零依赖版
  2. // 架构图/应用界面/场景插图/整页PPT生成器
  3. import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
  4. import { homedir } from 'node:os';
  5. import { join, dirname } from 'node:path';
  6. import { fileURLToPath } from 'node:url';
  7. const __dirname = dirname(fileURLToPath(import.meta.url));
  8. // ===== 模式注册表 =====
  9. export const MODES = {
  10. '--arch': {
  11. prefix: '纯白背景扁平风格架构图。',
  12. suffix: '简洁扁平商务风格,纯白背景适合PPT。',
  13. size: '1792x1024', cost: 0.5, desc: '架构/分层/闭环图',
  14. },
  15. '--app': {
  16. prefix: '现代SaaS应用界面截图,浅色主题。',
  17. suffix: '界面细节清晰锐利,中文界面文字,像真实在运行的产品。',
  18. size: '1792x1024', cost: 0.5, desc: '产品界面(表现力最强)',
  19. },
  20. '--explode': {
  21. prefix: '技术爆炸图,内部分层剖开并标注数据流向。',
  22. suffix: '扁平科技风格,纯白背景,层级与流向清晰。',
  23. size: '1792x1024', cost: 0.5, desc: '内部运转/拆解图',
  24. },
  25. '--product': {
  26. prefix: '产品设计效果图,纯白背景,多角度组合渲染。',
  27. suffix: '工业设计质感,材质细腻,白底适合PPT,写实渲染。',
  28. size: '1792x1024', cost: 0.5, desc: '产品设计图(硬件/工业)',
  29. },
  30. '--scene': {
  31. prefix: '扁平插画场景。',
  32. suffix: '清新明亮风格。',
  33. size: '1024x1024', cost: 0.3, desc: '场景/人物/痛点插图',
  34. },
  35. '--detail': {
  36. prefix: '精密细节特写图。',
  37. suffix: '写实质感,清晰锐利。',
  38. size: '1024x1024', cost: 0.3, desc: '零件/材质/实物特写',
  39. },
  40. '--slide': {
  41. prefix: '一页16:9演示幻灯片。',
  42. suffix: '留白充足,字号层级分明,商务演示风格。',
  43. size: '1920x1080', cost: 0.5, desc: '整页PPT直接使用',
  44. },
  45. };
  46. // ===== 凭据自动获取 =====
  47. export function getApiKey() {
  48. let key = process.env.FMODE_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || '';
  49. if (key && !key.includes('***') && key.length > 10) return key;
  50. try {
  51. const p = join(homedir(), '.fmode', 'config.json');
  52. if (existsSync(p)) {
  53. const c = JSON.parse(readFileSync(p, 'utf-8'));
  54. for (const k of ['api_key', 'FMODE_API_KEY', 'fmodeApiToken']) if (c[k]) return c[k];
  55. }
  56. } catch {}
  57. try {
  58. const p = join(homedir(), '.fmode', 'config.yaml');
  59. if (existsSync(p)) {
  60. for (const l of readFileSync(p, 'utf-8').split('\n')) {
  61. for (const kw of ['api_key', 'FMODE_API_KEY', 'fmodeApiToken']) {
  62. if (l.includes(kw)) {
  63. const v = l.split(':').slice(1).join(':').trim().replace(/["']/g, '');
  64. if (v && v !== '${FMODE_API_KEY}') return v;
  65. }
  66. }
  67. }
  68. }
  69. } catch {}
  70. for (const p of ['/opt/data/.env', '.env']) {
  71. try {
  72. if (existsSync(p)) {
  73. for (const l of readFileSync(p, 'utf-8').split('\n')) {
  74. if (l.includes('FMODE_API_KEY')) {
  75. const v = l.split('=').slice(1).join('=').trim().replace(/["']/g, '');
  76. if (v && !v.includes('***') && v.length > 10) return v;
  77. }
  78. }
  79. }
  80. } catch {}
  81. }
  82. throw new Error('No API Key found. Set FMODE_API_KEY env or ~/.fmode/config.json');
  83. }
  84. // ===== API调用(纯fetch,零依赖) =====
  85. export async function callImageAPI(prompt, size) {
  86. const key = getApiKey();
  87. const resp = await fetch('https://api.fmode.cn/v1/images/generations', {
  88. method: 'POST',
  89. headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
  90. body: JSON.stringify({ model: 'gpt-image-2.5-sunburst', prompt, n: 1, size }),
  91. signal: AbortSignal.timeout(300000),
  92. });
  93. if (!resp.ok) throw new Error(`API ${resp.status}: ${(await resp.text().catch(()=>'')).slice(0,200)}`);
  94. const data = await resp.json();
  95. if (data.data?.[0]?.b64_json) return Buffer.from(data.data[0].b64_json, 'base64');
  96. if (data.data?.[0]?.url) {
  97. const r = await fetch(data.data[0].url, { signal: AbortSignal.timeout(60000) });
  98. if (!r.ok) throw new Error(`Download failed: ${r.status}`);
  99. return Buffer.from(await r.arrayBuffer());
  100. }
  101. throw new Error('Response has no image data');
  102. }
  103. // ===== 统一生成 =====
  104. export async function gen(mode, prompt, name = 'output') {
  105. const cfg = MODES[mode];
  106. if (!cfg) throw new Error(`Unknown mode: ${mode}`);
  107. const fullPrompt = `${cfg.prefix}${prompt} ${cfg.suffix}`;
  108. const outdir = process.env.SKILL_IMAGE_OUTPUT || '.';
  109. mkdirSync(outdir, { recursive: true });
  110. const outPath = join(outdir, `${name}.png`);
  111. const imgData = await callImageAPI(fullPrompt, cfg.size);
  112. writeFileSync(outPath, imgData);
  113. console.log(` OK ${cfg.desc} | ${Math.round(imgData.length/1024)}KB | ${cfg.size} | ${outPath}`);
  114. return outPath;
  115. }
  116. export const genArch = (p, n) => gen('--arch', p, n);
  117. export const genApp = (p, n) => gen('--app', p, n);
  118. export const genExplode = (p, n) => gen('--explode', p, n);
  119. export const genProduct = (p, n) => gen('--product', p, n);
  120. export const genScene = (p, n) => gen('--scene', p, n);
  121. export const genDetail = (p, n) => gen('--detail', p, n);
  122. export const genSlide = (p, n) => gen('--slide', p, n);
  123. export async function batch(tasks) {
  124. let total = 0;
  125. for (const [mode, name, prompt] of tasks) {
  126. process.stdout.write(`${name}: `);
  127. try { await gen(mode, prompt, name); total += MODES[mode].cost; }
  128. catch (e) { console.error(` FAIL: ${e.message}`); }
  129. }
  130. console.log(`\nTotal cost: ¥${total.toFixed(1)}`);
  131. }
  132. async function main() {
  133. const args = process.argv.slice(2);
  134. if (!args.length || args[0] === '--help' || args[0] === '-h') {
  135. console.log(`
  136. fmode-image v2.0 — AI架构图和场景插图生成器
  137. 用法:
  138. npx fmode-image --arch "prompt" [name] 架构图 ¥0.5
  139. npx fmode-image --app "prompt" [name] 应用界面 ¥0.5
  140. npx fmode-image --scene "prompt" [name] 场景插图 ¥0.3
  141. npx fmode-image --slide "prompt" [name] 整页PPT ¥0.5
  142. npx fmode-image --explode "prompt" [name] 爆炸图 ¥0.5
  143. npx fmode-image --detail "prompt" [name] 细节图 ¥0.3
  144. `);
  145. process.exit(0);
  146. }
  147. const mode = args[0];
  148. if (!MODES[mode]) { console.error(`Unknown mode: ${mode}`); process.exit(1); }
  149. const prompt = args.slice(1, -1).join(' ') || args[1] || '';
  150. const name = args.length > 2 ? args[args.length - 1] : 'output';
  151. if (!prompt) { console.error('Error: prompt required'); process.exit(1); }
  152. try { await gen(mode, prompt, name); }
  153. catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
  154. }
  155. if (process.argv[1] === fileURLToPath(import.meta.url)) main();