# משימות והרצות

> תשאול, רשימה, דירוג, ביטול ומחיקה של הרצות Nodaro מ-TypeScript, בדיקת המצב של הרצות תהליכים וקבלת קריאות Webhook חתומות כשמשימה מסתיימת.

Source: https://nodaro.ai/he/docs/developers/sdk/jobs-and-executions

**משימה** היא יצירה אחת ב-Nodaro, כגון תמונה אחת, רינדור אחד של סרטון או קריינות אחת, ו**רשומת הרצה** מייצגת הרצה אחת של תהליך שלם. המשאב **`client.jobs`** קורא משימות, מחזיר רשימות שלהן, מבטל אותן ומוחק אותן, והמשאב **`client.executions`** קורא הרצות של תהליכים, מחזיר רשימות שלהן ומבטל אותן. המשאב **`client.webhooks`** רושם נקודות קצה ש-Nodaro קורא להן כשמשימה מסתיימת. המשאב **`client.videoPro`** עוצר או ממשיך הרצה ארוכה של רכיב יצירת וידאו Pro. המתודות קוראות לנקודות הקצה שבדפי ה-API [משימות](https://nodaro.ai/docs/developers/api/jobs) ו[הרצות](https://nodaro.ai/docs/developers/api/executions).

## מתודות
| מתודה | מה היא עושה |
| --- | --- |
| [`executions.get(id)`](#executionsgetid) | קריאה של הרצת תהליך, עם המצב של כל רכיב |
| [`executions.listForWorkflow(workflowId, params?)`](#executionslistforworkflowworkflowid-params) | רשימת ההרצות של תהליך |
| [`executions.cancel(id, params?)`](#executionscancelid-params) | עצירה של הרצת תהליך |
| [`jobs.get(id)`](#jobsgetid) | קריאה של משימה, עם הקלט והפלט שלה |
| [`jobs.list(params?)`](#jobslistparams) | רשימת המשימות שלכם, מהחדש לישן |
| [`jobs.rate(id, rating)`](#jobsrateid-rating) | דירוג של אחת מהמשימות שלכם כ-`good` או כ-`bad`, או מחיקת הדירוג |
| [`jobs.fromUpload(params)`](#jobsfromuploadparams) | רישום של תמונה שנערכה בדפדפן כמשימה |
| [`jobs.getStatus(id)`](#jobsgetstatusid) | קריאת הסטטוס של משימה, לתשאול |
| [`jobs.cancel(id)`](#jobscancelid) | עצירה של משימה והחזר של הקרדיטים ששוריינו לה |
| [`jobs.delete(id, options?)` ו-`jobs.restore(id)`](#jobsdeleteid-options-and-jobsrestoreid) | העברה של משימה לפח, שחזור שלה או מחיקה לצמיתות |
| [`webhooks.create(input)`](#clientwebhooks) ועוד | רישום, שינוי, בדיקה וביקורת של נקודות הקצה ש-Nodaro קורא להן כשמשימה מסתיימת |
| [`verifyWebhookSignature(options)`](#verifywebhooksignatureoptions) | בדיקת החתימה של קריאה שנקודת הקצה שלכם קיבלה |
| [`videoPro.stop(jobId)`](#videoprostopjobid) | עצירת הרצה של רכיב יצירת וידאו Pro ושמירת מה שכבר הסתיים |
| [`videoPro.continueRun(jobId, opts?)`](#videoprocontinuerunjobid-opts) | הרצת המשך להרצה של רכיב יצירת וידאו Pro, כמשימה חדשה |

## סטטוסים
משימה עוברת בין הסטטוסים האלה:

| סטטוס משימה | משמעות |
| --- | --- |
| `pending`, `queued` | ממתינה ל-worker. |
| `processing` | רצה. השדה `progress` עולה מ-0 עד 100 כשהמודל מדווח על התקדמות. |
| `pending_review` | מדיניות תוכן של הפריסה הזו מעכבת אותה עד שאדם יבדוק אותה. זה לא סטטוס סופי. |
| `completed` | הסתיימה. התוצאה נמצאת ב-`output_data`. |
| `failed` | נכשלה. השדה `error_message` מסביר למה, והשדה `error_hint` עשוי לסווג את הכשל. |
| `cancelled` | בוטלה. |

לרשומת הרצה יש סטטוסים משלה: `pending`, `running`, `completed`, `failed`, `cancelled`, `stopping`, `timed_out` ו-`discarded`. כל רכיב בתוכה נמצא באחד המצבים `pending`, `running`, `completed`, `failed` או `skipped`.

## client.executions
רשומת הרצה מייצגת הרצה אחת של תהליך. היא מקבצת משימה אחת לכל רכיב AI, ואת המצב של כל רכיב.

### executions.get(id)‎
מחזירה הרצת תהליך, כולל מפה של המצב של כל רכיב. כשהמזהה שייך למשימה של רכיב יחיד ולא להרצת תהליך, השרת עונה באותו מבנה, בשביל הרכיב האחד הזה.

```ts
get(id: string): Promise<{ data: WorkflowExecution }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה רשומת ההרצה ש-client.workflows.run()‎ החזירה." },
}}
/>

```ts
const { data } = await client.executions.get(executionId)
console.log(data.status, `${data.completedNodes}/${data.totalNodes}`, data.totalCreditsUsed)
```

ב-`WorkflowExecution` נמצאים השדות `id`, `kind`, `workflowId`, `status`, `outcome`, `triggerType` (`manual`, `webhook`, `schedule`, `app_run`, `single-node`, `telegram`, `telegram_account`, `api` או `mcp`), `nodeStates`, `totalNodes`, `completedNodes`, `failedNodes`, `totalCreditsUsed`, `errorMessage`, `startedAt`, `completedAt`, `createdAt` ו-`updatedAt`. הערך של `kind` הוא `execution` בהרצת תהליך, ו-`job` במשימה של רכיב יחיד. במשימה של רכיב יחיד שלקוח MCP התחיל, השדה `triggerType` הוא `mcp`.

**הרצות שלא מוצאות שום דבר חדש.** הרצה שלא מצאה שום דבר חדש מסתיימת בסטטוס `completed`, ולא `failed`. למשל, פיד לא מצא פוסטים חדשים, ולכן לרכיב שכותב אחריו לא היה על מה לכתוב. כשרכיב צריך טקסט, והטקסט שמחובר אליו מגיע מרכיב שלא הפיק כלום בהרצה הזו, מדלגים על הרכיב, והערך של `nodeStates[id].skipReason` הוא `"empty_input"`. אז ההרצה שהסתיימה מקבלת `outcome: "nothing_new"`. בכל הרצה אחרת שהסתיימה, השדה `outcome` הוא `"succeeded"`. עד שההרצה מסתיימת, השדה `outcome` לא מופיע.

```ts

const { data } = await client.executions.get(executionId)
if (data.outcome === "nothing_new") {
console.log(`Nothing new: ${countEmptyInputSkips(data.nodeStates)} node(s) had nothing to work on`)
}
executionOutcome(data.status, data.nodeStates) // the same answer, worked out in your code
```

**קריאת הפלט של רכיב.** השדה `nodeStates[nodeId].output` מופיע כשהרכיב בסטטוס `completed`. הוא יכול להופיע גם כשהרכיב בסטטוס `failed`, אבל ההרצה שמרה תוצאה שאפשר להשתמש בה, כמו שעושים הרכיבים שבונים סצנות תלת־ממד. בדקו אם השדה קיים, ולא את הסטטוס, ואף פעם אל תתייחסו ל-`output` קיים כאל הצלחה:

```ts

const node = data.nodeStates["scene-1"]
if (node.status === "failed") {
console.error(node.error) // the failure stands
if (node.output?.plan) {
// the draft the run kept is still here, and it was billed
}
}
nodeStateMayCarryOutput(node.status) // true for "completed" and "failed"
```

גם הקבוע `OUTPUT_BEARING_NODE_STATUSES`, הקבוצה של שני הסטטוסים האלה, מיוצא.

### executions.listForWorkflow(workflowId, params?)‎
מחזירה את ההרצות של תהליך אחד, מהחדש לישן, עמוד אחד בכל פעם. ברשימה מופיעות גם הרצות של רכיב יחיד שהתחילו בתהליך הזה.

```ts
listForWorkflow(workflowId: string, params?: ListExecutionsForWorkflowParams): Promise<{
data: WorkflowExecutionSummary[]
nextCursor?: string
}>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "מזהה התהליך." },
limit: { type: 'number', description: "גודל העמוד." },
cursor: { type: 'string', description: "הערך nextCursor מהעמוד הקודם." },
status: { type: 'string', description: "רשימה של סטטוסים מופרדים בפסיקים, כגון pending,running." },
source: { type: '"editor" | "all"', description: "הערך editor לא כולל הרצות שאפליקציות, Webhooks ותזמונים התחילו." },
}}
/>

```ts
const { data: runs, nextCursor } = await client.executions.listForWorkflow(workflowId, {
limit: 20,
status: "completed",
})
```

כל `WorkflowExecutionSummary` כולל את אותם `kind` ו-`outcome` כמו `get()`.

### executions.cancel(id, params?)‎
עוצרת הרצת תהליך. יש שלוש דרכים לעצור אותה:

- **מיד** (ברירת המחדל): המשימות שרצות מבוטלות, הקרדיטים ששוריינו להן מוחזרים, והסטטוס הופך ל-`cancelled`. גם משימת LLM שמחויבת לפי השימוש ורצה באותו רגע מבוטלת, אבל נגבים עליה הקרדיטים שהספק כבר חייב עליהם, ושאר הקרדיטים ששוריינו לה מוחזרים. על משימה כזו שנעצרה לפני שהקריאה שלה התחילה מקבלים החזר, חוץ ממה שניסיון קודם של אותה משימה כבר השתמש בו.
- **`mode: "after_current"`**: הסטטוס הופך ל-`stopping`. הרכיבים שרצים מסתיימים, והתוצאות שלהם מופיעות על הקנבס ובספרייה שלכם, ואז ההרצה נעצרת.
- **`mode: "discard"`**: רכיבים חדשים לא מתחילים, אבל המשימות שרצות לא מבוטלות, כי אי אפשר לעצור באמצע קריאה למודל חיצוני. המשימות מסתיימות ונשמרות בספרייה שלכם, אבל התוצאות שלהן לא חוזרות לקנבס. הסטטוס הופך ל-`discarded`, והקרדיטים לא מוחזרים, כי המשימות הושלמו.

```ts
cancel(id: string, params?: { mode?: "after_current" | "discard" }): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה רשומת ההרצה." },
mode: { type: '"after_current" | "discard"', description: "איך לעצור. השמיטו אותו כדי לעצור מיד." },
}}
/>

```ts
await client.executions.cancel(executionId, { mode: "after_current" })
```

## client.jobs
משימה היא יצירה אחת: תמונה אחת, רינדור אחד של סרטון, קריינות אחת. הרצת תהליך יוצרת משימה אחת לכל רכיב AI, וכל הרצה של רכיב יחיד יוצרת משימה אחת. השדות של משימה כתובים ב-snake_case, כמו שה-API שולח אותם.

### jobs.get(id)‎
מחזירה משימה, כולל הקלט והפלט שלה.

```ts
get(id: string): Promise<{ data: Job }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה המשימה." },
}}
/>

```ts
const { data: job } = await client.jobs.get(jobId)
if (job.status === "completed") console.log(job.output_data)
```

השדות של `Job`:

| שדה | תיאור |
| --- | --- |
| `id`, `status`, `progress` | מזהה המשימה, הסטטוס שלה וההתקדמות שלה, מ-0 עד 100. |
| `input_data`, `output_data` | הבקשה והתוצאה. ערכים שמיועדים לשרת בלבד מושמטים משניהם. |
| `error_message` | למה המשימה נכשלה, או `null`. |
| `error_hint` | סיבה מובנית לחלק מהכשלים. ראו בהמשך. |
| `credits` | הקרדיטים ששוריינו למשימה, או `null`. |
| `credit_status` | מה מצב הקרדיטים: `reserved`, `committed` או `refunded`. הערך הוא `null` במשימה שאין עליה חיוב. |
| `credits_charged` | כמה המשימה עלתה אחרי החיוב הסופי של השריון שלה, כולל השלבים שהיא הריצה. במשימה שמריצה בשבילכם משימות אחרות, כגון הכנת הייחוס, המסכה והסרטון בהרצה של Nodaro Motion, הקרדיטים על כל שלב נגבים ברגע שהחיוב של השלב נסגר. הערך יכול להיות נמוך מהשריון. כשהקרדיטים ששוריינו להרצה עצמה הוחזרו, הערך הוא `0` ועוד הקרדיטים שנגבו על שלבים שהחיוב שלהם כבר נסגר. הערך הוא `null` כל עוד השריון של המשימה עצמה פתוח, גם כשהחיוב של חלק מהשלבים כבר נסגר, כששום דבר בהרצה לא שוריין או כשלרגע אי אפשר היה לקרוא את השלבים שלה. כדי לחשב כמה הוצאתם, סכמו את השדה הזה. |
| `credits_held` | השריון של ההרצה: הערך של `credits` ועוד השריונות של השלבים. |
| `credits_released` | מה שחזר מ-`credits_held` אחרי החיוב הסופי של כל השריונות בהרצה, או `null` עד אז. |
| `job_type` | סוג המשימה. |
| `created_at`, `started_at`, `completed_at` | חותמות זמן. |
| `user_id` | הבעלים. |
| `source`, `source_detail` | מאיפה המשימה הגיעה: `sdk`, `cli`, `mcp`, `app`, `web`, `api` ועוד, עם פירוט כגון `sdk/2.17.0`. |
| `recovering` | הערך הוא `true` בזמן שהפלטפורמה מתקנת משימה שה-worker שלה נעצר אחרי שהמודל כבר מסר את התוצאה. |
| `favorited` | האם המשימה מסומנת כמועדפת אצל הבעלים שלה. ראו את הסעיף [`client.gallery`](https://nodaro.ai/docs/developers/sdk/media-and-uploads#clientgallery) בדף מדיה והעלאות. |
| `rating` | `"good"`, `"bad"` או `null`. ראו את הסעיף [`jobs.rate()`](#jobsrateid-rating). |
| `session_id` | [סשן היצירה](https://nodaro.ai/docs/developers/sdk/workflows#clientsessions) שבו המשימה נוצרה, או `null`. |

כשלרגע השרת לא מצליח לקרוא את `favorited` ואת `rating`, הוא משמיט אותם. התייחסו לשדה חסר כ״לא ידוע״, ואף פעם לא כ-`false`.

השדה `output_data` של משימת תמונה כולל את גודל התמונה בפיקסלים: השדות `width` ו-`height` של `imageUrl`, ו-`images` (`{ url, width, height }` לכל פריט) לצד `imageUrls`. השדות האלה לא מופיעים במשימות ישנות יותר. הטיפוס של `Job.output_data` הוא `unknown`, ולכן כדי לקרוא את השדות האלה עם טיפוסים, המירו אותו ל-`NodeJobOutput`. ראו את הסעיף [`runAndWait()`](https://nodaro.ai/docs/developers/sdk/nodes#runandwaittype-params-opts) בדף הרצת רכיבים.

לשדה **`error_hint`** יש חמישה סוגים. בדקו קודם את `kind`, ורק אז עברו לשאר השדות:

- **`safety-block`**: מסנן הבטיחות של המודל עצמו דחה את הפלט. השדה `class` הוא `copyright`, `likeness` או `safety`. השדה `retried` מציין אם Nodaro כבר ניסה שוב פעם אחת, והשדה `suggestedProvider`, כשהוא מופיע, מציין מודל שאפשר לנסות עליו שוב את אותה בקשה. הערך `subject: "input_video"`, כשהוא מופיע, פירושו שהספק דחה את סרטון הקלט של המשימה עצמו: שלחו קליפ אחר. הערך `subject: "input_image"` בחסימה מסוג `copyright` פירושו שהספק דחה אחת מתמונות הקלט, כי ייתכן שהיא מוגנת בזכויות יוצרים: שלחו תמונה אחרת. הערך `subject: "prompt"` פירושו שהמילים של הספק עצמו ציינו את הפרומפט: שנו את הפרומפט. הערך `subject: "audio"` פירושו שהספק האשים את האודיו שנוצר, ולא את התמונה. ראו את הסעיף [כשמודל חוסם את הפרומפט](https://nodaro.ai/docs/nodes/image/generate-image#when-a-model-blocks-the-prompt) בדף יצירת תמונה.
- **`policy-block`**: מדיניות תוכן של הפריסה הזו דחתה את הבקשה או את התוצאה. השדה `reason` נכתב בשביל משתמשים, ולכן הציגו אותו כמו שהוא.
- **`credits`**: למשימה נגמרו הקרדיטים באמצע, כי שלב שמשריין קרדיטים משלו נדחה, למשל הסרטון שהרצה של Nodaro Motion או של טרנד מתחילה. השדה `code` הוא הקוד של הדחייה: `insufficient_credits`, או מגבלה של תוכנית או של סביבת עבודה, כגון `free_video_limit` או `budget_exceeded`. התייחסו לקוד לא מוכר כמו ל-`insufficient_credits`. הקרדיטים של המשימה מוחזרים. בקשו מהמשתמש לטעון קרדיטים, או להגדיל את המגבלה, ואז הריצו את הבקשה שוב.
- **`input-refused`**: הספק דחה את סרטון הקלט של המשימה בלי לציין סוג של מדיניות תוכן. הערך של `subject` הוא `"input_video"`. לא מוצע מודל אחר: שלחו קליפ אחר.
- **`request-refused`**: הספק דחה את הבקשה כפי שנשלחה, וציין בה בעיה, כגון הגדרה, הפורמט או הגודל של קובץ, או אורך הפרומפט. אותה בקשה תידחה שוב: שנו את תמונות הקלט, את הסרטון, את הפרומפט או את ההגדרות. עם `subject: "input_video"`, המילים של הספק ציינו את סרטון הקלט כבעיה, למשל את האורך או הגודל שלו, או שלא נמצאה בו דמות: שלחו קליפ אחר. זו לא חסימה של מדיניות תוכן. בהרצה של Nodaro Motion או של טרנד, זו דחייה של שלב אחד.

### jobs.list(params?)‎
מחזירה את המשימות שלכם, מהחדש לישן, עמוד אחד בכל פעם (`GET /v1/jobs`).

```ts
list(params?: ListJobsParams): Promise<{
data: Job[]
next: string | null
}>
```

<TypeTable
type={{
status: { type: 'JobStatus | JobStatus[]', description: "סטטוס אחד או רשימה. משימה מתאימה כשיש לה אחד מהם. עם סטטוס לא מוכר, המתודה זורקת NodaroError עם הקוד validation_error." },
type: { type: 'string | string[]', description: "רק משימות שהנתיב הזה יצר, כגון llm-structured או video-analysis. ערך אחד או רשימה של עד 20." },
origin: { type: 'string', description: "רק משימות שהבקשה שלהן כללה את הערך הזה ב-origin, כלומר השם של האפליקציה ששלחה אותה. התאמה מדויקת." },
source: { type: 'string | string[]', description: "רק משימות שנוצרו מהממשק הזה: mcp, cli, sdk, web, extension, app, api או internal. ערך אחד או רשימה. עם source: \"mcp\" מקבלים את מה שלקוחות ה-MCP שלכם, כגון Claude או ChatGPT, יצרו." },
favorited: { type: 'boolean', description: "עם true נשארות רק המשימות שסימנתם כמועדפות, ועם false רק האחרות." },
rating: { type: '"good" | "bad" | "rated" | "unrated"', description: "הדירוג שלכם. הערך rated הוא דירוג כלשהו, והערך unrated הוא בלי דירוג." },
projectId: { type: 'string', description: "רק המשימות שנמצאות עכשיו בסשן של הפרויקט הזה, כל אחת פעם אחת. הערך \"none\" משאיר את המשימות שנמצאות בסשן שלא שייך לאף פרויקט. אי אפשר לשלב אותו עם inSession: false: השילוב נדחה עם 400." },
inSession: { type: 'boolean', description: "עם false נשארות המשימות שלא נמצאות באף סשן, שהאפליקציה מציגה תחת כללי. עם true נשארות המשימות שנמצאות בסשן אחד לפחות." },
withSessions: { type: 'boolean', description: "עם true, לכל משימה נוסף sessions: [{ id, title, projectId }]‎: הסשנים שהיא נמצאת בהם." },
batchId: { type: 'string', description: "רק המשימות של אצווה אחת של רכיב יצירת תמונה, שנוצרה עם count או עם models." },
limit: { type: 'number', default: '50', description: "גודל העמוד, מ-1 עד 100." },
cursor: { type: 'string', description: "הערך next מהעמוד הקודם." },
}}
/>

```ts
let cursor: string | undefined
do {
const page = await client.jobs.list({ type: "llm-structured", origin: "my-app", cursor })
for (const job of page.data) console.log(job.id, job.status)
cursor = page.next ?? undefined
} while (cursor)
```

כל מסנן משתלב עם האחרים. בעמוד יכולות להיות פחות שורות מ-`limit`, ואפילו אף שורה, ועדיין יהיה בו `next`. עברו לעמוד הבא לפי `next`, ואף פעם לא לפי מספר השורות.

```ts
// Your favorited images and videos that came out well
const { data: keepers } = await client.jobs.list({
type: ["generate-image", "generate-video"],
status: "completed",
favorited: true,
rating: "good",
})
```

### jobs.rate(id, rating)‎
מדרגת אחת מהמשימות שלכם כ-`"good"` או כ-`"bad"`, או מוחקת את הדירוג שלה עם `null` (`PUT /v1/jobs/:id/rating`). למשימה יש דירוג אחד, ורק הבעלים שלה יכול לדרג אותה. על משימה של אדם אחר, המתודה זורקת `NotFoundError`.

```ts
rate(id: string, rating: "good" | "bad" | null): Promise<{ data: { jobId: string; rating: "good" | "bad" | null } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה המשימה." },
rating: { type: '"good" | "bad" | null', required: true, description: "הדירוג, או null כדי למחוק אותו." },
}}
/>

```ts
await client.jobs.rate(jobId, "good")
await client.jobs.rate(jobId, null) // clear it
```

הדירוג מופיע אחר כך ב-`Job.rating`, ואפשר לסנן לפיו ב-`jobs.list()`. אפליקציות OAuth צריכות את ההרשאה `assets:write`. בשרת שאין בו דירוגים, המתודה זורקת `NodaroError` עם הקוד `not_available` והסטטוס 503.

### jobs.fromUpload(params)‎
רושמת תמונה שנערכה בדפדפן כמשימה (`POST /v1/jobs/from-upload`). קודם אחסנו את התמונה הערוכה עם [`uploads.upload`](https://nodaro.ai/docs/developers/sdk/media-and-uploads#uploadsuploadfile). התוצאה היא משימת `image-edit` שהסתיימה, פרטית וחינמית, וה-`output_data.imageUrl` שלה הוא ההעלאה. אחר כך אפשר לדרג אותה, לסמן אותה כמועדפת, לקבל אותה ברשימה עם `list({ type: "image-edit" })` ולשמור אותה בסשן, כמו כל משימת תמונה.

```ts
fromUpload(params: JobFromUploadParams): Promise<{ data: Job }>
```

<TypeTable
type={{
assetId: { type: 'string', required: true, description: "המזהה assetId של התמונה הערוכה, כפי ש-uploads.upload החזירה אותו." },
sourceJobId: { type: 'string', description: "המשימה שהתמונה שלה נערכה." },
sourceImageUrl: { type: 'string', description: "איזו מהתמונות של sourceJobId נערכה, כשהמשימה יצרה כמה תמונות. ברירת המחדל היא ה-imageUrl שלה." },
sourceAssetId: { type: 'string', description: "ההעלאה שנערכה, כשהמקור הוא לא משימה. לא יחד עם sourceJobId." },
editStateUrl: { type: 'string', description: "מצב העיצוב של העורך, שאוחסן עם uploads.uploadJson." },
sessionId: { type: 'string', description: "סשן היצירה שבו לשמור את המשימה." },
}}
/>

```ts
const upload = await client.uploads.upload(editedPng)
const { url: editStateUrl } = await client.uploads.uploadJson(designState)
const { data: job } = await client.jobs.fromUpload({
assetId: upload.assetId!,
sourceJobId,
editStateUrl,
sessionId,
})
```

העלאה אחת היא משימה אחת: שליחה חוזרת של אותו `assetId` מחזירה את אותה משימה, ולכן ניסיון חוזר אף פעם לא יוצר שתיים. עריכה של תמונה של דמות, אובייקט, יצור או מיקום שומרת את הקשר לנכס הזה. אפליקציות OAuth צריכות את ההרשאה `assets:write`.

- השגיאה `NotFoundError`: ההעלאה, המקור או הסשן לא שלכם.
- השגיאה `NodaroError` עם הקוד `validation_error`: ההעלאה היא לא תמונה שאחסנתם, או שלמקור אין תמונה מוגמרת.
- השגיאה `JobBlockedError`: מדיניות משימות של הפריסה דוחה את התמונה.
 ראו בדף ה-API [רישום תמונה ערוכה](https://nodaro.ai/docs/developers/api/jobs#record-an-edited-image).

### jobs.getStatus(id)‎
מחזירה רק את הסטטוס של משימה: `id`, `status`, `progress`, `output_data`, `error_message`, `error_hint`, `credit_status` ו-`completed_at`, מתי המשימה הסתיימה לפי השעון של השרת, או `null` עד שהיא מסתיימת (`GET /v1/jobs/:id/status`). המתודה משמיטה את נתוני הבקשה ואת שדות העלות, ולכן היא קלה בהרבה מ-`get()`. השתמשו בה בלולאות תשאול.

```ts
getStatus(id: string): Promise<{ data: JobStatusResult }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה המשימה." },
}}
/>

```ts
async function waitForJob(jobId: string) {
for (;;) {
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed") return data.output_data
if (data.status === "failed" || data.status === "cancelled") {
throw new Error(data.error_message ?? data.status)
}
await new Promise((resolve) => setTimeout(resolve, 2_000))
}
}
```

המתודה [`client.nodes.runAndWait()`](https://nodaro.ai/docs/developers/sdk/nodes#runandwaittype-params-opts) מריצה את הלולאה הזו בשבילכם, עם דיווח על ההתקדמות, ביטול ושגיאות עם טיפוסים.

### jobs.cancel(id)‎
מבטלת משימה, והקרדיטים ששוריינו לה מוחזרים. אפשר לבטל גם משימה שממתינה לבדיקה.

```ts
cancel(id: string): Promise<{ success: true; cancelled: number; inFlight?: boolean }>
```

משימת LLM שמחויבת לפי השימוש ורצה באותו רגע היא החריג. אם הקריאה שלה כבר התחילה, הביטול מחזיר `{ success: true, cancelled: 0, inFlight: true }`: המשימה מסתיימת, ונגבים עליה קרדיטים רק לפי השימוש בפועל. על משימה שנעצרה לפני שהקריאה שלה התחילה מקבלים החזר, חוץ ממה שניסיון קודם של אותה משימה כבר השתמש בו, למשל ניסיון חוזר אחרי ניסיון שחויב.

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה המשימה." },
}}
/>

```ts
const { cancelled } = await client.jobs.cancel(jobId)
```

### jobs.delete(id, options?)‎ ו-jobs.restore(id)‎
המתודה `delete` מעבירה משימה ל[פח](https://nodaro.ai/docs/developers/api/jobs#the-trash) (`DELETE /v1/jobs/:id`). מאותו רגע, המשימה לא מופיעה באף קריאה. אפשר לשחזר אותה במשך 30 ימים, ואחר כך היא נמחקת לצמיתות, יחד עם המדיה הפרטית שלה. עותק שנשמר בספרייה נשאר. המתודה `restore` (`POST /v1/jobs/:id/restore`) מחזירה את המשימה לכל סשן שהיא הייתה בו. האפשרות `{ forever: true }` מוחקת לצמיתות, כבר עכשיו, משימה שנמצאת בפח. על משימה פעילה, המתודה זורקת `NodaroError` עם הקוד `not_in_trash` (סטטוס 409).

רק הבעלים של המשימה יכול למחוק אותה או לשחזר אותה. משימה שעדיין רצה ממשיכה לרוץ, ולכן אם ה-worker שלה צריך להפסיק, בטלו אותה קודם. הקרדיטים לא מוחזרים. אפליקציות OAuth צריכות את ההרשאה `assets:write`. שתי המתודות מחזירות `{ success: true }`.

```ts
delete(id: string, options?: { forever?: boolean }): Promise<{ success: true }>
restore(id: string): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה המשימה." },
forever: { type: 'boolean', description: "רק ב-delete. עם true, משימה שכבר נמצאת בפח נמחקת לצמיתות." },
}}
/>

```ts
await client.jobs.delete(jobId)                   // to the Trash
await client.jobs.restore(jobId)                  // undo
await client.jobs.delete(jobId, { forever: true }) // after a delete: gone for good
```

המתודה `jobs.list()` אף פעם לא מחזירה משימה שנמחקה: היא נמצאת ב-[`client.trash`](https://nodaro.ai/docs/developers/sdk/workflows#clienttrash).

## client.webhooks
**Webhooks יוצאים** מודיעים לשרת שלכם כשמשימה מסתיימת, כך שהאפליקציה שלכם לא צריכה לבדוק את הסטטוס. רשמו נקודת קצה ב-HTTPS, ו-Nodaro ישלח אליה בקשת POST על כל אירוע `job.completed` ו-`job.failed` בחשבון שלכם. כל קריאה נחתמת עם הסוד של נקודת הקצה עצמה. קריאות שנכשלו נשלחות שוב במרווחים הולכים וגדלים, במשך כ-43 שעות, וכל ניסיון נשמר ביומן המסירות של נקודת הקצה. האירועים, הגוף, הכותרות ולוח הזמנים של הניסיונות החוזרים מפורטים בדף [ה-API של Webhooks](https://nodaro.ai/docs/developers/api/webhooks).

אסימונים של אפליקציות OAuth צריכים את ההרשאה `jobs:read`. נקודת קצה שאפליקציה מחוברת רושמת מקבלת רק את המשימות שהאפליקציה הזו שלחה בעצמה.

```ts
create(input: CreateWebhookEndpointInput): Promise<CreatedWebhookEndpoint>      // { data, secret }
list(): Promise<{ data: WebhookEndpoint[] }>
get(id: string): Promise<{ data: WebhookEndpoint }>
update(id: string, input: UpdateWebhookEndpointInput): Promise<{ data: WebhookEndpoint }>
delete(id: string): Promise<{ success: true }>
rotateSecret(id: string, input?: RotateWebhookSecretInput): Promise<RotatedWebhookSecret>   // { secret, previousSecretExpiresAt }
deliveries(id: string, params?: ListWebhookDeliveriesParams): Promise<WebhookDeliveryPage>
getDelivery(id: string, deliveryId: string): Promise<{ data: WebhookDelivery }>
test(id: string): Promise<{ data: WebhookDelivery }>
```

### webhooks.create(input)‎
רושמת נקודת קצה ומחזירה אותה עם ה-`secret` שלה (`whsec_…`). **הסוד מוצג רק פעם אחת**, ולכן שמרו אותו עם התצורה של השרת שלכם.

<TypeTable
type={{
url: { type: 'string', required: true, description: "כתובת HTTPS במארח ציבורי. כתובות localhost, כתובות פרטיות ושמות ברשת פרטית נדחים. כדי לבדוק מקומית, השתמשו בטאנל." },
events: { type: '("job.completed" | "job.failed")[]', default: 'שניהם', description: "האירועים לשליחה." },
description: { type: 'string', description: "הערה שלכם, שורה אחת של עד 500 תווים." },
}}
/>

```ts
const { data: endpoint, secret } = await client.webhooks.create({
url: "https://example.com/nodaro-webhook",
events: ["job.completed", "job.failed"],
})
await client.webhooks.test(endpoint.id) // sends a ping
```

לכל כתובת יכולה להיות נקודת קצה אחת (`409 url_taken`). לחשבון יכולות להיות עד 10 נקודות קצה (`400 limit_reached`).

### webhooks.update(id, input)‎
משנה כל אחד מהשדות `url`, `events`, `description` (הערך `null` מוחק אותו) ו-`isActive`. אחרי 50 מסירות ברצף שנכשלו סופית, Nodaro מכבה את נקודת הקצה ומגדיר את `disabledReason` לערך `"consecutive_failures"`. הערך `isActive: true` מפעיל אותה מחדש ומאפס את `consecutiveFailures`.

### webhooks.rotateSecret(id, input?)‎
יוצרת סוד חדש ומחזירה אותו פעם אחת. מעכשיו הסוד החדש חותם על כל מסירה, כולל ניסיונות חוזרים. הסוד הישן ממשיך לחתום לצידו במשך `gracePeriodSeconds`, מ-0 עד 604800 (ברירת המחדל: יום אחד), כדי שתוכלו לעדכן את השרת המקבל בלי הפסקה. במהלך הזמן הזה, הכותרת `Nodaro-Signature` כוללת `v1=` שני, והפונקציה `verifyWebhookSignature` מקבלת כל אחד מהם. כדי להפסיק מיד סוד שדלף, העבירו `{ gracePeriodSeconds: 0 }`.

```ts
const { secret, previousSecretExpiresAt } = await client.webhooks.rotateSecret(endpoint.id)
```

כשהסוד הישן הפסיק לפעול מיד, השדה `previousSecretExpiresAt` הוא `null`.

### webhooks.deliveries(id, params?)‎ ו-getDelivery(id, deliveryId)‎
המתודה `deliveries()` מחזירה את יומן המסירות, מהחדש לישן. היומן שומר 30 ימים. סננו עם `status` (`"pending"`, `"delivered"` או `"failed"`), ועברו בין העמודים עם `limit` (מ-1 עד 100, ברירת המחדל 20) ועם `cursor`. המתודה `getDelivery()` מחזירה מסירה אחת לפי המזהה שלה.

כל `WebhookDelivery` כולל `event`, `jobId`, `status`, `attempts`, `responseStatus`, `responseBody` (ה-2 KB הראשונים), `durationMs`, `error`, `requestBody`, `nextAttemptAt` ו-`deliveredAt`. השדה `requestBody` הוא הגוף המדויק שנשלח, והוא הופך ל-`null` ברגע שהמסירה מצליחה.

### webhooks.test(id)‎
מכניסה `ping` לתור ומחזירה את המסירה שלו. ה-ping נשלח בתוך שניות, בניסיון אחד, גם לנקודת קצה כבויה. בדקו את המצב שלו עם `getDelivery(id, delivery.id)`.

### verifyWebhookSignature(options)‎
בודקת קריאה שנקודת הקצה שלכם קיבלה, ומחזירה את האירוע אחרי פענוח. זו פונקציה שמיוצאת ברמה העליונה של `@nodaro/sdk`, ולא מתודה של `client`. היא משתמשת רק ב-Web Crypto, ולכן היא רצה ב-Node 20 ומעלה, ב-Deno, ב-Bun, ב-Cloudflare Workers, ב-Vercel Edge ובדפדפנים.

```ts
verifyWebhookSignature(options: {
payload: string | Uint8Array | ArrayBuffer   // the raw body, as received
header: string | readonly string[] | null | undefined   // Nodaro-Signature
secret: string                               // whsec_…
toleranceSeconds?: number                    // default 300; 0 turns the check off
}): Promise<WebhookEvent>
```

העבירו את הגוף הגולמי בדיוק כפי שהתקבל, ואף פעם לא JSON שפענחתם ועשיתם לו סריאליזציה מחדש. האירוע הוא `JobWebhookEvent`, שה-`data.job` שלו הוא `Job`, או `PingWebhookEvent`. כשהבדיקה נכשלת, הפונקציה זורקת `WebhookSignatureError` עם אחד מהקודים האלה: `missing_header`, `malformed_header`, `signature_mismatch`, `timestamp_outside_tolerance`, `invalid_payload`, `missing_secret` או `crypto_unavailable`. ההשוואה נעשית בזמן קבוע.

```ts

export async function POST(request: Request) {
try {
const event = await verifyWebhookSignature({
payload: await request.text(),
header: request.headers.get("nodaro-signature"),
secret: process.env.NODARO_WEBHOOK_SECRET!,
})
if (event.type === "job.completed") await saveResult(event.data.job)
return new Response(null, { status: 204 })
} catch (err) {
if (err instanceof WebhookSignatureError) return new Response(err.code, { status: 400 })
throw err
}
}
```

כשהשרת שלכם עונה בסטטוס 2xx כלשהו בתוך 10 שניות, המסירה נחשבת למסירה שהצליחה. השדה `id` שבגוף הוא מזהה המסירה, והוא נשאר זהה בכל ניסיון חוזר, ולכן השתמשו בו כדי לדלג על אירוע שכבר טיפלתם בו.

## client.videoPro
שליטה בהרצות של רכיב [יצירת וידאו Pro](https://nodaro.ai/docs/nodes/video/generate-video-pro), הרכיב שמרנדר סרטונים ארוכים במקטעים. המשאב עובד ב-Nodaro Cloud. התחילו את ההרצה כמו כל הרצה של רכיב, ואז השתמשו במתודות האלה על המשימה שלה.

### videoPro.stop(jobId)‎
עוצרת בצורה מסודרת משימה פעילה של רכיב יצירת וידאו Pro. המקטע שנוצר באותו רגע ננטש, אבל משלמים עליו בכל זאת, והמקטעים שנותרו לא נוצרים. המקטעים שהסתיימו מתאחדים לסרטון הסופי של המשימה, והקרדיטים המשוריינים שלא נדרשו מוחזרים. משימה שעוד לא התחילה מבוטלת, עם החזר מלא.

```ts
stop(jobId: string): Promise<StopVideoProResult>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "מזהה המשימה של רכיב יצירת וידאו Pro." },
}}
/>

```ts
await client.videoPro.stop(jobId)
const { data } = await client.jobs.getStatus(jobId) // becomes completed, with the partial video
```

המשיכו לבדוק את הסטטוס של המשימה מדי כמה שניות. היא מסתיימת עם `output_data.pro.stopped` בערך `true`.

### videoPro.continueRun(jobId, opts?)‎
ממשיכה הרצה שנעצרה, נכשלה או הסתיימה, כ**משימה חדשה**. המקטעים שלפני `fromSegment` משמשים שוב, וכל מקטע ממנו והלאה נוצר מחדש. משלמים רק על המקטעים שנוצרים מחדש, ועוד התשלום הקבוע של Pro.

```ts
continueRun(jobId: string, opts?: { fromSegment?: number }): Promise<ContinueVideoProResult>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "המשימה שממשיכים." },
fromSegment: { type: 'number', description: "המקטע הראשון שייווצר מחדש, כשהספירה מתחילה מ-1. כברירת מחדל, המקטע הראשון שלא נמסר." },
}}
/>

```ts
const { jobId: newJobId, fromSegment } = await client.videoPro.continueRun(jobId, { fromSegment: 4 })
```

התוצאה כוללת את ה-`jobId` החדש, שאת הסטטוס שלו צריך לבדוק, ויכולה לכלול גם `continuedFromJobId`, `fromSegment`, `segmentCount` ו-`deduped`.

## שאלות נפוצות

### מה ההבדל בין משימה לרשומת הרצה?

משימה היא יצירה אחת, כגון תמונה אחת או סרטון אחד. רשומת הרצה מייצגת הרצה אחת של תהליך שלם, והיא מקבצת משימה אחת לכל רכיב AI, יחד עם המצב של כל רכיב.

### לאיזו מתודה כדאי לקרוא בלולאת תשאול?

ל-client.jobs.getStatus(jobId)‎. המתודה מחזירה רק את הסטטוס, ההתקדמות, הפלט והשגיאה, ולכן היא קלה בהרבה מ-client.jobs.get.

### האם ביטול משימה מחזיר את הקרדיטים שלה?

כן. המתודה client.jobs.cancel עוצרת את המשימה, והקרדיטים ששוריינו לה מוחזרים. ביטול מיידי של הרצת תהליך עושה אותו דבר למשימות שרצות בה. החריג הוא משימת LLM שמחויבת לפי השימוש, שהקריאה שלה כבר התחילה: עליה נגבים קרדיטים לפי השימוש בפועל.

### האם Nodaro יכול להודיע לשרת כשמשימה מסתיימת, במקום שהאפליקציה תבדוק את הסטטוס?

כן. רשמו נקודת קצה ב-HTTPS עם client.webhooks.create. מאותו רגע, Nodaro שולח בקשת POST חתומה על כל אירוע job.completed ו-job.failed בחשבון שלכם. בדקו כל קריאה עם verifyWebhookSignature.

### מה המשמעות של הסטטוס pending_review?

הסטטוס pending_review פירושו שמדיניות תוכן של הפריסה הזו מעכבת את התוצאה עד שאדם יבדוק אותה. זה לא סטטוס סופי: בהמשך המשימה מסתיימת בסטטוס completed, failed או cancelled. המשיכו לחכות, ואל תריצו את הבקשה שוב.
