@infimind-next/ai-hive-mcp 0.4.2 → 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 +16 -8
- package/dist/api-client.d.ts +8 -2
- package/dist/api-client.js +64 -16
- package/dist/config.d.ts +6 -0
- package/dist/config.js +47 -0
- package/dist/index.js +0 -0
- package/dist/managed-config.js +5 -4
- package/dist/managed-download.d.ts +5 -0
- package/dist/managed-download.js +18 -12
- package/dist/managed-files.d.ts +25 -1
- package/dist/managed-files.js +98 -26
- package/dist/managed-images.d.ts +65 -21
- package/dist/managed-images.js +148 -60
- package/dist/managed-server.js +71 -36
- package/dist/model-response.d.ts +831 -0
- package/dist/model-response.js +296 -0
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +1 -0
- package/dist/rest-commands.js +33 -12
- package/dist/schemas.d.ts +14 -7
- package/dist/schemas.js +3 -8
- package/dist/server.d.ts +1 -1
- package/dist/server.js +29 -17
- package/dist/setup/run-setup.js +126 -7
- package/dist/user-notices.d.ts +44 -0
- package/dist/user-notices.js +39 -21
- package/package.json +8 -9
- package/skills/ai-hive-billing/SKILL.md +2 -0
- package/skills/ai-hive-image-video-generation/SKILL.md +2 -0
- package/skills/ai-hive-media-upload/SKILL.md +2 -0
- package/skills/ai-hive-model-selection/SKILL.md +7 -4
- package/skills/ai-hive-text-generation/SKILL.md +3 -1
package/README.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
|
-
0.
|
|
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.
|
|
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
|
|
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,13 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
|
|
|
122
122
|
|
|
123
123
|
`recoveryAction` 是给调用端的恢复指引,不要逐字转成用户待办。响应中断用原 `clientRequestId` 和相同参数恢复;明确失败后再次生成,或改变报价/输入时,使用新请求标识。结果未知时先核查,不自动重建付费任务。
|
|
124
124
|
|
|
125
|
-
0.4.2
|
|
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` 的配置也需要获取并启动新版包;固定旧版本的配置继续使用原发布包。
|
|
126
132
|
|
|
127
133
|
### 0.4.2:修正 Codex 技能目录
|
|
128
134
|
|
|
@@ -134,7 +140,7 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
|
|
|
134
140
|
|
|
135
141
|
### 0.4.0:一条命令接入
|
|
136
142
|
|
|
137
|
-
新增 `setup` 命令:检测本机客户端、写入 MCP 配置、安装随包发布的 5 个技能,并用 `user-info` 验证连接。新增 `models`、`model`、`usage`、`run` 四个命令行子命令,让没有 MCP 宿主的 Agent 也能选型、试跑和查账。`setup` 写入的配置统一走 `AI_HIVE_MCP_KEY`,MCP 服务的启动参数里不再带密钥。
|
|
143
|
+
新增 `setup` 命令:检测本机客户端、写入 MCP 配置、安装随包发布的 5 个技能,并用 `user-info` 验证连接。新增 `models`、`model`、`usage`、`run` 四个命令行子命令,让没有 MCP 宿主的 Agent 也能选型、试跑和查账。`setup` 写入的配置统一走 `AI_HIVE_MCP_KEY`,MCP 服务的启动参数里不再带密钥。
|
|
138
144
|
|
|
139
145
|
向下兼容:0.3.3 的 12 个工具名称、入参和返回保持不变,0.4.0 只新增 4 个只读工具;`server`、`auth`、`status`、`logout` 的行为不变。旧服务端不接受 `detail` 参数时,`list_models` 会自动去掉它重试一次。账单工具需要服务端已部署账单接口,未部署时会提示到控制台查看,不影响其他工具。
|
|
140
146
|
|
|
@@ -158,3 +164,5 @@ ai-hive-mcp run <模型 ID> "写一句欢迎语" # 付费文字调用
|
|
|
158
164
|
### 0.3.4:任务列表与详情费用(随 0.4.0 发布)
|
|
159
165
|
|
|
160
166
|
新增 `list_generation_tasks`;现有详情调用默认返回费用,API 与 MCP 使用相同口径。推送、服务端部署和 npm 发布需另行授权。
|
|
167
|
+
|
|
168
|
+
0.5.0 模型查询默认显式使用 V2,无需启动开关。先按类型或关键词筛选四字段轻目录,找到候选即可 get_model(默认参数与价格);已知 ID 直接 get_model。仅完整目录需求才继续 nextCursor。正式调用前预检并确认费用,未知提交结果先查询原任务,不自动付费重试。固定旧包保持原行为;使用 `@latest` 的配置需获取并启动新版包后采用 V2。
|
package/dist/api-client.d.ts
CHANGED
|
@@ -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(
|
|
54
|
-
getModel(publicModelId: string, routingMode?: string, options?:
|
|
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>;
|
package/dist/api-client.js
CHANGED
|
@@ -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
|
|
5
|
-
|
|
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
|
-
},
|
|
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(
|
|
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
|
-
|
|
239
|
-
headers
|
|
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 (
|
|
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
|
|
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
|
|
258
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
package/dist/managed-config.js
CHANGED
|
@@ -33,12 +33,13 @@ export const mcpManagedConfigSchema = z
|
|
|
33
33
|
})
|
|
34
34
|
.strict()
|
|
35
35
|
.superRefine((value, ctx) => {
|
|
36
|
-
if (
|
|
37
|
-
`${normalizedPath(value.projectRoot)}
|
|
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-
|
|
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("
|
|
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 {};
|
package/dist/managed-download.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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 >
|
|
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
|
|
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
|
+
}
|
package/dist/managed-files.d.ts
CHANGED
|
@@ -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
|
|
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;
|