@infimind-next/ai-hive-mcp 0.4.1 → 0.5.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.
@@ -1,9 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, realpath, stat } from "node:fs/promises";
4
- import { basename, isAbsolute, join, relative, sep } from "node:path";
4
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileTypeFromBuffer } from "file-type";
6
6
  export const MAX_MANAGED_IMAGE_BYTES = 30 * 1024 * 1024;
7
+ export const MAX_MANAGED_VIDEO_BYTES = 200 * 1024 * 1024;
8
+ // MCP 服务端单文件上传上限是 100MiB; 本地先拦一次, 避免白传.
9
+ const MAX_MANAGED_VIDEO_UPLOAD_BYTES = 100 * 1024 * 1024;
7
10
  const IMAGE_TYPES = new Set([
8
11
  "image/png",
9
12
  "image/jpeg",
@@ -15,7 +18,8 @@ const IMAGE_TYPES = new Set([
15
18
  "image/heif",
16
19
  "image/avif",
17
20
  ]);
18
- const PATH_ERROR = "图片不在已授权范围,请从 AI-HIVE 启动器选择图片";
21
+ const VIDEO_TYPES = new Set(["video/mp4", "video/webm", "video/quicktime"]);
22
+ const PATH_ERROR = "文件不在已授权范围,请从 AI-HIVE 启动器选择文件";
19
23
  const OUTPUT_ERROR = "结果目录已变化,请从 AI-HIVE 启动器重新打开项目";
20
24
  export function safeFilename(value) {
21
25
  if (!value ||
@@ -25,7 +29,7 @@ export function safeFilename(value) {
25
29
  /[\\/:\x00-\x1f\x7f]/.test(value) ||
26
30
  /[. ]$/.test(value) ||
27
31
  /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(value)) {
28
- throw new Error("请使用结果文件名,图片将保存在项目的 ai-hive-images 文件夹");
32
+ throw new Error("请使用结果文件名,媒体将保存在项目的 ai-hive-media 文件夹");
29
33
  }
30
34
  return value;
31
35
  }
@@ -39,6 +43,19 @@ export async function inspectImage(bytes) {
39
43
  }
40
44
  return detected;
41
45
  }
46
+ export async function inspectVideo(bytes) {
47
+ if (!bytes.byteLength || bytes.byteLength > MAX_MANAGED_VIDEO_BYTES) {
48
+ throw new Error("视频大小超过限制,最大 200MiB");
49
+ }
50
+ const detected = await fileTypeFromBuffer(bytes).catch(() => undefined);
51
+ if (!detected || !VIDEO_TYPES.has(detected.mime)) {
52
+ throw new Error("暂不支持这个视频,请选择 MP4、WebM 或 MOV 文件");
53
+ }
54
+ return detected;
55
+ }
56
+ export function inspectMedia(bytes, kind) {
57
+ return kind === "VIDEO" ? inspectVideo(bytes) : inspectImage(bytes);
58
+ }
42
59
  // 子进程的 cwd 由操作系统固定到目录 inode; 文件写入使用相对路径,
43
60
  // 防止校验后父目录被替换导致写入目录外. 不把任何账户密钥传给写入进程.
44
61
  const ATOMIC_WRITE_SCRIPT = `
@@ -47,7 +64,7 @@ const [name,dev,ino,overwrite]=process.argv.slice(1);
47
64
  const current=fs.statSync('.');
48
65
  if(String(current.dev)!==dev||String(current.ino)!==ino){process.stdout.write('DIRECTORY_CHANGED');process.exit(1)}
49
66
  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)});
67
+ process.stdin.on('data',chunk=>{total+=chunk.length;if(total>209715200){process.exit(1)}chunks.push(chunk)});
51
68
  process.stdin.on('end',()=>{try{
52
69
  const fd=fs.openSync(temp,'wx',0o600);try{fs.writeFileSync(fd,Buffer.concat(chunks));fs.fsyncSync(fd)}finally{fs.closeSync(fd)}
53
70
  if(overwrite==='true'){try{if(fs.lstatSync(name).isSymbolicLink())throw Error('SYMLINK')}catch(error){if(error.code!=='ENOENT')throw error}fs.renameSync(temp,name)}
@@ -90,7 +107,7 @@ export class ManagedDirectory {
90
107
  async writeAtomic(filename, bytes, overwrite = false) {
91
108
  safeFilename(filename);
92
109
  await this.assertUnchanged();
93
- if (bytes.byteLength > MAX_MANAGED_IMAGE_BYTES)
110
+ if (bytes.byteLength > MAX_MANAGED_VIDEO_BYTES)
94
111
  throw new Error("文件过大,请缩小后再试");
95
112
  await new Promise((resolve, reject) => {
96
113
  const child = spawn(process.execPath, [
@@ -144,32 +161,61 @@ export class ManagedFiles {
144
161
  projectRoot;
145
162
  projectIdentity;
146
163
  extra;
164
+ outputDirectory;
147
165
  output;
148
- constructor(projectRoot, projectIdentity, extra, output) {
166
+ constructor(projectRoot, projectIdentity, extra, outputDirectory) {
149
167
  this.projectRoot = projectRoot;
150
168
  this.projectIdentity = projectIdentity;
151
169
  this.extra = extra;
152
- this.output = output;
170
+ this.outputDirectory = outputDirectory;
153
171
  }
154
172
  static async create(input) {
155
173
  const projectRoot = await realpath(input.projectRoot);
156
174
  const information = await stat(projectRoot);
157
175
  if (!information.isDirectory())
158
176
  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)
177
+ // 项目根可能经过 /var -> /private/var 之类的 canonicalize; 输出目录必须按
178
+ // 原始项目根解析出相对名, 再拼到 canonical 根下.
179
+ const outputName = relative(resolve(input.projectRoot), resolve(input.outputDirectory));
180
+ if (outputName !== "ai-hive-media" &&
181
+ // 旧实例仍在用图片目录; 新配置统一写 ai-hive-media.
182
+ outputName !== "ai-hive-images")
162
183
  throw new Error(OUTPUT_ERROR);
184
+ const outputDirectory = join(projectRoot, outputName);
163
185
  const extra = new Set();
164
186
  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);
187
+ try {
188
+ const canonical = await realpath(path);
189
+ if ((await stat(canonical)).isFile())
190
+ extra.add(canonical);
191
+ }
192
+ catch {
193
+ // 配置后文件被移走时跳过; 具体读取会返回授权范围错误, 不影响其他工具启动.
194
+ }
169
195
  }
170
- return new ManagedFiles(projectRoot, { dev: information.dev, ino: information.ino }, extra, output);
196
+ return new ManagedFiles(projectRoot, { dev: information.dev, ino: information.ino }, extra, outputDirectory);
171
197
  }
172
- async readImage(path) {
198
+ async mediaOutput() {
199
+ if (this.output)
200
+ return this.output;
201
+ // 启动后项目根可能被删除或替换成符号链接; 懒创建前必须重新核对身份,
202
+ // 否则 mkdir -p 会跟随新根, 把生成结果写到项目外.
203
+ await this.assertProjectRoot();
204
+ const created = await ManagedDirectory.create(this.outputDirectory);
205
+ await this.assertProjectRoot();
206
+ this.output = created;
207
+ return created;
208
+ }
209
+ async assertProjectRoot() {
210
+ const project = await stat(this.projectRoot).catch(() => {
211
+ throw new Error(PATH_ERROR);
212
+ });
213
+ if (project.dev !== this.projectIdentity.dev ||
214
+ project.ino !== this.projectIdentity.ino ||
215
+ (await realpath(this.projectRoot).catch(() => "")) !== this.projectRoot)
216
+ throw new Error(PATH_ERROR);
217
+ }
218
+ async readMedia(path, kind) {
173
219
  if (!isAbsolute(path))
174
220
  throw new Error(PATH_ERROR);
175
221
  const project = await stat(this.projectRoot);
@@ -197,22 +243,26 @@ export class ManagedFiles {
197
243
  before.ino !== information.ino ||
198
244
  (await realpath(path)) !== canonical)
199
245
  throw new Error(PATH_ERROR);
200
- if (information.size > MAX_MANAGED_IMAGE_BYTES)
201
- throw new Error("图片大小超过限制,最大 30MiB");
246
+ const limit = kind === "VIDEO"
247
+ ? MAX_MANAGED_VIDEO_UPLOAD_BYTES
248
+ : MAX_MANAGED_IMAGE_BYTES;
249
+ const label = kind === "VIDEO" ? "视频" : "图片";
250
+ if (information.size > limit)
251
+ throw new Error(`${label}大小超过限制,最大 ${Math.floor(limit / 1024 / 1024)}MiB`);
202
252
  const bytes = Buffer.alloc(information.size);
203
253
  let offset = 0;
204
254
  while (offset < bytes.length) {
205
255
  const read = await file.read(bytes, offset, bytes.length - offset, offset);
206
256
  if (!read.bytesRead)
207
- throw new Error("图片已变化,请重新选择图片");
257
+ throw new Error(`${label}已变化,请重新选择`);
208
258
  offset += read.bytesRead;
209
259
  }
210
260
  const after = await file.stat();
211
261
  if (after.size !== information.size ||
212
262
  after.mtimeMs !== information.mtimeMs ||
213
263
  after.ctimeMs !== information.ctimeMs)
214
- throw new Error("图片已变化,请重新选择图片");
215
- const detected = await inspectImage(bytes);
264
+ throw new Error(`${label}已变化,请重新选择`);
265
+ const detected = await inspectMedia(bytes, kind);
216
266
  return {
217
267
  filename: basename(path),
218
268
  contentType: detected.mime,
@@ -224,17 +274,39 @@ export class ManagedFiles {
224
274
  await file.close();
225
275
  }
226
276
  }
227
- async saveImage(bytes, relativePath) {
228
- const detected = await inspectImage(bytes);
229
- const filename = safeFilename(relativePath ?? `image-${crypto.randomUUID()}.${detected.ext}`);
277
+ async readMediaAuto(path) {
278
+ const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
279
+ const kind = [
280
+ ".mp4",
281
+ ".webm",
282
+ ".mov",
283
+ ".m4v",
284
+ ].includes(extension)
285
+ ? "VIDEO"
286
+ : "IMAGE";
287
+ return this.readMedia(path, kind);
288
+ }
289
+ readImage(path) {
290
+ return this.readMedia(path, "IMAGE");
291
+ }
292
+ async saveMedia(bytes, kind, relativePath) {
293
+ const detected = await inspectMedia(bytes, kind);
294
+ const prefix = kind === "VIDEO" ? "video" : "image";
295
+ const filename = safeFilename(relativePath ?? `${prefix}-${crypto.randomUUID()}.${detected.ext}`);
230
296
  const extension = filename
231
297
  .slice(filename.lastIndexOf(".") + 1)
232
298
  .toLowerCase();
233
299
  if (!(extension === detected.ext ||
234
300
  (detected.ext === "jpg" && extension === "jpeg") ||
235
301
  (detected.ext === "tif" && extension === "tiff")))
236
- throw new Error("文件名的扩展名需要与图片格式一致");
237
- const path = await this.output.writeAtomic(filename, bytes);
302
+ throw new Error(kind === "VIDEO"
303
+ ? "文件名的扩展名需要与视频格式一致"
304
+ : "文件名的扩展名需要与图片格式一致");
305
+ const output = await this.mediaOutput();
306
+ const path = await output.writeAtomic(filename, bytes);
238
307
  return { path, contentType: detected.mime, sizeBytes: bytes.length };
239
308
  }
309
+ saveImage(bytes, relativePath) {
310
+ return this.saveMedia(bytes, "IMAGE", relativePath);
311
+ }
240
312
  }
@@ -1,7 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { AiHiveApiClient } from "./api-client.js";
3
+ import { type ModelSection } from "./model-response.js";
3
4
  import type { BearerTokenProvider } from "./auth-token-provider.js";
4
5
  import type { McpManagedConfig } from "./managed-config.js";
6
+ import { type ManagedMediaKind } from "./managed-files.js";
5
7
  export declare const managedPrepareShape: {
6
8
  publicModelId: z.ZodString;
7
9
  routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
@@ -10,8 +12,18 @@ export declare const managedPrepareShape: {
10
12
  imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
11
13
  params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
12
14
  };
13
- type ImageClient = Pick<AiHiveApiClient, "getUserInfo" | "listModels" | "getModel" | "generateImage" | "getGenerationTask" | "createMediaUpload" | "putUpload" | "completeMediaUpload">;
14
- export declare class ManagedImages {
15
+ export declare const managedPrepareVideoShape: {
16
+ publicModelId: z.ZodString;
17
+ routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
18
+ prompt: z.ZodEffects<z.ZodString, string, string>;
19
+ imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
20
+ videoMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
21
+ firstFrameMediaId: z.ZodOptional<z.ZodString>;
22
+ lastFrameMediaId: z.ZodOptional<z.ZodString>;
23
+ params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
24
+ };
25
+ type ImageClient = Pick<AiHiveApiClient, "getUserInfo" | "listModels" | "getModel" | "generateImage" | "generateVideo" | "getGenerationTask" | "createMediaUpload" | "putUpload" | "completeMediaUpload">;
26
+ export declare class ManagedMedia {
15
27
  private readonly config;
16
28
  private readonly client;
17
29
  private readonly tokenProvider;
@@ -23,38 +35,70 @@ export declare class ManagedImages {
23
35
  config: McpManagedConfig;
24
36
  client: ImageClient;
25
37
  tokenProvider: BearerTokenProvider;
26
- download?: (url: string) => Promise<Uint8Array>;
27
- }): Promise<ManagedImages>;
38
+ download?: (url: string, kind: ManagedMediaKind) => Promise<Uint8Array>;
39
+ }): Promise<ManagedMedia>;
28
40
  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>>;
41
+ listModels(rawInput: Record<string, unknown>): Promise<any[] | {
42
+ schemaVersion: 2;
43
+ notices: {
44
+ id: string;
45
+ revision: string;
46
+ kind: "RECHARGE_PROMOTION" | "ANNOUNCEMENT";
47
+ message: string;
48
+ title: string;
49
+ details: string;
50
+ expiresAt: string | null;
51
+ action?: {
52
+ label: string;
53
+ url: string;
54
+ } | undefined;
55
+ }[];
56
+ models: {
57
+ modelType: "TEXT" | "IMAGE" | "VIDEO";
58
+ publicModelId: string;
59
+ displayName: string;
60
+ materialMode: "standard" | "certified";
61
+ }[];
62
+ nextCursor: string | null;
63
+ }>;
64
+ getModel(publicModelId: string, routingMode?: string, options?: {
65
+ schemaVersion?: 1 | 2;
66
+ sections?: ModelSection[];
67
+ }): Promise<Record<string, unknown>>;
33
68
  upload(path: string): Promise<unknown>;
34
69
  prepare(input: unknown): Promise<{
35
70
  operationId: `${string}-${string}-${string}-${string}-${string}`;
36
71
  clientRequestId: `${string}-${string}-${string}-${string}-${string}`;
37
72
  }>;
73
+ prepareVideo(input: unknown): Promise<{
74
+ operationId: `${string}-${string}-${string}-${string}-${string}`;
75
+ clientRequestId: `${string}-${string}-${string}-${string}-${string}`;
76
+ }>;
77
+ private createPrepared;
38
78
  generate(operationId: string): Promise<{
39
79
  operationId: string;
40
80
  }>;
41
81
  recover(operationId: string): Promise<{
42
82
  operationId: string;
43
83
  }>;
44
- getTask(taskId: string): Promise<Record<string, unknown>>;
84
+ getTask(taskId: string, expectedKind?: ManagedMediaKind): Promise<Record<string, unknown>>;
45
85
  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;
86
+ operations: {
87
+ operationId: string;
88
+ kind: "IMAGE" | "VIDEO";
89
+ phase: "UNKNOWN" | "PREPARED" | "SUBMITTING" | "SUBMITTED" | "FAILED";
90
+ taskId: string | undefined;
91
+ prompt: string;
92
+ publicModelId: string;
93
+ createdAt: string;
94
+ estimatedReservationMicros: unknown;
95
+ savedResults: {
96
+ path: string;
97
+ itemId: string;
98
+ }[];
56
99
  }[];
57
- }[]>;
100
+ skipped: number;
101
+ }>;
58
102
  save(input: unknown): Promise<{
59
103
  path: string;
60
104
  contentType: string;
@@ -67,4 +111,4 @@ export declare class ManagedImages {
67
111
  private writeOperation;
68
112
  private withOperationLock;
69
113
  }
70
- export {};
114
+ export { ManagedMedia as ManagedImages };