mcp-duojie-image 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -6
- package/dist/errors.js +3 -1
- package/dist/index.js +12 -32
- package/dist/prompt.js +5 -0
- package/dist/provider.js +140 -41
- package/dist/task-response.js +45 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ MCP server for image generation via duojie.games GPT image API.
|
|
|
8
8
|
- Reference image support: requests with reference images route through `/v1/images/edits` (multipart form); text-only requests route through `/v1/images/generations` (JSON body)
|
|
9
9
|
- Prompt structuring helpers for use case, asset type, constraints, and effective prompt echo
|
|
10
10
|
- Optional chroma-key transparent background post-processing
|
|
11
|
-
- Async submission with task
|
|
11
|
+
- Async submission with explicit accepted/terminal semantics and required task polling
|
|
12
12
|
- File saving with overwrite/conflict handling
|
|
13
13
|
- Verified size templates for `1K`, `2K`, and `4K`
|
|
14
14
|
|
|
@@ -23,7 +23,11 @@ Required:
|
|
|
23
23
|
|
|
24
24
|
## Tool: `generate_image`
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Accepts an asynchronous image task and saves the image to disk when the task
|
|
27
|
+
finishes. A submitted response is not a successful generation. Call
|
|
28
|
+
`check_image_task` until it returns `status: "done"` or `status: "failed"`.
|
|
29
|
+
Non-terminal responses intentionally omit `success`; only terminal responses use
|
|
30
|
+
`success: true` or `success: false`.
|
|
27
31
|
|
|
28
32
|
Required parameters:
|
|
29
33
|
|
|
@@ -69,11 +73,19 @@ Response:
|
|
|
69
73
|
|
|
70
74
|
```json
|
|
71
75
|
{
|
|
72
|
-
"
|
|
76
|
+
"accepted": true,
|
|
77
|
+
"terminal": false,
|
|
73
78
|
"status": "submitted",
|
|
74
79
|
"task_id": "uuid",
|
|
80
|
+
"poll_required": true,
|
|
81
|
+
"retry_after_ms": 1000,
|
|
75
82
|
"effective_prompt": "Primary request: ...",
|
|
76
|
-
"
|
|
83
|
+
"prompt_stats": {
|
|
84
|
+
"characters": 123,
|
|
85
|
+
"utf8Bytes": 145,
|
|
86
|
+
"lines": 4
|
|
87
|
+
},
|
|
88
|
+
"message": "已接受异步生成任务;这不代表图片已生成,必须轮询 check_image_task"
|
|
77
89
|
}
|
|
78
90
|
```
|
|
79
91
|
|
|
@@ -90,6 +102,7 @@ Done response:
|
|
|
90
102
|
```json
|
|
91
103
|
{
|
|
92
104
|
"success": true,
|
|
105
|
+
"terminal": true,
|
|
93
106
|
"task_id": "uuid",
|
|
94
107
|
"status": "done",
|
|
95
108
|
"saved_path": "D:/images/output.png",
|
|
@@ -102,13 +115,32 @@ Failed response:
|
|
|
102
115
|
```json
|
|
103
116
|
{
|
|
104
117
|
"success": false,
|
|
118
|
+
"terminal": true,
|
|
105
119
|
"task_id": "uuid",
|
|
106
120
|
"status": "failed",
|
|
107
|
-
"error": "
|
|
108
|
-
"message": "
|
|
121
|
+
"error": "E_PROVIDER_EMPTY_RESPONSE",
|
|
122
|
+
"message": "Provider returned HTTP 200 but data contained no image",
|
|
123
|
+
"details": {
|
|
124
|
+
"request_id": "provider-request-id",
|
|
125
|
+
"response_summary": {
|
|
126
|
+
"status": 200,
|
|
127
|
+
"content_type": "application/json",
|
|
128
|
+
"body": {
|
|
129
|
+
"kind": "json_object",
|
|
130
|
+
"keys": ["data"],
|
|
131
|
+
"data_length": 1,
|
|
132
|
+
"data_0_keys": []
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
109
136
|
}
|
|
110
137
|
```
|
|
111
138
|
|
|
139
|
+
Provider diagnostics contain only response metadata and JSON shape/key summaries;
|
|
140
|
+
prompt text, image payloads, authorization values, and arbitrary upstream body
|
|
141
|
+
content are not echoed. The configured provider timeout covers receiving headers,
|
|
142
|
+
reading the response body, and downloading a returned image URL.
|
|
143
|
+
|
|
112
144
|
## Verified GPT size templates
|
|
113
145
|
|
|
114
146
|
These templates were tested directly against `/v1/images/generations`; all returned the requested dimensions.
|
package/dist/errors.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ASPECT_RATIOS, IMAGE_SIZES, generateImage } from "./provider.js";
|
|
|
8
8
|
import { saveImage } from "./saver.js";
|
|
9
9
|
import { AppError } from "./errors.js";
|
|
10
10
|
import { createTask, getTask, updateTask } from "./task-queue.js";
|
|
11
|
+
import { taskStatusResponse, taskSubmissionResponse } from "./task-response.js";
|
|
11
12
|
import { IMAGE_INPUT_ROLES, PROMPT_USE_CASES, preparePrompt } from "./prompt.js";
|
|
12
13
|
import { OUTPUT_FORMATS, removeChromaKey } from "./image-processing.js";
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
@@ -31,10 +32,10 @@ function toolText(data) {
|
|
|
31
32
|
}
|
|
32
33
|
const server = new McpServer({
|
|
33
34
|
name: "duojie-image",
|
|
34
|
-
version: "0.1
|
|
35
|
+
version: "0.9.1",
|
|
35
36
|
});
|
|
36
37
|
server.registerTool("generate_image", {
|
|
37
|
-
description: "
|
|
38
|
+
description: "Accept an asynchronous gpt-image-2 task. This submission is not generation success: after receiving task_id, you MUST call check_image_task until status is done or failed. Reference images use /v1/images/edits; text-only requests use /v1/images/generations.",
|
|
38
39
|
inputSchema: z.object({
|
|
39
40
|
prompt: z.string().min(1).describe("Prompt"),
|
|
40
41
|
output_dir: z.string().describe("Absolute output dir"),
|
|
@@ -104,7 +105,10 @@ server.registerTool("generate_image", {
|
|
|
104
105
|
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: code, message: String(err instanceof Error ? err.message : err) }) }], isError: true };
|
|
105
106
|
}
|
|
106
107
|
const task = createTask();
|
|
107
|
-
updateTask(task.id, {
|
|
108
|
+
updateTask(task.id, {
|
|
109
|
+
effectivePrompt: preparedPrompt.prompt,
|
|
110
|
+
promptStats: preparedPrompt.stats,
|
|
111
|
+
});
|
|
108
112
|
// Start background generation (don't await)
|
|
109
113
|
(async () => {
|
|
110
114
|
try {
|
|
@@ -146,6 +150,7 @@ server.registerTool("generate_image", {
|
|
|
146
150
|
completedAt: Date.now(),
|
|
147
151
|
errorCode: code,
|
|
148
152
|
error: msg,
|
|
153
|
+
errorDetails: err instanceof AppError ? err.details : undefined,
|
|
149
154
|
});
|
|
150
155
|
await server.sendLoggingMessage({
|
|
151
156
|
level: "error",
|
|
@@ -161,21 +166,10 @@ server.registerTool("generate_image", {
|
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
168
|
})();
|
|
164
|
-
return
|
|
165
|
-
content: [{
|
|
166
|
-
type: "text",
|
|
167
|
-
text: JSON.stringify({
|
|
168
|
-
success: true,
|
|
169
|
-
status: "submitted",
|
|
170
|
-
task_id: task.id,
|
|
171
|
-
effective_prompt: preparedPrompt.prompt,
|
|
172
|
-
message: `已提交异步生成:${args.output_filename};如需诊断失败原因,可调用 check_image_task`,
|
|
173
|
-
}),
|
|
174
|
-
}],
|
|
175
|
-
};
|
|
169
|
+
return toolText(taskSubmissionResponse(getTask(task.id) ?? task, args.output_filename));
|
|
176
170
|
});
|
|
177
171
|
server.registerTool("check_image_task", {
|
|
178
|
-
description: "
|
|
172
|
+
description: "Poll an asynchronous image task. Continue polling while poll_required=true. Only status=done with success=true means an image was generated and saved; status=failed is terminal and includes the provider error/details.",
|
|
179
173
|
inputSchema: z.object({
|
|
180
174
|
task_id: z.string().min(1).describe("Task id returned by generate_image"),
|
|
181
175
|
}),
|
|
@@ -191,22 +185,8 @@ server.registerTool("check_image_task", {
|
|
|
191
185
|
isError: true,
|
|
192
186
|
};
|
|
193
187
|
}
|
|
194
|
-
const response =
|
|
195
|
-
|
|
196
|
-
task_id: task.id,
|
|
197
|
-
status: task.status,
|
|
198
|
-
};
|
|
199
|
-
if (task.status === "done") {
|
|
200
|
-
response.saved_path = task.savedPath;
|
|
201
|
-
}
|
|
202
|
-
if (task.effectivePrompt) {
|
|
203
|
-
response.effective_prompt = task.effectivePrompt;
|
|
204
|
-
}
|
|
205
|
-
if (task.status === "failed") {
|
|
206
|
-
response.error = task.errorCode;
|
|
207
|
-
response.message = task.error;
|
|
208
|
-
}
|
|
209
|
-
return toolText(response);
|
|
188
|
+
const response = toolText(taskStatusResponse(task));
|
|
189
|
+
return task.status === "failed" ? { ...response, isError: true } : response;
|
|
210
190
|
});
|
|
211
191
|
const transport = new StdioServerTransport();
|
|
212
192
|
await server.connect(transport);
|
package/dist/prompt.js
CHANGED
|
@@ -84,5 +84,10 @@ export function preparePrompt(opts) {
|
|
|
84
84
|
prompt,
|
|
85
85
|
imagePaths: inputImages?.map((image) => image.path) ?? [],
|
|
86
86
|
imageInputs: inputImages,
|
|
87
|
+
stats: {
|
|
88
|
+
characters: Array.from(prompt).length,
|
|
89
|
+
utf8Bytes: Buffer.byteLength(prompt, "utf8"),
|
|
90
|
+
lines: prompt ? prompt.split("\n").length : 0,
|
|
91
|
+
},
|
|
87
92
|
};
|
|
88
93
|
}
|
package/dist/provider.js
CHANGED
|
@@ -2,42 +2,102 @@ import { AppError } from "./errors.js";
|
|
|
2
2
|
import { resizeIfNeeded } from "./resizer.js";
|
|
3
3
|
export const ASPECT_RATIOS = ["1:1", "3:2", "2:3", "16:9", "9:16"];
|
|
4
4
|
export const IMAGE_SIZES = ["1K", "2K", "4K"];
|
|
5
|
-
|
|
5
|
+
function getRequestId(res) {
|
|
6
|
+
return res.headers.get("x-request-id")
|
|
7
|
+
?? res.headers.get("request-id")
|
|
8
|
+
?? res.headers.get("x-trace-id")
|
|
9
|
+
?? res.headers.get("cf-ray");
|
|
10
|
+
}
|
|
11
|
+
function summarizeJson(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return { kind: "json_array", length: value.length };
|
|
14
|
+
}
|
|
15
|
+
if (!value || typeof value !== "object") {
|
|
16
|
+
return { kind: "json_scalar", type: value === null ? "null" : typeof value };
|
|
17
|
+
}
|
|
18
|
+
const record = value;
|
|
19
|
+
const summary = {
|
|
20
|
+
kind: "json_object",
|
|
21
|
+
keys: Object.keys(record).slice(0, 20),
|
|
22
|
+
};
|
|
23
|
+
if (Array.isArray(record.data)) {
|
|
24
|
+
summary.data_length = record.data.length;
|
|
25
|
+
const first = record.data[0];
|
|
26
|
+
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
27
|
+
summary.data_0_keys = Object.keys(first).slice(0, 20);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
|
|
31
|
+
const error = record.error;
|
|
32
|
+
summary.error_keys = Object.keys(error).slice(0, 20);
|
|
33
|
+
if (typeof error.code === "string" && /^[a-zA-Z0-9_.:-]{1,80}$/.test(error.code)) {
|
|
34
|
+
summary.error_code = error.code;
|
|
35
|
+
}
|
|
36
|
+
if (typeof error.type === "string" && /^[a-zA-Z0-9_.:-]{1,80}$/.test(error.type)) {
|
|
37
|
+
summary.error_type = error.type;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return summary;
|
|
41
|
+
}
|
|
42
|
+
function responseDetails(res, body) {
|
|
43
|
+
const summary = {
|
|
44
|
+
status: res.status,
|
|
45
|
+
content_type: res.headers.get("content-type"),
|
|
46
|
+
content_length: res.headers.get("content-length"),
|
|
47
|
+
...(body ? { body } : {}),
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
request_id: getRequestId(res),
|
|
51
|
+
response_summary: summary,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function isAbortError(err) {
|
|
55
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
56
|
+
}
|
|
57
|
+
async function delayForRetry(attempt) {
|
|
58
|
+
const delayMs = 1_000 * Math.pow(2, attempt) + Math.random() * 500;
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
60
|
+
}
|
|
61
|
+
async function fetchWithRetry(url, init, timeoutMs, consume, retries = 0) {
|
|
6
62
|
let lastErr;
|
|
7
63
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
8
64
|
const controller = new AbortController();
|
|
9
65
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
66
|
+
let receivedResponse;
|
|
10
67
|
try {
|
|
11
68
|
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
12
|
-
|
|
69
|
+
receivedResponse = res;
|
|
13
70
|
// 5xx: 上游随机故障(EOF等),立刻 abort body 连接并重试,不等 chunked 终止
|
|
14
71
|
if (res.status >= 500 && attempt < retries) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
catch { }
|
|
19
|
-
const delayMs = 1000 * Math.pow(2, attempt) + Math.random() * 500;
|
|
20
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
72
|
+
controller.abort();
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
await delayForRetry(attempt);
|
|
21
75
|
continue;
|
|
22
76
|
}
|
|
23
|
-
|
|
77
|
+
const result = await consume(res, controller.signal);
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
return result;
|
|
24
80
|
}
|
|
25
81
|
catch (err) {
|
|
26
82
|
clearTimeout(timer);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
if (err instanceof AppError) {
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
if (isAbortError(err)) {
|
|
87
|
+
throw new AppError("E_PROVIDER_TIMEOUT", `Provider request timed out after ${timeoutMs}ms`, {
|
|
88
|
+
timeout_ms: timeoutMs,
|
|
89
|
+
phase: receivedResponse ? "response_body_or_image_download" : "connection_or_headers",
|
|
90
|
+
...(receivedResponse ? responseDetails(receivedResponse) : {}),
|
|
91
|
+
});
|
|
34
92
|
}
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
// Once response headers have arrived, the generation may already be billable.
|
|
94
|
+
// Do not replay the POST for body/CDN failures and risk duplicate generation.
|
|
95
|
+
if (receivedResponse) {
|
|
96
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", "Provider response could not be read completely", responseDetails(receivedResponse));
|
|
37
97
|
}
|
|
38
98
|
lastErr = err;
|
|
39
99
|
if (attempt < retries) {
|
|
40
|
-
await
|
|
100
|
+
await delayForRetry(attempt);
|
|
41
101
|
}
|
|
42
102
|
}
|
|
43
103
|
}
|
|
@@ -72,34 +132,72 @@ function mapOpenAISize(size, aspectRatio) {
|
|
|
72
132
|
const ratio = aspectRatio ?? "1:1";
|
|
73
133
|
return SIZE_TEMPLATES[imageSize][ratio];
|
|
74
134
|
}
|
|
75
|
-
async function imageFromOpenAIResponse(res) {
|
|
76
|
-
|
|
135
|
+
async function imageFromOpenAIResponse(res, signal) {
|
|
136
|
+
let data;
|
|
137
|
+
try {
|
|
138
|
+
data = (await res.json());
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (isAbortError(err))
|
|
142
|
+
throw err;
|
|
143
|
+
if (!(err instanceof SyntaxError)) {
|
|
144
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", "Provider response body could not be read", responseDetails(res));
|
|
145
|
+
}
|
|
146
|
+
throw new AppError("E_PROVIDER_EMPTY_RESPONSE", "Provider returned HTTP 200 but the response body was not valid image JSON", responseDetails(res, { kind: "invalid_json" }));
|
|
147
|
+
}
|
|
77
148
|
const image = data.data?.[0];
|
|
78
149
|
if (image?.b64_json) {
|
|
79
|
-
|
|
150
|
+
const imageBuffer = Buffer.from(image.b64_json, "base64");
|
|
151
|
+
if (imageBuffer.length > 0) {
|
|
152
|
+
return imageBuffer;
|
|
153
|
+
}
|
|
80
154
|
}
|
|
81
155
|
if (image?.url) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
156
|
+
try {
|
|
157
|
+
const imgRes = await fetch(image.url, { signal });
|
|
158
|
+
if (!imgRes.ok) {
|
|
159
|
+
const summary = await readBodySummary(imgRes);
|
|
160
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Image URL fetch failed (HTTP ${imgRes.status})`, responseDetails(imgRes, summary));
|
|
161
|
+
}
|
|
162
|
+
const imageBuffer = Buffer.from(await imgRes.arrayBuffer());
|
|
163
|
+
if (imageBuffer.length > 0) {
|
|
164
|
+
return imageBuffer;
|
|
165
|
+
}
|
|
166
|
+
throw new AppError("E_PROVIDER_EMPTY_RESPONSE", "Provider image URL returned an empty body", responseDetails(imgRes, { kind: "empty", length: 0 }));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (isAbortError(err) || err instanceof AppError)
|
|
170
|
+
throw err;
|
|
171
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", "Image URL download failed", responseDetails(res));
|
|
85
172
|
}
|
|
86
|
-
return Buffer.from(await imgRes.arrayBuffer());
|
|
87
173
|
}
|
|
88
|
-
throw new AppError("
|
|
174
|
+
throw new AppError("E_PROVIDER_EMPTY_RESPONSE", "Provider returned HTTP 200 but data contained no image", responseDetails(res, summarizeJson(data)));
|
|
175
|
+
}
|
|
176
|
+
async function readBodySummary(res) {
|
|
177
|
+
const text = await res.text();
|
|
178
|
+
if (!text)
|
|
179
|
+
return { kind: "empty", length: 0 };
|
|
180
|
+
try {
|
|
181
|
+
return summarizeJson(JSON.parse(text));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return { kind: "text", length: text.length };
|
|
185
|
+
}
|
|
89
186
|
}
|
|
90
187
|
async function handleProviderError(res) {
|
|
188
|
+
// 5xx: 上游随机故障,不读 body(chunked 连接可能挂起),直接返回安全的响应头摘要。
|
|
189
|
+
if (res.status >= 500) {
|
|
190
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error (HTTP ${res.status})`, responseDetails(res));
|
|
191
|
+
}
|
|
192
|
+
const summary = await readBodySummary(res);
|
|
193
|
+
const details = responseDetails(res, summary);
|
|
91
194
|
if (res.status === 401 || res.status === 403) {
|
|
92
|
-
throw new AppError("E_PROVIDER_AUTH", `Auth failed (HTTP ${res.status})
|
|
195
|
+
throw new AppError("E_PROVIDER_AUTH", `Auth failed (HTTP ${res.status})`, details);
|
|
93
196
|
}
|
|
94
197
|
if (res.status === 429) {
|
|
95
|
-
throw new AppError("E_PROVIDER_RATE_LIMIT", "Rate limit exceeded");
|
|
198
|
+
throw new AppError("E_PROVIDER_RATE_LIMIT", "Rate limit exceeded", details);
|
|
96
199
|
}
|
|
97
|
-
|
|
98
|
-
if (res.status >= 500) {
|
|
99
|
-
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error ${res.status} (body skipped to avoid chunked hang)`);
|
|
100
|
-
}
|
|
101
|
-
const text = await res.text().catch(() => "");
|
|
102
|
-
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error ${res.status}: ${text.slice(0, 200)}`);
|
|
200
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error (HTTP ${res.status})`, details);
|
|
103
201
|
}
|
|
104
202
|
async function generateOpenAIImage(config, opts) {
|
|
105
203
|
const prompt = opts.prompt;
|
|
@@ -141,15 +239,16 @@ async function generateOpenAIImage(config, opts) {
|
|
|
141
239
|
}
|
|
142
240
|
// 启用 2 次重试,应对上游偶发的 EOF / 524(Cloudflare 100s 超时)。
|
|
143
241
|
// retries=0 时所有偶发失败一次过即死,指数退避代码是死代码。
|
|
144
|
-
|
|
242
|
+
return fetchWithRetry(url, {
|
|
145
243
|
method: "POST",
|
|
146
244
|
headers,
|
|
147
245
|
body,
|
|
148
|
-
}, config.timeoutMs,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
246
|
+
}, config.timeoutMs, async (res, signal) => {
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
await handleProviderError(res);
|
|
249
|
+
}
|
|
250
|
+
return imageFromOpenAIResponse(res, signal);
|
|
251
|
+
}, 2);
|
|
153
252
|
}
|
|
154
253
|
export async function generateImage(config, opts) {
|
|
155
254
|
return generateOpenAIImage(config, opts);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const POLL_RETRY_AFTER_MS = 1_000;
|
|
2
|
+
export function taskSubmissionResponse(task, outputFilename) {
|
|
3
|
+
return {
|
|
4
|
+
accepted: true,
|
|
5
|
+
terminal: false,
|
|
6
|
+
status: "submitted",
|
|
7
|
+
task_id: task.id,
|
|
8
|
+
poll_required: true,
|
|
9
|
+
retry_after_ms: POLL_RETRY_AFTER_MS,
|
|
10
|
+
effective_prompt: task.effectivePrompt,
|
|
11
|
+
prompt_stats: task.promptStats,
|
|
12
|
+
message: `已接受异步生成任务:${outputFilename}。这不代表图片已生成;必须调用 check_image_task,直到 status 为 done 或 failed。`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function taskStatusResponse(task) {
|
|
16
|
+
const response = {
|
|
17
|
+
task_id: task.id,
|
|
18
|
+
status: task.status,
|
|
19
|
+
terminal: task.status === "done" || task.status === "failed",
|
|
20
|
+
};
|
|
21
|
+
if (task.effectivePrompt) {
|
|
22
|
+
response.effective_prompt = task.effectivePrompt;
|
|
23
|
+
}
|
|
24
|
+
if (task.promptStats) {
|
|
25
|
+
response.prompt_stats = task.promptStats;
|
|
26
|
+
}
|
|
27
|
+
if (task.status === "done") {
|
|
28
|
+
response.success = true;
|
|
29
|
+
response.saved_path = task.savedPath;
|
|
30
|
+
}
|
|
31
|
+
else if (task.status === "failed") {
|
|
32
|
+
response.success = false;
|
|
33
|
+
response.error = task.errorCode;
|
|
34
|
+
response.message = task.error;
|
|
35
|
+
if (task.errorDetails) {
|
|
36
|
+
response.details = task.errorDetails;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
response.accepted = true;
|
|
41
|
+
response.poll_required = true;
|
|
42
|
+
response.retry_after_ms = POLL_RETRY_AFTER_MS;
|
|
43
|
+
}
|
|
44
|
+
return response;
|
|
45
|
+
}
|