Claude Opus 5.5 متاح الآن على SeedRouter

طلبات GPT Image 2 على دفعات دون فقدان المهام

أرسل مجموعة مطالبات GPT Image 2، واحفظ معرّفات المهام، واستأنف الاستطلاع بعد الانقطاع، ونزّل الصور من دون إعادة الإرسال.

اقرأ بصيغة Markdown

التوليد على دفعات باستخدام GPT Image 2 في SeedRouter يعني إرسال طلبات صور عادية وتتبّع كل معرّف مهمة يُعاد إليك. ويستخدم هذا الدليل سكربتًا صغيرًا يعمل لدى العميل، لا واجهة دفعات منفصلة ولا باقة بخصم على الكمية. كل مطالبة مهمة قائمة بذاتها؛ والسكربت يحفظ التقدّم حتى تتمكّن من مراجعة تلك المهام مجددًا بعد أي انقطاع.

هناك عملان منفصلان: توليد الصورة وتنزيلها. وإخفاق التنزيل لا يستدعي توليدًا جديدًا. وانتهاء مهلة الاستطلاع لا يعني أن مهمة الصورة أخفقت. والحفاظ على هذه الفوارق داخل السكربت يوفّر الجهد ويمنع الالتباس.

ماذا تحتاج قبل البدء؟

ستحتاج Node.js بالإصدار 22 أو أحدث، ومتغيّر بيئة SEEDROUTER_API_KEY على الخادم، ورصيدًا كافيًا في الحساب لمجموعتك التجريبية. ابدأ بمطالبتين اثنتين. وراجع الأسعار الحالية قبل زيادة عدد الصور.

احفظ الشيفرة التالية باسم images.mjs في مجلد عمل خاص. ينشئ السكربت هناك ملف image-tasks.json وينزّل ملفات PNG إلى images/. واحتفظ بملف الحالة: فهو ما يربط تسمياتك المحلية بالمهام المقبولة. ولا تضع أسرارًا ولا مطالبات تتضمّن بيانات عملاء حسّاسة في مستودع علني.

تطلب الأمثلة صورة واحدة لكل مطالبة. استخدم المعامل n حين تريد عدة مخرجات للمطالبة نفسها؛ ولا تخلط بين ذلك وبين إرسال مطالبات مختلفة. ويوثّق SeedRouter المدى المقبول وبقية القيود في مرجع النموذج.

أرسل مرة واحدة ثم احفظ معرّفات المهام

يكتب السكربت مدخلة بحالة unconfirmed قبل إرسال الطلب. فإذا توقّفت العملية قبل حفظ الاستجابة، لن يعيد التشغيل التالي إرسال ذلك العنصر بصمت. وافحص المهمة في حسابك قبل أن تقرّر ما تفعله بمدخلة غير مؤكّدة.

import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';

const mode = process.argv[2];
if (!['submit', 'poll'].includes(mode)) {
  throw new Error('Use: node images.mjs submit | poll');
}
const key = process.env.SEEDROUTER_API_KEY;
if (!key) throw new Error('Set SEEDROUTER_API_KEY on your server.');
const base = 'https://api.seedrouter.ai/v1';
const headers = { Authorization: `Bearer ${key}` };
const file = 'image-tasks.json';
let state;
try {
  state = JSON.parse(await readFile(file, 'utf8'));
} catch (error) {
  if (error.code !== 'ENOENT') throw error;
  state = {};
}
const save = async () => {
  await writeFile(`${file}.tmp`, JSON.stringify(state, null, 2), { mode: 0o600 });
  await rename(`${file}.tmp`, file);
};
const pause = () => new Promise((resolve) => setTimeout(resolve, 3000));
const jobs = [
  ['blue-mug', 'A cobalt-blue ceramic mug on a pale gray tabletop.'],
  ['green-bowl', 'A green ceramic bowl on a pale gray tabletop.'],
];

