@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.
package/README.md CHANGED
@@ -4,14 +4,14 @@
4
4
 
5
5
  ## 安装
6
6
 
7
- 0.4.1 已发布(2026-10-23)。在 MCP 客户端中加入以下配置,把 `<APIKEY>` 换成你在蜂巢 AI 网页端「MCP 接入」里生成的 MCP 密钥:
7
+ 使用下面的配置安装 0.5.0。把 `<APIKEY>` 换成你在蜂巢 AI 网页端「MCP 接入」里生成的 MCP 密钥:
8
8
 
9
9
  ```json
10
10
  {
11
11
  "mcpServers": {
12
12
  "ai-hive": {
13
13
  "command": "npx",
14
- "args": ["-y", "@infimind-next/ai-hive-mcp@0.4.1"],
14
+ "args": ["-y", "@infimind-next/ai-hive-mcp@0.5.0"],
15
15
  "env": { "AI_HIVE_MCP_KEY": "<APIKEY>" }
16
16
  }
17
17
  }
@@ -41,8 +41,8 @@ AI_HIVE_MCP_KEY=<APIKEY> npx -y @infimind-next/ai-hive-mcp@latest setup
41
41
  没有 MCP 宿主时,也可以用同一个 MCP 密钥直接调用。`run` 会先读取所选线路的报价再提交,金额单位是蜂蜜;只有充值实付金额才是元。输出默认适合人读,加 `--json` 返回原始结构:
42
42
 
43
43
  ```bash
44
- ai-hive-mcp models --type IMAGE --json # 列模型与售价
45
- ai-hive-mcp model <模型 ID> --json # 查参数契约
44
+ ai-hive-mcp models --type IMAGE --json # 筛选模型目录
45
+ ai-hive-mcp model <模型 ID> --json # 查参数与售价
46
46
  ai-hive-mcp usage --json # 查积分与账单汇总
47
47
  ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
