gpteam 0.1.18 → 0.1.19
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 +6 -3
- package/lib/cli.js +1 -1
- package/lib/config.js +1 -0
- package/lib/help.js +2 -2
- package/lib/image-mcp/errors.js +51 -6
- package/lib/image-mcp/files.js +65 -6
- package/lib/image-mcp/image.js +254 -31
- package/lib/image-mcp/server.js +56 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,12 +19,15 @@ The Image MCP config uses cc-switch-style per-client env blocks. Codex writes `[
|
|
|
19
19
|
The Image MCP exposes both a synchronous compatibility tool and a local async job flow:
|
|
20
20
|
|
|
21
21
|
- `create_image_job`: recommended for normal use. It starts a local background image job and returns `job_id` quickly, which avoids losing the whole generation when a VPN, proxy, or client has a 60-second idle timeout.
|
|
22
|
-
- `get_image_job_status`: checks whether the local job is queued, running, succeeded, failed, or
|
|
22
|
+
- `get_image_job_status`: checks whether the local job is queued, running, succeeded, failed, canceled, or expired.
|
|
23
23
|
- `cancel_image_job`: cancels a queued/running local job.
|
|
24
|
-
- `download_image_result`: returns the completed file metadata and image content.
|
|
24
|
+
- `download_image_result`: returns the completed file metadata and image content. Use `metadata_only`, `include_image`, and `include_revised_prompt` to control large result payloads.
|
|
25
|
+
- `get_capabilities`: returns supported sizes, formats, quality values, async support, cancellation semantics, queue limits, and image-to-image support.
|
|
25
26
|
- `generate_image`: waits for completion and writes the image file immediately. This is kept for synchronous compatibility.
|
|
26
27
|
|
|
27
|
-
Image MCP results are returned as stable JSON text and MCP `structuredContent`. Successful results include file path, model, size, format, quality, byte size, SHA-256, MIME type, image dimensions, duration, retry count, `job_id`,
|
|
28
|
+
Image MCP results are returned as stable JSON text and MCP `structuredContent`. Successful results include final file path, model, action, size, format, quality, byte size, SHA-256, MIME type, image dimensions, duration, retry count, `job_id`, `trace_id`, and optional `idempotency_key`. Error results use stable `error.code`, `error.message`, `error.retryable`, `error.stage`, `error.upstream_status`, and `error.trace_id` fields while keeping compatibility fields such as `category` and `http_status`.
|
|
29
|
+
|
|
30
|
+
The MCP supports normal text-to-image generation and image-to-image/edit inputs. Pass `images` as data URLs, HTTPS URLs, or local file paths. Pass `mask` the same way for masked edits, and use `input_fidelity` when needed. File writes create missing directories, avoid overwriting existing files by adding `-v2`, `-v3`, etc., and validate PNG/JPEG/WebP before returning success. `overwrite: true` is available for explicit replacement. `return_revised_prompt` controls whether the upstream revised prompt is included in the result.
|
|
28
31
|
|
|
29
32
|
Claude Code is written to `~/.claude/settings.json` under the `env` section, using the GPTeam `/anthropic` base URL. OpenClaw writes `models.providers.gpteam` and also selects `gpteam/<model>` under `agents.defaults.model`, so the chosen model is active without an extra manual step.
|
|
30
33
|
|
package/lib/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function runCli(argv = []) {
|
|
|
72
72
|
console.log(`入口:${formatNodeLabel(selectedNode)}`);
|
|
73
73
|
console.log(`地址:${selectedNode.baseUrl}`);
|
|
74
74
|
if (['codex', 'opencode', 'claude-code'].includes(client.id)) {
|
|
75
|
-
printHint(theme, '已写入 GPTeam Image MCP
|
|
75
|
+
printHint(theme, '已写入 GPTeam Image MCP。客户端对话里需要生图或图生图时优先调用 create_image_job,再用 get_image_job_status 和 download_image_result 取结果,get_capabilities 可查看支持能力,MCP 使用专用环境变量读取 API key。');
|
|
76
76
|
}
|
|
77
77
|
} finally {
|
|
78
78
|
rl.close();
|
package/lib/config.js
CHANGED
package/lib/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const PACKAGE_NAME = 'gpteam';
|
|
2
|
-
export const PACKAGE_VERSION = '0.1.
|
|
2
|
+
export const PACKAGE_VERSION = '0.1.19';
|
|
3
3
|
|
|
4
4
|
export function getHelpText() {
|
|
5
5
|
return [
|
|
@@ -22,6 +22,6 @@ export function getHelpText() {
|
|
|
22
22
|
' --help 显示帮助',
|
|
23
23
|
' --version 显示版本',
|
|
24
24
|
'',
|
|
25
|
-
'说明:输入 key 后会先请求 /v1/models 校验,通过后才继续。选好客户端后会检测本机命令是否可用,缺失时提示安装。Windows 暂不支持自动配置 OpenClaw。测速会请求 GET /api/health 和流式 POST /v1/responses。入口之间并行测速,写新配置前会先备份旧配置。Codex、OpenCode、Claude Code 会写入 GPTeam Image MCP
|
|
25
|
+
'说明:输入 key 后会先请求 /v1/models 校验,通过后才继续。选好客户端后会检测本机命令是否可用,缺失时提示安装。Windows 暂不支持自动配置 OpenClaw。测速会请求 GET /api/health 和流式 POST /v1/responses。入口之间并行测速,写新配置前会先备份旧配置。Codex、OpenCode、Claude Code 会写入 GPTeam Image MCP,生图和图生图工具从 MCP env 读取 key,长时间任务优先用 create_image_job 异步任务,get_capabilities 可查看支持能力。'
|
|
26
26
|
].join('\n');
|
|
27
27
|
}
|
package/lib/image-mcp/errors.js
CHANGED
|
@@ -6,8 +6,10 @@ export class ImageMCPError extends Error {
|
|
|
6
6
|
this.name = 'ImageMCPError';
|
|
7
7
|
this.code = String(options.code || 'image_mcp_error');
|
|
8
8
|
this.category = String(options.category || 'unknown');
|
|
9
|
+
this.stage = String(options.stage || stageFromCategory(this.category));
|
|
9
10
|
this.retryable = Boolean(options.retryable);
|
|
10
11
|
this.http_status = Number.isFinite(options.http_status) ? options.http_status : undefined;
|
|
12
|
+
this.upstream_status = Number.isFinite(options.upstream_status) ? options.upstream_status : this.http_status;
|
|
11
13
|
this.details = options.details || undefined;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
@@ -23,6 +25,7 @@ export async function imageErrorFromHTTPResponse(response, apiKey) {
|
|
|
23
25
|
return new ImageMCPError(message, {
|
|
24
26
|
code: upstreamCode || 'rate_limit_exceeded',
|
|
25
27
|
category: 'upstream_rate_limit',
|
|
28
|
+
stage: 'upstream',
|
|
26
29
|
http_status: status,
|
|
27
30
|
retryable: true
|
|
28
31
|
});
|
|
@@ -31,6 +34,7 @@ export async function imageErrorFromHTTPResponse(response, apiKey) {
|
|
|
31
34
|
return new ImageMCPError(message, {
|
|
32
35
|
code: upstreamCode || 'upstream_server_error',
|
|
33
36
|
category: status === 408 ? 'timeout' : 'upstream_server',
|
|
37
|
+
stage: 'upstream',
|
|
34
38
|
http_status: status,
|
|
35
39
|
retryable: true
|
|
36
40
|
});
|
|
@@ -39,6 +43,7 @@ export async function imageErrorFromHTTPResponse(response, apiKey) {
|
|
|
39
43
|
return new ImageMCPError(message, {
|
|
40
44
|
code: upstreamCode || 'content_safety_rejected',
|
|
41
45
|
category: 'content_safety',
|
|
46
|
+
stage: 'upstream',
|
|
42
47
|
http_status: status,
|
|
43
48
|
retryable: false
|
|
44
49
|
});
|
|
@@ -46,6 +51,7 @@ export async function imageErrorFromHTTPResponse(response, apiKey) {
|
|
|
46
51
|
return new ImageMCPError(message, {
|
|
47
52
|
code: upstreamCode || (status === 401 || status === 403 ? 'authentication_failed' : 'invalid_request_error'),
|
|
48
53
|
category: status === 401 || status === 403 ? 'authentication' : 'parameter',
|
|
54
|
+
stage: status === 401 || status === 403 ? 'upstream' : 'validate',
|
|
49
55
|
http_status: status,
|
|
50
56
|
retryable: false
|
|
51
57
|
});
|
|
@@ -55,7 +61,8 @@ export function imageErrorFromFetch(error, options = {}) {
|
|
|
55
61
|
if (options.cancelled) {
|
|
56
62
|
return new ImageMCPError('图片生成任务已取消。', {
|
|
57
63
|
code: 'job_cancelled',
|
|
58
|
-
category: '
|
|
64
|
+
category: 'canceled',
|
|
65
|
+
stage: 'cancel',
|
|
59
66
|
retryable: false
|
|
60
67
|
});
|
|
61
68
|
}
|
|
@@ -64,25 +71,33 @@ export function imageErrorFromFetch(error, options = {}) {
|
|
|
64
71
|
return new ImageMCPError(message, {
|
|
65
72
|
code: timeoutLike ? 'network_timeout' : 'network_error',
|
|
66
73
|
category: timeoutLike ? 'timeout' : 'network',
|
|
74
|
+
stage: 'network',
|
|
67
75
|
retryable: true
|
|
68
76
|
});
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
export function serializeImageError(error) {
|
|
79
|
+
export function serializeImageError(error, meta = {}) {
|
|
72
80
|
if (error instanceof ImageMCPError) {
|
|
73
81
|
return {
|
|
74
82
|
code: error.code,
|
|
75
|
-
|
|
83
|
+
message: error.message,
|
|
76
84
|
retryable: error.retryable,
|
|
85
|
+
stage: error.stage,
|
|
86
|
+
upstream_status: error.upstream_status,
|
|
87
|
+
trace_id: meta.trace_id || '',
|
|
88
|
+
category: error.category,
|
|
77
89
|
http_status: error.http_status,
|
|
78
|
-
|
|
90
|
+
details: error.details
|
|
79
91
|
};
|
|
80
92
|
}
|
|
81
93
|
return {
|
|
82
94
|
code: 'image_mcp_error',
|
|
83
|
-
|
|
95
|
+
message: error && error.message ? String(error.message) : String(error || 'unknown error'),
|
|
84
96
|
retryable: false,
|
|
85
|
-
|
|
97
|
+
stage: 'unknown',
|
|
98
|
+
upstream_status: undefined,
|
|
99
|
+
trace_id: meta.trace_id || '',
|
|
100
|
+
category: 'unknown'
|
|
86
101
|
};
|
|
87
102
|
}
|
|
88
103
|
|
|
@@ -114,3 +129,33 @@ function isContentSafetyError(code, message) {
|
|
|
114
129
|
const text = `${code || ''} ${message || ''}`.toLowerCase();
|
|
115
130
|
return /content|safety|policy|moderation/.test(text);
|
|
116
131
|
}
|
|
132
|
+
|
|
133
|
+
function stageFromCategory(category) {
|
|
134
|
+
switch (String(category || '').toLowerCase()) {
|
|
135
|
+
case 'parameter':
|
|
136
|
+
return 'validate';
|
|
137
|
+
case 'configuration':
|
|
138
|
+
case 'environment':
|
|
139
|
+
return 'configuration';
|
|
140
|
+
case 'file_system':
|
|
141
|
+
case 'response_invalid':
|
|
142
|
+
return 'local';
|
|
143
|
+
case 'authentication':
|
|
144
|
+
case 'content_safety':
|
|
145
|
+
case 'upstream_rate_limit':
|
|
146
|
+
case 'upstream_server':
|
|
147
|
+
return 'upstream';
|
|
148
|
+
case 'timeout':
|
|
149
|
+
case 'network':
|
|
150
|
+
return 'network';
|
|
151
|
+
case 'canceled':
|
|
152
|
+
case 'cancelled':
|
|
153
|
+
return 'cancel';
|
|
154
|
+
case 'not_found':
|
|
155
|
+
return 'lookup';
|
|
156
|
+
case 'queue':
|
|
157
|
+
return 'queue';
|
|
158
|
+
default:
|
|
159
|
+
return 'unknown';
|
|
160
|
+
}
|
|
161
|
+
}
|
package/lib/image-mcp/files.js
CHANGED
|
@@ -8,11 +8,15 @@ export const DEFAULT_IMAGE_FORMAT = 'png';
|
|
|
8
8
|
|
|
9
9
|
export function writeImageOutput(input = {}, options = {}) {
|
|
10
10
|
const bytes = decodeBase64Image(input.b64);
|
|
11
|
-
const image =
|
|
11
|
+
const image = inspectImageBytes(bytes);
|
|
12
12
|
const format = image.format || normalizeImageFormat(input.format || input.output_format || DEFAULT_IMAGE_FORMAT);
|
|
13
|
-
const outputPath =
|
|
13
|
+
const outputPath = writeImageFile(input.output_path, format, bytes, {
|
|
14
|
+
...options,
|
|
15
|
+
overwrite: Boolean(input.overwrite)
|
|
16
|
+
});
|
|
14
17
|
return {
|
|
15
18
|
file: outputPath,
|
|
19
|
+
final_file: outputPath,
|
|
16
20
|
path: outputPath,
|
|
17
21
|
bytes: bytes.length,
|
|
18
22
|
sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
@@ -25,6 +29,23 @@ export function writeImageOutput(input = {}, options = {}) {
|
|
|
25
29
|
};
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
export function localImageToDataURL(rawPath, options = {}) {
|
|
33
|
+
const filePath = path.resolve(expandHome(String(rawPath || ''), options.home || os.homedir()));
|
|
34
|
+
const bytes = fs.readFileSync(filePath);
|
|
35
|
+
const image = inspectImageBytes(bytes);
|
|
36
|
+
return `data:${image.mime_type};base64,${bytes.toString('base64')}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeImageFile(rawOutputPath, format, bytes, options) {
|
|
40
|
+
if (options.overwrite) {
|
|
41
|
+
const outputPath = resolveOutputPath(rawOutputPath, format, options);
|
|
42
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
43
|
+
atomicReplaceFile(outputPath, bytes);
|
|
44
|
+
return outputPath;
|
|
45
|
+
}
|
|
46
|
+
return writeImageFileNonOverwriting(rawOutputPath, format, bytes, options);
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
function writeImageFileNonOverwriting(rawOutputPath, format, bytes, options) {
|
|
29
50
|
let lastConflict;
|
|
30
51
|
for (let attempt = 0; attempt < 10000; attempt += 1) {
|
|
@@ -41,6 +62,7 @@ function writeImageFileNonOverwriting(rawOutputPath, format, bytes, options) {
|
|
|
41
62
|
throw lastConflict || new ImageMCPError('无法生成不覆盖现有文件的输出路径。', {
|
|
42
63
|
code: 'output_path_conflict',
|
|
43
64
|
category: 'file_system',
|
|
65
|
+
stage: 'local',
|
|
44
66
|
retryable: false
|
|
45
67
|
});
|
|
46
68
|
}
|
|
@@ -58,6 +80,7 @@ function decodeBase64Image(value) {
|
|
|
58
80
|
throw new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
|
|
59
81
|
code: 'image_data_missing',
|
|
60
82
|
category: 'response_invalid',
|
|
83
|
+
stage: 'local',
|
|
61
84
|
retryable: false
|
|
62
85
|
});
|
|
63
86
|
}
|
|
@@ -66,13 +89,14 @@ function decodeBase64Image(value) {
|
|
|
66
89
|
throw new ImageMCPError('GPTeam 图片接口返回的图片数据为空。', {
|
|
67
90
|
code: 'image_data_empty',
|
|
68
91
|
category: 'response_invalid',
|
|
92
|
+
stage: 'local',
|
|
69
93
|
retryable: false
|
|
70
94
|
});
|
|
71
95
|
}
|
|
72
96
|
return bytes;
|
|
73
97
|
}
|
|
74
98
|
|
|
75
|
-
function
|
|
99
|
+
export function inspectImageBytes(bytes) {
|
|
76
100
|
const png = inspectPNG(bytes);
|
|
77
101
|
if (png) return png;
|
|
78
102
|
const jpeg = inspectJPEG(bytes);
|
|
@@ -82,6 +106,7 @@ function inspectImage(bytes) {
|
|
|
82
106
|
throw new ImageMCPError('写入前图片校验失败:无法识别 PNG/JPEG/WebP。', {
|
|
83
107
|
code: 'image_mime_invalid',
|
|
84
108
|
category: 'response_invalid',
|
|
109
|
+
stage: 'local',
|
|
85
110
|
retryable: false
|
|
86
111
|
});
|
|
87
112
|
}
|
|
@@ -152,6 +177,7 @@ function resolveAvailableOutputPath(rawOutputPath, format, options) {
|
|
|
152
177
|
throw new ImageMCPError('无法生成不覆盖现有文件的输出路径。', {
|
|
153
178
|
code: 'output_path_conflict',
|
|
154
179
|
category: 'file_system',
|
|
180
|
+
stage: 'local',
|
|
155
181
|
retryable: false
|
|
156
182
|
});
|
|
157
183
|
}
|
|
@@ -162,11 +188,11 @@ function resolveOutputPath(rawOutputPath, format, options) {
|
|
|
162
188
|
const name = `gpteam-image-${new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-')}.${format}`;
|
|
163
189
|
if (rawOutputPath) {
|
|
164
190
|
const expanded = expandHome(String(rawOutputPath), home);
|
|
165
|
-
if (isDirectoryLike(expanded)) return path.
|
|
166
|
-
return withImageExtension(expanded, format);
|
|
191
|
+
if (isDirectoryLike(expanded)) return path.resolve(expanded, name);
|
|
192
|
+
return path.resolve(withImageExtension(expanded, format));
|
|
167
193
|
}
|
|
168
194
|
const outputDir = expandHome(firstNonEmpty(env.GPTEAM_IMAGE_OUTPUT_DIR, defaultImageOutputDir(home)), home);
|
|
169
|
-
return path.
|
|
195
|
+
return path.resolve(outputDir, name);
|
|
170
196
|
}
|
|
171
197
|
|
|
172
198
|
function atomicWriteFile(outputPath, bytes) {
|
|
@@ -201,6 +227,39 @@ function atomicWriteFile(outputPath, bytes) {
|
|
|
201
227
|
throw new ImageMCPError(conflict ? '输出文件已存在,正在重新选择不覆盖路径。' : `图片文件写入失败:${error.message}`, {
|
|
202
228
|
code: conflict ? 'output_path_conflict' : 'file_write_failed',
|
|
203
229
|
category: 'file_system',
|
|
230
|
+
stage: 'local',
|
|
231
|
+
retryable: false
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function atomicReplaceFile(outputPath, bytes) {
|
|
237
|
+
const tempPath = path.join(path.dirname(outputPath), `.${path.basename(outputPath)}.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
238
|
+
let fd;
|
|
239
|
+
try {
|
|
240
|
+
fd = fs.openSync(tempPath, 'wx', 0o600);
|
|
241
|
+
fs.writeFileSync(fd, bytes);
|
|
242
|
+
fs.fsyncSync(fd);
|
|
243
|
+
fs.closeSync(fd);
|
|
244
|
+
fd = undefined;
|
|
245
|
+
fs.renameSync(tempPath, outputPath);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (fd !== undefined) {
|
|
248
|
+
try {
|
|
249
|
+
fs.closeSync(fd);
|
|
250
|
+
} catch {
|
|
251
|
+
// 忽略关闭临时文件失败,下面会清理临时路径。
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
fs.rmSync(tempPath, { force: true });
|
|
256
|
+
} catch {
|
|
257
|
+
// 忽略清理失败,返回主要写入错误。
|
|
258
|
+
}
|
|
259
|
+
throw new ImageMCPError(`图片文件写入失败:${error.message}`, {
|
|
260
|
+
code: 'file_write_failed',
|
|
261
|
+
category: 'file_system',
|
|
262
|
+
stage: 'local',
|
|
204
263
|
retryable: false
|
|
205
264
|
});
|
|
206
265
|
}
|
package/lib/image-mcp/image.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
redactSecret,
|
|
9
9
|
serializeImageError
|
|
10
10
|
} from './errors.js';
|
|
11
|
-
import { DEFAULT_IMAGE_FORMAT, normalizeImageFormat, writeImageOutput } from './files.js';
|
|
11
|
+
import { DEFAULT_IMAGE_FORMAT, localImageToDataURL, normalizeImageFormat, writeImageOutput } from './files.js';
|
|
12
12
|
|
|
13
13
|
export const DEFAULT_BASE_URL = 'https://api.gpteamservices.com/v1';
|
|
14
14
|
export const DEFAULT_IMAGE_MODEL = 'gpt-image-2';
|
|
@@ -17,11 +17,15 @@ export { DEFAULT_IMAGE_FORMAT };
|
|
|
17
17
|
const defaultMaxAttempts = 3;
|
|
18
18
|
const defaultRetryDelayMs = 800;
|
|
19
19
|
const defaultRequestTimeoutMs = 5 * 60 * 1000;
|
|
20
|
+
const defaultMaxConcurrentJobs = 2;
|
|
21
|
+
const defaultMaxQueuedJobs = 20;
|
|
22
|
+
const defaultJobTTLMS = 30 * 60 * 1000;
|
|
23
|
+
const terminalStatuses = new Set(['succeeded', 'failed', 'canceled', 'expired']);
|
|
20
24
|
|
|
21
25
|
const defaultJobStore = createImageJobStore();
|
|
22
26
|
|
|
23
|
-
export function buildImageGenerationPayload(input = {}) {
|
|
24
|
-
|
|
27
|
+
export function buildImageGenerationPayload(input = {}, options = {}) {
|
|
28
|
+
const payload = {
|
|
25
29
|
model: String(input.model || DEFAULT_IMAGE_MODEL),
|
|
26
30
|
prompt: String(input.prompt || '').trim(),
|
|
27
31
|
response_format: 'b64_json',
|
|
@@ -29,6 +33,15 @@ export function buildImageGenerationPayload(input = {}) {
|
|
|
29
33
|
quality: String(input.quality || 'high'),
|
|
30
34
|
output_format: normalizeImageFormat(input.format || input.output_format || DEFAULT_IMAGE_FORMAT)
|
|
31
35
|
};
|
|
36
|
+
const imageOptions = { ...options, home: options.home };
|
|
37
|
+
const images = normalizeInputImages(input.images || input.image, imageOptions);
|
|
38
|
+
if (images.length > 0) payload.images = images.map((imageURL) => ({ image_url: imageURL }));
|
|
39
|
+
const mask = normalizeImageReference(input.mask, imageOptions);
|
|
40
|
+
if (mask) payload.mask = { image_url: mask };
|
|
41
|
+
copyOptionalImageToolOption(payload, input, 'background');
|
|
42
|
+
copyOptionalImageToolOption(payload, input, 'moderation');
|
|
43
|
+
copyOptionalImageToolOption(payload, input, 'input_fidelity');
|
|
44
|
+
return payload;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
export function loadGPTeamCredentials(options = {}) {
|
|
@@ -42,6 +55,7 @@ export function loadGPTeamCredentials(options = {}) {
|
|
|
42
55
|
throw new ImageMCPError('没有找到 GPTeam API key。请在 MCP 配置 env 中设置 GPTEAM_API_KEY,或先运行 npx gpteam 完成本地配置。', {
|
|
43
56
|
code: 'api_key_missing',
|
|
44
57
|
category: 'configuration',
|
|
58
|
+
stage: 'configuration',
|
|
45
59
|
retryable: false
|
|
46
60
|
});
|
|
47
61
|
}
|
|
@@ -79,16 +93,18 @@ export async function generateImage(input = {}, options = {}) {
|
|
|
79
93
|
throw new ImageMCPError('prompt 不能为空', {
|
|
80
94
|
code: 'prompt_required',
|
|
81
95
|
category: 'parameter',
|
|
96
|
+
stage: 'validate',
|
|
82
97
|
retryable: false
|
|
83
98
|
});
|
|
84
99
|
}
|
|
85
100
|
const credentials = loadGPTeamCredentials(options);
|
|
86
|
-
const payload = buildImageGenerationPayload(input);
|
|
101
|
+
const payload = buildImageGenerationPayload(input, options);
|
|
87
102
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
88
103
|
if (typeof fetchImpl !== 'function') {
|
|
89
104
|
throw new ImageMCPError('当前 Node.js 运行时不支持 fetch,请升级到 Node.js 18.18 或更高版本。', {
|
|
90
105
|
code: 'fetch_unavailable',
|
|
91
106
|
category: 'environment',
|
|
107
|
+
stage: 'configuration',
|
|
92
108
|
retryable: false
|
|
93
109
|
});
|
|
94
110
|
}
|
|
@@ -99,6 +115,7 @@ export async function generateImage(input = {}, options = {}) {
|
|
|
99
115
|
const file = writeImageOutput({
|
|
100
116
|
b64: result.b64,
|
|
101
117
|
output_path: input.output_path,
|
|
118
|
+
overwrite: input.overwrite,
|
|
102
119
|
format
|
|
103
120
|
}, options);
|
|
104
121
|
const includeRevisedPrompt = resolveRevisedPromptFlag(input);
|
|
@@ -111,37 +128,65 @@ export async function generateImage(input = {}, options = {}) {
|
|
|
111
128
|
b64: result.b64,
|
|
112
129
|
revisedPrompt: includeRevisedPrompt ? result.revisedPrompt : '',
|
|
113
130
|
retryCount: result.retryCount,
|
|
114
|
-
durationMs
|
|
131
|
+
durationMs,
|
|
132
|
+
idempotencyKey: normalizeIdempotencyKey(input.idempotency_key)
|
|
115
133
|
});
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
export function createImageJobStore(options = {}) {
|
|
119
137
|
return {
|
|
120
138
|
jobs: new Map(),
|
|
139
|
+
queue: [],
|
|
140
|
+
runningCount: 0,
|
|
141
|
+
idempotency: new Map(),
|
|
121
142
|
now: typeof options.now === 'function' ? options.now : Date.now,
|
|
122
|
-
ttlMs: Number(options.ttlMs ||
|
|
143
|
+
ttlMs: Number(options.ttlMs || defaultJobTTLMS),
|
|
144
|
+
maxConcurrent: Number.isFinite(options.maxConcurrent) ? options.maxConcurrent : defaultMaxConcurrentJobs,
|
|
145
|
+
maxQueue: Number.isFinite(options.maxQueue) ? options.maxQueue : defaultMaxQueuedJobs
|
|
123
146
|
};
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
export function createImageJob(input = {}, options = {}) {
|
|
127
150
|
const store = options.store || defaultJobStore;
|
|
151
|
+
configureJobStore(store, options);
|
|
128
152
|
cleanupImageJobs(store);
|
|
153
|
+
const idempotencyKey = normalizeIdempotencyKey(input.idempotency_key);
|
|
154
|
+
if (idempotencyKey) {
|
|
155
|
+
const existingJobID = store.idempotency.get(idempotencyKey);
|
|
156
|
+
const existingJob = existingJobID ? store.jobs.get(existingJobID) : null;
|
|
157
|
+
if (existingJob) return publicJobStatus(existingJob);
|
|
158
|
+
store.idempotency.delete(idempotencyKey);
|
|
159
|
+
}
|
|
160
|
+
if ((queuedJobCount(store) + store.runningCount) >= (store.maxConcurrent + store.maxQueue)) {
|
|
161
|
+
return resultFromError(new ImageMCPError('图片任务队列已满,请稍后重试。', {
|
|
162
|
+
code: 'queue_full',
|
|
163
|
+
category: 'queue',
|
|
164
|
+
stage: 'queue',
|
|
165
|
+
retryable: true
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
129
168
|
const jobID = makeID('img');
|
|
130
169
|
const traceID = makeID('tr');
|
|
131
170
|
const controller = new AbortController();
|
|
132
171
|
const job = {
|
|
133
172
|
job_id: jobID,
|
|
134
173
|
trace_id: traceID,
|
|
174
|
+
idempotency_key: idempotencyKey,
|
|
135
175
|
status: 'queued',
|
|
136
176
|
ok: true,
|
|
137
177
|
created_at: new Date(store.now()).toISOString(),
|
|
138
178
|
updated_at: new Date(store.now()).toISOString(),
|
|
179
|
+
expires_at: new Date(store.now() + store.ttlMs).toISOString(),
|
|
139
180
|
controller,
|
|
181
|
+
input: { ...input, idempotency_key: idempotencyKey },
|
|
182
|
+
options,
|
|
140
183
|
result: null,
|
|
141
184
|
error: null
|
|
142
185
|
};
|
|
143
186
|
store.jobs.set(jobID, job);
|
|
144
|
-
|
|
187
|
+
if (idempotencyKey) store.idempotency.set(idempotencyKey, jobID);
|
|
188
|
+
store.queue.push(jobID);
|
|
189
|
+
queueMicrotask(() => scheduleImageJobs(store));
|
|
145
190
|
return publicJobStatus(job);
|
|
146
191
|
}
|
|
147
192
|
|
|
@@ -157,18 +202,26 @@ export function cancelImageJob(input = {}, options = {}) {
|
|
|
157
202
|
const store = options.store || defaultJobStore;
|
|
158
203
|
const job = store.jobs.get(String(input.job_id || ''));
|
|
159
204
|
if (!job) return missingJobResult(input.job_id);
|
|
160
|
-
if (job.status
|
|
205
|
+
if (isTerminalStatus(job.status)) return publicJobStatus(job);
|
|
161
206
|
job.controller.abort();
|
|
162
|
-
job.status = '
|
|
207
|
+
job.status = 'canceled';
|
|
163
208
|
job.ok = false;
|
|
164
209
|
job.updated_at = new Date(store.now()).toISOString();
|
|
165
210
|
job.error = {
|
|
166
211
|
code: 'job_cancelled',
|
|
167
|
-
|
|
212
|
+
message: '图片生成任务已取消。',
|
|
168
213
|
retryable: false,
|
|
169
|
-
|
|
214
|
+
stage: 'cancel',
|
|
215
|
+
upstream_status: undefined,
|
|
216
|
+
trace_id: job.trace_id,
|
|
217
|
+
category: 'canceled'
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
...publicJobStatus(job),
|
|
221
|
+
ok: true,
|
|
222
|
+
cancellation_mode: 'best_effort',
|
|
223
|
+
cancellation_note: '已中止本地请求。若上游已经开始生成,无法保证上游也同步取消。'
|
|
170
224
|
};
|
|
171
|
-
return { ...publicJobStatus(job), ok: true };
|
|
172
225
|
}
|
|
173
226
|
|
|
174
227
|
export function downloadImageResult(input = {}, options = {}) {
|
|
@@ -176,12 +229,23 @@ export function downloadImageResult(input = {}, options = {}) {
|
|
|
176
229
|
const job = store.jobs.get(String(input.job_id || ''));
|
|
177
230
|
if (!job) return missingJobResult(input.job_id);
|
|
178
231
|
if (job.status !== 'succeeded') return publicJobStatus(job);
|
|
179
|
-
return
|
|
232
|
+
return shapeDownloadResult(job.result, input);
|
|
180
233
|
}
|
|
181
234
|
|
|
182
235
|
export function structuredToolResult(result) {
|
|
183
236
|
if (!result || typeof result !== 'object') {
|
|
184
|
-
return {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
error: {
|
|
240
|
+
code: 'empty_result',
|
|
241
|
+
message: 'empty result',
|
|
242
|
+
retryable: false,
|
|
243
|
+
stage: 'unknown',
|
|
244
|
+
upstream_status: undefined,
|
|
245
|
+
trace_id: '',
|
|
246
|
+
category: 'unknown'
|
|
247
|
+
}
|
|
248
|
+
};
|
|
185
249
|
}
|
|
186
250
|
const clone = { ...result };
|
|
187
251
|
delete clone.b64;
|
|
@@ -206,7 +270,32 @@ export function resultFromError(error, meta = {}) {
|
|
|
206
270
|
job_id: meta.job_id || '',
|
|
207
271
|
trace_id: meta.trace_id || '',
|
|
208
272
|
status: 'failed',
|
|
209
|
-
error: serializeImageError(error)
|
|
273
|
+
error: serializeImageError(error, meta)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getCapabilities(options = {}) {
|
|
278
|
+
const env = options.env || process.env;
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
model: DEFAULT_IMAGE_MODEL,
|
|
282
|
+
default_model: DEFAULT_IMAGE_MODEL,
|
|
283
|
+
supports_async: true,
|
|
284
|
+
supports_cancel: true,
|
|
285
|
+
cancel_semantics: 'best_effort',
|
|
286
|
+
supports_idempotency_key: true,
|
|
287
|
+
supports_image_to_image: true,
|
|
288
|
+
supports_mask: true,
|
|
289
|
+
sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'],
|
|
290
|
+
formats: ['png', 'jpeg', 'webp'],
|
|
291
|
+
quality: ['low', 'medium', 'high', 'auto'],
|
|
292
|
+
max_prompt_length: 32000,
|
|
293
|
+
statuses: ['queued', 'running', 'succeeded', 'failed', 'canceled', 'expired'],
|
|
294
|
+
default_output_format: DEFAULT_IMAGE_FORMAT,
|
|
295
|
+
default_request_timeout_ms: resolveBoundedInt(1, env.GPTEAM_IMAGE_REQUEST_TIMEOUT_MS, defaultRequestTimeoutMs),
|
|
296
|
+
default_max_attempts: resolveBoundedInt(1, env.GPTEAM_IMAGE_MAX_ATTEMPTS, defaultMaxAttempts),
|
|
297
|
+
max_concurrent_jobs: resolveBoundedInt(1, env.GPTEAM_IMAGE_MAX_CONCURRENT, defaultMaxConcurrentJobs),
|
|
298
|
+
max_queued_jobs: resolveBoundedInt(0, env.GPTEAM_IMAGE_MAX_QUEUE, defaultMaxQueuedJobs)
|
|
210
299
|
};
|
|
211
300
|
}
|
|
212
301
|
|
|
@@ -230,8 +319,9 @@ async function fetchImageWithRetry(fetchImpl, credentials, payload, options) {
|
|
|
230
319
|
|
|
231
320
|
async function fetchImageOnce(fetchImpl, credentials, payload, options) {
|
|
232
321
|
const requestSignal = createRequestSignal(options);
|
|
322
|
+
const endpoint = imageEndpointFromPayload(payload);
|
|
233
323
|
try {
|
|
234
|
-
const response = await fetchImpl(`${credentials.baseUrl}
|
|
324
|
+
const response = await fetchImpl(`${credentials.baseUrl}${endpoint}`, {
|
|
235
325
|
method: 'POST',
|
|
236
326
|
headers: {
|
|
237
327
|
Authorization: `Bearer ${credentials.apiKey}`,
|
|
@@ -246,9 +336,10 @@ async function fetchImageOnce(fetchImpl, credentials, payload, options) {
|
|
|
246
336
|
const b64 = first && typeof first.b64_json === 'string' ? first.b64_json : '';
|
|
247
337
|
if (!b64) {
|
|
248
338
|
throw new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
339
|
+
code: 'image_data_missing',
|
|
340
|
+
category: 'response_invalid',
|
|
341
|
+
stage: 'local',
|
|
342
|
+
retryable: false
|
|
252
343
|
});
|
|
253
344
|
}
|
|
254
345
|
return {
|
|
@@ -272,8 +363,10 @@ function buildSuccessResult(input) {
|
|
|
272
363
|
ok: true,
|
|
273
364
|
status: 'succeeded',
|
|
274
365
|
file: input.file.file,
|
|
366
|
+
final_file: input.file.final_file,
|
|
275
367
|
path: input.file.file,
|
|
276
368
|
model: input.payload.model,
|
|
369
|
+
action: imageActionFromPayload(input.payload),
|
|
277
370
|
size: input.payload.size,
|
|
278
371
|
format: input.file.format,
|
|
279
372
|
output_format: input.file.format,
|
|
@@ -288,6 +381,7 @@ function buildSuccessResult(input) {
|
|
|
288
381
|
retry_count: input.retryCount,
|
|
289
382
|
job_id: input.jobID,
|
|
290
383
|
trace_id: input.traceID,
|
|
384
|
+
idempotency_key: input.idempotencyKey || undefined,
|
|
291
385
|
b64: input.b64
|
|
292
386
|
};
|
|
293
387
|
if (input.revisedPrompt) {
|
|
@@ -297,38 +391,58 @@ function buildSuccessResult(input) {
|
|
|
297
391
|
return result;
|
|
298
392
|
}
|
|
299
393
|
|
|
300
|
-
|
|
301
|
-
|
|
394
|
+
function scheduleImageJobs(store) {
|
|
395
|
+
cleanupImageJobs(store);
|
|
396
|
+
while (store.runningCount < store.maxConcurrent && store.queue.length > 0) {
|
|
397
|
+
const jobID = store.queue.shift();
|
|
398
|
+
const job = store.jobs.get(jobID);
|
|
399
|
+
if (!job || job.status !== 'queued') continue;
|
|
400
|
+
store.runningCount += 1;
|
|
401
|
+
queueMicrotask(() => runImageJob(store, job));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function runImageJob(store, job) {
|
|
406
|
+
if (job.status === 'canceled') {
|
|
407
|
+
store.runningCount = Math.max(0, store.runningCount - 1);
|
|
408
|
+
scheduleImageJobs(store);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
302
411
|
job.status = 'running';
|
|
303
412
|
job.updated_at = new Date(store.now()).toISOString();
|
|
304
413
|
try {
|
|
305
|
-
const result = await generateImage({ ...input, job_id: job.job_id, trace_id: job.trace_id }, {
|
|
306
|
-
...options,
|
|
414
|
+
const result = await generateImage({ ...job.input, job_id: job.job_id, trace_id: job.trace_id }, {
|
|
415
|
+
...job.options,
|
|
307
416
|
signal: job.controller.signal
|
|
308
417
|
});
|
|
309
|
-
if (job.status === '
|
|
418
|
+
if (job.status === 'canceled') return;
|
|
310
419
|
job.status = 'succeeded';
|
|
311
420
|
job.ok = true;
|
|
312
421
|
job.result = result;
|
|
313
422
|
} catch (error) {
|
|
314
|
-
if (job.status !== '
|
|
423
|
+
if (job.status !== 'canceled') {
|
|
315
424
|
job.status = 'failed';
|
|
316
425
|
job.ok = false;
|
|
317
|
-
job.error = serializeImageError(error);
|
|
426
|
+
job.error = serializeImageError(error, { trace_id: job.trace_id });
|
|
318
427
|
}
|
|
319
428
|
} finally {
|
|
320
429
|
job.updated_at = new Date(store.now()).toISOString();
|
|
430
|
+
store.runningCount = Math.max(0, store.runningCount - 1);
|
|
431
|
+
scheduleImageJobs(store);
|
|
321
432
|
}
|
|
322
433
|
}
|
|
323
434
|
|
|
324
435
|
function publicJobStatus(job) {
|
|
325
436
|
const base = {
|
|
326
|
-
ok: job.status !== 'failed' && job.status !== '
|
|
437
|
+
ok: job.status !== 'failed' && job.status !== 'canceled' && job.status !== 'expired',
|
|
327
438
|
job_id: job.job_id,
|
|
328
439
|
trace_id: job.trace_id,
|
|
440
|
+
idempotency_key: job.idempotency_key || undefined,
|
|
329
441
|
status: job.status,
|
|
442
|
+
legacy_status: job.status === 'canceled' ? 'cancelled' : undefined,
|
|
330
443
|
created_at: job.created_at,
|
|
331
|
-
updated_at: job.updated_at
|
|
444
|
+
updated_at: job.updated_at,
|
|
445
|
+
expires_at: job.expires_at
|
|
332
446
|
};
|
|
333
447
|
if (job.result) return { ...base, ...structuredToolResult(job.result) };
|
|
334
448
|
if (job.error) return { ...base, error: job.error };
|
|
@@ -342,9 +456,12 @@ function missingJobResult(jobID) {
|
|
|
342
456
|
status: 'not_found',
|
|
343
457
|
error: {
|
|
344
458
|
code: 'job_not_found',
|
|
345
|
-
|
|
459
|
+
message: '没有找到这个图片任务。',
|
|
346
460
|
retryable: false,
|
|
347
|
-
|
|
461
|
+
stage: 'lookup',
|
|
462
|
+
upstream_status: undefined,
|
|
463
|
+
trace_id: '',
|
|
464
|
+
category: 'not_found'
|
|
348
465
|
}
|
|
349
466
|
};
|
|
350
467
|
}
|
|
@@ -352,10 +469,116 @@ function missingJobResult(jobID) {
|
|
|
352
469
|
function cleanupImageJobs(store) {
|
|
353
470
|
const threshold = store.now() - store.ttlMs;
|
|
354
471
|
for (const [jobID, job] of store.jobs.entries()) {
|
|
472
|
+
const created = Date.parse(job.created_at || '');
|
|
473
|
+
if (job.status === 'queued' && Number.isFinite(created) && created < threshold) {
|
|
474
|
+
job.status = 'expired';
|
|
475
|
+
job.ok = false;
|
|
476
|
+
job.updated_at = new Date(store.now()).toISOString();
|
|
477
|
+
job.error = {
|
|
478
|
+
code: 'job_expired',
|
|
479
|
+
message: '图片任务已过期,请重新创建任务。',
|
|
480
|
+
retryable: false,
|
|
481
|
+
stage: 'queue',
|
|
482
|
+
upstream_status: undefined,
|
|
483
|
+
trace_id: job.trace_id,
|
|
484
|
+
category: 'queue'
|
|
485
|
+
};
|
|
486
|
+
}
|
|
355
487
|
if (job.status === 'queued' || job.status === 'running') continue;
|
|
356
488
|
const updated = Date.parse(job.updated_at || job.created_at || '');
|
|
357
|
-
if (Number.isFinite(updated) && updated < threshold) store
|
|
489
|
+
if (Number.isFinite(updated) && updated < threshold) removeImageJob(store, jobID, job);
|
|
358
490
|
}
|
|
491
|
+
store.queue = store.queue.filter((jobID) => {
|
|
492
|
+
const job = store.jobs.get(jobID);
|
|
493
|
+
return job && job.status === 'queued';
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function configureJobStore(store, options = {}) {
|
|
498
|
+
const env = options.env || process.env;
|
|
499
|
+
store.maxConcurrent = resolveBoundedInt(1, options.maxConcurrent, env.GPTEAM_IMAGE_MAX_CONCURRENT, store.maxConcurrent || defaultMaxConcurrentJobs);
|
|
500
|
+
store.maxQueue = resolveBoundedInt(0, options.maxQueue, env.GPTEAM_IMAGE_MAX_QUEUE, store.maxQueue || defaultMaxQueuedJobs);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function queuedJobCount(store) {
|
|
504
|
+
return store.queue.filter((jobID) => {
|
|
505
|
+
const job = store.jobs.get(jobID);
|
|
506
|
+
return job && job.status === 'queued';
|
|
507
|
+
}).length;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function removeImageJob(store, jobID, job) {
|
|
511
|
+
store.jobs.delete(jobID);
|
|
512
|
+
if (job && job.idempotency_key && store.idempotency.get(job.idempotency_key) === jobID) {
|
|
513
|
+
store.idempotency.delete(job.idempotency_key);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function isTerminalStatus(status) {
|
|
518
|
+
return terminalStatuses.has(String(status || ''));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function normalizeIdempotencyKey(value) {
|
|
522
|
+
return String(value || '').trim().slice(0, 200);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function shapeDownloadResult(result, input = {}) {
|
|
526
|
+
const includeImage = !input.metadata_only && input.include_image !== false;
|
|
527
|
+
const includeRevisedPrompt = input.include_revised_prompt !== false;
|
|
528
|
+
const output = {
|
|
529
|
+
...result,
|
|
530
|
+
status: 'succeeded'
|
|
531
|
+
};
|
|
532
|
+
if (!includeImage) {
|
|
533
|
+
delete output.b64;
|
|
534
|
+
delete output.mimeType;
|
|
535
|
+
}
|
|
536
|
+
if (!includeRevisedPrompt) {
|
|
537
|
+
delete output.revised_prompt;
|
|
538
|
+
delete output.revisedPrompt;
|
|
539
|
+
}
|
|
540
|
+
return output;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function imageEndpointFromPayload(payload) {
|
|
544
|
+
return imageActionFromPayload(payload) === 'edit' ? '/images/edits' : '/images/generations';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function imageActionFromPayload(payload) {
|
|
548
|
+
return Array.isArray(payload.images) && payload.images.length > 0 ? 'edit' : 'generate';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function normalizeInputImages(value, options = {}) {
|
|
552
|
+
const rawImages = Array.isArray(value) ? value : (value ? [value] : []);
|
|
553
|
+
return rawImages.map((item) => normalizeImageReference(item, options)).filter(Boolean);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function normalizeImageReference(value, options = {}) {
|
|
557
|
+
if (!value) return '';
|
|
558
|
+
if (typeof value === 'object') {
|
|
559
|
+
return normalizeImageReference(value.image_url || value.url || value.path || value.file, options);
|
|
560
|
+
}
|
|
561
|
+
const text = String(value || '').trim();
|
|
562
|
+
if (!text) return '';
|
|
563
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(text)) return text;
|
|
564
|
+
if (/^https?:\/\//i.test(text)) return text;
|
|
565
|
+
try {
|
|
566
|
+
return localImageToDataURL(text, options);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw new ImageMCPError(`读取输入图片失败:${error.message}`, {
|
|
569
|
+
code: 'input_image_read_failed',
|
|
570
|
+
category: 'file_system',
|
|
571
|
+
stage: 'local',
|
|
572
|
+
retryable: false
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function copyOptionalImageToolOption(payload, input, key) {
|
|
578
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) return;
|
|
579
|
+
const value = input[key];
|
|
580
|
+
if (value === undefined || value === null || value === '') return;
|
|
581
|
+
payload[key] = value;
|
|
359
582
|
}
|
|
360
583
|
|
|
361
584
|
function createRequestSignal(options = {}) {
|
package/lib/image-mcp/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createImageJob,
|
|
8
8
|
downloadImageResult,
|
|
9
9
|
generateImage,
|
|
10
|
+
getCapabilities,
|
|
10
11
|
getImageJobStatus,
|
|
11
12
|
resultFromError,
|
|
12
13
|
structuredToolResult,
|
|
@@ -38,6 +39,29 @@ const imageInputProperties = {
|
|
|
38
39
|
type: 'string',
|
|
39
40
|
description: 'Optional output file path or directory.'
|
|
40
41
|
},
|
|
42
|
+
overwrite: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Overwrite output_path when it already exists. Defaults to false.',
|
|
45
|
+
default: false
|
|
46
|
+
},
|
|
47
|
+
idempotency_key: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Optional idempotency key. Reusing it returns the same local job in this MCP process.'
|
|
50
|
+
},
|
|
51
|
+
images: {
|
|
52
|
+
type: 'array',
|
|
53
|
+
items: { type: 'string' },
|
|
54
|
+
description: 'Optional input images for image-to-image or edit. Values may be data URLs, HTTPS URLs, or local file paths.'
|
|
55
|
+
},
|
|
56
|
+
mask: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Optional mask image for image edit. Value may be a data URL, HTTPS URL, or local file path.'
|
|
59
|
+
},
|
|
60
|
+
input_fidelity: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Optional image edit fidelity.',
|
|
63
|
+
enum: ['low', 'high']
|
|
64
|
+
},
|
|
41
65
|
return_revised_prompt: {
|
|
42
66
|
type: 'boolean',
|
|
43
67
|
description: 'Return upstream revised prompt when available.',
|
|
@@ -69,7 +93,16 @@ const tools = [
|
|
|
69
93
|
{
|
|
70
94
|
name: 'download_image_result',
|
|
71
95
|
description: 'Return the saved local file metadata and image content for a completed image job.',
|
|
72
|
-
inputSchema:
|
|
96
|
+
inputSchema: downloadSchema()
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'get_capabilities',
|
|
100
|
+
description: 'Return GPTeam Image MCP capabilities, supported sizes, formats, quality levels, async support, and queue limits.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {},
|
|
104
|
+
additionalProperties: false
|
|
105
|
+
}
|
|
73
106
|
},
|
|
74
107
|
{
|
|
75
108
|
name: 'generate_image',
|
|
@@ -134,6 +167,8 @@ export async function callImageTool(toolName, args = {}, deps = {}) {
|
|
|
134
167
|
return cancelImageJob(args || {}, deps);
|
|
135
168
|
case 'download_image_result':
|
|
136
169
|
return downloadImageResult(args || {}, deps);
|
|
170
|
+
case 'get_capabilities':
|
|
171
|
+
return getCapabilities(deps);
|
|
137
172
|
case 'generate_image':
|
|
138
173
|
return await generateImage(args || {}, deps);
|
|
139
174
|
default:
|
|
@@ -145,6 +180,26 @@ export async function callImageTool(toolName, args = {}, deps = {}) {
|
|
|
145
180
|
}
|
|
146
181
|
}
|
|
147
182
|
|
|
183
|
+
function downloadSchema() {
|
|
184
|
+
const schema = jobIDSchema();
|
|
185
|
+
schema.properties.metadata_only = {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
description: 'Return only metadata without MCP image content.',
|
|
188
|
+
default: false
|
|
189
|
+
};
|
|
190
|
+
schema.properties.include_image = {
|
|
191
|
+
type: 'boolean',
|
|
192
|
+
description: 'Include image content when the job succeeded.',
|
|
193
|
+
default: true
|
|
194
|
+
};
|
|
195
|
+
schema.properties.include_revised_prompt = {
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
description: 'Include revised prompt when available.',
|
|
198
|
+
default: true
|
|
199
|
+
};
|
|
200
|
+
return schema;
|
|
201
|
+
}
|
|
202
|
+
|
|
148
203
|
function jobIDSchema() {
|
|
149
204
|
return {
|
|
150
205
|
type: 'object',
|