commit 789bb38e69626c4f8431e91a003fffc1326d5ec1 Author: boris Date: Fri Apr 24 01:12:20 2026 -0700 feat: initialize OmniClaw skills registry diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95a3689 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +._* +node_modules/ +.env +.env.* +!.env.example +*.log +tmp/ + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..257f980 --- /dev/null +++ b/AGENTS.md @@ -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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4203955 --- /dev/null +++ b/README.md @@ -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.md + agents/openai.yaml + references/ + scripts/ + skill.registry.json +apis/ + / + .zh.md + .en.md +registry/ + manifest.schema.json + manifest.json +packages/ + -.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/-.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. + diff --git a/apis/sub2api/gpt-image-2.en.md b/apis/sub2api/gpt-image-2.en.md new file mode 100644 index 0000000..65d4a03 --- /dev/null +++ b/apis/sub2api/gpt-image-2.en.md @@ -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= +``` + +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. + diff --git a/apis/sub2api/gpt-image-2.zh.md b/apis/sub2api/gpt-image-2.zh.md new file mode 100644 index 0000000..baf5e45 --- /dev/null +++ b/apis/sub2api/gpt-image-2.zh.md @@ -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` | + +## 安全边界 + +业务侧应在请求前过滤明显违规内容,尤其是: + +- 未成年人或年轻化人物的性化内容 +- 非自愿、胁迫、性暴力场景 +- 明确裸露或露骨性行为 +- 违法、仇恨、极端暴力内容 + +建议提示词显式写清“成年人、非露骨、无裸露、完全穿着”等约束,降低被上游拒绝或返回非图片文本的概率。 + diff --git a/packages/sub2api-gpt-image-2-0.1.0.tar.gz b/packages/sub2api-gpt-image-2-0.1.0.tar.gz new file mode 100644 index 0000000..9ed3ef0 Binary files /dev/null and b/packages/sub2api-gpt-image-2-0.1.0.tar.gz differ diff --git a/registry/manifest.json b/registry/manifest.json new file mode 100644 index 0000000..8696751 --- /dev/null +++ b/registry/manifest.json @@ -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" + ] + } + ] +} diff --git a/registry/manifest.schema.json b/registry/manifest.schema.json new file mode 100644 index 0000000..25ca2dc --- /dev/null +++ b/registry/manifest.schema.json @@ -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" } } + } + } + } + } +} + diff --git a/skills/sub2api-gpt-image-2/SKILL.md b/skills/sub2api-gpt-image-2/SKILL.md new file mode 100644 index 0000000..0c62dc0 --- /dev/null +++ b/skills/sub2api-gpt-image-2/SKILL.md @@ -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. + diff --git a/skills/sub2api-gpt-image-2/agents/openai.yaml b/skills/sub2api-gpt-image-2/agents/openai.yaml new file mode 100644 index 0000000..8cc4fc1 --- /dev/null +++ b/skills/sub2api-gpt-image-2/agents/openai.yaml @@ -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." + diff --git a/skills/sub2api-gpt-image-2/references/api.md b/skills/sub2api-gpt-image-2/references/api.md new file mode 100644 index 0000000..d82d8d2 --- /dev/null +++ b/skills/sub2api-gpt-image-2/references/api.md @@ -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 +``` + diff --git a/skills/sub2api-gpt-image-2/scripts/generate_image.py b/skills/sub2api-gpt-image-2/scripts/generate_image.py new file mode 100755 index 0000000..899147f --- /dev/null +++ b/skills/sub2api-gpt-image-2/scripts/generate_image.py @@ -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()) + diff --git a/skills/sub2api-gpt-image-2/skill.registry.json b/skills/sub2api-gpt-image-2/skill.registry.json new file mode 100644 index 0000000..45be2eb --- /dev/null +++ b/skills/sub2api-gpt-image-2/skill.registry.json @@ -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" +} + diff --git a/tools/build-registry.mjs b/tools/build-registry.mjs new file mode 100755 index 0000000..3212be2 --- /dev/null +++ b/tools/build-registry.mjs @@ -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); +}); diff --git a/tools/install.sh b/tools/install.sh new file mode 100755 index 0000000..e8c4252 --- /dev/null +++ b/tools/install.sh @@ -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 [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." diff --git a/tools/validate.mjs b/tools/validate.mjs new file mode 100755 index 0000000..9facb73 --- /dev/null +++ b/tools/validate.mjs @@ -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."); +