@xinizai/pi-image-gen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +337 -0
- package/dist/core/cache.d.ts +11 -0
- package/dist/core/cache.js +42 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/capabilities.d.ts +2 -0
- package/dist/core/capabilities.js +65 -0
- package/dist/core/capabilities.js.map +1 -0
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/global-config.d.ts +25 -0
- package/dist/core/global-config.js +172 -0
- package/dist/core/global-config.js.map +1 -0
- package/dist/core/image-service.d.ts +14 -0
- package/dist/core/image-service.js +42 -0
- package/dist/core/image-service.js.map +1 -0
- package/dist/core/model-browser.d.ts +5 -0
- package/dist/core/model-browser.js +76 -0
- package/dist/core/model-browser.js.map +1 -0
- package/dist/core/model-registry.d.ts +10 -0
- package/dist/core/model-registry.js +44 -0
- package/dist/core/model-registry.js.map +1 -0
- package/dist/core/provider.d.ts +2 -0
- package/dist/core/provider.js +8 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/types.d.ts +122 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +490 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +16 -0
- package/dist/providers/openai-compatible.js +216 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/tools/shared.d.ts +42 -0
- package/dist/tools/shared.js +241 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +65 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/download.d.ts +8 -0
- package/dist/utils/download.js +139 -0
- package/dist/utils/download.js.map +1 -0
- package/dist/utils/files.d.ts +18 -0
- package/dist/utils/files.js +83 -0
- package/dist/utils/files.js.map +1 -0
- package/package.json +34 -0
- package/src/core/cache.ts +43 -0
- package/src/core/capabilities.ts +55 -0
- package/src/core/errors.ts +92 -0
- package/src/core/global-config.ts +167 -0
- package/src/core/image-service.ts +40 -0
- package/src/core/model-browser.ts +73 -0
- package/src/core/model-registry.ts +43 -0
- package/src/core/provider.ts +8 -0
- package/src/core/types.ts +135 -0
- package/src/index.ts +394 -0
- package/src/providers/openai-compatible.ts +198 -0
- package/src/tools/shared.ts +235 -0
- package/src/utils/config.ts +61 -0
- package/src/utils/download.ts +103 -0
- package/src/utils/files.ts +78 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ImageGenError, errorFromStatus } from "../core/errors.js";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { lookup } from "node:dns/promises";
|
|
4
|
+
import { detectImageFormat, extensionForMime, saveImageBuffer } from "./files.js";
|
|
5
|
+
export function assertSafeHttpUrl(raw) {
|
|
6
|
+
let url;
|
|
7
|
+
try {
|
|
8
|
+
url = new URL(raw);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new ImageGenError("invalid_image", "图片 URL 无效。", "使用有效 https/http URL。 ");
|
|
12
|
+
}
|
|
13
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
14
|
+
throw new ImageGenError("invalid_image", "图片 URL 协议不受支持。", "只允许 http/https。 ");
|
|
15
|
+
if (!url.hostname || isBlockedHost(url.hostname))
|
|
16
|
+
throw new ImageGenError("invalid_image", "图片 URL 目标地址不安全。", "不能访问 localhost、内网地址或云元数据地址。 ");
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
export async function fetchWithTimeout(url, init, timeoutMs) {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
+
try {
|
|
23
|
+
const signal = init.signal ? AbortSignal.any([init.signal, controller.signal]) : controller.signal;
|
|
24
|
+
return await fetch(url, { ...init, signal });
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (controller.signal.aborted)
|
|
28
|
+
throw new ImageGenError("timeout", "请求超时。", "稍后重试或增大 IMAGE_TIMEOUT_SECONDS。 ");
|
|
29
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
30
|
+
throw new ImageGenError("network_error", "请求已取消。", "重新发起操作即可。 ");
|
|
31
|
+
throw new ImageGenError("network_error", "网络请求失败。", e instanceof Error ? e.message : "检查网络连接。 ");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function readResponseTextLimited(response, maxBytes) {
|
|
38
|
+
const len = Number(response.headers.get("content-length") ?? 0);
|
|
39
|
+
if (len > maxBytes)
|
|
40
|
+
throw new ImageGenError("response_too_large", "API 响应超过安全大小限制。", "降低 n 或联系服务商。 ");
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
if (Buffer.byteLength(text) > maxBytes)
|
|
43
|
+
throw new ImageGenError("response_too_large", "API 响应超过安全大小限制。", "降低 n 或联系服务商。 ");
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
export async function parseJsonLimited(response, maxBytes) {
|
|
47
|
+
const text = await readResponseTextLimited(response, maxBytes);
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new ImageGenError("invalid_json", "API 返回了无效 JSON。", "确认服务商是否兼容 OpenAI 风格 API。 ");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isBlockedHost(hostname) {
|
|
56
|
+
const host = hostname.toLowerCase().replace(/[.]$/, "");
|
|
57
|
+
if (host === "localhost" || host.endsWith(".localhost") || host === "metadata.google.internal")
|
|
58
|
+
return true;
|
|
59
|
+
const ip = isIP(host);
|
|
60
|
+
if (ip === 4) {
|
|
61
|
+
const p = host.split(".").map(Number);
|
|
62
|
+
return p.length === 4 && p[0] !== undefined && p[1] !== undefined && (p[0] === 10 || p[0] === 127 || (p[0] === 169 && p[1] === 254) || (p[0] === 192 && p[1] === 168) || (p[0] === 172 && p[1] >= 16 && p[1] <= 31));
|
|
63
|
+
}
|
|
64
|
+
if (ip === 6)
|
|
65
|
+
return host === "::1" || host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80:");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
async function resolvesToPrivateHost(hostname) {
|
|
69
|
+
if (isBlockedHost(hostname))
|
|
70
|
+
return true;
|
|
71
|
+
try {
|
|
72
|
+
const records = await lookup(hostname, { all: true });
|
|
73
|
+
return records.some((record) => isBlockedHost(record.address));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function rejectHtml(buffer) {
|
|
80
|
+
const prefix = buffer.subarray(0, 256).toString("utf8").trimStart().toLowerCase();
|
|
81
|
+
if (prefix.startsWith("<!doctype html") || prefix.startsWith("<html") || prefix.startsWith("<head") || prefix.startsWith("<body"))
|
|
82
|
+
throw new ImageGenError("invalid_image", "下载内容不是图片,可能是鉴权页或错误页面。", "检查 Provider 图片 URL 和权限。 ");
|
|
83
|
+
}
|
|
84
|
+
export async function downloadImage(url, outputDir, timeoutMs, maxBytes, authorization) {
|
|
85
|
+
let target = assertSafeHttpUrl(url);
|
|
86
|
+
if (await resolvesToPrivateHost(target.hostname))
|
|
87
|
+
throw new ImageGenError("invalid_image", "图片 URL 解析到不安全的内网地址。", "不能访问 localhost、内网地址或云元数据地址。 ");
|
|
88
|
+
const origin = target.origin;
|
|
89
|
+
let response;
|
|
90
|
+
for (let redirects = 0; redirects <= 3; redirects += 1) {
|
|
91
|
+
const init = { method: "GET", redirect: "manual" };
|
|
92
|
+
if (authorization && target.origin === origin)
|
|
93
|
+
init.headers = { Authorization: authorization };
|
|
94
|
+
response = await fetchWithTimeout(target.toString(), init, timeoutMs);
|
|
95
|
+
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
96
|
+
break;
|
|
97
|
+
const location = response.headers.get("location");
|
|
98
|
+
if (!location)
|
|
99
|
+
throw new ImageGenError("invalid_image", "图片下载重定向缺少目标地址。", "检查 Provider 返回的图片 URL。 ");
|
|
100
|
+
target = assertSafeHttpUrl(new URL(location, target).toString());
|
|
101
|
+
if (await resolvesToPrivateHost(target.hostname))
|
|
102
|
+
throw new ImageGenError("invalid_image", "图片重定向目标地址不安全。", "不能重定向到内网地址。 ");
|
|
103
|
+
if (redirects === 3)
|
|
104
|
+
throw new ImageGenError("network_error", "图片下载重定向次数过多。", "使用稳定的图片 URL。 ");
|
|
105
|
+
}
|
|
106
|
+
if (!response)
|
|
107
|
+
throw new ImageGenError("network_error", "图片下载失败。", "检查图片 URL。 ");
|
|
108
|
+
if (!response.ok)
|
|
109
|
+
throw errorFromStatus(response.status, "下载图片");
|
|
110
|
+
const len = Number(response.headers.get("content-length") ?? 0);
|
|
111
|
+
if (len > maxBytes)
|
|
112
|
+
throw new ImageGenError("download_too_large", "下载图片超过大小限制。", "设置更小输出或增大 IMAGE_MAX_DOWNLOAD_BYTES。 ");
|
|
113
|
+
const chunks = [];
|
|
114
|
+
let total = 0;
|
|
115
|
+
if (!response.body)
|
|
116
|
+
throw new ImageGenError("invalid_image", "下载响应没有内容。", "检查图片 URL。 ");
|
|
117
|
+
const reader = response.body.getReader();
|
|
118
|
+
while (true) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done)
|
|
121
|
+
break;
|
|
122
|
+
if (value) {
|
|
123
|
+
total += value.byteLength;
|
|
124
|
+
if (total > maxBytes)
|
|
125
|
+
throw new ImageGenError("download_too_large", "下载图片超过大小限制。", "设置更小输出或增大 IMAGE_MAX_DOWNLOAD_BYTES。 ");
|
|
126
|
+
chunks.push(value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const content = Buffer.concat(chunks);
|
|
130
|
+
rejectHtml(content);
|
|
131
|
+
const declaredMime = response.headers.get("content-type")?.split(";")[0];
|
|
132
|
+
const detected = detectImageFormat(content);
|
|
133
|
+
if (!detected)
|
|
134
|
+
throw new ImageGenError("invalid_image", "下载内容不是有效图片。", "检查 Provider 图片 URL 和响应内容。 ");
|
|
135
|
+
const mimeType = declaredMime?.startsWith("image/") && extensionForMime(declaredMime) === detected.ext ? declaredMime : detected.mimeType;
|
|
136
|
+
const path = await saveImageBuffer(content, outputDir, detected.ext);
|
|
137
|
+
return { path, mimeType };
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.js","sourceRoot":"","sources":["../../src/utils/download.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function detectImageFormat(buffer: Buffer): {
|
|
2
|
+
mimeType: string;
|
|
3
|
+
ext: string;
|
|
4
|
+
} | undefined;
|
|
5
|
+
export declare function extensionForMime(mime: string, fallback?: string): string;
|
|
6
|
+
export declare function saveImageBuffer(buffer: Buffer, outputDir: string, ext: string): Promise<string>;
|
|
7
|
+
export declare function loadLocalImage(input: string): Promise<{
|
|
8
|
+
name: string;
|
|
9
|
+
buffer: Buffer;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function normalizeImagePath(input: string): string;
|
|
13
|
+
export declare function mimeFromExt(ext: string): string;
|
|
14
|
+
export declare function decodeBase64Image(data: string): {
|
|
15
|
+
buffer: Buffer;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
ext: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { ImageGenError } from "../core/errors.js";
|
|
6
|
+
const allowed = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
7
|
+
export function detectImageFormat(buffer) {
|
|
8
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
9
|
+
return { mimeType: "image/png", ext: "png" };
|
|
10
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
|
|
11
|
+
return { mimeType: "image/jpeg", ext: "jpg" };
|
|
12
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP")
|
|
13
|
+
return { mimeType: "image/webp", ext: "webp" };
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function extensionForMime(mime, fallback = "png") {
|
|
17
|
+
if (mime.includes("jpeg") || mime.includes("jpg"))
|
|
18
|
+
return "jpg";
|
|
19
|
+
if (mime.includes("webp"))
|
|
20
|
+
return "webp";
|
|
21
|
+
if (mime.includes("png"))
|
|
22
|
+
return "png";
|
|
23
|
+
return fallback.replace(/^\./, "");
|
|
24
|
+
}
|
|
25
|
+
export async function saveImageBuffer(buffer, outputDir, ext) {
|
|
26
|
+
if (buffer.length === 0)
|
|
27
|
+
throw new ImageGenError("invalid_image", "图片内容为空。", "检查 API 返回或输入文件。 ");
|
|
28
|
+
validateImageSignature(buffer, ext);
|
|
29
|
+
const cleanExt = ext.toLowerCase().replace(/^\./, "");
|
|
30
|
+
if (!["png", "jpg", "jpeg", "webp"].includes(cleanExt))
|
|
31
|
+
throw new ImageGenError("unsupported_format", `不支持的图片格式:${ext}`, "使用 png、jpg、jpeg 或 webp。 ");
|
|
32
|
+
await mkdir(outputDir, { recursive: true });
|
|
33
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
34
|
+
const path = resolve(outputDir, `${date}-${randomUUID().slice(0, 8)}.${cleanExt === "jpeg" ? "jpg" : cleanExt}`);
|
|
35
|
+
await writeFile(path, buffer, { flag: "wx" });
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
export async function loadLocalImage(input) {
|
|
39
|
+
const path = normalizeImagePath(input);
|
|
40
|
+
const ext = extname(path).toLowerCase();
|
|
41
|
+
if (!allowed.has(ext))
|
|
42
|
+
throw new ImageGenError("unsupported_format", `不支持的图片格式:${ext || "未知"}`, "仅支持 .png、.jpg、.jpeg、.webp。 ");
|
|
43
|
+
const buffer = await readFile(path).catch((cause) => {
|
|
44
|
+
throw new ImageGenError("invalid_image", `无法读取图片:${path}`, cause instanceof Error ? cause.message : "确认路径存在且可读。 ");
|
|
45
|
+
});
|
|
46
|
+
validateImageSignature(buffer, ext);
|
|
47
|
+
return { name: basename(path), buffer, mimeType: mimeFromExt(ext) };
|
|
48
|
+
}
|
|
49
|
+
export function normalizeImagePath(input) {
|
|
50
|
+
const clean = input.startsWith("@") ? input.slice(1) : input;
|
|
51
|
+
if (clean.startsWith("file://"))
|
|
52
|
+
return fileURLToPath(clean);
|
|
53
|
+
if (/^https?:\/\//i.test(clean))
|
|
54
|
+
throw new ImageGenError("unsupported_format", "此处需要本地图片路径。", "远程图片会由下载模块处理。 ");
|
|
55
|
+
return resolve(clean);
|
|
56
|
+
}
|
|
57
|
+
export function mimeFromExt(ext) {
|
|
58
|
+
switch (ext.toLowerCase()) {
|
|
59
|
+
case ".jpg":
|
|
60
|
+
case ".jpeg": return "image/jpeg";
|
|
61
|
+
case ".webp": return "image/webp";
|
|
62
|
+
case ".png": return "image/png";
|
|
63
|
+
default: throw new ImageGenError("unsupported_format", `不支持的图片格式:${ext}`, "仅支持 png、jpg、jpeg、webp。 ");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function validateImageSignature(buffer, ext) {
|
|
67
|
+
const clean = ext.toLowerCase().replace(/^\./, "");
|
|
68
|
+
const png = buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
69
|
+
const jpg = buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
|
70
|
+
const webp = buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP";
|
|
71
|
+
if ((clean === "png" && !png) || ((clean === "jpg" || clean === "jpeg") && !jpg) || (clean === "webp" && !webp))
|
|
72
|
+
throw new ImageGenError("invalid_image", "内容不是有效的图片文件。", "Provider 可能返回了鉴权页或错误页面。 ");
|
|
73
|
+
}
|
|
74
|
+
export function decodeBase64Image(data) {
|
|
75
|
+
const match = data.match(/^data:(image\/(png|jpeg|jpg|webp));base64,(.+)$/i);
|
|
76
|
+
const mimeType = match?.[1] ?? "image/png";
|
|
77
|
+
const payload = match?.[3] ?? data;
|
|
78
|
+
const buffer = Buffer.from(payload, "base64");
|
|
79
|
+
if (buffer.length === 0)
|
|
80
|
+
throw new ImageGenError("invalid_image", "API 返回了空 base64 图片。", "检查服务商响应。 ");
|
|
81
|
+
return { buffer, mimeType, ext: extensionForMime(mimeType) };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/utils/files.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xinizai/pi-image-gen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal Pi Agent image generation extension with OpenAI-compatible provider discovery.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"test": "node --test --import tsx test/*.test.ts",
|
|
18
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["pi", "pi-agent", "extension", "image-generation", "openai-compatible"],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@earendil-works/pi-ai": "^0.84.2",
|
|
24
|
+
"@earendil-works/pi-coding-agent": "^0.84.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.0.0",
|
|
28
|
+
"tsx": "^4.20.0",
|
|
29
|
+
"typescript": "^5.9.0"
|
|
30
|
+
},
|
|
31
|
+
"pi": {
|
|
32
|
+
"extensions": ["./dist/index.js"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile, rename, chmod } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { GLOBAL_DIR } from "./global-config.js";
|
|
5
|
+
import type { DiscoveryResult, ImageGenConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export class ModelDiscoveryCache {
|
|
8
|
+
constructor(private readonly cacheDir = resolve(GLOBAL_DIR, "cache", "providers")) {}
|
|
9
|
+
|
|
10
|
+
async get(config: ImageGenConfig, now = Date.now()): Promise<DiscoveryResult | undefined> {
|
|
11
|
+
const entry = await this.read(config.providerId).catch(() => undefined);
|
|
12
|
+
if (!entry) return undefined;
|
|
13
|
+
if (entry.key !== this.key(config)) return undefined;
|
|
14
|
+
const discovered = Date.parse(entry.result.discoveredAt);
|
|
15
|
+
if (!Number.isFinite(discovered) || now - discovered > config.cacheTtlMs) return undefined;
|
|
16
|
+
return entry.result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async set(config: ImageGenConfig, result: DiscoveryResult): Promise<void> {
|
|
20
|
+
await mkdir(this.cacheDir, { recursive: true });
|
|
21
|
+
const file = this.fileFor(config.providerId);
|
|
22
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
23
|
+
await writeFile(temp, JSON.stringify({ key: this.key(config), result: { ...result, apiKeyFingerprint: this.apiKeyFingerprint(config) } }, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
24
|
+
await chmod(temp, 0o600).catch(() => undefined);
|
|
25
|
+
await rename(temp, file);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
key(config: ImageGenConfig): string {
|
|
29
|
+
return createHash("sha256").update(`${config.providerId}\n${config.providerType}\n${config.baseUrl}\n${config.apiKeyUpdatedAt}\n${this.apiKeyFingerprint(config)}`).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
apiKeyFingerprint(config: ImageGenConfig): string {
|
|
33
|
+
return createHash("sha256").update(config.apiKey).digest("hex").slice(0, 12);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fileFor(providerId: string): string {
|
|
37
|
+
return resolve(this.cacheDir, `${providerId.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async read(providerId: string): Promise<{ key: string; result: DiscoveryResult }> {
|
|
41
|
+
return JSON.parse(await readFile(this.fileFor(providerId), "utf8")) as { key: string; result: DiscoveryResult };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ModelCapability, ModelInfo } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const imageHints = ["image", "img", "dall-e", "dalle", "gpt-image", "flux", "stable-diffusion", "sdxl", "midjourney", "ideogram", "recraft"];
|
|
4
|
+
const visionHints = ["vision", "vl", "visual", "omni", "gpt-4o", "qwen-vl", "llava"];
|
|
5
|
+
const textHints = ["gpt", "claude", "llama", "mistral", "deepseek", "qwen", "kimi", "mimo"];
|
|
6
|
+
|
|
7
|
+
export function detectCapabilities(id: string, metadata: Record<string, unknown>, manual?: ModelCapability[]): Pick<ModelInfo, "capabilities" | "capabilitySource" | "inputModalities" | "outputModalities" | "supportsImageInput" | "supportsMultipleImages" | "supportsSize" | "supportsQuality" | "supportsAspectRatio"> {
|
|
8
|
+
if (manual?.length) return base(manual, "manual", metadata);
|
|
9
|
+
const metaCaps = explicitCapabilities(metadata);
|
|
10
|
+
if (metaCaps.length) return base(metaCaps, "metadata", metadata);
|
|
11
|
+
const haystack = `${id} ${JSON.stringify(metadata)}`.toLowerCase();
|
|
12
|
+
const caps = new Set<ModelCapability>();
|
|
13
|
+
if (imageHints.some((h) => haystack.includes(h))) {
|
|
14
|
+
caps.add("image_generation");
|
|
15
|
+
if (haystack.includes("edit") || haystack.includes("gpt-image")) caps.add("image_edit");
|
|
16
|
+
if (haystack.includes("variation") || haystack.includes("dall-e")) caps.add("image_variation");
|
|
17
|
+
}
|
|
18
|
+
if (visionHints.some((h) => haystack.includes(h))) caps.add("vision");
|
|
19
|
+
if (caps.size === 0 && textHints.some((h) => haystack.includes(h))) caps.add("text");
|
|
20
|
+
if (caps.size === 0) caps.add("unknown");
|
|
21
|
+
return base([...caps], caps.has("unknown") ? "unknown" : "heuristic", metadata);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function explicitCapabilities(metadata: Record<string, unknown>): ModelCapability[] {
|
|
25
|
+
const values: string[] = [];
|
|
26
|
+
for (const key of ["capabilities", "capability", "modalities", "input_modalities", "output_modalities", "supported_generation_methods"]) {
|
|
27
|
+
const value = metadata[key];
|
|
28
|
+
if (Array.isArray(value)) values.push(...value.filter((v): v is string => typeof v === "string"));
|
|
29
|
+
else if (typeof value === "string") values.push(value);
|
|
30
|
+
}
|
|
31
|
+
const text = values.join(" ").toLowerCase();
|
|
32
|
+
const caps = new Set<ModelCapability>();
|
|
33
|
+
if (/image[_ -]?generation|text-to-image|generate_images|image_out|output:image/.test(text)) caps.add("image_generation");
|
|
34
|
+
if (/image[_ -]?edit|edit_images|inpaint|input:image/.test(text)) caps.add("image_edit");
|
|
35
|
+
if (/image[_ -]?variation|variations/.test(text)) caps.add("image_variation");
|
|
36
|
+
if (/vision|image_input|input:image/.test(text)) caps.add("vision");
|
|
37
|
+
if (/text|chat|completion/.test(text)) caps.add("text");
|
|
38
|
+
return [...caps];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function base(capabilities: ModelCapability[], source: ModelInfo["capabilitySource"], metadata: Record<string, unknown>) {
|
|
42
|
+
const lower = JSON.stringify(metadata).toLowerCase();
|
|
43
|
+
const supportsImageInput = capabilities.includes("image_edit") || capabilities.includes("vision") || lower.includes("input:image") || lower.includes("image_input");
|
|
44
|
+
return {
|
|
45
|
+
capabilities,
|
|
46
|
+
capabilitySource: source,
|
|
47
|
+
inputModalities: supportsImageInput ? ["text", "image"] : ["text"],
|
|
48
|
+
outputModalities: capabilities.includes("image_generation") || capabilities.includes("image_edit") || capabilities.includes("image_variation") ? ["image"] : ["text"],
|
|
49
|
+
supportsImageInput,
|
|
50
|
+
supportsMultipleImages: lower.includes("multiple") || lower.includes("multi_image"),
|
|
51
|
+
supportsSize: true,
|
|
52
|
+
supportsQuality: lower.includes("quality") || capabilities.includes("image_generation"),
|
|
53
|
+
supportsAspectRatio: lower.includes("aspect") || capabilities.includes("image_generation"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type ImageErrorCode =
|
|
2
|
+
| "missing_config"
|
|
3
|
+
| "invalid_base_url"
|
|
4
|
+
| "auth_failed"
|
|
5
|
+
| "forbidden"
|
|
6
|
+
| "not_found"
|
|
7
|
+
| "rate_limited"
|
|
8
|
+
| "server_error"
|
|
9
|
+
| "timeout"
|
|
10
|
+
| "network_error"
|
|
11
|
+
| "invalid_request"
|
|
12
|
+
| "invalid_json"
|
|
13
|
+
| "invalid_image"
|
|
14
|
+
| "unsupported_format"
|
|
15
|
+
| "model_unavailable"
|
|
16
|
+
| "capability_unknown"
|
|
17
|
+
| "response_too_large"
|
|
18
|
+
| "request_too_large"
|
|
19
|
+
| "download_too_large"
|
|
20
|
+
| "unsupported_provider";
|
|
21
|
+
|
|
22
|
+
export class ImageGenError extends Error {
|
|
23
|
+
constructor(
|
|
24
|
+
public readonly code: ImageErrorCode,
|
|
25
|
+
message: string,
|
|
26
|
+
public readonly suggestion: string,
|
|
27
|
+
public readonly status?: number,
|
|
28
|
+
) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "ImageGenError";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toUserMessage(): string {
|
|
34
|
+
const status = this.status ? `HTTP ${this.status}。` : "";
|
|
35
|
+
return `${this.message}\n原因:${status}${reasonForCode(this.code)}\n解决办法:${this.suggestion}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sanitizeSecret(text: string, apiKey?: string): string {
|
|
40
|
+
let out = text;
|
|
41
|
+
if (apiKey) out = out.split(apiKey).join(maskApiKey(apiKey));
|
|
42
|
+
out = out.replace(/Bearer\s+[A-Za-z0-9._\-]+/g, "Bearer ****");
|
|
43
|
+
out = out.replace(/sk-[A-Za-z0-9._\-]{8,}/g, (m) => maskApiKey(m));
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function maskApiKey(key: string): string {
|
|
48
|
+
if (!key) return "未配置";
|
|
49
|
+
if (key.length <= 8) return "****";
|
|
50
|
+
return `${key.slice(0, 3)}-****${key.slice(-4)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function errorFromStatus(status: number, action: string, detail?: string): ImageGenError {
|
|
54
|
+
switch (status) {
|
|
55
|
+
case 401: return new ImageGenError("auth_failed", `${action}失败:认证失败。`, "检查 IMAGE_API_KEY 是否正确、是否过期。", status);
|
|
56
|
+
case 403: return new ImageGenError("forbidden", `${action}失败:权限不足。`, "确认 API Key 有访问模型和图片接口的权限。", status);
|
|
57
|
+
case 404: return new ImageGenError("not_found", `${action}失败:接口不存在。`, "确认 IMAGE_BASE_URL 是否包含正确版本路径,例如 https://host/v1。", status);
|
|
58
|
+
case 408: return new ImageGenError("timeout", `${action}失败:请求超时。`, "稍后重试,或增大 IMAGE_TIMEOUT_SECONDS。", status);
|
|
59
|
+
case 409: return new ImageGenError("model_unavailable", `${action}失败:请求冲突或模型不可用。`, "尝试换模型,或检查服务商状态。", status);
|
|
60
|
+
case 413: return new ImageGenError("request_too_large", `${action}失败:请求体过大。`, "降低图片尺寸、减少 n,或使用更小的输入图片。", status);
|
|
61
|
+
case 429: return new ImageGenError("rate_limited", `${action}失败:触发限流。`, "稍后重试,或检查额度/并发限制。", status);
|
|
62
|
+
case 500:
|
|
63
|
+
case 502:
|
|
64
|
+
case 503: return new ImageGenError("server_error", `${action}失败:服务端错误。`, "稍后重试;若持续失败请联系 API 服务商。", status);
|
|
65
|
+
default: return new ImageGenError(status >= 500 ? "server_error" : status >= 400 ? "invalid_request" : "network_error", `${action}失败:HTTP ${status}${detail ? `:${detail}` : "。"}`, status >= 400 && status < 500 ? "检查模型名称、图片尺寸、响应格式和服务商文档。" : "检查网络连接和服务商状态。", status);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function reasonForCode(code: ImageErrorCode): string {
|
|
70
|
+
const map: Record<ImageErrorCode, string> = {
|
|
71
|
+
missing_config: "缺少必要配置。",
|
|
72
|
+
invalid_base_url: "Base URL 格式不正确或不是 http/https。",
|
|
73
|
+
auth_failed: "API Key 无效或未授权。",
|
|
74
|
+
forbidden: "当前凭证没有访问权限。",
|
|
75
|
+
not_found: "目标 API 路径不存在。",
|
|
76
|
+
rate_limited: "请求过快或额度不足。",
|
|
77
|
+
server_error: "API 服务端返回错误。",
|
|
78
|
+
timeout: "网络请求超过超时时间。",
|
|
79
|
+
network_error: "网络连接失败或响应异常。",
|
|
80
|
+
invalid_request: "请求已到达 API,但参数或模型不符合服务商要求。",
|
|
81
|
+
invalid_json: "API 返回的 JSON 无法解析。",
|
|
82
|
+
invalid_image: "图片内容为空或不是有效图片。",
|
|
83
|
+
unsupported_format: "仅支持 png、jpg、jpeg、webp。",
|
|
84
|
+
model_unavailable: "模型不存在、不可用或不支持该操作。",
|
|
85
|
+
capability_unknown: "无法自动确认模型能力。",
|
|
86
|
+
response_too_large: "响应超过安全大小限制。",
|
|
87
|
+
request_too_large: "请求体超过服务商限制。",
|
|
88
|
+
download_too_large: "下载图片超过大小限制。",
|
|
89
|
+
unsupported_provider: "暂不支持该 Provider 类型。",
|
|
90
|
+
};
|
|
91
|
+
return map[code];
|
|
92
|
+
}
|