if (mode === 'submit') {
  for (const [name, prompt] of jobs) {
    if (state[name]) continue;
    state[name] = { status: 'unconfirmed' };
    await save();
    try {
      const response = await fetch(`${base}/images/generations`, {
        method: 'POST',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          model: 'gpt-image-2', prompt,
          size: '1024x1024', quality: 'low', output_format: 'png', n: 1,
        }),
        signal: AbortSignal.timeout(60000),
      });
      const task = await response.json();
      if (typeof task.id === 'string' && task.id) {
        state[name] = { id: task.id, status: 'processing' };
        await save();
      }
      if (!response.ok || !state[name].id) {
        throw new Error('Submission needs review.');
      }
    } catch {
      console.error(`${name}: stopped; inspect the saved state before continuing.`);
      process.exitCode = 1;
      break;
    }
  }
} else {
  await mkdir('images', { recursive: true });
  const deadline = Date.now() + 600000;
  do {
    for (const [name, entry] of Object.entries(state)) {
      if (!entry.id || entry.status === 'failed' || entry.downloaded) continue;
      try {
        const response = await fetch(`${base}/tasks/${encodeURIComponent(entry.id)}`, {
          headers, signal: AbortSignal.timeout(30000),
        });
        if (!response.ok) throw new Error('Task check failed.');
        const task = await response.json();
        if (!['processing', 'completed', 'failed'].includes(task.status)) {
          throw new Error('Unexpected task status.');
        }
        entry.status = task.status;
        await save();
        if (task.status !== 'completed') continue;
        const images = task.output?.data;
        if (!Array.isArray(images) || !images.length) {
          throw new Error('Completed task has no image URLs.');
        }
        for (const [index, image] of images.entries()) {
          const url = new URL(image.url);
          if (url.protocol !== 'https:') throw new Error('Expected an HTTPS image URL.');
          // The download is a separate request: never attach the API key.
          const result = await fetch(url, { signal: AbortSignal.timeout(60000) });
          if (!result.ok || !result.headers.get('content-type')?.startsWith('image/png')) {
            throw new Error('PNG download failed.');
          }
          await writeFile(`images/${name}-${index}.png`, Buffer.from(await result.arrayBuffer()));
        }
        entry.downloaded = true;
        await save();
      } catch {
        console.error(`${name}: check or download incomplete; task ID retained.`);
      }
    }
    if (!Object.values(state).some((entry) => entry.id && entry.status !== 'failed' && !entry.downloaded)) break;
    await pause();
  } while (Date.now() < deadline);
}
console.log(JSON.stringify(state, null, 2));

شغّل node images.mjs submit ثم node images.mjs poll. وشغّل نسخة واحدة فقط من السكربت في كل مرة، ولا تغيّر تسميات المدخلات في ملف الحالة. فهذا مثال محلي صغير، لا نظام تخزين متعدّد العمليات. وفي خدمة ذات عدة نسخ من التطبيق، استخدم تخزينًا دائمًا بملكية حصرية لكل عملية إرسال بدل مشاركة ملف JSON هذا.

استأنف الاستطلاع من دون بدء توليد جديد

أعد تشغيل node images.mjs poll بعد انتهاء المهلة المحلية أو انقطاع الشبكة. فهو يقرأ معرّفات المهام المحفوظة ويستطلعها بطلبات GET. ولا يرسل طلب توليد آخر. أما المهمة المكتملة التي لم يكتمل تنزيلها فتُفحص مجددًا وتُنزَّل ملفات PNG الخاصة بها بالأسماء المحدّدة نفسها.

مهلة العشر دقائق تخصّ هذا السكربت التوضيحي وحده. فهي ليست زمن إنجاز موعودًا ولا إلغاءً من جهة الخادم. وقد ينتهي طلب شبكة واحد قيد التنفيذ بعد انقضاء مهلة الحلقة. ويحدّد مرجع دورة حياة المهام الحالات الفعلية: processing وcompleted وfailed.

