gpteam 0.1.18 → 0.1.20
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 +253 -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. `input_fidelity` is accepted for compatibility but is not forwarded to the current GPTeam Image 2 bridge because upstream Codex image edits reject it. 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.20';
|
|
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,14 @@ 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
|
+
return payload;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export function loadGPTeamCredentials(options = {}) {
|
|
@@ -42,6 +54,7 @@ export function loadGPTeamCredentials(options = {}) {
|
|
|
42
54
|
throw new ImageMCPError('没有找到 GPTeam API key。请在 MCP 配置 env 中设置 GPTEAM_API_KEY,或先运行 npx gpteam 完成本地配置。', {
|
|
43
55
|
code: 'api_key_missing',
|
|
44
56
|
category: 'configuration',
|
|
57
|
+
stage: 'configuration',
|
|
45
58
|
retryable: false
|
|
46
59
|
});
|
|
47
60
|
}
|
|
@@ -79,16 +92,18 @@ export async function generateImage(input = {}, options = {}) {
|
|
|
79
92
|
throw new ImageMCPError('prompt 不能为空', {
|
|
80
93
|
code: 'prompt_required',
|
|
81
94
|
category: 'parameter',
|
|
95
|
+
stage: 'validate',
|
|
82
96
|
retryable: false
|
|
83
97
|
});
|
|
84
98
|
}
|
|
85
99
|
const credentials = loadGPTeamCredentials(options);
|
|
86
|
-
const payload = buildImageGenerationPayload(input);
|
|
100
|
+
const payload = buildImageGenerationPayload(input, options);
|
|
87
101
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
88
102
|
if (typeof fetchImpl !== 'function') {
|
|
89
103
|
throw new ImageMCPError('当前 Node.js 运行时不支持 fetch,请升级到 Node.js 18.18 或更高版本。', {
|
|
90
104
|
code: 'fetch_unavailable',
|
|
91
105
|
category: 'environment',
|
|
106
|
+
stage: 'configuration',
|
|
92
107
|
retryable: false
|
|
93
108
|
});
|
|
94
109
|
}
|
|
@@ -99,6 +114,7 @@ export async function generateImage(input = {}, options = {}) {
|
|
|
99
114
|
const file = writeImageOutput({
|
|
100
115
|
b64: result.b64,
|
|
101
116
|
output_path: input.output_path,
|
|
117
|
+
overwrite: input.overwrite,
|
|
102
118
|
format
|
|
103
119
|
}, options);
|
|
104
120
|
const includeRevisedPrompt = resolveRevisedPromptFlag(input);
|
|
@@ -111,37 +127,65 @@ export async function generateImage(input = {}, options = {}) {
|
|
|
111
127
|
b64: result.b64,
|
|
112
128
|
revisedPrompt: includeRevisedPrompt ? result.revisedPrompt : '',
|
|
113
129
|
retryCount: result.retryCount,
|
|
114
|
-
durationMs
|
|
130
|
+
durationMs,
|
|
131
|
+
idempotencyKey: normalizeIdempotencyKey(input.idempotency_key)
|
|
115
132
|
});
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
export function createImageJobStore(options = {}) {
|
|
119
136
|
return {
|
|
120
137
|
jobs: new Map(),
|
|
138
|
+
queue: [],
|
|
139
|
+
runningCount: 0,
|
|
140
|
+
idempotency: new Map(),
|
|
121
141
|
now: typeof options.now === 'function' ? options.now : Date.now,
|
|
122
|
-
ttlMs: Number(options.ttlMs ||
|
|
142
|
+
ttlMs: Number(options.ttlMs || defaultJobTTLMS),
|
|
143
|
+
maxConcurrent: Number.isFinite(options.maxConcurrent) ? options.maxConcurrent : defaultMaxConcurrentJobs,
|
|
144
|
+
maxQueue: Number.isFinite(options.maxQueue) ? options.maxQueue : defaultMaxQueuedJobs
|
|
123
145
|
};
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
export function createImageJob(input = {}, options = {}) {
|
|
127
149
|
const store = options.store || defaultJobStore;
|
|
150
|
+
configureJobStore(store, options);
|
|
128
151
|
cleanupImageJobs(store);
|
|
152
|
+
const idempotencyKey = normalizeIdempotencyKey(input.idempotency_key);
|
|
153
|
+
if (idempotencyKey) {
|
|
154
|
+
const existingJobID = store.idempotency.get(idempotencyKey);
|
|
155
|
+
const existingJob = existingJobID ? store.jobs.get(existingJobID) : null;
|
|
156
|
+
if (existingJob) return publicJobStatus(existingJob);
|
|
157
|
+
store.idempotency.delete(idempotencyKey);
|
|
158
|
+
}
|
|
159
|
+
if ((queuedJobCount(store) + store.runningCount) >= (store.maxConcurrent + store.maxQueue)) {
|
|
160
|
+
return resultFromError(new ImageMCPError('图片任务队列已满,请稍后重试。', {
|
|
161
|
+
code: 'queue_full',
|
|
162
|
+
category: 'queue',
|
|
163
|
+
stage: 'queue',
|
|
164
|
+
retryable: true
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
129
167
|
const jobID = makeID('img');
|
|
130
168
|
const traceID = makeID('tr');
|
|
131
169
|
const controller = new AbortController();
|
|
132
170
|
const job = {
|
|
133
171
|
job_id: jobID,
|
|
134
172
|
trace_id: traceID,
|
|
173
|
+
idempotency_key: idempotencyKey,
|
|
135
174
|
status: 'queued',
|
|
136
175
|
ok: true,
|
|
137
176
|
created_at: new Date(store.now()).toISOString(),
|
|
138
177
|
updated_at: new Date(store.now()).toISOString(),
|
|
178
|
+
expires_at: new Date(store.now() + store.ttlMs).toISOString(),
|
|
139
179
|
controller,
|
|
180
|
+
input: { ...input, idempotency_key: idempotencyKey },
|
|
181
|
+
options,
|
|
140
182
|
result: null,
|
|
141
183
|
error: null
|
|
142
184
|
};
|
|
143
185
|
store.jobs.set(jobID, job);
|
|
144
|
-
|
|
186
|
+
if (idempotencyKey) store.idempotency.set(idempotencyKey, jobID);
|
|
187
|
+
store.queue.push(jobID);
|
|
188
|
+
queueMicrotask(() => scheduleImageJobs(store));
|
|
145
189
|
return publicJobStatus(job);
|
|
146
190
|
}
|
|
147
191
|
|
|
@@ -157,18 +201,26 @@ export function cancelImageJob(input = {}, options = {}) {
|
|
|
157
201
|
const store = options.store || defaultJobStore;
|
|
158
202
|
const job = store.jobs.get(String(input.job_id || ''));
|
|
159
203
|
if (!job) return missingJobResult(input.job_id);
|
|
160
|
-
if (job.status
|
|
204
|
+
if (isTerminalStatus(job.status)) return publicJobStatus(job);
|
|
161
205
|
job.controller.abort();
|
|
162
|
-
job.status = '
|
|
206
|
+
job.status = 'canceled';
|
|
163
207
|
job.ok = false;
|
|
164
208
|
job.updated_at = new Date(store.now()).toISOString();
|
|
165
209
|
job.error = {
|
|
166
210
|
code: 'job_cancelled',
|
|
167
|
-
|
|
211
|
+
message: '图片生成任务已取消。',
|
|
168
212
|
retryable: false,
|
|
169
|
-
|
|
213
|
+
stage: 'cancel',
|
|
214
|
+
upstream_status: undefined,
|
|
215
|
+
trace_id: job.trace_id,
|
|
216
|
+
category: 'canceled'
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
...publicJobStatus(job),
|
|
220
|
+
ok: true,
|
|
221
|
+
cancellation_mode: 'best_effort',
|
|
222
|
+
cancellation_note: '已中止本地请求。若上游已经开始生成,无法保证上游也同步取消。'
|
|
170
223
|
};
|
|
171
|
-
return { ...publicJobStatus(job), ok: true };
|
|
172
224
|
}
|
|
173
225
|
|
|
174
226
|
export function downloadImageResult(input = {}, options = {}) {
|
|
@@ -176,12 +228,23 @@ export function downloadImageResult(input = {}, options = {}) {
|
|
|
176
228
|
const job = store.jobs.get(String(input.job_id || ''));
|
|
177
229
|
if (!job) return missingJobResult(input.job_id);
|
|
178
230
|
if (job.status !== 'succeeded') return publicJobStatus(job);
|
|
179
|
-
return
|
|
231
|
+
return shapeDownloadResult(job.result, input);
|
|
180
232
|
}
|
|
181
233
|
|
|
182
234
|
export function structuredToolResult(result) {
|
|
183
235
|
if (!result || typeof result !== 'object') {
|
|
184
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: {
|
|
239
|
+
code: 'empty_result',
|
|
240
|
+
message: 'empty result',
|
|
241
|
+
retryable: false,
|
|
242
|
+
stage: 'unknown',
|
|
243
|
+
upstream_status: undefined,
|
|
244
|
+
trace_id: '',
|
|
245
|
+
category: 'unknown'
|
|
246
|
+
}
|
|
247
|
+
};
|
|
185
248
|
}
|
|
186
249
|
const clone = { ...result };
|
|
187
250
|
delete clone.b64;
|
|
@@ -206,7 +269,32 @@ export function resultFromError(error, meta = {}) {
|
|
|
206
269
|
job_id: meta.job_id || '',
|
|
207
270
|
trace_id: meta.trace_id || '',
|
|
208
271
|
status: 'failed',
|
|
209
|
-
error: serializeImageError(error)
|
|
272
|
+
error: serializeImageError(error, meta)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function getCapabilities(options = {}) {
|
|
277
|
+
const env = options.env || process.env;
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
model: DEFAULT_IMAGE_MODEL,
|
|
281
|
+
default_model: DEFAULT_IMAGE_MODEL,
|
|
282
|
+
supports_async: true,
|
|
283
|
+
supports_cancel: true,
|
|
284
|
+
cancel_semantics: 'best_effort',
|
|
285
|
+
supports_idempotency_key: true,
|
|
286
|
+
supports_image_to_image: true,
|
|
287
|
+
supports_mask: true,
|
|
288
|
+
sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'],
|
|
289
|
+
formats: ['png', 'jpeg', 'webp'],
|
|
290
|
+
quality: ['low', 'medium', 'high', 'auto'],
|
|
291
|
+
max_prompt_length: 32000,
|
|
292
|
+
statuses: ['queued', 'running', 'succeeded', 'failed', 'canceled', 'expired'],
|
|
293
|
+
default_output_format: DEFAULT_IMAGE_FORMAT,
|
|
294
|
+
default_request_timeout_ms: resolveBoundedInt(1, env.GPTEAM_IMAGE_REQUEST_TIMEOUT_MS, defaultRequestTimeoutMs),
|
|
295
|
+
default_max_attempts: resolveBoundedInt(1, env.GPTEAM_IMAGE_MAX_ATTEMPTS, defaultMaxAttempts),
|
|
296
|
+
max_concurrent_jobs: resolveBoundedInt(1, env.GPTEAM_IMAGE_MAX_CONCURRENT, defaultMaxConcurrentJobs),
|
|
297
|
+
max_queued_jobs: resolveBoundedInt(0, env.GPTEAM_IMAGE_MAX_QUEUE, defaultMaxQueuedJobs)
|
|
210
298
|
};
|
|
211
299
|
}
|
|
212
300
|
|
|
@@ -230,8 +318,9 @@ async function fetchImageWithRetry(fetchImpl, credentials, payload, options) {
|
|
|
230
318
|
|
|
231
319
|
async function fetchImageOnce(fetchImpl, credentials, payload, options) {
|
|
232
320
|
const requestSignal = createRequestSignal(options);
|
|
321
|
+
const endpoint = imageEndpointFromPayload(payload);
|
|
233
322
|
try {
|
|
234
|
-
const response = await fetchImpl(`${credentials.baseUrl}
|
|
323
|
+
const response = await fetchImpl(`${credentials.baseUrl}${endpoint}`, {
|
|
235
324
|
method: 'POST',
|
|
236
325
|
headers: {
|
|
237
326
|
Authorization: `Bearer ${credentials.apiKey}`,
|
|
@@ -246,9 +335,10 @@ async function fetchImageOnce(fetchImpl, credentials, payload, options) {
|
|
|
246
335
|
const b64 = first && typeof first.b64_json === 'string' ? first.b64_json : '';
|
|
247
336
|
if (!b64) {
|
|
248
337
|
throw new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
338
|
+
code: 'image_data_missing',
|
|
339
|
+
category: 'response_invalid',
|
|
340
|
+
stage: 'local',
|
|
341
|
+
retryable: false
|
|
252
342
|
});
|
|
253
343
|
}
|
|
254
344
|
return {
|
|
@@ -272,8 +362,10 @@ function buildSuccessResult(input) {
|
|
|
272
362
|
ok: true,
|
|
273
363
|
status: 'succeeded',
|
|
274
364
|
file: input.file.file,
|
|
365
|
+
final_file: input.file.final_file,
|
|
275
366
|
path: input.file.file,
|
|
276
367
|
model: input.payload.model,
|
|
368
|
+
action: imageActionFromPayload(input.payload),
|
|
277
369
|
size: input.payload.size,
|
|
278
370
|
format: input.file.format,
|
|
279
371
|
output_format: input.file.format,
|
|
@@ -288,6 +380,7 @@ function buildSuccessResult(input) {
|
|
|
288
380
|
retry_count: input.retryCount,
|
|
289
381
|
job_id: input.jobID,
|
|
290
382
|
trace_id: input.traceID,
|
|
383
|
+
idempotency_key: input.idempotencyKey || undefined,
|
|
291
384
|
b64: input.b64
|
|
292
385
|
};
|
|
293
386
|
if (input.revisedPrompt) {
|
|
@@ -297,38 +390,58 @@ function buildSuccessResult(input) {
|
|
|
297
390
|
return result;
|
|
298
391
|
}
|
|
299
392
|
|
|
300
|
-
|
|
301
|
-
|
|
393
|
+
function scheduleImageJobs(store) {
|
|
394
|
+
cleanupImageJobs(store);
|
|
395
|
+
while (store.runningCount < store.maxConcurrent && store.queue.length > 0) {
|
|
396
|
+
const jobID = store.queue.shift();
|
|
397
|
+
const job = store.jobs.get(jobID);
|
|
398
|
+
if (!job || job.status !== 'queued') continue;
|
|
399
|
+
store.runningCount += 1;
|
|
400
|
+
queueMicrotask(() => runImageJob(store, job));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function runImageJob(store, job) {
|
|
405
|
+
if (job.status === 'canceled') {
|
|
406
|
+
store.runningCount = Math.max(0, store.runningCount - 1);
|
|
407
|
+
scheduleImageJobs(store);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
302
410
|
job.status = 'running';
|
|
303
411
|
job.updated_at = new Date(store.now()).toISOString();
|
|
304
412
|
try {
|
|
305
|
-
const result = await generateImage({ ...input, job_id: job.job_id, trace_id: job.trace_id }, {
|
|
306
|
-
...options,
|
|
413
|
+
const result = await generateImage({ ...job.input, job_id: job.job_id, trace_id: job.trace_id }, {
|
|
414
|
+
...job.options,
|
|
307
415
|
signal: job.controller.signal
|
|
308
416
|
});
|
|
309
|
-
if (job.status === '
|
|
417
|
+
if (job.status === 'canceled') return;
|
|
310
418
|
job.status = 'succeeded';
|
|
311
419
|
job.ok = true;
|
|
312
420
|
job.result = result;
|
|
313
421
|
} catch (error) {
|
|
314
|
-
if (job.status !== '
|
|
422
|
+
if (job.status !== 'canceled') {
|
|
315
423
|
job.status = 'failed';
|
|
316
424
|
job.ok = false;
|
|
317
|
-
job.error = serializeImageError(error);
|
|
425
|
+
job.error = serializeImageError(error, { trace_id: job.trace_id });
|
|
318
426
|
}
|
|
319
427
|
} finally {
|
|
320
428
|
job.updated_at = new Date(store.now()).toISOString();
|
|
429
|
+
store.runningCount = Math.max(0, store.runningCount - 1);
|
|
430
|
+
scheduleImageJobs(store);
|
|
321
431
|
}
|
|
322
432
|
}
|
|
323
433
|
|
|
324
434
|
function publicJobStatus(job) {
|
|
325
435
|
const base = {
|
|
326
|
-
ok: job.status !== 'failed' && job.status !== '
|
|
436
|
+
ok: job.status !== 'failed' && job.status !== 'canceled' && job.status !== 'expired',
|
|
327
437
|
job_id: job.job_id,
|
|
328
438
|
trace_id: job.trace_id,
|
|
439
|
+
idempotency_key: job.idempotency_key || undefined,
|
|
329
440
|
status: job.status,
|
|
441
|
+
legacy_status: job.status === 'canceled' ? 'cancelled' : undefined,
|
|
330
442
|
created_at: job.created_at,
|
|
331
|
-
updated_at: job.updated_at
|
|
443
|
+
updated_at: job.updated_at,
|
|
444
|
+
expires_at: job.expires_at
|
|
332
445
|
};
|
|
333
446
|
if (job.result) return { ...base, ...structuredToolResult(job.result) };
|
|
334
447
|
if (job.error) return { ...base, error: job.error };
|
|
@@ -342,9 +455,12 @@ function missingJobResult(jobID) {
|
|
|
342
455
|
status: 'not_found',
|
|
343
456
|
error: {
|
|
344
457
|
code: 'job_not_found',
|
|
345
|
-
|
|
458
|
+
message: '没有找到这个图片任务。',
|
|
346
459
|
retryable: false,
|
|
347
|
-
|
|
460
|
+
stage: 'lookup',
|
|
461
|
+
upstream_status: undefined,
|
|
462
|
+
trace_id: '',
|
|
463
|
+
category: 'not_found'
|
|
348
464
|
}
|
|
349
465
|
};
|
|
350
466
|
}
|
|
@@ -352,10 +468,116 @@ function missingJobResult(jobID) {
|
|
|
352
468
|
function cleanupImageJobs(store) {
|
|
353
469
|
const threshold = store.now() - store.ttlMs;
|
|
354
470
|
for (const [jobID, job] of store.jobs.entries()) {
|
|
471
|
+
const created = Date.parse(job.created_at || '');
|
|
472
|
+
if (job.status === 'queued' && Number.isFinite(created) && created < threshold) {
|
|
473
|
+
job.status = 'expired';
|
|
474
|
+
job.ok = false;
|
|
475
|
+
job.updated_at = new Date(store.now()).toISOString();
|
|
476
|
+
job.error = {
|
|
477
|
+
code: 'job_expired',
|
|
478
|
+
message: '图片任务已过期,请重新创建任务。',
|
|
479
|
+
retryable: false,
|
|
480
|
+
stage: 'queue',
|
|
481
|
+
upstream_status: undefined,
|
|
482
|
+
trace_id: job.trace_id,
|
|
483
|
+
category: 'queue'
|
|
484
|
+
};
|
|
485
|
+
}
|
|
355
486
|
if (job.status === 'queued' || job.status === 'running') continue;
|
|
356
487
|
const updated = Date.parse(job.updated_at || job.created_at || '');
|
|
357
|
-
if (Number.isFinite(updated) && updated < threshold) store
|
|
488
|
+
if (Number.isFinite(updated) && updated < threshold) removeImageJob(store, jobID, job);
|
|
358
489
|
}
|
|
490
|
+
store.queue = store.queue.filter((jobID) => {
|
|
491
|
+
const job = store.jobs.get(jobID);
|
|
492
|
+
return job && job.status === 'queued';
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function configureJobStore(store, options = {}) {
|
|
497
|
+
const env = options.env || process.env;
|
|
498
|
+
store.maxConcurrent = resolveBoundedInt(1, options.maxConcurrent, env.GPTEAM_IMAGE_MAX_CONCURRENT, store.maxConcurrent || defaultMaxConcurrentJobs);
|
|
499
|
+
store.maxQueue = resolveBoundedInt(0, options.maxQueue, env.GPTEAM_IMAGE_MAX_QUEUE, store.maxQueue || defaultMaxQueuedJobs);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function queuedJobCount(store) {
|
|
503
|
+
return store.queue.filter((jobID) => {
|
|
504
|
+
const job = store.jobs.get(jobID);
|
|
505
|
+
return job && job.status === 'queued';
|
|
506
|
+
}).length;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function removeImageJob(store, jobID, job) {
|
|
510
|
+
store.jobs.delete(jobID);
|
|
511
|
+
if (job && job.idempotency_key && store.idempotency.get(job.idempotency_key) === jobID) {
|
|
512
|
+
store.idempotency.delete(job.idempotency_key);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function isTerminalStatus(status) {
|
|
517
|
+
return terminalStatuses.has(String(status || ''));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function normalizeIdempotencyKey(value) {
|
|
521
|
+
return String(value || '').trim().slice(0, 200);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function shapeDownloadResult(result, input = {}) {
|
|
525
|
+
const includeImage = !input.metadata_only && input.include_image !== false;
|
|
526
|
+
const includeRevisedPrompt = input.include_revised_prompt !== false;
|
|
527
|
+
const output = {
|
|
528
|
+
...result,
|
|
529
|
+
status: 'succeeded'
|
|
530
|
+
};
|
|
531
|
+
if (!includeImage) {
|
|
532
|
+
delete output.b64;
|
|
533
|
+
delete output.mimeType;
|
|
534
|
+
}
|
|
535
|
+
if (!includeRevisedPrompt) {
|
|
536
|
+
delete output.revised_prompt;
|
|
537
|
+
delete output.revisedPrompt;
|
|
538
|
+
}
|
|
539
|
+
return output;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function imageEndpointFromPayload(payload) {
|
|
543
|
+
return imageActionFromPayload(payload) === 'edit' ? '/images/edits' : '/images/generations';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function imageActionFromPayload(payload) {
|
|
547
|
+
return Array.isArray(payload.images) && payload.images.length > 0 ? 'edit' : 'generate';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function normalizeInputImages(value, options = {}) {
|
|
551
|
+
const rawImages = Array.isArray(value) ? value : (value ? [value] : []);
|
|
552
|
+
return rawImages.map((item) => normalizeImageReference(item, options)).filter(Boolean);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function normalizeImageReference(value, options = {}) {
|
|
556
|
+
if (!value) return '';
|
|
557
|
+
if (typeof value === 'object') {
|
|
558
|
+
return normalizeImageReference(value.image_url || value.url || value.path || value.file, options);
|
|
559
|
+
}
|
|
560
|
+
const text = String(value || '').trim();
|
|
561
|
+
if (!text) return '';
|
|
562
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(text)) return text;
|
|
563
|
+
if (/^https?:\/\//i.test(text)) return text;
|
|
564
|
+
try {
|
|
565
|
+
return localImageToDataURL(text, options);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
throw new ImageMCPError(`读取输入图片失败:${error.message}`, {
|
|
568
|
+
code: 'input_image_read_failed',
|
|
569
|
+
category: 'file_system',
|
|
570
|
+
stage: 'local',
|
|
571
|
+
retryable: false
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function copyOptionalImageToolOption(payload, input, key) {
|
|
577
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) return;
|
|
578
|
+
const value = input[key];
|
|
579
|
+
if (value === undefined || value === null || value === '') return;
|
|
580
|
+
payload[key] = value;
|
|
359
581
|
}
|
|
360
582
|
|
|
361
583
|
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: 'Accepted for compatibility. The GPTeam Image 2 bridge currently ignores this option because the upstream Codex image tool rejects it on edits.',
|
|
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',
|