@infimind-next/ai-hive-mcp 0.3.3 → 0.4.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.
Files changed (42) hide show
  1. package/README.md +74 -12
  2. package/dist/api-client.d.ts +15 -0
  3. package/dist/api-client.js +89 -9
  4. package/dist/cli.d.ts +4 -0
  5. package/dist/cli.js +34 -0
  6. package/dist/config.d.ts +19 -1
  7. package/dist/config.js +104 -7
  8. package/dist/index.js +9 -1
  9. package/dist/managed-config.d.ts +75 -0
  10. package/dist/managed-config.js +71 -0
  11. package/dist/managed-download.d.ts +13 -0
  12. package/dist/managed-download.js +138 -0
  13. package/dist/managed-files.d.ts +35 -0
  14. package/dist/managed-files.js +240 -0
  15. package/dist/managed-images.d.ts +70 -0
  16. package/dist/managed-images.js +449 -0
  17. package/dist/managed-server.d.ts +15 -0
  18. package/dist/managed-server.js +195 -0
  19. package/dist/managed-token-provider.d.ts +11 -0
  20. package/dist/managed-token-provider.js +31 -0
  21. package/dist/rest-commands.d.ts +14 -0
  22. package/dist/rest-commands.js +164 -0
  23. package/dist/schemas.d.ts +118 -10
  24. package/dist/schemas.js +79 -0
  25. package/dist/server.d.ts +4 -3
  26. package/dist/server.js +116 -34
  27. package/dist/setup/detect.d.ts +9 -0
  28. package/dist/setup/detect.js +34 -0
  29. package/dist/setup/merge.d.ts +5 -0
  30. package/dist/setup/merge.js +142 -0
  31. package/dist/setup/run-setup.d.ts +20 -0
  32. package/dist/setup/run-setup.js +332 -0
  33. package/dist/setup/skills.d.ts +8 -0
  34. package/dist/setup/skills.js +35 -0
  35. package/dist/setup/targets.d.ts +14 -0
  36. package/dist/setup/targets.js +46 -0
  37. package/package.json +11 -9
  38. package/skills/ai-hive-billing/SKILL.md +33 -0
  39. package/skills/ai-hive-image-video-generation/SKILL.md +41 -0
  40. package/skills/ai-hive-media-upload/SKILL.md +33 -0
  41. package/skills/ai-hive-model-selection/SKILL.md +50 -0
  42. package/skills/ai-hive-text-generation/SKILL.md +39 -0
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ export declare const mcpManagedConfigSchema: z.ZodEffects<z.ZodObject<{
3
+ version: z.ZodLiteral<1>;
4
+ apiBaseUrl: z.ZodEffects<z.ZodString, string, string>;
5
+ activationId: z.ZodString;
6
+ userId: z.ZodString;
7
+ credentialHelper: z.ZodObject<{
8
+ command: z.ZodEffects<z.ZodString, string, string>;
9
+ args: z.ZodArray<z.ZodString, "many">;
10
+ }, "strict", z.ZodTypeAny, {
11
+ command: string;
12
+ args: string[];
13
+ }, {
14
+ command: string;
15
+ args: string[];
16
+ }>;
17
+ projectRoot: z.ZodEffects<z.ZodString, string, string>;
18
+ extraImagePaths: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
19
+ outputDirectory: z.ZodEffects<z.ZodString, string, string>;
20
+ stateDirectory: z.ZodEffects<z.ZodString, string, string>;
21
+ }, "strict", z.ZodTypeAny, {
22
+ version: 1;
23
+ apiBaseUrl: string;
24
+ activationId: string;
25
+ userId: string;
26
+ credentialHelper: {
27
+ command: string;
28
+ args: string[];
29
+ };
30
+ projectRoot: string;
31
+ extraImagePaths: string[];
32
+ outputDirectory: string;
33
+ stateDirectory: string;
34
+ }, {
35
+ version: 1;
36
+ apiBaseUrl: string;
37
+ activationId: string;
38
+ userId: string;
39
+ credentialHelper: {
40
+ command: string;
41
+ args: string[];
42
+ };
43
+ projectRoot: string;
44
+ extraImagePaths: string[];
45
+ outputDirectory: string;
46
+ stateDirectory: string;
47
+ }>, {
48
+ version: 1;
49
+ apiBaseUrl: string;
50
+ activationId: string;
51
+ userId: string;
52
+ credentialHelper: {
53
+ command: string;
54
+ args: string[];
55
+ };
56
+ projectRoot: string;
57
+ extraImagePaths: string[];
58
+ outputDirectory: string;
59
+ stateDirectory: string;
60
+ }, {
61
+ version: 1;
62
+ apiBaseUrl: string;
63
+ activationId: string;
64
+ userId: string;
65
+ credentialHelper: {
66
+ command: string;
67
+ args: string[];
68
+ };
69
+ projectRoot: string;
70
+ extraImagePaths: string[];
71
+ outputDirectory: string;
72
+ stateDirectory: string;
73
+ }>;
74
+ export type McpManagedConfig = z.infer<typeof mcpManagedConfigSchema>;
75
+ export declare function loadManagedConfig(path: string): Promise<McpManagedConfig>;
@@ -0,0 +1,71 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute } from "node:path";
3
+ import { z } from "zod";
4
+ // npm 包独立发布; 此契约由 parity 测试与 contracts/mcp-managed 保持一致.
5
+ const absolutePath = z
6
+ .string()
7
+ .min(1)
8
+ .refine((value) => value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value), "需要绝对路径");
9
+ const normalizedPath = (value) => value.replace(/\\/g, "/").replace(/\/+$/, "");
10
+ export const mcpManagedConfigSchema = z
11
+ .object({
12
+ version: z.literal(1),
13
+ apiBaseUrl: z
14
+ .string()
15
+ .url()
16
+ .refine((value) => {
17
+ const url = new URL(value);
18
+ return (url.protocol === "https:" &&
19
+ !url.username &&
20
+ !url.password &&
21
+ !url.search &&
22
+ !url.hash);
23
+ }),
24
+ activationId: z.string().min(1).max(200),
25
+ userId: z.string().min(1).max(200),
26
+ credentialHelper: z
27
+ .object({ command: absolutePath, args: z.array(z.string()) })
28
+ .strict(),
29
+ projectRoot: absolutePath,
30
+ extraImagePaths: z.array(absolutePath).max(100),
31
+ outputDirectory: absolutePath,
32
+ stateDirectory: absolutePath,
33
+ })
34
+ .strict()
35
+ .superRefine((value, ctx) => {
36
+ if (normalizedPath(value.outputDirectory) !==
37
+ `${normalizedPath(value.projectRoot)}/ai-hive-images`) {
38
+ ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ path: ["outputDirectory"],
41
+ message: "结果目录必须位于项目的 ai-hive-images 文件夹",
42
+ });
43
+ }
44
+ const args = value.credentialHelper.args;
45
+ const prefixAllowed = args.length === 4 ||
46
+ (args.length === 5 && absolutePath.safeParse(args[0]).success);
47
+ if (!prefixAllowed ||
48
+ JSON.stringify(args.slice(-4)) !==
49
+ JSON.stringify([
50
+ "--credential-helper",
51
+ "mcp",
52
+ "--activation-id",
53
+ value.activationId,
54
+ ])) {
55
+ ctx.addIssue({
56
+ code: z.ZodIssueCode.custom,
57
+ path: ["credentialHelper", "args"],
58
+ message: "凭据助手需要绑定当前连接",
59
+ });
60
+ }
61
+ });
62
+ export async function loadManagedConfig(path) {
63
+ try {
64
+ if (!isAbsolute(path))
65
+ throw new Error();
66
+ return mcpManagedConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
67
+ }
68
+ catch {
69
+ throw new Error("图片工具配置无效,请回到 AI-HIVE 启动器重新打开项目");
70
+ }
71
+ }
@@ -0,0 +1,13 @@
1
+ type Address = {
2
+ address: string;
3
+ family: number;
4
+ };
5
+ type Resolver = (hostname: string) => Promise<Address[]>;
6
+ export declare function resolvePublicHttpsTarget(rawUrl: string, resolver?: Resolver): Promise<{
7
+ url: URL;
8
+ hostname: string;
9
+ address: string;
10
+ family: number;
11
+ }>;
12
+ export declare function downloadGeneratedImage(rawUrl: string, timeoutMs?: number): Promise<Uint8Array>;
13
+ export {};
@@ -0,0 +1,138 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ import { request } from "node:https";
4
+ import { MAX_MANAGED_IMAGE_BYTES } from "./managed-files.js";
5
+ const DOWNLOAD_ERROR = "生成已完成,图片暂时无法保存,请稍后查询原任务再试";
6
+ function isPublicAddress(address) {
7
+ if (isIP(address) === 4) {
8
+ const [a = 0, b = 0, c = 0] = address.split(".").map(Number);
9
+ return !(a === 0 ||
10
+ a === 10 ||
11
+ a === 127 ||
12
+ a >= 224 ||
13
+ (a === 169 && b === 254) ||
14
+ (a === 172 && b >= 16 && b <= 31) ||
15
+ (a === 192 && (b === 168 || b === 0 || b === 2)) ||
16
+ (a === 100 && b >= 64 && b <= 127) ||
17
+ (a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100))) ||
18
+ (a === 203 && b === 0 && c === 113));
19
+ }
20
+ // 仅接受原生全球单播; 排除 mapped IPv4、NAT64、ULA、link-local 等转译范围.
21
+ if (isIP(address) !== 6)
22
+ return false;
23
+ const [first = 0, second = 0] = address
24
+ .split(":")
25
+ .slice(0, 2)
26
+ .map((part) => Number.parseInt(part || "0", 16));
27
+ return (first >= 0x2000 &&
28
+ first <= 0x3ffd &&
29
+ first !== 0x2002 &&
30
+ !(first === 0x2001 && (second <= 0x1ff || second === 0xdb8)));
31
+ }
32
+ export async function resolvePublicHttpsTarget(rawUrl, resolver = (hostname) => lookup(hostname, { all: true, verbatim: true })) {
33
+ let url;
34
+ try {
35
+ url = new URL(rawUrl);
36
+ }
37
+ catch {
38
+ throw new Error(DOWNLOAD_ERROR);
39
+ }
40
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
41
+ if (url.protocol !== "https:" ||
42
+ url.username ||
43
+ url.password ||
44
+ url.hash ||
45
+ (url.port && url.port !== "443") ||
46
+ hostname === "localhost" ||
47
+ hostname.endsWith(".localhost") ||
48
+ hostname.endsWith(".local"))
49
+ throw new Error(DOWNLOAD_ERROR);
50
+ const addresses = isIP(hostname)
51
+ ? [{ address: hostname, family: isIP(hostname) }]
52
+ : await resolver(hostname).catch(() => {
53
+ throw new Error(DOWNLOAD_ERROR);
54
+ });
55
+ if (!addresses.length ||
56
+ addresses.some(({ address }) => !isPublicAddress(address)))
57
+ throw new Error(DOWNLOAD_ERROR);
58
+ const selected = addresses[0];
59
+ return { url, hostname, address: selected.address, family: selected.family };
60
+ }
61
+ export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
62
+ const deadline = Date.now() + timeoutMs;
63
+ let currentUrl = rawUrl;
64
+ for (let redirects = 0; redirects <= 3; redirects += 1) {
65
+ let dnsTimeout;
66
+ const target = await Promise.race([
67
+ resolvePublicHttpsTarget(currentUrl),
68
+ new Promise((_resolve, reject) => {
69
+ dnsTimeout = setTimeout(() => reject(new Error(DOWNLOAD_ERROR)), Math.max(1, deadline - Date.now()));
70
+ }),
71
+ ]).finally(() => clearTimeout(dnsTimeout));
72
+ const remaining = deadline - Date.now();
73
+ if (remaining <= 0)
74
+ throw new Error(DOWNLOAD_ERROR);
75
+ const response = await new Promise((resolve, reject) => {
76
+ const fail = () => reject(new Error(DOWNLOAD_ERROR));
77
+ const req = request(target.url, {
78
+ method: "GET",
79
+ agent: false,
80
+ family: target.family,
81
+ headers: { Accept: "image/*" },
82
+ // 已校验地址用于实际连接, TLS 仍验证原始域名; 每次 redirect 都重新校验.
83
+ lookup: (_hostname, _options, callback) => callback(null, target.address, target.family),
84
+ }, (res) => {
85
+ if ([301, 302, 303, 307, 308].includes(res.statusCode ?? 0) &&
86
+ res.headers.location) {
87
+ res.destroy();
88
+ try {
89
+ resolve({
90
+ redirect: new URL(res.headers.location, target.url).href,
91
+ });
92
+ }
93
+ catch {
94
+ fail();
95
+ }
96
+ return;
97
+ }
98
+ if (res.statusCode !== 200 ||
99
+ Number(res.headers["content-length"] ?? 0) >
100
+ MAX_MANAGED_IMAGE_BYTES ||
101
+ !String(res.headers["content-type"] ?? "")
102
+ .toLowerCase()
103
+ .startsWith("image/")) {
104
+ res.destroy();
105
+ fail();
106
+ return;
107
+ }
108
+ const chunks = [];
109
+ let size = 0;
110
+ res.on("data", (chunk) => {
111
+ size += chunk.length;
112
+ if (size > MAX_MANAGED_IMAGE_BYTES) {
113
+ res.destroy();
114
+ fail();
115
+ return;
116
+ }
117
+ chunks.push(chunk);
118
+ });
119
+ res.on("end", () => resolve({ body: Buffer.concat(chunks) }));
120
+ res.on("error", fail);
121
+ res.on("aborted", fail);
122
+ });
123
+ const timer = setTimeout(() => {
124
+ req.destroy();
125
+ fail();
126
+ }, remaining);
127
+ req.on("close", () => clearTimeout(timer));
128
+ req.on("error", fail);
129
+ req.end();
130
+ });
131
+ if (response.body)
132
+ return response.body;
133
+ if (!response.redirect || redirects === 3)
134
+ throw new Error(DOWNLOAD_ERROR);
135
+ currentUrl = response.redirect;
136
+ }
137
+ throw new Error(DOWNLOAD_ERROR);
138
+ }
@@ -0,0 +1,35 @@
1
+ export declare const MAX_MANAGED_IMAGE_BYTES: number;
2
+ export declare function safeFilename(value: string): string;
3
+ export declare function inspectImage(bytes: Uint8Array): Promise<import("file-type").FileTypeResult>;
4
+ export declare class ManagedDirectory {
5
+ readonly path: string;
6
+ private readonly identity;
7
+ private constructor();
8
+ static create(path: string): Promise<ManagedDirectory>;
9
+ assertUnchanged(): Promise<void>;
10
+ writeAtomic(filename: string, bytes: Uint8Array, overwrite?: boolean): Promise<string>;
11
+ read(filename: string): Promise<NonSharedBuffer>;
12
+ }
13
+ export declare class ManagedFiles {
14
+ private readonly projectRoot;
15
+ private readonly projectIdentity;
16
+ private readonly extra;
17
+ private readonly output;
18
+ private constructor();
19
+ static create(input: {
20
+ projectRoot: string;
21
+ extraImagePaths: string[];
22
+ outputDirectory: string;
23
+ }): Promise<ManagedFiles>;
24
+ readImage(path: string): Promise<{
25
+ filename: string;
26
+ contentType: string;
27
+ sizeBytes: number;
28
+ body: Blob;
29
+ }>;
30
+ saveImage(bytes: Uint8Array, relativePath?: string): Promise<{
31
+ path: string;
32
+ contentType: string;
33
+ sizeBytes: number;
34
+ }>;
35
+ }
@@ -0,0 +1,240 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, realpath, stat } from "node:fs/promises";
4
+ import { basename, isAbsolute, join, relative, sep } from "node:path";
5
+ import { fileTypeFromBuffer } from "file-type";
6
+ export const MAX_MANAGED_IMAGE_BYTES = 30 * 1024 * 1024;
7
+ const IMAGE_TYPES = new Set([
8
+ "image/png",
9
+ "image/jpeg",
10
+ "image/webp",
11
+ "image/gif",
12
+ "image/bmp",
13
+ "image/tiff",
14
+ "image/heic",
15
+ "image/heif",
16
+ "image/avif",
17
+ ]);
18
+ const PATH_ERROR = "图片不在已授权范围,请从 AI-HIVE 启动器选择图片";
19
+ const OUTPUT_ERROR = "结果目录已变化,请从 AI-HIVE 启动器重新打开项目";
20
+ export function safeFilename(value) {
21
+ if (!value ||
22
+ value.length > 200 ||
23
+ value === "." ||
24
+ value === ".." ||
25
+ /[\\/:\x00-\x1f\x7f]/.test(value) ||
26
+ /[. ]$/.test(value) ||
27
+ /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(value)) {
28
+ throw new Error("请使用结果文件名,图片将保存在项目的 ai-hive-images 文件夹");
29
+ }
30
+ return value;
31
+ }
32
+ export async function inspectImage(bytes) {
33
+ if (!bytes.byteLength || bytes.byteLength > MAX_MANAGED_IMAGE_BYTES) {
34
+ throw new Error("图片大小超过限制,最大 30MiB");
35
+ }
36
+ const detected = await fileTypeFromBuffer(bytes).catch(() => undefined);
37
+ if (!detected || !IMAGE_TYPES.has(detected.mime)) {
38
+ throw new Error("暂不支持这张图片,请选择有效的图片文件");
39
+ }
40
+ return detected;
41
+ }
42
+ // 子进程的 cwd 由操作系统固定到目录 inode; 文件写入使用相对路径,
43
+ // 防止校验后父目录被替换导致写入目录外. 不把任何账户密钥传给写入进程.
44
+ const ATOMIC_WRITE_SCRIPT = `
45
+ const fs=require('node:fs');const crypto=require('node:crypto');
46
+ const [name,dev,ino,overwrite]=process.argv.slice(1);
47
+ const current=fs.statSync('.');
48
+ if(String(current.dev)!==dev||String(current.ino)!==ino){process.stdout.write('DIRECTORY_CHANGED');process.exit(1)}
49
+ let temp='.hive-'+crypto.randomUUID()+'.tmp';let chunks=[];let total=0;
50
+ process.stdin.on('data',chunk=>{total+=chunk.length;if(total>31457280){process.exit(1)}chunks.push(chunk)});
51
+ process.stdin.on('end',()=>{try{
52
+ const fd=fs.openSync(temp,'wx',0o600);try{fs.writeFileSync(fd,Buffer.concat(chunks));fs.fsyncSync(fd)}finally{fs.closeSync(fd)}
53
+ if(overwrite==='true'){try{if(fs.lstatSync(name).isSymbolicLink())throw Error('SYMLINK')}catch(error){if(error.code!=='ENOENT')throw error}fs.renameSync(temp,name)}
54
+ else{fs.linkSync(temp,name);fs.unlinkSync(temp)}
55
+ process.stdout.write('OK');
56
+ }catch(error){try{fs.unlinkSync(temp)}catch{}process.stdout.write(error.code==='EEXIST'?'EXISTS':'FAILED');process.exitCode=1}});
57
+ `;
58
+ export class ManagedDirectory {
59
+ path;
60
+ identity;
61
+ constructor(path, identity) {
62
+ this.path = path;
63
+ this.identity = identity;
64
+ }
65
+ static async create(path) {
66
+ if (!isAbsolute(path))
67
+ throw new Error(OUTPUT_ERROR);
68
+ await mkdir(path, { recursive: true, mode: 0o700 });
69
+ const information = await lstat(path);
70
+ if (!information.isDirectory() || information.isSymbolicLink())
71
+ throw new Error(OUTPUT_ERROR);
72
+ const canonical = await realpath(path);
73
+ return new ManagedDirectory(canonical, {
74
+ dev: information.dev,
75
+ ino: information.ino,
76
+ });
77
+ }
78
+ async assertUnchanged() {
79
+ const information = await lstat(this.path).catch(() => {
80
+ throw new Error(OUTPUT_ERROR);
81
+ });
82
+ if (!information.isDirectory() ||
83
+ information.isSymbolicLink() ||
84
+ information.dev !== this.identity.dev ||
85
+ information.ino !== this.identity.ino ||
86
+ (await realpath(this.path)) !== this.path) {
87
+ throw new Error(OUTPUT_ERROR);
88
+ }
89
+ }
90
+ async writeAtomic(filename, bytes, overwrite = false) {
91
+ safeFilename(filename);
92
+ await this.assertUnchanged();
93
+ if (bytes.byteLength > MAX_MANAGED_IMAGE_BYTES)
94
+ throw new Error("文件过大,请缩小后再试");
95
+ await new Promise((resolve, reject) => {
96
+ const child = spawn(process.execPath, [
97
+ "-e",
98
+ ATOMIC_WRITE_SCRIPT,
99
+ "--",
100
+ filename,
101
+ String(this.identity.dev),
102
+ String(this.identity.ino),
103
+ String(overwrite),
104
+ ], {
105
+ cwd: this.path,
106
+ windowsHide: true,
107
+ stdio: ["pipe", "pipe", "ignore"],
108
+ env: Object.fromEntries(Object.entries(process.env).filter(([name]) => !/KEY|TOKEN|SECRET|^(ELECTRON_RUN_AS_NODE|NODE_OPTIONS|NODE_PATH)$/i.test(name))),
109
+ });
110
+ let output = "";
111
+ child.stdout.on("data", (chunk) => {
112
+ output += chunk.toString();
113
+ });
114
+ child.stdin.on("error", () => { });
115
+ child.on("error", () => reject(new Error(OUTPUT_ERROR)));
116
+ child.on("close", (code) => code === 0 && output === "OK"
117
+ ? resolve()
118
+ : reject(new Error(output === "EXISTS"
119
+ ? "文件已存在,请换一个文件名"
120
+ : OUTPUT_ERROR)));
121
+ child.stdin.end(bytes);
122
+ });
123
+ await this.assertUnchanged();
124
+ return join(this.path, filename);
125
+ }
126
+ async read(filename) {
127
+ safeFilename(filename);
128
+ await this.assertUnchanged();
129
+ const file = await open(join(this.path, filename), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
130
+ try {
131
+ const information = await file.stat();
132
+ if (!information.isFile() || information.size > MAX_MANAGED_IMAGE_BYTES)
133
+ throw new Error(OUTPUT_ERROR);
134
+ const result = await file.readFile();
135
+ await this.assertUnchanged();
136
+ return result;
137
+ }
138
+ finally {
139
+ await file.close();
140
+ }
141
+ }
142
+ }
143
+ export class ManagedFiles {
144
+ projectRoot;
145
+ projectIdentity;
146
+ extra;
147
+ output;
148
+ constructor(projectRoot, projectIdentity, extra, output) {
149
+ this.projectRoot = projectRoot;
150
+ this.projectIdentity = projectIdentity;
151
+ this.extra = extra;
152
+ this.output = output;
153
+ }
154
+ static async create(input) {
155
+ const projectRoot = await realpath(input.projectRoot);
156
+ const information = await stat(projectRoot);
157
+ if (!information.isDirectory())
158
+ throw new Error(PATH_ERROR);
159
+ const expectedOutput = join(projectRoot, "ai-hive-images");
160
+ const output = await ManagedDirectory.create(input.outputDirectory);
161
+ if (output.path !== expectedOutput)
162
+ throw new Error(OUTPUT_ERROR);
163
+ const extra = new Set();
164
+ for (const path of input.extraImagePaths) {
165
+ const canonical = await realpath(path);
166
+ if (!(await stat(canonical)).isFile())
167
+ throw new Error(PATH_ERROR);
168
+ extra.add(canonical);
169
+ }
170
+ return new ManagedFiles(projectRoot, { dev: information.dev, ino: information.ino }, extra, output);
171
+ }
172
+ async readImage(path) {
173
+ if (!isAbsolute(path))
174
+ throw new Error(PATH_ERROR);
175
+ const project = await stat(this.projectRoot);
176
+ if (project.dev !== this.projectIdentity.dev ||
177
+ project.ino !== this.projectIdentity.ino)
178
+ throw new Error(PATH_ERROR);
179
+ const canonical = await realpath(path).catch(() => {
180
+ throw new Error(PATH_ERROR);
181
+ });
182
+ const fromRoot = relative(this.projectRoot, canonical);
183
+ if (!(fromRoot &&
184
+ fromRoot !== ".." &&
185
+ !fromRoot.startsWith(`..${sep}`) &&
186
+ !isAbsolute(fromRoot)) &&
187
+ !this.extra.has(canonical))
188
+ throw new Error(PATH_ERROR);
189
+ const before = await lstat(path);
190
+ if (before.isSymbolicLink() || !before.isFile())
191
+ throw new Error(PATH_ERROR);
192
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
193
+ try {
194
+ const information = await file.stat();
195
+ if (!information.isFile() ||
196
+ before.dev !== information.dev ||
197
+ before.ino !== information.ino ||
198
+ (await realpath(path)) !== canonical)
199
+ throw new Error(PATH_ERROR);
200
+ if (information.size > MAX_MANAGED_IMAGE_BYTES)
201
+ throw new Error("图片大小超过限制,最大 30MiB");
202
+ const bytes = Buffer.alloc(information.size);
203
+ let offset = 0;
204
+ while (offset < bytes.length) {
205
+ const read = await file.read(bytes, offset, bytes.length - offset, offset);
206
+ if (!read.bytesRead)
207
+ throw new Error("图片已变化,请重新选择图片");
208
+ offset += read.bytesRead;
209
+ }
210
+ const after = await file.stat();
211
+ if (after.size !== information.size ||
212
+ after.mtimeMs !== information.mtimeMs ||
213
+ after.ctimeMs !== information.ctimeMs)
214
+ throw new Error("图片已变化,请重新选择图片");
215
+ const detected = await inspectImage(bytes);
216
+ return {
217
+ filename: basename(path),
218
+ contentType: detected.mime,
219
+ sizeBytes: bytes.length,
220
+ body: new Blob([bytes], { type: detected.mime }),
221
+ };
222
+ }
223
+ finally {
224
+ await file.close();
225
+ }
226
+ }
227
+ async saveImage(bytes, relativePath) {
228
+ const detected = await inspectImage(bytes);
229
+ const filename = safeFilename(relativePath ?? `image-${crypto.randomUUID()}.${detected.ext}`);
230
+ const extension = filename
231
+ .slice(filename.lastIndexOf(".") + 1)
232
+ .toLowerCase();
233
+ if (!(extension === detected.ext ||
234
+ (detected.ext === "jpg" && extension === "jpeg") ||
235
+ (detected.ext === "tif" && extension === "tiff")))
236
+ throw new Error("文件名的扩展名需要与图片格式一致");
237
+ const path = await this.output.writeAtomic(filename, bytes);
238
+ return { path, contentType: detected.mime, sizeBytes: bytes.length };
239
+ }
240
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { AiHiveApiClient } from "./api-client.js";
3
+ import type { BearerTokenProvider } from "./auth-token-provider.js";
4
+ import type { McpManagedConfig } from "./managed-config.js";
5
+ export declare const managedPrepareShape: {
6
+ publicModelId: z.ZodString;
7
+ routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
8
+ prompt: z.ZodEffects<z.ZodString, string, string>;
9
+ batchSize: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodNumber, z.ZodNull]>, number, number | null>>;
10
+ imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
11
+ params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
12
+ };
13
+ type ImageClient = Pick<AiHiveApiClient, "getUserInfo" | "listModels" | "getModel" | "generateImage" | "getGenerationTask" | "createMediaUpload" | "putUpload" | "completeMediaUpload">;
14
+ export declare class ManagedImages {
15
+ private readonly config;
16
+ private readonly client;
17
+ private readonly tokenProvider;
18
+ private readonly files;
19
+ private readonly state;
20
+ private readonly download;
21
+ private constructor();
22
+ static create(input: {
23
+ config: McpManagedConfig;
24
+ client: ImageClient;
25
+ tokenProvider: BearerTokenProvider;
26
+ download?: (url: string) => Promise<Uint8Array>;
27
+ }): Promise<ManagedImages>;
28
+ getUserInfo(): Promise<Record<string, unknown>>;
29
+ listModels(input: {
30
+ detail?: "lite" | "full";
31
+ }): Promise<any[]>;
32
+ getModel(publicModelId: string, routingMode?: string): Promise<Record<string, unknown>>;
33
+ upload(path: string): Promise<unknown>;
34
+ prepare(input: unknown): Promise<{
35
+ operationId: `${string}-${string}-${string}-${string}-${string}`;
36
+ clientRequestId: `${string}-${string}-${string}-${string}-${string}`;
37
+ }>;
38
+ generate(operationId: string): Promise<{
39
+ operationId: string;
40
+ }>;
41
+ recover(operationId: string): Promise<{
42
+ operationId: string;
43
+ }>;
44
+ getTask(taskId: string): Promise<Record<string, unknown>>;
45
+ list(): Promise<{
46
+ operationId: string;
47
+ phase: "UNKNOWN" | "PREPARED" | "SUBMITTING" | "SUBMITTED" | "FAILED";
48
+ taskId: string | undefined;
49
+ prompt: string;
50
+ publicModelId: string;
51
+ createdAt: string;
52
+ estimatedReservationMicros: unknown;
53
+ savedResults: {
54
+ path: string;
55
+ itemId: string;
56
+ }[];
57
+ }[]>;
58
+ save(input: unknown): Promise<{
59
+ path: string;
60
+ contentType: string;
61
+ sizeBytes: number;
62
+ taskId: string;
63
+ itemId: string;
64
+ }>;
65
+ private recordSavedResult;
66
+ private readOperation;
67
+ private writeOperation;
68
+ private withOperationLock;
69
+ }
70
+ export {};