تحتاج المدخلة بحالة unconfirmed مراجعة يدوية لأن السكربت لا يملك معرّف مهمة محفوظًا. فلا تحذف تلك المدخلة وتعيد الإرسال لمجرد أن ترى إن كانت ستنجح في المرة الثانية. راجع أولًا سجل المهام في حسابك. فإذا استعدت المعرّف، فضعه في المدخلة المحفوظة واستخدم الاستطلاع؛ وإلا فاحسم أمر الإرسال غير المؤكّد قبل أن تقرّر التوليد من جديد.

ما مقدار التوازي المناسب؟

ابدأ بالإرسال المتتابع ومجموعة تجريبية صغيرة، كما يفعل هذا المثال. فكل مهمة مقبولة تستمرّ بينما يرسل السكربت التالية، لكن الطلبات لا تنطلق دفعة واحدة بلا حدّ. ولا يوحي هذا الكود بأي رقم توازٍ آمن يصلح للجميع.

في تطبيق أكبر، اجعل الحدّ الأقصى للمهام غير المكتملة إعدادًا صريحًا. وتوقّف عن قبول عمل جديد عند بلوغ ذلك الحدّ، وواصل استطلاع المهام المقبولة أصلًا. وتعامل مع حدود الحساب وفق إرشادات الأخطاء الموثّقة، لا بتكرار طلبات POST على عمياء.

احتفظ بسجل منفصل للنتائج التي تحتاج مراجعة إبداعية. فصورة مكتملة لا تعجبك ليست مهمة فاشلة في الواجهة. إنها قرار تحريري جديد، وإن أرسلت طلبًا آخر فهي أيضًا عملية توليد جديدة تُحتسب في الميزانية.

افحص المجموعة قبل اعتبارها منجزة

قارن تسمياتك الأصلية بملف الحالة وبالملفات المنزَّلة. فلكل تسمية ينبغي تفسير: مُنزَّلة، أو ما زالت قيد المعالجة، أو أخفقت، أو تنتظر مراجعة الإرسال. وعدّ الملفات وحده قد يخفي مهمة لم تكتمل أو تنزيلًا توقّف في منتصفه.

وافتح الصور نفسها كذلك. فالسكربت يتحقّق من حالة HTTP ونوع محتوى PNG، لكنه لا يستطيع الحكم إن كان الكوب باللون المطلوب أو إن كان الملصق مقروءًا. وقيّم المتطلّب البصري بمعزل عن نجاح النقل.

إذا غيّرت output_format، فحدّث نوع المحتوى المتوقّع وامتداد الملف معًا. فإعادة تسمية ملف JPEG منزَّل إلى .png لا تحوّله. وقد ثبّت المثال صيغة PNG عمدًا حتى لا تحجب مشكلة الصيغ الإضافية موضوع استعادة المهام.

الأسئلة الشائعة

هل هذه واجهة الدفعات من OpenAI؟

لا. فالسكربت يرسل مهام صور اعتيادية في SeedRouter. وهو لا يستخدم نقطة نهاية لواجهة دفعات ولا يعني وجود تسعير خاص بالدفعات.

هل يمكنني استئناف التنزيلات وحدها؟

نعم. شغّل أمر الاستطلاع مع ملف الحالة المحفوظ. فهو يجلب مخرجات المهام المكتملة من جديد وينزّل النتائج الناقصة من دون إنشاء مهمة صورة أخرى.

هل توقف مهمة فاشلة بقية المهام؟

تسجّل حلقة الاستطلاع تلك المهمة على أنها فاشلة وتواصل فحص البقية. وراجع العنصر الفاشل على حدة. والمهمة التي تخفق لا تُحتسب؛ كما أن انتهاء المهلة لديك وحده لا يثبت الإخفاق.

احتفظ بسجل المهام مع الصور

يعتمد التوليد الموثوق على دفعات في GPT Image 2 على الحفاظ على الصلة بين كل مطالبة ومعرّف مهمتها. فاحفظ ذلك السجل، واستأنف المهام القائمة، وأعد محاولة التنزيل بمعزل عن التوليد. ومتى تصرّفت مجموعة صغيرة كما هو متوقّع، فزد حجم العمل ضمن ميزانية تستطيع متابعتها بالاستعانة بـدليل الأسعار.

أدلة ذات صلة