feat: initialize OmniClaw skills registry
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
._*
|
||||
node_modules/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
8
AGENTS.md
Normal file
8
AGENTS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Repository Instructions
|
||||
|
||||
- Keep skills self-contained and installable by copying one folder into `$CODEX_HOME/skills`.
|
||||
- Do not commit real credentials, API keys, OAuth tokens, generated images, or private logs.
|
||||
- Run `node tools/validate.mjs` before committing.
|
||||
- Run `node tools/build-registry.mjs` after changing any skill metadata, skill file, reference, script, or API document.
|
||||
- Commit `registry/manifest.json` and `packages/*.tar.gz` because public installers and website pages may consume them directly.
|
||||
|
||||
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# OmniClaw Skills Registry
|
||||
|
||||
OmniClaw Skills Registry is the source repository for reusable AI-agent skills and API operation guides published by OmniClaw.
|
||||
|
||||
It is designed for three consumers:
|
||||
|
||||
- Humans browsing API documentation and skill usage guides.
|
||||
- Codex or other AI agents installing skills into their local skill directory.
|
||||
- The OmniClaw release website rendering a searchable public catalog from `registry/manifest.json`.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```text
|
||||
skills/
|
||||
<skill-name>/
|
||||
SKILL.md
|
||||
agents/openai.yaml
|
||||
references/
|
||||
scripts/
|
||||
skill.registry.json
|
||||
apis/
|
||||
<product>/
|
||||
<api>.zh.md
|
||||
<api>.en.md
|
||||
registry/
|
||||
manifest.schema.json
|
||||
manifest.json
|
||||
packages/
|
||||
<skill-name>-<version>.tar.gz
|
||||
tools/
|
||||
build-registry.mjs
|
||||
validate.mjs
|
||||
install.sh
|
||||
```
|
||||
|
||||
## Install A Skill
|
||||
|
||||
Default install target is `${CODEX_HOME:-$HOME/.codex}/skills`.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/tools/install.sh \
|
||||
| bash -s -- sub2api-gpt-image-2
|
||||
```
|
||||
|
||||
Alternative with git clone:
|
||||
|
||||
```bash
|
||||
git clone https://git.omniclaw.store/zsb/omniclaw-skills.git
|
||||
cd omniclaw-skills
|
||||
./tools/install.sh sub2api-gpt-image-2
|
||||
```
|
||||
|
||||
Restart Codex after installing new skills.
|
||||
|
||||
## Current Skills
|
||||
|
||||
- `sub2api-gpt-image-2`: Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible APIs.
|
||||
|
||||
## Development
|
||||
|
||||
Validate everything:
|
||||
|
||||
```bash
|
||||
node tools/validate.mjs
|
||||
```
|
||||
|
||||
Rebuild manifest and skill packages:
|
||||
|
||||
```bash
|
||||
node tools/build-registry.mjs
|
||||
```
|
||||
|
||||
The build script writes:
|
||||
|
||||
- `registry/manifest.json`
|
||||
- `packages/<skill-name>-<version>.tar.gz`
|
||||
|
||||
Commit generated registry and packages so the release website and remote installers can consume stable artifacts directly.
|
||||
|
||||
## Publishing Contract
|
||||
|
||||
Every skill must include:
|
||||
|
||||
- `SKILL.md`
|
||||
- `skill.registry.json`
|
||||
- `agents/openai.yaml`
|
||||
|
||||
Every API guide should provide both:
|
||||
|
||||
- `*.zh.md`
|
||||
- `*.en.md`
|
||||
|
||||
Do not commit real API keys, OAuth tokens, generated images, or private runtime logs.
|
||||
|
||||
370
apis/sub2api/gpt-image-2.en.md
Normal file
370
apis/sub2api/gpt-image-2.en.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# GPT Image 2 API Guide
|
||||
|
||||
This guide describes how to call `gpt-image-2` through sub2api or any OpenAI-compatible gateway.
|
||||
|
||||
Default examples use:
|
||||
|
||||
```text
|
||||
BASE_URL=https://claude.omniclaw.store/v1
|
||||
API_KEY=<sub2api API key generated from the /keys page>
|
||||
```
|
||||
|
||||
Do not use ChatGPT OAuth tokens from `.codex/auth.json` as API keys.
|
||||
|
||||
## Quick Summary
|
||||
|
||||
- Direct image generation: call `POST /v1/images/generations` with `model: "gpt-image-2"`.
|
||||
- Image editing: call `POST /v1/images/edits` with multipart `image[]` files and an optional `mask`.
|
||||
- Agent/Codex workflows: keep the main model as a text/agent model such as `gpt-5.5`, then call image generation through the Responses API `image_generation` tool.
|
||||
- Do not use `gpt-image-2` as the Codex main model.
|
||||
- `gpt-image-2` normally returns base64 image data at `data[0].b64_json`.
|
||||
- `3840x2160` 4K output works but is high-latency and high-cost; use 180-300 second timeouts for production.
|
||||
|
||||
## Official Capability Summary
|
||||
|
||||
`gpt-image-2` is an image generation and editing model with text input, image input, and image output support.
|
||||
|
||||
Model aliases:
|
||||
|
||||
```text
|
||||
gpt-image-2
|
||||
gpt-image-2-2026-04-21
|
||||
```
|
||||
|
||||
Supported API surfaces:
|
||||
|
||||
```text
|
||||
/v1/images/generations
|
||||
/v1/images/edits
|
||||
/v1/responses # via image_generation tool
|
||||
```
|
||||
|
||||
Official references:
|
||||
|
||||
- https://developers.openai.com/api/docs/models/gpt-image-2
|
||||
- https://developers.openai.com/api/docs/guides/image-generation
|
||||
- https://developers.openai.com/api/reference/resources/images
|
||||
|
||||
## Authentication
|
||||
|
||||
```bash
|
||||
export BASE_URL="https://claude.omniclaw.store/v1"
|
||||
export API_KEY="sk-..."
|
||||
```
|
||||
|
||||
JSON requests require:
|
||||
|
||||
```http
|
||||
Authorization: Bearer $API_KEY
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
For multipart image edits, let `curl -F` or the SDK set `Content-Type`.
|
||||
|
||||
## Image Generation
|
||||
|
||||
### Minimal Request
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A compact Apple-style dashboard UI, clean white background",
|
||||
"size": "1024x1024",
|
||||
"quality": "medium",
|
||||
"output_format": "png",
|
||||
"n": 1
|
||||
}' > image.json
|
||||
```
|
||||
|
||||
Decode the response:
|
||||
|
||||
```bash
|
||||
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
|
||||
```
|
||||
|
||||
### 4K Request
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
--max-time 300 \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A modern product poster, cinematic lighting, premium realistic photography",
|
||||
"size": "3840x2160",
|
||||
"quality": "medium",
|
||||
"output_format": "png",
|
||||
"n": 1
|
||||
}' > image-4k.json
|
||||
```
|
||||
|
||||
Production recommendation: first validate prompts with `1024x1024` or `1536x1024`, then upscale the request to `3840x2160`. `4K + high` can be slow and expensive.
|
||||
|
||||
## Generation Parameters
|
||||
|
||||
| Parameter | Type | Recommended value | Notes |
|
||||
|---|---|---|---|
|
||||
| `model` | string | `gpt-image-2` | Required. The snapshot `gpt-image-2-2026-04-21` is also valid. |
|
||||
| `prompt` | string | detailed natural language | Required. Include subject, environment, camera, style, lighting, and constraints. |
|
||||
| `n` | number | `1` | Number of images. Prefer single-image requests for retry and billing attribution. |
|
||||
| `size` | string | `1024x1024`, `1536x1024`, `3840x2160` | Flexible sizes are supported when they satisfy the model constraints. |
|
||||
| `quality` | string | `low`, `medium`, `high`, `auto` | Use `low` for drafts, `medium` for normal output, `high` for final assets. |
|
||||
| `output_format` | string | `png`, `jpeg`, `webp` | Default is usually `png`; use `jpeg` for latency-sensitive outputs. |
|
||||
| `output_compression` | number | `0-100` | Only applies to `jpeg` and `webp`. |
|
||||
| `background` | string | `auto`, `opaque` | `gpt-image-2` currently does not support `transparent`. |
|
||||
| `moderation` | string | `auto`, `low` | Adjusts filtering level but does not bypass safety policy. |
|
||||
| `stream` | boolean | `false` | Enables SSE image streaming. |
|
||||
| `partial_images` | number | `0-3` | Streaming only; partial images increase output token cost. |
|
||||
| `user` | string | end-user ID | Useful for audit and abuse monitoring. |
|
||||
|
||||
## Size Constraints
|
||||
|
||||
`size` can be `auto` or a valid `widthxheight` value:
|
||||
|
||||
- Maximum edge length is `3840px`.
|
||||
- Width and height must both be multiples of `16px`.
|
||||
- Long edge to short edge ratio must be at most `3:1`.
|
||||
- Total pixels must be between `655,360` and `8,294,400`.
|
||||
|
||||
Common values:
|
||||
|
||||
```text
|
||||
1024x1024
|
||||
1536x1024
|
||||
1024x1536
|
||||
2048x2048
|
||||
2048x1152
|
||||
3840x2160
|
||||
2160x3840
|
||||
auto
|
||||
```
|
||||
|
||||
Treat outputs larger than `2560x1440` as experimental high-pixel workloads with higher latency, higher cost, and higher failure probability.
|
||||
|
||||
## Response Shape
|
||||
|
||||
Typical response:
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 1770000000,
|
||||
"background": "opaque",
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "...",
|
||||
"revised_prompt": "..."
|
||||
}
|
||||
],
|
||||
"model": "gpt-image-2",
|
||||
"output_format": "png",
|
||||
"quality": "medium",
|
||||
"size": "1024x1024",
|
||||
"usage": {
|
||||
"input_tokens": 43,
|
||||
"input_tokens_details": {
|
||||
"image_tokens": 0,
|
||||
"text_tokens": 43
|
||||
},
|
||||
"output_tokens": 196,
|
||||
"output_tokens_details": {
|
||||
"image_tokens": 196,
|
||||
"text_tokens": 0
|
||||
},
|
||||
"total_tokens": 239
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Production systems should store:
|
||||
|
||||
- `model`
|
||||
- `size`
|
||||
- `quality`
|
||||
- `output_format`
|
||||
- `usage.total_tokens`
|
||||
- `usage.input_tokens`
|
||||
- `usage.output_tokens`
|
||||
- latency
|
||||
- upstream account, group, user, and key identifiers
|
||||
|
||||
## Image Editing
|
||||
|
||||
### Single-image Edit
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/edits" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "model=gpt-image-2" \
|
||||
-F "image[]=@input.png" \
|
||||
-F "prompt=Replace the sofa with a minimalist white lounge chair" \
|
||||
-F "size=1024x1024" \
|
||||
-F "quality=medium" \
|
||||
-F "output_format=png" \
|
||||
> edit.json
|
||||
```
|
||||
|
||||
### Masked Local Edit
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/edits" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "model=gpt-image-2" \
|
||||
-F "image[]=@input.png" \
|
||||
-F "mask=@mask.png" \
|
||||
-F "prompt=Change only the transparent masked region into a glass button" \
|
||||
-F "size=1024x1024" \
|
||||
-F "quality=medium" \
|
||||
> edit-mask.json
|
||||
```
|
||||
|
||||
Mask requirements:
|
||||
|
||||
- `image` and `mask` must have the same format and dimensions.
|
||||
- Files must be under 50MB.
|
||||
- `mask` must include an alpha channel.
|
||||
- Do not pass `input_fidelity` for `gpt-image-2`; the model processes image inputs at high fidelity by default.
|
||||
|
||||
## Responses API With `image_generation`
|
||||
|
||||
Use this when an agent should reason about the task before generating an image. The main model should be a text/agent model, such as `gpt-5.5`.
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/responses" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-5.5",
|
||||
"input": "Generate a clean product poster for an AI proxy service.",
|
||||
"tools": [
|
||||
{
|
||||
"type": "image_generation",
|
||||
"quality": "medium",
|
||||
"size": "1536x1024",
|
||||
"output_format": "png"
|
||||
}
|
||||
]
|
||||
}' > response-image.json
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- `model` is the main reasoning model, not `gpt-image-2`.
|
||||
- The `image_generation` tool performs the image work.
|
||||
- sub2api may inject the image tool for official Codex clients, but application calls should pass it explicitly.
|
||||
|
||||
## Streaming Images
|
||||
|
||||
The Images API supports SSE streaming:
|
||||
|
||||
```bash
|
||||
curl -N "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A futuristic city skyline at sunrise",
|
||||
"stream": true,
|
||||
"partial_images": 2,
|
||||
"size": "1536x1024",
|
||||
"quality": "medium"
|
||||
}'
|
||||
```
|
||||
|
||||
Events:
|
||||
|
||||
```text
|
||||
image_generation.partial_image
|
||||
image_generation.completed
|
||||
```
|
||||
|
||||
`partial_images` can be `0-3`. Each partial image adds output token cost.
|
||||
|
||||
## SDK Examples
|
||||
|
||||
### Node.js
|
||||
|
||||
```ts
|
||||
import fs from "node:fs";
|
||||
import OpenAI from "openai";
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: process.env.API_KEY,
|
||||
baseURL: process.env.BASE_URL ?? "https://claude.omniclaw.store/v1",
|
||||
});
|
||||
|
||||
const result = await client.images.generate({
|
||||
model: "gpt-image-2",
|
||||
prompt: "A premium product poster for an AI service",
|
||||
size: "1536x1024",
|
||||
quality: "medium",
|
||||
output_format: "png",
|
||||
n: 1,
|
||||
});
|
||||
|
||||
const b64 = result.data?.[0]?.b64_json;
|
||||
if (!b64) throw new Error("No image returned");
|
||||
fs.writeFileSync("image.png", Buffer.from(b64, "base64"));
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```py
|
||||
import base64
|
||||
import os
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ["API_KEY"],
|
||||
base_url=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"),
|
||||
)
|
||||
|
||||
result = client.images.generate(
|
||||
model="gpt-image-2",
|
||||
prompt="A premium product poster for an AI service",
|
||||
size="1536x1024",
|
||||
quality="medium",
|
||||
output_format="png",
|
||||
n=1,
|
||||
)
|
||||
|
||||
b64 = result.data[0].b64_json
|
||||
with open("image.png", "wb") as f:
|
||||
f.write(base64.b64decode(b64))
|
||||
```
|
||||
|
||||
## Production Dispatch
|
||||
|
||||
- Routing: prefer plus/team/pro OpenAI OAuth accounts for image workloads.
|
||||
- Timeout: use 120 seconds for normal images and 300 seconds for 4K.
|
||||
- Retry: only retry transient network failures and 502/503/504 with low retry counts.
|
||||
- Concurrency: 4K output produces many image tokens; use low per-account concurrency. Standard 1024 images can use higher concurrency.
|
||||
- Billing: record `usage` and charge based on input and output tokens. 4K can produce far more output tokens than 1024 images.
|
||||
- Latency: use `jpeg` and `quality: low` for drafts or latency-sensitive previews.
|
||||
- Fallback: if `4K/high` fails, retry `4K/medium`; if that still fails, generate `1536x1024/medium` and upscale separately.
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Symptom | Likely cause | Action |
|
||||
|---|---|---|
|
||||
| `401 INVALID_API_KEY` | Key is not a sub2api key or is disabled/deleted | Generate a new key from `/keys` |
|
||||
| `400 invalid_request_error` | Incompatible params such as transparent background or invalid size | Check `size`, `background`, and `quality` |
|
||||
| `429 usage_limit_reached` | Upstream account usage window hit | Switch plus/team/pro account or wait for reset |
|
||||
| `502 Upstream request failed` | Upstream did not return image data, network failed, or content was refused | Inspect server logs, simplify prompt, lower quality or size |
|
||||
| Request takes over 2 minutes | High pixels or complex prompt | Increase timeout, use streaming, or test lower resolution first |
|
||||
| `/v1/models` does not show `gpt-image-2` | Codex/text model list is not the Images API capability list | Call `/v1/images/generations` directly |
|
||||
|
||||
## Safety Boundary
|
||||
|
||||
Filter clearly disallowed content before sending requests, especially:
|
||||
|
||||
- Sexualized minors or young-looking subjects
|
||||
- Non-consensual sexual content, coercion, or sexual violence
|
||||
- Explicit nudity or graphic sexual activity
|
||||
- Illegal, hateful, or extreme violent content
|
||||
|
||||
For safe romantic scenes, explicitly constrain prompts with terms such as adult, non-explicit, no nudity, and fully clothed.
|
||||
|
||||
365
apis/sub2api/gpt-image-2.zh.md
Normal file
365
apis/sub2api/gpt-image-2.zh.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# GPT Image 2 API 调用文档
|
||||
|
||||
本文档面向 sub2api/OpenAI-compatible 网关调用 `gpt-image-2`。示例默认使用:
|
||||
|
||||
```text
|
||||
BASE_URL=https://claude.omniclaw.store/v1
|
||||
API_KEY=<从 /keys 页面生成的 sub2api key>
|
||||
```
|
||||
|
||||
不要把 `.codex/auth.json` 里的 ChatGPT OAuth token 当 API key 使用。
|
||||
|
||||
## 快速结论
|
||||
|
||||
- 直接生成图片:使用 `POST /v1/images/generations`,`model` 传 `gpt-image-2`。
|
||||
- 编辑图片:使用 `POST /v1/images/edits`,multipart 上传 `image[]`、可选 `mask`。
|
||||
- Agent/Codex 场景:主模型仍用 `gpt-5.5`,通过 Responses API 的 `image_generation` tool 调图像能力;不要把 Codex 主模型设成 `gpt-image-2`。
|
||||
- `gpt-image-2` 返回 base64 图片数据,通常是 `data[0].b64_json`。
|
||||
- `3840x2160` 4K 可用,但属于高像素、长耗时场景;生产调用应设置 180-300 秒超时。
|
||||
|
||||
## 官方能力摘要
|
||||
|
||||
`gpt-image-2` 是图片生成和编辑模型,支持文本输入、图片输入、图片输出。模型别名和快照:
|
||||
|
||||
```text
|
||||
gpt-image-2
|
||||
gpt-image-2-2026-04-21
|
||||
```
|
||||
|
||||
支持端点:
|
||||
|
||||
```text
|
||||
/v1/images/generations
|
||||
/v1/images/edits
|
||||
/v1/responses # 通过 image_generation tool
|
||||
```
|
||||
|
||||
官方参考:
|
||||
|
||||
- https://developers.openai.com/api/docs/models/gpt-image-2
|
||||
- https://developers.openai.com/api/docs/guides/image-generation
|
||||
- https://developers.openai.com/api/reference/resources/images
|
||||
|
||||
## 认证
|
||||
|
||||
```bash
|
||||
export BASE_URL="https://claude.omniclaw.store/v1"
|
||||
export API_KEY="sk-..."
|
||||
```
|
||||
|
||||
所有 JSON 请求带:
|
||||
|
||||
```http
|
||||
Authorization: Bearer $API_KEY
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
multipart 编辑接口由 `curl -F` 或 SDK 自动设置 `Content-Type`。
|
||||
|
||||
## 生成图片
|
||||
|
||||
### 最小请求
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A compact Apple-style dashboard UI, clean white background",
|
||||
"size": "1024x1024",
|
||||
"quality": "medium",
|
||||
"output_format": "png",
|
||||
"n": 1
|
||||
}' > image.json
|
||||
```
|
||||
|
||||
解码:
|
||||
|
||||
```bash
|
||||
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
|
||||
```
|
||||
|
||||
### 4K 请求
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
--max-time 300 \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A modern product poster, cinematic lighting, premium realistic photography",
|
||||
"size": "3840x2160",
|
||||
"quality": "medium",
|
||||
"output_format": "png",
|
||||
"n": 1
|
||||
}' > image-4k.json
|
||||
```
|
||||
|
||||
生产建议:4K + `high` 很慢且成本高。先用 `1024x1024` 或 `1536x1024` 验证提示词,再升到 `3840x2160`。
|
||||
|
||||
## 生成参数
|
||||
|
||||
| 参数 | 类型 | 建议值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `model` | string | `gpt-image-2` | 必填。也可用快照 `gpt-image-2-2026-04-21`。 |
|
||||
| `prompt` | string | 详细自然语言 | 必填。写清主体、环境、镜头、风格、限制。 |
|
||||
| `n` | number | `1` | 生成数量。生产建议单张并发调度,便于重试和计费。 |
|
||||
| `size` | string | `1024x1024`、`1536x1024`、`3840x2160` | `gpt-image-2` 支持灵活尺寸,见下方尺寸约束。 |
|
||||
| `quality` | string | `low`、`medium`、`high`、`auto` | 草稿用 `low`,常规用 `medium`,最终图用 `high`。 |
|
||||
| `output_format` | string | `png`、`jpeg`、`webp` | 默认 `png`。延迟敏感优先 `jpeg`。 |
|
||||
| `output_compression` | number | `0-100` | 仅 `jpeg`/`webp` 有意义。 |
|
||||
| `background` | string | `auto`、`opaque` | `gpt-image-2` 当前不支持 `transparent`。 |
|
||||
| `moderation` | string | `auto`、`low` | 控制图像生成过滤强度;仍需遵守内容政策。 |
|
||||
| `stream` | boolean | `false` | 开启 SSE 流式图片事件。 |
|
||||
| `partial_images` | number | `0-3` | 流式时返回部分图片;会增加输出 token 成本。 |
|
||||
| `user` | string | 用户 ID | 终端用户标识,便于审计和滥用监控。 |
|
||||
|
||||
## 尺寸约束
|
||||
|
||||
`gpt-image-2` 的 `size` 可以是 `auto`,也可以是满足约束的 `宽x高`:
|
||||
|
||||
- 最大边不超过 `3840px`
|
||||
- 宽和高都必须是 `16px` 的倍数
|
||||
- 长边/短边比例不超过 `3:1`
|
||||
- 总像素在 `655,360` 到 `8,294,400` 之间
|
||||
|
||||
常用尺寸:
|
||||
|
||||
```text
|
||||
1024x1024 # 方图,通常最快
|
||||
1536x1024 # 横图
|
||||
1024x1536 # 竖图
|
||||
2048x2048 # 2K 方图
|
||||
2048x1152 # 2K 横图
|
||||
3840x2160 # 4K 横图
|
||||
2160x3840 # 4K 竖图
|
||||
auto
|
||||
```
|
||||
|
||||
超过 `2560x1440` 的输出通常应按实验性高像素场景处理:高延迟、高成本、失败概率更高。
|
||||
|
||||
## 返回结构
|
||||
|
||||
典型响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 1770000000,
|
||||
"background": "opaque",
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "...",
|
||||
"revised_prompt": "..."
|
||||
}
|
||||
],
|
||||
"model": "gpt-image-2",
|
||||
"output_format": "png",
|
||||
"quality": "medium",
|
||||
"size": "1024x1024",
|
||||
"usage": {
|
||||
"input_tokens": 43,
|
||||
"input_tokens_details": {
|
||||
"image_tokens": 0,
|
||||
"text_tokens": 43
|
||||
},
|
||||
"output_tokens": 196,
|
||||
"output_tokens_details": {
|
||||
"image_tokens": 196,
|
||||
"text_tokens": 0
|
||||
},
|
||||
"total_tokens": 239
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
业务侧应持久化:
|
||||
|
||||
- `model`
|
||||
- `size`
|
||||
- `quality`
|
||||
- `output_format`
|
||||
- `usage.total_tokens`
|
||||
- `usage.input_tokens`
|
||||
- `usage.output_tokens`
|
||||
- 请求耗时
|
||||
- 上游账号/分组/用户/key
|
||||
|
||||
## 编辑图片
|
||||
|
||||
### 单图编辑
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/edits" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "model=gpt-image-2" \
|
||||
-F "image[]=@input.png" \
|
||||
-F "prompt=Replace the sofa with a minimalist white lounge chair" \
|
||||
-F "size=1024x1024" \
|
||||
-F "quality=medium" \
|
||||
-F "output_format=png" \
|
||||
> edit.json
|
||||
```
|
||||
|
||||
### 局部遮罩编辑
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/edits" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "model=gpt-image-2" \
|
||||
-F "image[]=@input.png" \
|
||||
-F "mask=@mask.png" \
|
||||
-F "prompt=Change only the transparent masked region into a glass button" \
|
||||
-F "size=1024x1024" \
|
||||
-F "quality=medium" \
|
||||
> edit-mask.json
|
||||
```
|
||||
|
||||
遮罩要求:
|
||||
|
||||
- `image` 和 `mask` 必须同格式、同尺寸
|
||||
- 文件小于 50MB
|
||||
- `mask` 必须包含 alpha 通道
|
||||
- `gpt-image-2` 不要传 `input_fidelity`;它自动按高保真处理输入图
|
||||
|
||||
## Responses API 调用 image_generation tool
|
||||
|
||||
用于多轮 Agent、让模型先理解需求再调用图片工具。主模型使用文本/Agent 模型,例如 `gpt-5.5`。
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/responses" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-5.5",
|
||||
"input": "Generate a clean product poster for an AI proxy service.",
|
||||
"tools": [
|
||||
{
|
||||
"type": "image_generation",
|
||||
"quality": "medium",
|
||||
"size": "1536x1024",
|
||||
"output_format": "png"
|
||||
}
|
||||
]
|
||||
}' > response-image.json
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- `model` 是主推理模型,不是 `gpt-image-2`
|
||||
- `image_generation` 工具负责图片生成
|
||||
- sub2api 对 Codex 官方客户端请求会注入 `image_generation` 工具提示,但业务调用仍建议显式传 tool
|
||||
|
||||
## 流式图片
|
||||
|
||||
Image API 支持流式生成:
|
||||
|
||||
```bash
|
||||
curl -N "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A futuristic city skyline at sunrise",
|
||||
"stream": true,
|
||||
"partial_images": 2,
|
||||
"size": "1536x1024",
|
||||
"quality": "medium"
|
||||
}'
|
||||
```
|
||||
|
||||
事件类型:
|
||||
|
||||
```text
|
||||
image_generation.partial_image
|
||||
image_generation.completed
|
||||
```
|
||||
|
||||
`partial_images` 可设 `0-3`。每张 partial image 会额外产生输出 token 成本。
|
||||
|
||||
## SDK 示例
|
||||
|
||||
### Node.js
|
||||
|
||||
```ts
|
||||
import fs from "node:fs";
|
||||
import OpenAI from "openai";
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: process.env.API_KEY,
|
||||
baseURL: process.env.BASE_URL ?? "https://claude.omniclaw.store/v1",
|
||||
});
|
||||
|
||||
const result = await client.images.generate({
|
||||
model: "gpt-image-2",
|
||||
prompt: "A premium product poster for an AI service",
|
||||
size: "1536x1024",
|
||||
quality: "medium",
|
||||
output_format: "png",
|
||||
n: 1,
|
||||
});
|
||||
|
||||
const b64 = result.data?.[0]?.b64_json;
|
||||
if (!b64) throw new Error("No image returned");
|
||||
fs.writeFileSync("image.png", Buffer.from(b64, "base64"));
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```py
|
||||
import base64
|
||||
import os
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ["API_KEY"],
|
||||
base_url=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"),
|
||||
)
|
||||
|
||||
result = client.images.generate(
|
||||
model="gpt-image-2",
|
||||
prompt="A premium product poster for an AI service",
|
||||
size="1536x1024",
|
||||
quality="medium",
|
||||
output_format="png",
|
||||
n=1,
|
||||
)
|
||||
|
||||
b64 = result.data[0].b64_json
|
||||
with open("image.png", "wb") as f:
|
||||
f.write(base64.b64decode(b64))
|
||||
```
|
||||
|
||||
## 生产调度建议
|
||||
|
||||
- 路由:图片生成优先使用 plus/team/pro OpenAI OAuth 账号,避免 free 账号能力不足或限流。
|
||||
- 超时:普通图设置 120 秒,4K 设置 300 秒。
|
||||
- 重试:只对网络错误、502/503/504 做有限重试;不要对内容政策拒绝无限重试。
|
||||
- 并发:4K 请求输出 token 高,建议单账号小并发;普通 1024 图可更高并发。
|
||||
- 成本:记录 `usage` 并按 `input_tokens + output_tokens` 计费;4K 输出 token 可能远高于 1024。
|
||||
- 延迟:延迟敏感优先 `jpeg`,草稿用 `quality: low`。
|
||||
- 失败降级:4K/high 失败时降为 4K/medium;仍失败则 1536x1024/medium 先出图,再走放大流程。
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 现象 | 可能原因 | 处理 |
|
||||
|---|---|---|
|
||||
| `401 INVALID_API_KEY` | key 不是 sub2api key,或已删除/停用 | 从 `/keys` 重新生成 key |
|
||||
| `400 invalid_request_error` | 参数不兼容,例如透明背景、尺寸不合法 | 检查 `size`、`background`、`quality` |
|
||||
| `429 usage_limit_reached` | 命中 OpenAI 账号用量窗口 | 切换 plus/team/pro 账号或等待恢复 |
|
||||
| `502 Upstream request failed` | 上游没返回图片、网络断开、内容被拒绝文本化 | 看服务端日志;必要时改提示词/降质量/改尺寸 |
|
||||
| 超过 2 分钟 | 高像素或复杂提示词 | 设置更长超时,使用流式或先低分辨率验证 |
|
||||
| `/v1/models` 不显示 `gpt-image-2` | Codex 主模型列表不等于图片接口能力列表 | 直接调用 `/v1/images/generations` |
|
||||
|
||||
## 安全边界
|
||||
|
||||
业务侧应在请求前过滤明显违规内容,尤其是:
|
||||
|
||||
- 未成年人或年轻化人物的性化内容
|
||||
- 非自愿、胁迫、性暴力场景
|
||||
- 明确裸露或露骨性行为
|
||||
- 违法、仇恨、极端暴力内容
|
||||
|
||||
建议提示词显式写清“成年人、非露骨、无裸露、完全穿着”等约束,降低被上游拒绝或返回非图片文本的概率。
|
||||
|
||||
BIN
packages/sub2api-gpt-image-2-0.1.0.tar.gz
Normal file
BIN
packages/sub2api-gpt-image-2-0.1.0.tar.gz
Normal file
Binary file not shown.
117
registry/manifest.json
Normal file
117
registry/manifest.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"generated_at": "2026-04-24T08:11:41.942Z",
|
||||
"repository": {
|
||||
"name": "zsb/omniclaw-skills",
|
||||
"git_url": "https://git.omniclaw.store/zsb/omniclaw-skills.git",
|
||||
"raw_base_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main",
|
||||
"branch": "main"
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"name": "sub2api-gpt-image-2",
|
||||
"version": "0.1.0",
|
||||
"title": "Sub2API GPT Image 2",
|
||||
"description": "Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible image APIs.",
|
||||
"path": "skills/sub2api-gpt-image-2",
|
||||
"raw_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/skills/sub2api-gpt-image-2/SKILL.md",
|
||||
"package": "packages/sub2api-gpt-image-2-0.1.0.tar.gz",
|
||||
"package_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/packages/sub2api-gpt-image-2-0.1.0.tar.gz",
|
||||
"sha256": "d226c0327193646879e5d9de081a9e29400efa15e194f9c5e00620bd21cceb22",
|
||||
"size_bytes": 5420,
|
||||
"api_docs": [
|
||||
{
|
||||
"id": "sub2api/gpt-image-2.zh",
|
||||
"title": "GPT Image 2 API 调用文档",
|
||||
"product": "sub2api",
|
||||
"language": "zh",
|
||||
"path": "apis/sub2api/gpt-image-2.zh.md",
|
||||
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.zh.md",
|
||||
"tags": [
|
||||
"sub2api",
|
||||
"gpt-image-2",
|
||||
"zh"
|
||||
],
|
||||
"source_urls": [
|
||||
"https://claude.omniclaw.store/v1",
|
||||
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||
"https://developers.openai.com/api/reference/resources/images"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sub2api/gpt-image-2.en",
|
||||
"title": "GPT Image 2 API Guide",
|
||||
"product": "sub2api",
|
||||
"language": "en",
|
||||
"path": "apis/sub2api/gpt-image-2.en.md",
|
||||
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.en.md",
|
||||
"tags": [
|
||||
"sub2api",
|
||||
"gpt-image-2",
|
||||
"en"
|
||||
],
|
||||
"source_urls": [
|
||||
"https://claude.omniclaw.store/v1",
|
||||
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||
"https://developers.openai.com/api/reference/resources/images"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"sub2api",
|
||||
"openai",
|
||||
"gpt-image-2",
|
||||
"images",
|
||||
"responses",
|
||||
"codex"
|
||||
],
|
||||
"agents": [
|
||||
"codex",
|
||||
"openai-compatible-agent"
|
||||
],
|
||||
"updated_at": "2026-04-24"
|
||||
}
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"id": "sub2api/gpt-image-2.en",
|
||||
"title": "GPT Image 2 API Guide",
|
||||
"product": "sub2api",
|
||||
"language": "en",
|
||||
"path": "apis/sub2api/gpt-image-2.en.md",
|
||||
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.en.md",
|
||||
"tags": [
|
||||
"sub2api",
|
||||
"gpt-image-2",
|
||||
"en"
|
||||
],
|
||||
"source_urls": [
|
||||
"https://claude.omniclaw.store/v1",
|
||||
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||
"https://developers.openai.com/api/reference/resources/images"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sub2api/gpt-image-2.zh",
|
||||
"title": "GPT Image 2 API 调用文档",
|
||||
"product": "sub2api",
|
||||
"language": "zh",
|
||||
"path": "apis/sub2api/gpt-image-2.zh.md",
|
||||
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.zh.md",
|
||||
"tags": [
|
||||
"sub2api",
|
||||
"gpt-image-2",
|
||||
"zh"
|
||||
],
|
||||
"source_urls": [
|
||||
"https://claude.omniclaw.store/v1",
|
||||
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||
"https://developers.openai.com/api/reference/resources/images"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
59
registry/manifest.schema.json
Normal file
59
registry/manifest.schema.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://release.omniclaw.store/skills/manifest.schema.json",
|
||||
"title": "OmniClaw Skills Registry Manifest",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "generated_at", "repository", "skills", "apis"],
|
||||
"properties": {
|
||||
"schema_version": { "type": "string" },
|
||||
"generated_at": { "type": "string" },
|
||||
"repository": {
|
||||
"type": "object",
|
||||
"required": ["name", "git_url", "raw_base_url", "branch"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"git_url": { "type": "string" },
|
||||
"raw_base_url": { "type": "string" },
|
||||
"branch": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "version", "title", "description", "path", "package", "sha256"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"path": { "type": "string" },
|
||||
"package": { "type": "string" },
|
||||
"sha256": { "type": "string" },
|
||||
"size_bytes": { "type": "number" },
|
||||
"api_docs": { "type": "array" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"agents": { "type": "array", "items": { "type": "string" } },
|
||||
"updated_at": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"apis": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "product", "language", "path"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"product": { "type": "string" },
|
||||
"language": { "type": "string" },
|
||||
"path": { "type": "string" },
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"source_urls": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
skills/sub2api-gpt-image-2/SKILL.md
Normal file
85
skills/sub2api-gpt-image-2/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: sub2api-gpt-image-2
|
||||
description: Use when Codex or another agent needs to call, document, troubleshoot, or generate code for GPT Image 2 through sub2api/OpenAI-compatible image APIs, including /v1/images/generations, /v1/images/edits, Responses image_generation tools, 4K image settings, streaming partial images, base64 decoding, and production error handling.
|
||||
---
|
||||
|
||||
# Sub2API GPT Image 2
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Treat `gpt-image-2` as an image generation/editing model, not a Codex or agent main model.
|
||||
2. Use `/v1/images/generations` for direct text-to-image and `/v1/images/edits` for reference-image or mask edits.
|
||||
3. Use `/v1/responses` with a text/agent model such as `gpt-5.5` plus an `image_generation` tool when an agent should reason before generating images.
|
||||
4. Never print, commit, or hard-code real API keys. Read credentials from `API_KEY` and `BASE_URL`.
|
||||
5. For detailed parameter rules, read `references/api.md`.
|
||||
6. For quick non-streaming generation tests, run `scripts/generate_image.py`.
|
||||
|
||||
## Defaults
|
||||
|
||||
Use these defaults unless the task says otherwise:
|
||||
|
||||
```text
|
||||
BASE_URL=https://claude.omniclaw.store/v1
|
||||
model=gpt-image-2
|
||||
size=1024x1024
|
||||
quality=medium
|
||||
output_format=png
|
||||
n=1
|
||||
timeout=300 for 4K, 120 otherwise
|
||||
```
|
||||
|
||||
Use `3840x2160` only when explicitly requested or when a final 4K asset is needed. For prompt iteration, generate smaller images first.
|
||||
|
||||
## Direct Generation
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
export API_KEY="sk-..."
|
||||
export BASE_URL="https://claude.omniclaw.store/v1"
|
||||
python3 ~/.codex/skills/sub2api-gpt-image-2/scripts/generate_image.py \
|
||||
--prompt "A premium product poster for an AI service" \
|
||||
--size 1536x1024 \
|
||||
--quality medium \
|
||||
--output image.png
|
||||
```
|
||||
|
||||
Use `--dry-run` to inspect payloads without calling the API.
|
||||
|
||||
## Editing
|
||||
|
||||
Use multipart form requests with `image[]=@file.png`; add `mask=@mask.png` only for local masked edits. For `gpt-image-2`, omit `input_fidelity` because it always processes image inputs at high fidelity.
|
||||
|
||||
## Responses Tool Pattern
|
||||
|
||||
For agentic workflows:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-5.5",
|
||||
"input": "Generate a clean product poster for an AI proxy service.",
|
||||
"tools": [
|
||||
{
|
||||
"type": "image_generation",
|
||||
"size": "1536x1024",
|
||||
"quality": "medium",
|
||||
"output_format": "png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Do not set the Responses `model` to `gpt-image-2`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `401`: wrong or disabled sub2api key.
|
||||
- `400`: invalid params such as unsupported transparent background or invalid size.
|
||||
- `429`: upstream account usage limit; prefer plus/team/pro accounts.
|
||||
- `502`: upstream did not return image output, network failed, or content was refused; inspect sub2api logs and simplify the prompt.
|
||||
- Long latency: 4K/high can take minutes; use `jpeg`, `quality=low|medium`, or smaller sizes for drafts.
|
||||
|
||||
## Safety
|
||||
|
||||
Reject or rewrite sexualized young-looking subjects, minors, non-consensual sexual content, explicit nudity, and other disallowed content before calling the API.
|
||||
|
||||
5
skills/sub2api-gpt-image-2/agents/openai.yaml
Normal file
5
skills/sub2api-gpt-image-2/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Sub2API GPT Image 2"
|
||||
short_description: "Generate and debug GPT Image 2 calls."
|
||||
default_prompt: "Use sub2api-gpt-image-2 to generate, edit, stream, or troubleshoot GPT Image 2 API requests."
|
||||
|
||||
106
skills/sub2api-gpt-image-2/references/api.md
Normal file
106
skills/sub2api-gpt-image-2/references/api.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# GPT Image 2 API Reference For sub2api
|
||||
|
||||
Source links:
|
||||
|
||||
- https://developers.openai.com/api/docs/models/gpt-image-2
|
||||
- https://developers.openai.com/api/docs/guides/image-generation
|
||||
- https://developers.openai.com/api/reference/resources/images
|
||||
- Repository docs: `apis/sub2api/gpt-image-2.zh.md` and `apis/sub2api/gpt-image-2.en.md`
|
||||
|
||||
## Endpoints
|
||||
|
||||
```text
|
||||
POST /v1/images/generations
|
||||
POST /v1/images/edits
|
||||
POST /v1/responses # image_generation tool with a text/agent model
|
||||
```
|
||||
|
||||
`/v1/models` may only list Codex/text models in this deployment. That does not mean `/v1/images/generations` cannot accept `gpt-image-2`.
|
||||
|
||||
## Generation Parameters
|
||||
|
||||
| Parameter | Values | Notes |
|
||||
|---|---|---|
|
||||
| `model` | `gpt-image-2`, `gpt-image-2-2026-04-21` | Required for Images API. |
|
||||
| `prompt` | string | Required. Be concrete about subject, composition, style, lighting, and safety constraints. |
|
||||
| `n` | `1-10` | Prefer `1` for production retries and billing attribution. |
|
||||
| `size` | `auto` or valid `WxH` | Common: `1024x1024`, `1536x1024`, `1024x1536`, `3840x2160`. |
|
||||
| `quality` | `low`, `medium`, `high`, `auto` | Use `low` for drafts, `medium` for normal, `high` for final. |
|
||||
| `output_format` | `png`, `jpeg`, `webp` | `jpeg` is usually faster than `png`. |
|
||||
| `output_compression` | `0-100` | Applies to `jpeg` and `webp`. |
|
||||
| `background` | `auto`, `opaque` | `gpt-image-2` does not support `transparent`. |
|
||||
| `moderation` | `auto`, `low` | Changes filter strictness but does not bypass policy. |
|
||||
| `stream` | boolean | Enables SSE image events. |
|
||||
| `partial_images` | `0-3` | Streaming only; each partial image adds output token cost. |
|
||||
| `user` | string | End-user identifier for abuse monitoring. |
|
||||
|
||||
## Size Constraints
|
||||
|
||||
- Maximum edge length: `3840px`.
|
||||
- Both edges must be multiples of `16px`.
|
||||
- Long edge to short edge ratio must be at most `3:1`.
|
||||
- Total pixels must be between `655360` and `8294400`.
|
||||
- Outputs larger than `2560x1440` should be treated as high-latency experimental outputs.
|
||||
|
||||
## Edit Notes
|
||||
|
||||
- Use multipart form data.
|
||||
- Send reference images as `image[]`.
|
||||
- Optional `mask` must match input image format and dimensions, be under 50MB, and include an alpha channel.
|
||||
- Omit `input_fidelity` for `gpt-image-2`.
|
||||
|
||||
## Response Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "...",
|
||||
"revised_prompt": "..."
|
||||
}
|
||||
],
|
||||
"model": "gpt-image-2",
|
||||
"size": "1024x1024",
|
||||
"quality": "medium",
|
||||
"output_format": "png",
|
||||
"usage": {
|
||||
"input_tokens": 43,
|
||||
"output_tokens": 196,
|
||||
"total_tokens": 239
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Always decode `data[0].b64_json` unless streaming.
|
||||
|
||||
## Production Dispatch
|
||||
|
||||
- Prefer plus/team/pro upstream OpenAI OAuth accounts for image workloads.
|
||||
- Set client timeout to `120s` for normal images and `300s` for 4K.
|
||||
- Retry only transient network/5xx failures with low retry counts.
|
||||
- Do not retry policy or validation errors without changing the prompt or params.
|
||||
- Log `usage`, latency, size, quality, output format, account ID, API key ID, and request ID.
|
||||
|
||||
## Valid curl Template
|
||||
|
||||
```bash
|
||||
curl -sS "$BASE_URL/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
--max-time 300 \
|
||||
-d '{
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "A premium product poster for an AI service",
|
||||
"size": "1536x1024",
|
||||
"quality": "medium",
|
||||
"output_format": "png",
|
||||
"n": 1
|
||||
}' > image.json
|
||||
```
|
||||
|
||||
Decode:
|
||||
|
||||
```bash
|
||||
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
|
||||
```
|
||||
|
||||
123
skills/sub2api-gpt-image-2/scripts/generate_image.py
Executable file
123
skills/sub2api-gpt-image-2/scripts/generate_image.py
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate an image with gpt-image-2 through an OpenAI-compatible /v1/images/generations endpoint."
|
||||
)
|
||||
parser.add_argument("--base-url", default=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"))
|
||||
parser.add_argument("--api-key-env", default="API_KEY")
|
||||
parser.add_argument("--prompt", required=True)
|
||||
parser.add_argument("--output", default="image.png")
|
||||
parser.add_argument("--model", default="gpt-image-2")
|
||||
parser.add_argument("--size", default="1024x1024")
|
||||
parser.add_argument("--quality", default="medium", choices=["low", "medium", "high", "auto"])
|
||||
parser.add_argument("--format", default="png", choices=["png", "jpeg", "webp"])
|
||||
parser.add_argument("--compression", type=int, default=None)
|
||||
parser.add_argument("--background", default=None, choices=["auto", "opaque"])
|
||||
parser.add_argument("--moderation", default=None, choices=["auto", "low"])
|
||||
parser.add_argument("--n", type=int, default=1)
|
||||
parser.add_argument("--timeout", type=float, default=300.0)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def build_payload(args: argparse.Namespace) -> dict:
|
||||
payload = {
|
||||
"model": args.model,
|
||||
"prompt": args.prompt,
|
||||
"size": args.size,
|
||||
"quality": args.quality,
|
||||
"output_format": args.format,
|
||||
"n": args.n,
|
||||
}
|
||||
if args.compression is not None:
|
||||
payload["output_compression"] = args.compression
|
||||
if args.background:
|
||||
payload["background"] = args.background
|
||||
if args.moderation:
|
||||
payload["moderation"] = args.moderation
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
payload = build_payload(args)
|
||||
url = args.base_url.rstrip("/") + "/images/generations"
|
||||
|
||||
if args.dry_run:
|
||||
print(json.dumps({"url": url, "payload": payload}, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
api_key = os.environ.get(args.api_key_env)
|
||||
if not api_key:
|
||||
print(f"Missing API key env: {args.api_key_env}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
method="POST",
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=args.timeout) as resp:
|
||||
status = resp.status
|
||||
raw = resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
raw = exc.read()
|
||||
print(f"HTTP {exc.code}", file=sys.stderr)
|
||||
print(raw.decode("utf-8", "replace")[:4000], file=sys.stderr)
|
||||
return 1
|
||||
except Exception as exc:
|
||||
print(f"request failed: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
elapsed = time.monotonic() - started
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
if "error" in data:
|
||||
print(json.dumps(data["error"], ensure_ascii=False), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
items = data.get("data") or []
|
||||
if not items or "b64_json" not in items[0]:
|
||||
print("No b64_json image returned", file=sys.stderr)
|
||||
print(json.dumps(data, ensure_ascii=False)[:4000], file=sys.stderr)
|
||||
return 1
|
||||
|
||||
image = base64.b64decode(items[0]["b64_json"])
|
||||
with open(args.output, "wb") as f:
|
||||
f.write(image)
|
||||
|
||||
summary = {
|
||||
"status": status,
|
||||
"elapsed_seconds": round(elapsed, 3),
|
||||
"output": args.output,
|
||||
"bytes": len(image),
|
||||
"model": data.get("model"),
|
||||
"size": data.get("size"),
|
||||
"quality": data.get("quality"),
|
||||
"output_format": data.get("output_format"),
|
||||
"usage": data.get("usage"),
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
11
skills/sub2api-gpt-image-2/skill.registry.json
Normal file
11
skills/sub2api-gpt-image-2/skill.registry.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "sub2api-gpt-image-2",
|
||||
"version": "0.1.0",
|
||||
"title": "Sub2API GPT Image 2",
|
||||
"description": "Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible image APIs.",
|
||||
"tags": ["sub2api", "openai", "gpt-image-2", "images", "responses", "codex"],
|
||||
"agents": ["codex", "openai-compatible-agent"],
|
||||
"api_docs": ["sub2api/gpt-image-2.zh", "sub2api/gpt-image-2.en"],
|
||||
"updated_at": "2026-04-24"
|
||||
}
|
||||
|
||||
157
tools/build-registry.mjs
Executable file
157
tools/build-registry.mjs
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { createReadStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const root = path.resolve(new URL("..", import.meta.url).pathname);
|
||||
const skillsDir = path.join(root, "skills");
|
||||
const apisDir = path.join(root, "apis");
|
||||
const packagesDir = path.join(root, "packages");
|
||||
const registryDir = path.join(root, "registry");
|
||||
|
||||
const repository = {
|
||||
name: "zsb/omniclaw-skills",
|
||||
git_url: "https://git.omniclaw.store/zsb/omniclaw-skills.git",
|
||||
raw_base_url: "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main",
|
||||
branch: "main",
|
||||
};
|
||||
|
||||
async function exists(file) {
|
||||
try {
|
||||
await stat(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sha256(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = createHash("sha256");
|
||||
const stream = createReadStream(file);
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(hash.digest("hex")));
|
||||
});
|
||||
}
|
||||
|
||||
async function collectApiDocs() {
|
||||
const docs = [];
|
||||
if (!(await exists(apisDir))) {
|
||||
return docs;
|
||||
}
|
||||
const products = await readdir(apisDir, { withFileTypes: true });
|
||||
for (const productEntry of products) {
|
||||
if (!productEntry.isDirectory()) continue;
|
||||
const product = productEntry.name;
|
||||
const productDir = path.join(apisDir, product);
|
||||
const files = await readdir(productDir, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
if (!file.isFile() || !file.name.endsWith(".md")) continue;
|
||||
const fullPath = path.join(productDir, file.name);
|
||||
const relPath = path.relative(root, fullPath);
|
||||
const text = await readFile(fullPath, "utf8");
|
||||
const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() || file.name;
|
||||
const language = file.name.endsWith(".zh.md") ? "zh" : file.name.endsWith(".en.md") ? "en" : "unknown";
|
||||
const id = `${product}/${file.name.replace(/\.md$/, "")}`;
|
||||
const sourceUrls = [...text.matchAll(/https:\/\/[^\s)]+/g)].map((m) =>
|
||||
m[0].replace(/[",)}>]+$/g, ""),
|
||||
);
|
||||
docs.push({
|
||||
id,
|
||||
title,
|
||||
product,
|
||||
language,
|
||||
path: relPath,
|
||||
url: `${repository.raw_base_url}/${relPath}`,
|
||||
tags: Array.from(new Set([product, ...id.split(/[/.]/).filter(Boolean)])),
|
||||
source_urls: Array.from(new Set(sourceUrls)),
|
||||
});
|
||||
}
|
||||
}
|
||||
return docs.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
async function buildSkillPackage(skillName, version) {
|
||||
await mkdir(packagesDir, { recursive: true });
|
||||
const packageName = `${skillName}-${version}.tar.gz`;
|
||||
const packagePath = path.join(packagesDir, packageName);
|
||||
await rm(packagePath, { force: true });
|
||||
const result = spawnSync("tar", ["-czf", packagePath, "-C", skillsDir, skillName], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`tar failed for ${skillName}`);
|
||||
}
|
||||
const fileStat = await stat(packagePath);
|
||||
return {
|
||||
packageName,
|
||||
packagePath,
|
||||
sizeBytes: fileStat.size,
|
||||
digest: await sha256(packagePath),
|
||||
};
|
||||
}
|
||||
|
||||
async function collectSkills(apiDocs) {
|
||||
const skills = [];
|
||||
if (!(await exists(skillsDir))) {
|
||||
return skills;
|
||||
}
|
||||
const entries = await readdir(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const skillName = entry.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
const metadataPath = path.join(skillDir, "skill.registry.json");
|
||||
const skillPath = path.join(skillDir, "SKILL.md");
|
||||
if (!(await exists(metadataPath)) || !(await exists(skillPath))) {
|
||||
continue;
|
||||
}
|
||||
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
|
||||
const pkg = await buildSkillPackage(skillName, metadata.version);
|
||||
const relatedDocs = (metadata.api_docs || []).map((docId) => {
|
||||
const doc = apiDocs.find((item) => item.id === docId);
|
||||
return doc || { id: docId };
|
||||
});
|
||||
skills.push({
|
||||
name: skillName,
|
||||
version: metadata.version,
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
path: `skills/${skillName}`,
|
||||
raw_url: `${repository.raw_base_url}/skills/${skillName}/SKILL.md`,
|
||||
package: `packages/${pkg.packageName}`,
|
||||
package_url: `${repository.raw_base_url}/packages/${pkg.packageName}`,
|
||||
sha256: pkg.digest,
|
||||
size_bytes: pkg.sizeBytes,
|
||||
api_docs: relatedDocs,
|
||||
tags: metadata.tags || [],
|
||||
agents: metadata.agents || ["codex"],
|
||||
updated_at: metadata.updated_at,
|
||||
});
|
||||
}
|
||||
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(registryDir, { recursive: true });
|
||||
const apis = await collectApiDocs();
|
||||
const skills = await collectSkills(apis);
|
||||
const manifest = {
|
||||
schema_version: "1.0.0",
|
||||
generated_at: new Date().toISOString(),
|
||||
repository,
|
||||
skills,
|
||||
apis,
|
||||
};
|
||||
await writeFile(path.join(registryDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`Built registry: ${skills.length} skills, ${apis.length} API docs`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
99
tools/install.sh
Executable file
99
tools/install.sh
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_NAME="${1:-}"
|
||||
VERSION="${2:-latest}"
|
||||
REPO_URL="${OMNICLAW_SKILLS_REPO:-https://git.omniclaw.store/zsb/omniclaw-skills.git}"
|
||||
RAW_BASE_URL="${OMNICLAW_SKILLS_RAW_BASE_URL:-https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main}"
|
||||
DEST_DIR="${CODEX_HOME:-$HOME/.codex}/skills"
|
||||
|
||||
if [[ -z "$SKILL_NAME" ]]; then
|
||||
echo "Usage: install.sh <skill-name> [version]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd mktemp
|
||||
require_cmd node
|
||||
require_cmd tar
|
||||
require_cmd shasum
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
manifest="$tmp_dir/manifest.json"
|
||||
if [[ -d "$RAW_BASE_URL" && -f "$RAW_BASE_URL/registry/manifest.json" ]]; then
|
||||
cp "$RAW_BASE_URL/registry/manifest.json" "$manifest"
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$RAW_BASE_URL/registry/manifest.json" -o "$manifest" || true
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO "$manifest" "$RAW_BASE_URL/registry/manifest.json" || true
|
||||
else
|
||||
: > "$manifest"
|
||||
fi
|
||||
|
||||
if [[ ! -s "$manifest" ]]; then
|
||||
require_cmd git
|
||||
git clone --depth 1 "$REPO_URL" "$tmp_dir/repo"
|
||||
if [[ ! -f "$tmp_dir/repo/registry/manifest.json" ]]; then
|
||||
echo "Unable to find registry/manifest.json from $REPO_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$tmp_dir/repo/registry/manifest.json" "$manifest"
|
||||
RAW_BASE_URL="$tmp_dir/repo"
|
||||
fi
|
||||
|
||||
node_script='
|
||||
const fs = require("fs");
|
||||
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
const name = process.argv[2];
|
||||
const version = process.argv[3];
|
||||
const skill = manifest.skills.find((item) => item.name === name && (version === "latest" || item.version === version));
|
||||
if (!skill) {
|
||||
console.error(`Skill not found: ${name}@${version}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(skill));
|
||||
'
|
||||
|
||||
skill_json="$(node -e "$node_script" "$manifest" "$SKILL_NAME" "$VERSION")"
|
||||
package_path="$(printf '%s' "$skill_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>console.log(JSON.parse(s).package));')"
|
||||
expected_sha="$(printf '%s' "$skill_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>console.log(JSON.parse(s).sha256));')"
|
||||
|
||||
archive="$tmp_dir/skill.tar.gz"
|
||||
if [[ -d "$RAW_BASE_URL" ]]; then
|
||||
cp "$RAW_BASE_URL/$package_path" "$archive"
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$RAW_BASE_URL/$package_path" -o "$archive"
|
||||
else
|
||||
wget -qO "$archive" "$RAW_BASE_URL/$package_path"
|
||||
fi
|
||||
|
||||
actual_sha="$(shasum -a 256 "$archive" | awk '{print $1}')"
|
||||
if [[ "$actual_sha" != "$expected_sha" ]]; then
|
||||
echo "Checksum mismatch for $package_path" >&2
|
||||
echo "expected: $expected_sha" >&2
|
||||
echo "actual: $actual_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST_DIR"
|
||||
target="$DEST_DIR/$SKILL_NAME"
|
||||
if [[ -e "$target" ]]; then
|
||||
backup="$target.backup.$(date +%Y%m%d%H%M%S)"
|
||||
mv "$target" "$backup"
|
||||
echo "Existing skill moved to $backup"
|
||||
fi
|
||||
|
||||
tar -xzf "$archive" -C "$DEST_DIR"
|
||||
echo "Installed $SKILL_NAME to $DEST_DIR/$SKILL_NAME"
|
||||
echo "Restart Codex to pick up new skills."
|
||||
105
tools/validate.mjs
Executable file
105
tools/validate.mjs
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(new URL("..", import.meta.url).pathname);
|
||||
const skillsDir = path.join(root, "skills");
|
||||
const apisDir = path.join(root, "apis");
|
||||
|
||||
const errors = [];
|
||||
|
||||
async function exists(file) {
|
||||
try {
|
||||
await stat(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function check(condition, message) {
|
||||
if (!condition) errors.push(message);
|
||||
}
|
||||
|
||||
function parseSkillFrontmatter(text, file) {
|
||||
const match = text.match(/^---\n([\s\S]*?)\n---/);
|
||||
check(Boolean(match), `${file}: missing YAML frontmatter`);
|
||||
if (!match) return {};
|
||||
const data = {};
|
||||
for (const line of match[1].split("\n")) {
|
||||
const parts = line.split(":");
|
||||
if (parts.length < 2) continue;
|
||||
const key = parts.shift().trim();
|
||||
const value = parts.join(":").trim().replace(/^"|"$/g, "");
|
||||
data[key] = value;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function validateSkills() {
|
||||
if (!(await exists(skillsDir))) return;
|
||||
const entries = await readdir(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const name = entry.name;
|
||||
const dir = path.join(skillsDir, name);
|
||||
const skillFile = path.join(dir, "SKILL.md");
|
||||
const registryFile = path.join(dir, "skill.registry.json");
|
||||
const agentFile = path.join(dir, "agents", "openai.yaml");
|
||||
check(await exists(skillFile), `${name}: missing SKILL.md`);
|
||||
check(await exists(registryFile), `${name}: missing skill.registry.json`);
|
||||
check(await exists(agentFile), `${name}: missing agents/openai.yaml`);
|
||||
if (!(await exists(skillFile)) || !(await exists(registryFile))) continue;
|
||||
|
||||
const skillText = await readFile(skillFile, "utf8");
|
||||
const frontmatter = parseSkillFrontmatter(skillText, `${name}/SKILL.md`);
|
||||
check(frontmatter.name === name, `${name}: frontmatter name must equal folder name`);
|
||||
check(Boolean(frontmatter.description), `${name}: missing frontmatter description`);
|
||||
|
||||
let metadata;
|
||||
try {
|
||||
metadata = JSON.parse(await readFile(registryFile, "utf8"));
|
||||
} catch (error) {
|
||||
errors.push(`${name}: invalid skill.registry.json: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
check(metadata.name === name, `${name}: metadata name must equal folder name`);
|
||||
check(Boolean(metadata.version), `${name}: missing version`);
|
||||
check(Boolean(metadata.title), `${name}: missing title`);
|
||||
check(Boolean(metadata.description), `${name}: missing description`);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateApiDocs() {
|
||||
if (!(await exists(apisDir))) return;
|
||||
const products = await readdir(apisDir, { withFileTypes: true });
|
||||
for (const productEntry of products) {
|
||||
if (!productEntry.isDirectory()) continue;
|
||||
const productDir = path.join(apisDir, productEntry.name);
|
||||
const files = (await readdir(productDir)).filter((name) => name.endsWith(".md"));
|
||||
const stems = new Map();
|
||||
for (const file of files) {
|
||||
const stem = file.replace(/\.(zh|en)\.md$/, "");
|
||||
if (!stems.has(stem)) stems.set(stem, new Set());
|
||||
if (file.endsWith(".zh.md")) stems.get(stem).add("zh");
|
||||
if (file.endsWith(".en.md")) stems.get(stem).add("en");
|
||||
const text = await readFile(path.join(productDir, file), "utf8");
|
||||
check(/^#\s+.+/m.test(text), `${productEntry.name}/${file}: missing H1 title`);
|
||||
}
|
||||
for (const [stem, languages] of stems) {
|
||||
check(languages.has("zh"), `${productEntry.name}/${stem}: missing zh doc`);
|
||||
check(languages.has("en"), `${productEntry.name}/${stem}: missing en doc`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await validateSkills();
|
||||
await validateApiDocs();
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.map((item) => `- ${item}`).join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Validation passed.");
|
||||
|
||||
Reference in New Issue
Block a user