48
48
  ```
@@ -55,7 +55,7 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
55
55
 
56
56
  专属蜂蜜由服务端按实际模型自动选择抵扣,无需新增调用参数。不能仅凭普通蜂蜜积分为 0 拒绝模型调用;只有实际调用返回积分不足时,才使用 `rechargeUrl` 引导充值并重试。
57
57
 
58
- - `list_models`:按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格
58
+ - `list_models`:按类型或关键词筛选四字段轻目录,找到候选后用 get_model 读取参数和价格
59
59
  - `get_model`:查询单个模型的参数 schema、媒体要求、计费因素和调用示例
60
60
  - `upload_media_from_path`:上传本地图片、视频、音频或文档,返回 mediaId
61
61
  - `chat_text`:文本对话
@@ -71,7 +71,7 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
71
71
 
72
72
  ### 账单查询
73
73
 
74
- 3 个账单工具均为只读,需要所连接服务端支持。查询当前账户在网页、API、MCP 等全部入口的记录,不接受手机号或其他用户 ID。现有模型、任务和账户工具的调用方式不变;本地包版本和发布状态不因本次代码变更而改变。
74
+ 3 个账单工具均为只读,需要所连接服务端支持。查询当前账户在网页、API、MCP 等全部入口的记录,不接受手机号或其他用户 ID。现有模型、任务和账户工具的调用方式不变。
75
75
 
76
76
  账单单位为 HONEY(蜂蜜虚拟积分),积分数值沿用服务端的 micros 整数字符串,避免精度损失。消费仅指实际结算,预占、释放分别展示,不计入消费。查询某批生成任务时,传入该批的 `taskIds`,不要用前后积分之差推断批次费用;尚未结算的任务不能当作免费。
77
77
 
@@ -122,7 +122,17 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
122
122
 
123
123
  `recoveryAction` 是给调用端的恢复指引,不要逐字转成用户待办。响应中断用原 `clientRequestId` 和相同参数恢复;明确失败后再次生成,或改变报价/输入时,使用新请求标识。结果未知时先核查,不自动重建付费任务。
124
124
 
125
- 0.4.1 已发布(2026-10-23),registry 最新版本为 0.4.1。发布物、兼容验证与服务端验收边界见 [0.3.2 发布记录](../../docs/release/mcp-0.3.2-runbook.md)。
125
+ 0.4.2 的历史发布物与验证范围见 [0.4.2 发布记录](../../docs/release/mcp-0.4.2-runbook.md)。
126
+
127
+ ### 0.5.0:按需查询模型
128
+
129
+ 模型查询默认使用 V2:先按类型或关键词读取分页目录,再查询候选模型的参数和价格。已知模型 ID 时可直接查询详情;需要旧格式时显式传 `schemaVersion: 1`,目录查询传旧 `detail` 也继续采用旧格式。V2 不与 `detail` 同传。调用、预检与费用确认规则保持不变,需要所连接服务端支持 V2。
130
+
131
+ 升级后需让客户端实际启动新包并刷新工具说明。使用 `@latest` 的配置也需要获取并启动新版包;固定旧版本的配置继续使用原发布包。
132
+
133
+ ### 0.4.2:修正 Codex 技能目录
134
+
135
+ 将 Codex 技能安装到 `~/.agents/skills`,与 Codex 的技能发现路径一致;`setup` 的其他行为、工具名称、入参和返回保持不变。
126
136
 
127
137
  ### 0.4.1:修正发布说明
128
138
 
@@ -130,7 +140,7 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
130
140
 
131
141
  ### 0.4.0:一条命令接入
132
142
 
133
- 新增 `setup` 命令:检测本机客户端、写入 MCP 配置、安装随包发布的 5 个技能,并用 `user-info` 验证连接。新增 `models`、`model`、`usage`、`run` 四个命令行子命令,让没有 MCP 宿主的 Agent 也能选型、试跑和查账。`setup` 写入的配置统一走 `AI_HIVE_MCP_KEY`,MCP 服务的启动参数里不再带密钥。0.4.0 与 0.4.1 均已发布,`@latest` 指向 0.4.1。
143
+ 新增 `setup` 命令:检测本机客户端、写入 MCP 配置、安装随包发布的 5 个技能,并用 `user-info` 验证连接。新增 `models`、`model`、`usage`、`run` 四个命令行子命令,让没有 MCP 宿主的 Agent 也能选型、试跑和查账。`setup` 写入的配置统一走 `AI_HIVE_MCP_KEY`,MCP 服务的启动参数里不再带密钥。
134
144
 
135
145
  向下兼容:0.3.3 的 12 个工具名称、入参和返回保持不变,0.4.0 只新增 4 个只读工具;`server`、`auth`、`status`、`logout` 的行为不变。旧服务端不接受 `detail` 参数时,`list_models` 会自动去掉它重试一次。账单工具需要服务端已部署账单接口,未部署时会提示到控制台查看,不影响其他工具。
136
146
 
@@ -154,3 +164,5 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
154
164
  ### 0.3.4:任务列表与详情费用(随 0.4.0 发布)
155
165
 
156
166
  新增 `list_generation_tasks`;现有详情调用默认返回费用,API 与 MCP 使用相同口径。推送、服务端部署和 npm 发布需另行授权。
167
+
168
+ 0.5.0 模型查询默认显式使用 V2,无需启动开关。先按类型或关键词筛选四字段轻目录,找到候选即可 get_model(默认参数与价格);已知 ID 直接 get_model。仅完整目录需求才继续 nextCursor。正式调用前预检并确认费用,未知提交结果先查询原任务,不自动付费重试。固定旧包保持原行为;使用 `@latest` 的配置需获取并启动新版包后采用 V2。
@@ -1,10 +1,16 @@
1
1
  import type { BillingQueryInput, ListBillingRecordsInput } from "./schemas.js";
2
2
  import type { BearerTokenProvider } from "./auth-token-provider.js";
3
+ import { type ModelSection } from "./model-response.js";
3
4
  import { type McpClientInfo } from "./client-info.js";
4
5
  import { type UpdateNotifier } from "./update-notifier.js";
5
6
  type FetchImpl = typeof fetch;
6
7
  export type AnnouncementOptions = {
7
8
  announcements?: boolean;
9
+ purpose?: "CATALOG" | "DETAIL" | "SNAPSHOT_RESOLUTION";
10
+ };
11
+ export type ModelReadOptions = AnnouncementOptions & {
12
+ schemaVersion?: 1 | 2;
13
+ sections?: ModelSection[];
8
14
  };
9
15
  type AiHiveApiClientInput = {
10
16
  endpoint: string;
@@ -50,8 +56,8 @@ export declare class AiHiveApiClient {
50
56
  getUserInfo(): Promise<unknown>;
51
57
  getMaterialMode(options?: AnnouncementOptions): Promise<unknown>;
52
58
  quoteVideoMaterials(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
53
- listModels(input: Record<string, unknown>, options?: AnnouncementOptions): Promise<unknown>;
54
- getModel(publicModelId: string, routingMode?: string, options?: AnnouncementOptions): Promise<unknown>;
59
+ listModels(rawInput: Record<string, unknown>, options?: AnnouncementOptions): Promise<unknown>;
60
+ getModel(publicModelId: string, routingMode?: string, options?: ModelReadOptions): Promise<unknown>;
55
61
  createMediaUpload(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
56
62
  completeMediaUpload(mediaId: string, options?: AnnouncementOptions): Promise<unknown>;
57
63
  chatText(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
@@ -1,9 +1,16 @@
1
+ import { MCP_PACKAGE_VERSION } from "./package-version.js";
2
+ import { parseModelListInput, parseModelDetailInput, parseModelListResponseV2, parseModelDetailResponseV2, } from "./model-response.js";
1
3
  import { encodeMcpClientInfo, MCP_CLIENT_INFO_HEADER, } from "./client-info.js";
2
4
  import { LATEST_CLIENT_VERSION_HEADER, } from "./update-notifier.js";
3
5
  function announcementHeaders(options) {
4
- return options?.announcements === true
5
- ? { "X-AI-Hive-Announcements": "1" }
6
- : {};
6
+ return {
7
+ ...(options?.announcements === true
8
+ ? { "X-AI-Hive-Announcements": "1" }
9
+ : {}),
10
+ ...(options?.purpose
11
+ ? { "x-ai-hive-model-read-purpose": options.purpose }
12
+ : {}),
13
+ };
7
14
  }
8
15
  export class AiHiveApiError extends Error {
9
16
  status;
@@ -103,14 +110,14 @@ export class AiHiveApiClient {
103
110
  this.updateNotifier = input.updateNotifier;
104
111
  this.clientKind = input.clientKind;
105
112
  }
106
- async withTimeout(operation) {
113
+ async withTimeout(operation, timeoutMs = this.timeoutMs) {
107
114
  const controller = new AbortController();
108
115
  let timeout;
109
116
  const timeoutPromise = new Promise((_, reject) => {
110
117
  timeout = setTimeout(() => {
111
118
  controller.abort();
112
119
  reject(timeoutError());
113
- }, this.timeoutMs);
120
+ }, timeoutMs);
114
121
  });
115
122
  try {
116
123
  return await Promise.race([operation(controller.signal), timeoutPromise]);
@@ -159,6 +166,7 @@ export class AiHiveApiClient {
159
166
  signal,
160
167
  headers: {
161
168
  Authorization: `Bearer ${token}`,
169
+ "x-ai-hive-mcp-package-version": MCP_PACKAGE_VERSION,
162
170
  ...(this.clientKind ? { "x-ai-hive-client": this.clientKind } : {}),
163
171
  ...(encodedClientInfo
164
172
  ? { [MCP_CLIENT_INFO_HEADER]: encodedClientInfo }
@@ -222,7 +230,12 @@ export class AiHiveApiClient {
222
230
  headers: announcementHeaders(options),
223
231
  });
224
232
  }
225
- async listModels(input, options) {
233
+ async listModels(rawInput, options) {
234
+ const input = parseModelListInput(rawInput);
235
+ const headers = announcementHeaders({
236
+ ...options,
237
+ purpose: options?.purpose ?? "CATALOG",
238
+ });
226
239
  const buildPath = (withDetail) => {
227
240
  const query = new URLSearchParams();
228
241
  if (typeof input.modelType === "string") {
@@ -231,21 +244,32 @@ export class AiHiveApiClient {
231
244
  if (withDetail && typeof input.detail === "string") {
232
245
  query.set("detail", input.detail);
233
246
  }
247
+ if (input.schemaVersion !== undefined)
248
+ query.set("schemaVersion", String(input.schemaVersion));
249
+ for (const key of ["query", "routingMode", "cursor"])
250
+ if (input[key] !== undefined)
251
+ query.set(key, input[key]);
252
+ if (input.limit !== undefined)
253
+ query.set("limit", String(input.limit));
234
254
  const serialized = query.toString();
235
255
  return `/mcp/models${serialized ? `?${serialized}` : ""}`;
236
256
  };
237
257
  try {
238
- return redactInternalPricing(await this.request(buildPath(true), {
239
- headers: announcementHeaders(options),
258
+ const result = redactInternalPricing(await this.request(buildPath(true), {
259
+ headers,
240
260
  }));
261
+ return input.schemaVersion === 2
262
+ ? parseModelListResponseV2(result)
263
+ : result;
241
264
  }
242
265
  catch (error) {
243
266
  // 旧服务端的严格入参不认识 detail; 去掉它重试一次, 让新客户端的选型命令继续可用.
244
- if (typeof input.detail === "string" &&
267
+ if (input.schemaVersion !== 2 &&
268
+ typeof input.detail === "string" &&
245
269
  error instanceof AiHiveApiError &&
246
270
  (error.status === 400 || error.status === 404)) {
247
271
  return redactInternalPricing(await this.request(buildPath(false), {
248
- headers: announcementHeaders(options),
272
+ headers,
249
273
  }));
250
274
  }
251
275
  throw error;
@@ -254,14 +278,35 @@ export class AiHiveApiClient {
254
278
  // getModel 与 listModels 的 detail 参数依赖服务端新部署; 旧服务端会 404 或被
255
279
  // strict 入参校验拒绝, 所以 npm 0.3.0 必须在服务端升级之后发布.
256
280
  async getModel(publicModelId, routingMode, options) {
257
- const query = routingMode
258
- ? `?${new URLSearchParams({ routingMode }).toString()}`
259
- : "";
281
+ const input = parseModelDetailInput({
282
+ publicModelId,
283
+ routingMode,
284
+ schemaVersion: options?.schemaVersion,
285
+ sections: options?.sections,
286
+ });
287
+ const query = new URLSearchParams();
288
+ if (input.routingMode !== undefined)
289
+ query.set("routingMode", input.routingMode);
290
+ if (input.schemaVersion !== undefined)
291
+ query.set("schemaVersion", String(input.schemaVersion));
292
+ if (input.sections !== undefined)
293
+ query.set("sections", input.sections.join(","));
294
+ const serialized = query.toString();
260
295
  try {
261
- return redactInternalPricing(await this.request(`/mcp/models/${encodeURIComponent(publicModelId)}${query}`, { headers: announcementHeaders(options) }));
296
+ const result = redactInternalPricing(await this.request(`/mcp/models/${encodeURIComponent(publicModelId)}${serialized ? `?${serialized}` : ""}`, {
297
+ headers: announcementHeaders({
298
+ ...options,
299
+ purpose: options?.purpose ?? "DETAIL",
300
+ }),
301
+ }));
302
+ return input.schemaVersion === 2
303
+ ? parseModelDetailResponseV2(result, publicModelId, input.sections)
304
+ : result;
262
305
  }
263
306
  catch (error) {
264
- if (error instanceof AiHiveApiError && error.status === 404) {
307
+ if (input.schemaVersion !== 2 &&
308
+ error instanceof AiHiveApiError &&
309
+ error.status === 404) {
265
310
  throw new Error(GET_MODEL_UNSUPPORTED_MESSAGE);
266
311
  }
267
312
  throw error;
@@ -387,6 +432,9 @@ export class AiHiveApiClient {
387
432
  return this.request(`/mcp/generation/tasks/${encodeURIComponent(input.taskId)}?include=user-notices`, { headers: announcementHeaders(options) });
388
433
  }
389
434
  async putUpload(input) {
435
+ // 图片/视频上传是原始字节 PUT, 30 秒只够小图; 按 5 秒/MiB 给足时间, 上限 10 分钟.
436
+ const sizeBytes = input.body instanceof Blob ? input.body.size : 0;
437
+ const uploadTimeoutMs = Math.min(10 * 60_000, Math.max(this.timeoutMs, Math.ceil(sizeBytes / (1024 * 1024)) * 5_000));
390
438
  await this.withTimeout(async (signal) => {
391
439
  const response = await this.fetchImpl(input.url, {
392
440
  method: input.method,
@@ -397,6 +445,6 @@ export class AiHiveApiClient {
397
445
  if (!response.ok) {
398
446
  throw new Error("文件上传失败,请稍后重试");
399
447
  }
400
- });
448
+ }, uploadTimeoutMs);
401
449
  }
402
450
  }
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ModelSection } from "./model-response.js";
1
2
  export type CliCommand = "server" | "auth" | "status" | "logout" | "setup" | "models" | "model" | "usage" | "run";
2
3
  export type SetupClientId = "claude-code" | "cursor" | "codex" | "claude-desktop" | "opencode";
3
4
  export declare const SETUP_CLIENT_IDS: readonly SetupClientId[];
@@ -24,6 +25,11 @@ export type McpClientConfig = {
24
25
  prompt?: string;
25
26
  modelType?: ModelType;
26
27
  detail?: "lite" | "full";
28
+ schemaVersion?: 1 | 2;
29
+ query?: string;
30
+ cursor?: string;
31
+ limit?: number;
32
+ sections?: ModelSection[];
27
33
  routingMode?: string;
28
34
  setup?: SetupOptions;
29
35
  };
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolveWorkBuddyCredentialsFile } from "./workbuddy-credentials.js";
2
2
  import { isAbsolute } from "node:path";
3
+ import { parseModelListInput, parseModelDetailInput, modelSectionSchema, } from "./model-response.js";
3
4
  export const SETUP_CLIENT_IDS = [
4
5
  "claude-code",
5
6
  "cursor",
@@ -44,6 +45,11 @@ const VALUE_OPTIONS = new Set([
44
45
  "prompt",
45
46
  "type",
46
47
  "detail",
48
+ "schema-version",
49
+ "query",
50
+ "cursor",
51
+ "limit",
52
+ "sections",
47
53
  "routing-mode",
48
54
  ]);
49
55
  function parseClientIds(value) {
@@ -155,10 +161,51 @@ export function parseConfig(input) {
155
161
  throw new Error("参数无效");
156
162
  }
157
163
  config.detail = detail;
164
+ for (const key of ["query", "cursor"])
165
+ if (values.has(key))
166
+ config[key] = values.get(key);
167
+ if (values.has("limit"))
168
+ config.limit = Number(values.get("limit"));
169
+ if (values.has("routing-mode"))
170
+ config.routingMode = values.get("routing-mode");
158
171
  }
159
172
  if (command === "model") {
160
173
  config.modelId = values.get("model") ?? positionals[0];
161
174
  config.routingMode = values.get("routing-mode");
175
+ if (values.has("sections")) {
176
+ const sections = values.get("sections");
177
+ config.sections =
178
+ sections === ""
179
+ ? []
180
+ : sections
181
+ .split(",")
182
+ .map((section) => modelSectionSchema.parse(section.trim()));
183
+ }
184
+ }
185
+ if (command === "models" || command === "model") {
186
+ if (values.has("schema-version")) {
187
+ const selected = Number(values.get("schema-version"));
188
+ if (selected !== 1 && selected !== 2)
189
+ throw new Error("查询版本只支持 1 或 2");
190
+ config.schemaVersion = selected;
191
+ }
192
+ if (command === "models")
193
+ parseModelListInput({
194
+ schemaVersion: config.schemaVersion,
195
+ modelType: config.modelType,
196
+ detail: config.detail,
197
+ query: config.query,
198
+ cursor: config.cursor,
199
+ limit: config.limit,
200
+ routingMode: config.routingMode,
201
+ }, 2);
202
+ else
203
+ parseModelDetailInput({
204
+ schemaVersion: config.schemaVersion,
205
+ publicModelId: config.modelId ?? "missing",
206
+ routingMode: config.routingMode,
207
+ sections: config.sections,
208
+ }, 2);
162
209
  }
163
210
  if (command === "run") {
164
211
  config.modelId = values.get("model") ?? positionals[0];
package/dist/index.js CHANGED
File without changes
@@ -33,12 +33,13 @@ export const mcpManagedConfigSchema = z
33
33
  })
34
34
  .strict()
35
35
  .superRefine((value, ctx) => {
36
- if (normalizedPath(value.outputDirectory) !==
37
- `${normalizedPath(value.projectRoot)}/ai-hive-images`) {
36
+ if (!["ai-hive-media", "ai-hive-images"]
37
+ .map((name) => `${normalizedPath(value.projectRoot)}/${name}`)
38
+ .includes(normalizedPath(value.outputDirectory))) {
38
39
  ctx.addIssue({
39
40
  code: z.ZodIssueCode.custom,
40
41
  path: ["outputDirectory"],
41
- message: "结果目录必须位于项目的 ai-hive-images 文件夹",
42
+ message: "结果目录必须位于项目的 ai-hive-media 文件夹",
42
43
  });
43
44
  }
44
45
  const args = value.credentialHelper.args;
@@ -66,6 +67,6 @@ export async function loadManagedConfig(path) {
66
67
  return mcpManagedConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
67
68
  }
68
69
  catch {
69
- throw new Error("图片工具配置无效,请回到 AI-HIVE 启动器重新打开项目");
70
+ throw new Error("媒体工具配置无效,请回到 AI-HIVE 启动器重新打开项目");
70
71
  }
71
72
  }
@@ -1,3 +1,4 @@
1
+ import { type ManagedMediaKind } from "./managed-files.js";
1
2
  type Address = {
2
3
  address: string;
3
4
  family: number;
@@ -9,5 +10,9 @@ export declare function resolvePublicHttpsTarget(rawUrl: string, resolver?: Reso
9
10
  address: string;
10
11
  family: number;
11
12
  }>;
13
+ export declare function downloadGeneratedMedia(rawUrl: string, kind: ManagedMediaKind, timeoutMs?: number): Promise<{
14
+ body: Uint8Array;
15
+ contentType: string;
16
+ }>;
12
17
  export declare function downloadGeneratedImage(rawUrl: string, timeoutMs?: number): Promise<Uint8Array>;
13
18
  export {};
@@ -1,8 +1,8 @@
1
1
  import { lookup } from "node:dns/promises";
2
2
  import { isIP } from "node:net";
3
3
  import { request } from "node:https";
4
- import { MAX_MANAGED_IMAGE_BYTES } from "./managed-files.js";
5
- const DOWNLOAD_ERROR = "生成已完成,图片暂时无法保存,请稍后查询原任务再试";
4
+ import { MAX_MANAGED_IMAGE_BYTES, MAX_MANAGED_VIDEO_BYTES, } from "./managed-files.js";
5
+ const DOWNLOAD_ERROR = "生成已完成,媒体暂时无法保存,请稍后查询原任务再试";
6
6
  function isPublicAddress(address) {
7
7
  if (isIP(address) === 4) {
8
8
  const [a = 0, b = 0, c = 0] = address.split(".").map(Number);
@@ -58,7 +58,9 @@ export async function resolvePublicHttpsTarget(rawUrl, resolver = (hostname) =>
58
58
  const selected = addresses[0];
59
59
  return { url, hostname, address: selected.address, family: selected.family };
60
60
  }
61
- export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
61
+ export async function downloadGeneratedMedia(rawUrl, kind, timeoutMs = kind === "VIDEO" ? 10 * 60_000 : 60_000) {
62
+ const limit = kind === "VIDEO" ? MAX_MANAGED_VIDEO_BYTES : MAX_MANAGED_IMAGE_BYTES;
63
+ const prefix = kind === "VIDEO" ? "video/" : "image/";
62
64
  const deadline = Date.now() + timeoutMs;
63
65
  let currentUrl = rawUrl;
64
66
  for (let redirects = 0; redirects <= 3; redirects += 1) {
@@ -78,7 +80,7 @@ export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
78
80
  method: "GET",
79
81
  agent: false,
80
82
  family: target.family,
81
- headers: { Accept: "image/*" },
83
+ headers: { Accept: `${prefix}*` },
82
84
  // 已校验地址用于实际连接, TLS 仍验证原始域名; 每次 redirect 都重新校验.
83
85
  lookup: (_hostname, _options, callback) => callback(null, target.address, target.family),
84
86
  }, (res) => {
@@ -95,12 +97,10 @@ export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
95
97
  }
96
98
  return;
97
99
  }
100
+ const contentType = String(res.headers["content-type"] ?? "").toLowerCase();
98
101
  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/")) {
102
+ Number(res.headers["content-length"] ?? 0) > limit ||
103
+ !contentType.startsWith(prefix)) {
104
104
  res.destroy();
105
105
  fail();
106
106
  return;
@@ -109,14 +109,14 @@ export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
109
109
  let size = 0;
110
110
  res.on("data", (chunk) => {
111
111
  size += chunk.length;
112
- if (size > MAX_MANAGED_IMAGE_BYTES) {
112
+ if (size > limit) {
113
113
  res.destroy();
114
114
  fail();
115
115
  return;
116
116
  }
117
117
  chunks.push(chunk);
118
118
  });
119
- res.on("end", () => resolve({ body: Buffer.concat(chunks) }));
119
+ res.on("end", () => resolve({ body: Buffer.concat(chunks), contentType }));
120
120
  res.on("error", fail);
121
121
  res.on("aborted", fail);
122
122
  });
@@ -129,10 +129,16 @@ export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
129
129
  req.end();
130
130
  });
131
131
  if (response.body)
132
- return response.body;
132
+ return {
133
+ body: response.body,
134
+ contentType: response.contentType ?? "",
135
+ };
133
136
  if (!response.redirect || redirects === 3)
134
137
  throw new Error(DOWNLOAD_ERROR);
135
138
  currentUrl = response.redirect;
136
139
  }
137
140
  throw new Error(DOWNLOAD_ERROR);
138
141
  }
142
+ export async function downloadGeneratedImage(rawUrl, timeoutMs = 60_000) {
143
+ return (await downloadGeneratedMedia(rawUrl, "IMAGE", timeoutMs)).body;
144
+ }
@@ -1,6 +1,10 @@
1
1
  export declare const MAX_MANAGED_IMAGE_BYTES: number;
2
+ export declare const MAX_MANAGED_VIDEO_BYTES: number;
3
+ export type ManagedMediaKind = "IMAGE" | "VIDEO";
2
4
  export declare function safeFilename(value: string): string;
3
5
  export declare function inspectImage(bytes: Uint8Array): Promise<import("file-type").FileTypeResult>;
6
+ export declare function inspectVideo(bytes: Uint8Array): Promise<import("file-type").FileTypeResult>;
7
+ export declare function inspectMedia(bytes: Uint8Array, kind: ManagedMediaKind): Promise<import("file-type").FileTypeResult>;
4
8
  export declare class ManagedDirectory {
5
9
  readonly path: string;
6
10
  private readonly identity;
@@ -14,19 +18,39 @@ export declare class ManagedFiles {
14
18
  private readonly projectRoot;
15
19
  private readonly projectIdentity;
16
20
  private readonly extra;
17
- private readonly output;
21
+ private readonly outputDirectory;
22
+ private output?;
18
23
  private constructor();
19
24
  static create(input: {
20
25
  projectRoot: string;
21
26
  extraImagePaths: string[];
22
27
  outputDirectory: string;
23
28
  }): Promise<ManagedFiles>;
29
+ private mediaOutput;
30
+ private assertProjectRoot;
31
+ readMedia(path: string, kind: ManagedMediaKind): Promise<{
32
+ filename: string;
33
+ contentType: string;
34
+ sizeBytes: number;
35
+ body: Blob;
36
+ }>;
37
+ readMediaAuto(path: string): Promise<{
38
+ filename: string;
39
+ contentType: string;
40
+ sizeBytes: number;
41
+ body: Blob;
42
+ }>;
24
43
  readImage(path: string): Promise<{
25
44
  filename: string;
26
45
  contentType: string;
27
46
  sizeBytes: number;
28
47
  body: Blob;
29
48
  }>;
49
+ saveMedia(bytes: Uint8Array, kind: ManagedMediaKind, relativePath?: string): Promise<{
50
+ path: string;
51
+ contentType: string;
52
+ sizeBytes: number;
53
+ }>;
30
54
  saveImage(bytes: Uint8Array, relativePath?: string): Promise<{
31
55
  path: string;
32
56
  contentType: string;