gpteam 0.1.22 → 0.1.23

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/lib/help.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.22';
2
+ export const PACKAGE_VERSION = '0.1.23';
3
3
 
4
4
  export function getHelpText() {
5
5
  return [
@@ -14,6 +14,29 @@ export const DEFAULT_BASE_URL = 'https://api.gpteamservices.com/v1';
14
14
  export const DEFAULT_IMAGE_MODEL = 'gpt-image-2';
15
15
  export { DEFAULT_IMAGE_FORMAT };
16
16
 
17
+ export const IMAGE_FORMATS = ['png', 'jpeg', 'webp'];
18
+ export const IMAGE_QUALITY_LEVELS = ['low', 'medium', 'high', 'auto'];
19
+ export const IMAGE_BACKGROUND_OPTIONS = ['auto', 'opaque'];
20
+ export const IMAGE_MODERATION_OPTIONS = ['auto', 'low'];
21
+ export const POPULAR_IMAGE_SIZES = [
22
+ { label: '1K 方图', size: '1024x1024', aspect_ratio: '1:1' },
23
+ { label: '1K 横图', size: '1536x1024', aspect_ratio: '3:2' },
24
+ { label: '1K 竖图', size: '1024x1536', aspect_ratio: '2:3' },
25
+ { label: '2K 方图', size: '2048x2048', aspect_ratio: '1:1' },
26
+ { label: '2K 宽屏', size: '2048x1152', aspect_ratio: '16:9' },
27
+ { label: '2K 竖幅', size: '1152x2048', aspect_ratio: '9:16' },
28
+ { label: '4K 横图', size: '3840x2160', aspect_ratio: '16:9' },
29
+ { label: '4K 竖图', size: '2160x3840', aspect_ratio: '9:16' },
30
+ { label: '自动', size: 'auto', aspect_ratio: 'auto' }
31
+ ];
32
+ export const IMAGE_SIZE_CONSTRAINTS = {
33
+ max_edge_px: 3840,
34
+ edge_multiple_px: 16,
35
+ max_long_to_short_ratio: 3,
36
+ min_total_pixels: 655360,
37
+ max_total_pixels: 8294400
38
+ };
39
+
17
40
  const defaultMaxAttempts = 3;
18
41
  const defaultRetryDelayMs = 800;
19
42
  const defaultRequestTimeoutMs = 5 * 60 * 1000;
@@ -32,7 +55,7 @@ export function buildImageGenerationPayload(input = {}, options = {}) {
32
55
  stream: true,
33
56
  size: String(input.size || '1024x1024'),
34
57
  quality: String(input.quality || 'high'),
35
- output_format: normalizeImageFormat(input.format || input.output_format || DEFAULT_IMAGE_FORMAT)
58
+ output_format: resolveImageOutputFormat(input)
36
59
  };
37
60
  const imageOptions = { ...options, home: options.home };
38
61
  const images = normalizeInputImages(collectInputImageValues(input), imageOptions);
@@ -41,6 +64,7 @@ export function buildImageGenerationPayload(input = {}, options = {}) {
41
64
  if (mask) payload.mask = { image_url: mask };
42
65
  copyOptionalImageToolOption(payload, input, 'background');
43
66
  copyOptionalImageToolOption(payload, input, 'moderation');
67
+ copyOptionalImageToolOption(payload, input, 'output_compression');
44
68
  return payload;
45
69
  }
46
70
 
@@ -88,15 +112,7 @@ export function normalizeBaseUrl(value) {
88
112
 
89
113
  export async function generateImage(input = {}, options = {}) {
90
114
  const startedAt = now(options);
91
- const prompt = String(input.prompt || '').trim();
92
- if (!prompt) {
93
- throw new ImageMCPError('prompt 不能为空', {
94
- code: 'prompt_required',
95
- category: 'parameter',
96
- stage: 'validate',
97
- retryable: false
98
- });
99
- }
115
+ validateImageInput(input, { requirePrompt: true });
100
116
  const credentials = loadGPTeamCredentials(options);
101
117
  const payload = buildImageGenerationPayload(input, options);
102
118
  const fetchImpl = options.fetch || globalThis.fetch;
@@ -147,6 +163,11 @@ export function createImageJobStore(options = {}) {
147
163
  }
148
164
 
149
165
  export function createImageJob(input = {}, options = {}) {
166
+ try {
167
+ validateImageInput(input, { requirePrompt: true });
168
+ } catch (error) {
169
+ return resultFromError(error);
170
+ }
150
171
  const store = options.store || defaultJobStore;
151
172
  configureJobStore(store, options);
152
173
  cleanupImageJobs(store);
@@ -288,9 +309,18 @@ export function getCapabilities(options = {}) {
288
309
  supports_mask: true,
289
310
  image_input_fields: ['images', 'image', 'image_path', 'image_paths', 'input_image', 'input_images'],
290
311
  mask_input_fields: ['mask', 'mask_path'],
291
- sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'],
292
- formats: ['png', 'jpeg', 'webp'],
293
- quality: ['low', 'medium', 'high', 'auto'],
312
+ sizes: POPULAR_IMAGE_SIZES.map((item) => item.size),
313
+ popular_sizes: POPULAR_IMAGE_SIZES,
314
+ size_presets: ['1K', '2K', '4K', 'auto'],
315
+ size_constraints: IMAGE_SIZE_CONSTRAINTS,
316
+ aspect_ratios: ['1:1', '3:2', '2:3', '16:9', '9:16', 'custom'],
317
+ supports_custom_size: true,
318
+ formats: IMAGE_FORMATS,
319
+ output_formats: IMAGE_FORMATS,
320
+ quality: IMAGE_QUALITY_LEVELS,
321
+ background: IMAGE_BACKGROUND_OPTIONS,
322
+ moderation: IMAGE_MODERATION_OPTIONS,
323
+ supports_output_compression: true,
294
324
  max_prompt_length: 32000,
295
325
  statuses: ['queued', 'running', 'succeeded', 'failed', 'canceled', 'expired'],
296
326
  default_output_format: DEFAULT_IMAGE_FORMAT,
@@ -301,6 +331,22 @@ export function getCapabilities(options = {}) {
301
331
  };
302
332
  }
303
333
 
334
+ export function validateImageInput(input = {}, options = {}) {
335
+ if (options.requirePrompt && !String(input.prompt || '').trim()) {
336
+ throw imageParamError('prompt_required', 'prompt 不能为空', 'prompt', input.prompt, {
337
+ hint: '请先让用户提供图片提示词。'
338
+ });
339
+ }
340
+ validateImageSize(input.size);
341
+ validateEnumImageParam('quality', input.quality, IMAGE_QUALITY_LEVELS);
342
+ validateImageFormatParam(input);
343
+ validateEnumImageParam('background', input.background, IMAGE_BACKGROUND_OPTIONS, {
344
+ hint: 'gpt-image-2 不支持 transparent,请使用 auto 或 opaque。'
345
+ });
346
+ validateEnumImageParam('moderation', input.moderation, IMAGE_MODERATION_OPTIONS);
347
+ validateOutputCompression(input.output_compression);
348
+ }
349
+
304
350
  async function fetchImageWithRetry(fetchImpl, credentials, payload, options) {
305
351
  const maxAttempts = resolveBoundedInt(1, options.maxAttempts, options.env && options.env.GPTEAM_IMAGE_MAX_ATTEMPTS, defaultMaxAttempts);
306
352
  const retryDelayMs = resolveBoundedInt(0, options.retryDelayMs, options.env && options.env.GPTEAM_IMAGE_RETRY_DELAY_MS, defaultRetryDelayMs);
@@ -722,6 +768,107 @@ function resolveRevisedPromptFlag(input) {
722
768
  return true;
723
769
  }
724
770
 
771
+ function validateImageSize(value) {
772
+ if (value === undefined || value === null || value === '') return;
773
+ const text = String(value).trim().toLowerCase();
774
+ if (text === 'auto') return;
775
+ const match = text.match(/^(\d{2,5})x(\d{2,5})$/);
776
+ if (!match) {
777
+ throw imageParamError('invalid_size', 'size 必须是 auto 或类似 1024x1024 的宽高格式。', 'size', value, {
778
+ supported_values: POPULAR_IMAGE_SIZES.map((item) => item.size),
779
+ hint: '常用:1024x1024、1536x1024、1024x1536、2048x2048、2048x1152、1152x2048、3840x2160、2160x3840。'
780
+ });
781
+ }
782
+ const width = Number(match[1]);
783
+ const height = Number(match[2]);
784
+ const longSide = Math.max(width, height);
785
+ const shortSide = Math.min(width, height);
786
+ const pixels = width * height;
787
+ const valid = width % IMAGE_SIZE_CONSTRAINTS.edge_multiple_px === 0 &&
788
+ height % IMAGE_SIZE_CONSTRAINTS.edge_multiple_px === 0 &&
789
+ longSide <= IMAGE_SIZE_CONSTRAINTS.max_edge_px &&
790
+ longSide / shortSide <= IMAGE_SIZE_CONSTRAINTS.max_long_to_short_ratio &&
791
+ pixels >= IMAGE_SIZE_CONSTRAINTS.min_total_pixels &&
792
+ pixels <= IMAGE_SIZE_CONSTRAINTS.max_total_pixels;
793
+ if (!valid) {
794
+ throw imageParamError('invalid_size', 'size 超出 gpt-image-2 支持范围。', 'size', value, {
795
+ constraints: IMAGE_SIZE_CONSTRAINTS,
796
+ supported_values: POPULAR_IMAGE_SIZES.map((item) => item.size),
797
+ hint: '宽高需为 16 的倍数,长边不超过 3840,长短边比例不超过 3:1。'
798
+ });
799
+ }
800
+ }
801
+
802
+ function validateEnumImageParam(field, value, supportedValues, extra = {}) {
803
+ if (value === undefined || value === null || value === '') return;
804
+ const normalized = String(value).trim().toLowerCase();
805
+ if (supportedValues.includes(normalized)) return;
806
+ throw imageParamError(`invalid_${field}`, `${field} 参数不支持。`, field, value, {
807
+ supported_values: supportedValues,
808
+ ...extra
809
+ });
810
+ }
811
+
812
+ function validateImageFormatParam(input) {
813
+ const formatValue = input.format;
814
+ const outputFormatValue = input.output_format;
815
+ const format = normalizeDeclaredImageFormat(formatValue);
816
+ const outputFormat = normalizeDeclaredImageFormat(outputFormatValue);
817
+ if (formatValue !== undefined && formatValue !== null && formatValue !== '' && !IMAGE_FORMATS.includes(format)) {
818
+ throw invalidImageFormatError('format', formatValue);
819
+ }
820
+ if (outputFormatValue !== undefined && outputFormatValue !== null && outputFormatValue !== '' && !IMAGE_FORMATS.includes(outputFormat)) {
821
+ throw invalidImageFormatError('output_format', outputFormatValue);
822
+ }
823
+ if (format && outputFormat && format !== outputFormat) {
824
+ throw imageParamError('invalid_format_conflict', 'format 和 output_format 不一致。', 'output_format', outputFormatValue, {
825
+ format,
826
+ output_format: outputFormat,
827
+ hint: '两个字段同时传入时必须表示同一种格式,例如 jpg 和 jpeg 可以同时使用。'
828
+ });
829
+ }
830
+ }
831
+
832
+ function validateOutputCompression(value) {
833
+ if (value === undefined || value === null || value === '') return;
834
+ const amount = Number(value);
835
+ if (Number.isInteger(amount) && amount >= 0 && amount <= 100) return;
836
+ throw imageParamError('invalid_output_compression', 'output_compression 必须是 0 到 100 的整数。', 'output_compression', value, {
837
+ supported_values: ['0-100']
838
+ });
839
+ }
840
+
841
+ function resolveImageOutputFormat(input = {}) {
842
+ return normalizeImageFormat(input.output_format || input.format || DEFAULT_IMAGE_FORMAT);
843
+ }
844
+
845
+ function normalizeDeclaredImageFormat(value) {
846
+ if (value === undefined || value === null || value === '') return '';
847
+ const normalized = String(value).trim().toLowerCase();
848
+ return normalized === 'jpg' ? 'jpeg' : normalized;
849
+ }
850
+
851
+ function invalidImageFormatError(field, received) {
852
+ return imageParamError('invalid_format', 'format/output_format 参数不支持。', field, received, {
853
+ supported_values: IMAGE_FORMATS,
854
+ hint: '支持 png、jpeg、webp,jpg 会按 jpeg 处理。'
855
+ });
856
+ }
857
+
858
+ function imageParamError(code, message, field, received, details = {}) {
859
+ return new ImageMCPError(message, {
860
+ code,
861
+ category: 'parameter',
862
+ stage: 'validate',
863
+ retryable: false,
864
+ details: {
865
+ field,
866
+ received,
867
+ ...details
868
+ }
869
+ });
870
+ }
871
+
725
872
  function resolveCodexHome(env, home) {
726
873
  return expandHome(firstNonEmpty(env.GPTEAM_CODEX_HOME, env.CODEX_HOME, path.join(home, '.codex')), home);
727
874
  }
@@ -8,85 +8,118 @@ import {
8
8
  downloadImageResult,
9
9
  generateImage,
10
10
  getCapabilities,
11
+ IMAGE_BACKGROUND_OPTIONS,
12
+ IMAGE_FORMATS,
13
+ IMAGE_MODERATION_OPTIONS,
14
+ IMAGE_QUALITY_LEVELS,
15
+ POPULAR_IMAGE_SIZES,
11
16
  getImageJobStatus,
12
17
  resultFromError,
13
18
  structuredToolResult,
14
19
  toolResultContent
15
20
  } from './image.js';
16
21
 
22
+ const imageToolPromptingInstruction = '调用前请先确认用户真实需求:尺寸或比例、质量、输出格式、保存目录或文件名。缺少这些关键信息时应先追问,不要直接默认生成。';
23
+
17
24
  const imageInputProperties = {
18
25
  prompt: {
19
26
  type: 'string',
20
- description: 'Image prompt.'
27
+ description: '图片提示词。需要尽量具体,若用户只给模糊需求,应先追问风格、主体、场景和用途。'
21
28
  },
22
29
  size: {
23
30
  type: 'string',
24
- description: 'Image size, for example 1024x1024.',
25
- default: '1024x1024'
31
+ description: `图片尺寸。支持 auto、常用尺寸 ${POPULAR_IMAGE_SIZES.map((item) => `${item.label} ${item.size}`).join('、')},也支持满足约束的自定义宽高,例如 9:16 可用 1152x2048 或 2160x3840。`
26
32
  },
27
33
  quality: {
28
34
  type: 'string',
29
- description: 'Image quality.',
30
- default: 'high'
35
+ description: '图片质量。生图前应让用户确认速度优先还是质量优先。',
36
+ enum: IMAGE_QUALITY_LEVELS
31
37
  },
32
38
  format: {
33
39
  type: 'string',
34
- description: 'Output image format.',
35
- enum: ['png', 'jpeg', 'webp'],
36
- default: 'png'
40
+ description: '输出图片格式。',
41
+ enum: IMAGE_FORMATS
42
+ },
43
+ output_format: {
44
+ type: 'string',
45
+ description: '输出图片格式别名,和 format 等价。',
46
+ enum: IMAGE_FORMATS
37
47
  },
38
48
  output_path: {
39
49
  type: 'string',
40
- description: 'Optional output file path or directory.'
50
+ description: '保存路径,可以是目录或完整文件路径。缺少时建议先询问用户想保存到哪里。'
41
51
  },
42
52
  overwrite: {
43
53
  type: 'boolean',
44
- description: 'Overwrite output_path when it already exists. Defaults to false.',
54
+ description: '文件已存在时是否覆盖。默认 false,会自动生成 -v2、-v3 等新文件名。',
45
55
  default: false
46
56
  },
47
57
  idempotency_key: {
48
58
  type: 'string',
49
- description: 'Optional idempotency key. Reusing it returns the same local job in this MCP process.'
59
+ description: '幂等键。客户端超时后用同一个 key 重试,会复用同一进程内的本地任务,避免重复生成。'
60
+ },
61
+ image: {
62
+ type: 'string',
63
+ description: '单张输入图片,支持 data URL、HTTPS URL 或本地文件路径,用于图生图或编辑。'
50
64
  },
51
65
  images: {
52
66
  type: 'array',
53
67
  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.'
68
+ description: '多张输入图片,支持 data URL、HTTPS URL 或本地文件路径,用于图生图或编辑。'
55
69
  },
56
70
  image_path: {
57
71
  type: 'string',
58
- description: 'Optional single local image path alias for image-to-image or edit.'
72
+ description: '单张本地输入图片路径别名,用于图生图或编辑。'
59
73
  },
60
74
  image_paths: {
61
75
  type: 'array',
62
76
  items: { type: 'string' },
63
- description: 'Optional local image path aliases for image-to-image or edit.'
77
+ description: '多张本地输入图片路径别名,用于图生图或编辑。'
64
78
  },
65
79
  input_image: {
66
80
  type: 'string',
67
- description: 'Optional single input image alias. Accepts a data URL, HTTPS URL, or local file path.'
81
+ description: '单张输入图片别名,支持 data URLHTTPS URL 或本地文件路径。'
68
82
  },
69
83
  input_images: {
70
84
  type: 'array',
71
85
  items: { type: 'string' },
72
- description: 'Optional input image aliases. Accepts data URLs, HTTPS URLs, or local file paths.'
86
+ description: '多张输入图片别名,支持 data URL、HTTPS URL 或本地文件路径。'
73
87
  },
74
88
  mask: {
75
89
  type: 'string',
76
- description: 'Optional mask image for image edit. Value may be a data URL, HTTPS URL, or local file path.'
90
+ description: '编辑遮罩图片,支持 data URLHTTPS URL 或本地文件路径。'
77
91
  },
78
92
  mask_path: {
79
93
  type: 'string',
80
- description: 'Optional local mask image path alias for image edit.'
94
+ description: '本地遮罩图片路径别名。'
81
95
  },
82
96
  input_fidelity: {
83
97
  type: 'string',
84
- description: 'Accepted for compatibility. The GPTeam Image 2 bridge currently ignores this option because the upstream Codex image tool rejects it on edits.',
98
+ description: '兼容字段。当前 GPTeam Image 2 桥接会忽略该字段,因为上游 Codex 图片工具会拒绝 edits 中的该参数。',
85
99
  enum: ['low', 'high']
86
100
  },
101
+ background: {
102
+ type: 'string',
103
+ description: '背景策略。gpt-image-2 当前支持 auto 或 opaque。',
104
+ enum: IMAGE_BACKGROUND_OPTIONS
105
+ },
106
+ moderation: {
107
+ type: 'string',
108
+ description: '内容安全策略。通常保持 auto,需要更低过滤强度时可用 low。',
109
+ enum: IMAGE_MODERATION_OPTIONS
110
+ },
111
+ output_compression: {
112
+ type: 'integer',
113
+ description: '输出压缩比例,0 到 100 的整数。'
114
+ },
115
+ include_revised_prompt: {
116
+ type: 'boolean',
117
+ description: '是否返回上游修订后的提示词。',
118
+ default: true
119
+ },
87
120
  return_revised_prompt: {
88
121
  type: 'boolean',
89
- description: 'Return upstream revised prompt when available.',
122
+ description: '是否返回上游修订后的提示词,兼容旧字段。',
90
123
  default: true
91
124
  }
92
125
  };
@@ -94,7 +127,7 @@ const imageInputProperties = {
94
127
  const tools = [
95
128
  {
96
129
  name: 'create_image_job',
97
- description: 'Recommended for normal use. Create a local background GPTeam Image 2 job and return immediately with a job_id.',
130
+ description: `推荐常规使用。创建本地后台 GPTeam Image 2 任务并立即返回 job_id。${imageToolPromptingInstruction}`,
98
131
  inputSchema: {
99
132
  type: 'object',
100
133
  properties: imageInputProperties,
@@ -104,22 +137,22 @@ const tools = [
104
137
  },
105
138
  {
106
139
  name: 'get_image_job_status',
107
- description: 'Get the status of a local GPTeam Image 2 job.',
140
+ description: '查询本地 GPTeam Image 2 图片任务状态。',
108
141
  inputSchema: jobIDSchema()
109
142
  },
110
143
  {
111
144
  name: 'cancel_image_job',
112
- description: 'Cancel a local GPTeam Image 2 job when it is still queued or running.',
145
+ description: '取消仍在 queued running 的本地 GPTeam Image 2 图片任务。取消是 best-effort,上游已开始生成时不保证同步取消。',
113
146
  inputSchema: jobIDSchema()
114
147
  },
115
148
  {
116
149
  name: 'download_image_result',
117
- description: 'Return the saved local file metadata and image content for a completed image job.',
150
+ description: '下载已完成图片任务的本地文件元数据和图片内容,可选择只返回 metadata',
118
151
  inputSchema: downloadSchema()
119
152
  },
120
153
  {
121
154
  name: 'get_capabilities',
122
- description: 'Return GPTeam Image MCP capabilities, supported sizes, formats, quality levels, async support, and queue limits.',
155
+ description: '返回 GPTeam Image MCP 能力,包括支持尺寸、格式、质量、异步任务、取消语义、队列上限和参数约束。',
123
156
  inputSchema: {
124
157
  type: 'object',
125
158
  properties: {},
@@ -128,7 +161,7 @@ const tools = [
128
161
  },
129
162
  {
130
163
  name: 'generate_image',
131
- description: 'Compatibility tool. Generate an image through GPTeam Image 2, wait for completion, and save it locally.',
164
+ description: `兼容同步工具。通过 GPTeam Image 2 生成图片,等待完成并保存到本地。${imageToolPromptingInstruction}耗时较长时优先使用 create_image_job。`,
132
165
  inputSchema: {
133
166
  type: 'object',
134
167
  properties: imageInputProperties,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
5
5
  "type": "module",
6
6
  "bin": {