@infimind-next/ai-hive-mcp 0.2.5 → 0.3.1
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 +63 -0
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +137 -19
- package/dist/cli.js +2 -0
- package/dist/config.js +10 -2
- package/dist/index.js +0 -0
- package/dist/local-media.js +13 -0
- package/dist/schemas.d.ts +24 -12
- package/dist/schemas.js +50 -10
- package/dist/server.d.ts +12 -2
- package/dist/server.js +189 -8
- package/dist/update-notifier.d.ts +20 -0
- package/dist/update-notifier.js +46 -0
- package/package.json +8 -9
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @infimind-next/ai-hive-mcp
|
|
2
|
+
|
|
3
|
+
蜂巢 AI 的本地 MCP 服务,让你在自己的 MCP 客户端里调用文本、图片和视频模型。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
在 MCP 客户端中加入以下配置,把 `<APIKEY>` 换成你在蜂巢 AI 网页端「MCP 接入」里生成的 API Key:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"ai-hive": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@infimind-next/ai-hive-mcp@0.3.0", "--key", "<APIKEY>"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
要求 Node.js >= 20.19.0。本包通过标准 MCP stdio 与客户端通信,仅保证与支持 MCP 的客户端配合使用。
|
|
21
|
+
|
|
22
|
+
## 工具
|
|
23
|
+
|
|
24
|
+
- `get_user_info`:查询当前用户和余额
|
|
25
|
+
- `list_models`:按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格
|
|
26
|
+
- `get_model`:查询单个模型的参数 schema、媒体要求、计费因素和调用示例
|
|
27
|
+
- `upload_media_from_path`:上传本地图片、视频或音频,返回 mediaId
|
|
28
|
+
- `chat_text`:文本对话
|
|
29
|
+
- `generate_image`:图片预检或正式生成
|
|
30
|
+
- `generate_video`:视频预检或正式生成
|
|
31
|
+
- `get_generation_task`:查询生成任务进度和结果
|
|
32
|
+
|
|
33
|
+
### 认证素材模式
|
|
34
|
+
|
|
35
|
+
0.3.1 增加可选的 `get_material_mode`(查询资格与网页授权入口)、`quote_video_materials`(冻结素材并报价),以及 `generate_video` 的 `materialMode` / `materialQuoteId` 字段。这些工具不是生成前置要求;旧版 0.3.0 也可直接使用认证模型。实际发布版本以 npm registry 为准。用户必须登录网页明确授权,MCP 不能代签;确认报价后按认证版模型单价按生成时长计费,认证服务费已内嵌其中,不再另收素材服务费。
|
|
36
|
+
|
|
37
|
+
## 调用流程
|
|
38
|
+
|
|
39
|
+
图片和视频生成固定按 5 步走:
|
|
40
|
+
|
|
41
|
+
1. 用 `list_models` 选择模型,记录 `publicModelId`。
|
|
42
|
+
2. 用 `get_model` 查看参数契约,`params` 的字段名、类型、枚举值和默认值逐项来自返回的 `paramsSchema`。
|
|
43
|
+
3. 调用 `generate_image` 或 `generate_video` 时传 `dryRun: true` 预检,核对返回的 `estimatedReservationMicros` 估价(单位:人民币 micros,1 元 = 1,000,000 micros)。
|
|
44
|
+
4. 确认费用后去掉 `dryRun` 正式生成,`params` 替换为预检返回的 `resolvedParams`,并传入预检返回的 `pricingSnapshot` 和本次调用稳定唯一的 `clientRequestId`。
|
|
45
|
+
5. 用 `get_generation_task` 查询任务进度和结果,不重复创建任务。
|
|
46
|
+
|
|
47
|
+
## 重试与价格变化
|
|
48
|
+
|
|
49
|
+
- 传输中断或超时后重试:复用同一个 `clientRequestId` 和相同业务参数,不会重复扣费。
|
|
50
|
+
- 修正参数、余额或媒体问题后重试:必须使用新的 `clientRequestId`。
|
|
51
|
+
- 正式生成提示价格已更新:保持业务参数和原 `clientRequestId` 不变,重新预检获取最新 `pricingSnapshot` 后重试。
|
|
52
|
+
- 返回 `REQUEST_OUTCOME_UNKNOWN`:停止自动重试,联系管理员核查。
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### 认证版的用户路径
|
|
56
|
+
|
|
57
|
+
用户提供普通素材即可,后台自动处理认证。使用对应认证模型与当前价格快照,先 dryRun 预检资格、素材和费用;通过后沿用标准版提交格式,认证字段和专用报价工具都可省略。收到任务 ID 后只查询原任务。
|
|
58
|
+
|
|
59
|
+
仅缺少授权、充值资格或余额时向用户说明必要操作,并提供[认证与充值入口](https://ai-hive.iclip.cn/certified-materials#guide);不擅自换模型提交。费用变化时重新确认,预检通过不代表内容审核通过。
|
|
60
|
+
|
|
61
|
+
更新信息仅在用户询问或与当前任务有关时介绍,不向用户转述内部参数、错误码、报价与重试机制。客户端可选升级提示默认静默,仅开启 `--verbose` 时输出维护诊断。技术维护记录见仓库 `docs/openapi/technical-changelog.md`。
|
|
62
|
+
|
|
63
|
+
`recoveryAction` 是给调用端的恢复指引,不要逐字转成用户待办。响应中断用原 `clientRequestId` 和相同参数恢复;明确失败后再次生成,或改变报价/输入时,使用新请求标识。结果未知时先核查,不自动重建付费任务。
|
package/dist/api-client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BearerTokenProvider } from "./auth-token-provider.js";
|
|
2
2
|
import { type McpClientInfo } from "./client-info.js";
|
|
3
|
+
import { type UpdateNotifier } from "./update-notifier.js";
|
|
3
4
|
type FetchImpl = typeof fetch;
|
|
4
5
|
type AiHiveApiClientInput = {
|
|
5
6
|
endpoint: string;
|
|
@@ -7,25 +8,50 @@ type AiHiveApiClientInput = {
|
|
|
7
8
|
timeoutMs: number;
|
|
8
9
|
fetchImpl?: FetchImpl;
|
|
9
10
|
clientInfoProvider?: () => McpClientInfo | undefined;
|
|
11
|
+
updateNotifier?: UpdateNotifier;
|
|
10
12
|
};
|
|
13
|
+
export declare class AiHiveApiError extends Error {
|
|
14
|
+
readonly status: number | undefined;
|
|
15
|
+
readonly details?: {
|
|
16
|
+
errorCode: string;
|
|
17
|
+
message: string;
|
|
18
|
+
helpUrl?: string;
|
|
19
|
+
actionUrl?: string;
|
|
20
|
+
retryable?: boolean;
|
|
21
|
+
recoveryAction?: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
constructor(status: number | undefined, message: string, details?: {
|
|
24
|
+
errorCode: string;
|
|
25
|
+
message: string;
|
|
26
|
+
helpUrl?: string;
|
|
27
|
+
actionUrl?: string;
|
|
28
|
+
retryable?: boolean;
|
|
29
|
+
recoveryAction?: string;
|
|
30
|
+
} | undefined);
|
|
31
|
+
}
|
|
11
32
|
export declare class AiHiveApiClient {
|
|
12
33
|
private readonly endpoint;
|
|
13
34
|
private readonly tokenProvider;
|
|
14
35
|
private readonly timeoutMs;
|
|
15
36
|
private readonly fetchImpl;
|
|
16
37
|
private readonly clientInfoProvider?;
|
|
38
|
+
private readonly updateNotifier?;
|
|
17
39
|
constructor(input: AiHiveApiClientInput);
|
|
18
40
|
private withTimeout;
|
|
19
41
|
private request;
|
|
20
42
|
private send;
|
|
21
43
|
private readResponse;
|
|
22
44
|
getUserInfo(): Promise<unknown>;
|
|
45
|
+
getMaterialMode(): Promise<unknown>;
|
|
46
|
+
quoteVideoMaterials(input: unknown): Promise<unknown>;
|
|
23
47
|
listModels(input: Record<string, unknown>): Promise<unknown>;
|
|
48
|
+
getModel(publicModelId: string, routingMode?: string): Promise<unknown>;
|
|
24
49
|
createMediaUpload(input: unknown): Promise<unknown>;
|
|
25
50
|
completeMediaUpload(mediaId: string): Promise<unknown>;
|
|
26
51
|
chatText(input: unknown): Promise<unknown>;
|
|
27
52
|
generateImage(input: unknown): Promise<unknown>;
|
|
28
53
|
generateVideo(input: unknown): Promise<unknown>;
|
|
54
|
+
private requestGeneration;
|
|
29
55
|
getGenerationTask(input: {
|
|
30
56
|
taskId: string;
|
|
31
57
|
}): Promise<unknown>;
|
package/dist/api-client.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { encodeMcpClientInfo, MCP_CLIENT_INFO_HEADER, } from "./client-info.js";
|
|
2
|
+
import { LATEST_CLIENT_VERSION_HEADER, } from "./update-notifier.js";
|
|
3
|
+
export class AiHiveApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(status, message, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.details = details;
|
|
10
|
+
this.name = "AiHiveApiError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const GET_MODEL_UNSUPPORTED_MESSAGE = "当前 AI-HIVE 服务端版本不支持 get_model,请先升级服务端后再试";
|
|
14
|
+
const DRY_RUN_UNSUPPORTED_MESSAGE = "当前 AI-HIVE 服务端版本不支持 dryRun,请先升级服务端后再试";
|
|
2
15
|
function formatIssue(value) {
|
|
3
16
|
if (typeof value === "string") {
|
|
4
17
|
return value;
|
|
@@ -31,8 +44,34 @@ function errorMessage(body) {
|
|
|
31
44
|
}
|
|
32
45
|
return "请求失败";
|
|
33
46
|
}
|
|
47
|
+
class AiHiveTimeoutError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "AiHiveTimeoutError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// timeoutError 被 request/putUpload 共用, 覆盖只读接口与大文件上传,
|
|
54
|
+
// 文案必须保持通用: 这里只描述状态, 付费调用专属的幂等恢复口径
|
|
55
|
+
// 在 chatText/requestGeneration 处用 PAID_TIMEOUT_MESSAGE 替换.
|
|
56
|
+
const GENERIC_TIMEOUT_MESSAGE = "等待响应超时,服务端可能仍在处理,请稍后重试";
|
|
57
|
+
// 付费调用超时后服务端可能仍在生成(最终成功则已扣费), MCP/OpenAPI 没有
|
|
58
|
+
// "查询会话结果"的能力, 唯一安全的恢复手段是幂等重调; 改参数重发会被当成
|
|
59
|
+
// 新请求再次扣费, 与"同 prompt 提交 4 次扣 4 次"的客诉同因.
|
|
60
|
+
const PAID_TIMEOUT_MESSAGE = "等待响应超时,服务端可能仍在生成,也可能已完成并扣费。请复用同一 clientRequestId 和相同业务参数重新调用,由服务端幂等去重并返回首次结果;修正参数后必须更换新的 clientRequestId";
|
|
34
61
|
function timeoutError() {
|
|
35
|
-
return new
|
|
62
|
+
return new AiHiveTimeoutError(GENERIC_TIMEOUT_MESSAGE);
|
|
63
|
+
}
|
|
64
|
+
function isTimeoutError(error) {
|
|
65
|
+
return error instanceof AiHiveTimeoutError;
|
|
66
|
+
}
|
|
67
|
+
function redactInternalPricing(value) {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map(redactInternalPricing);
|
|
70
|
+
}
|
|
71
|
+
if (value && typeof value === "object") {
|
|
72
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => key === "originalMicros" ? [] : [[key, redactInternalPricing(item)]]));
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
36
75
|
}
|
|
37
76
|
async function discardResponseBody(response) {
|
|
38
77
|
try {
|
|
@@ -48,12 +87,14 @@ export class AiHiveApiClient {
|
|
|
48
87
|
timeoutMs;
|
|
49
88
|
fetchImpl;
|
|
50
89
|
clientInfoProvider;
|
|
90
|
+
updateNotifier;
|
|
51
91
|
constructor(input) {
|
|
52
92
|
this.endpoint = input.endpoint.replace(/\/+$/, "");
|
|
53
93
|
this.tokenProvider = input.tokenProvider;
|
|
54
94
|
this.timeoutMs = input.timeoutMs;
|
|
55
95
|
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
56
96
|
this.clientInfoProvider = input.clientInfoProvider;
|
|
97
|
+
this.updateNotifier = input.updateNotifier;
|
|
57
98
|
}
|
|
58
99
|
async withTimeout(operation) {
|
|
59
100
|
const controller = new AbortController();
|
|
@@ -98,6 +139,9 @@ export class AiHiveApiClient {
|
|
|
98
139
|
response = await this.send(path, init, signal, refreshedToken);
|
|
99
140
|
}
|
|
100
141
|
}
|
|
142
|
+
// 只看最终响应的版本头; 401 重试前的旧响应已废弃, 不重复参与判断.
|
|
143
|
+
// headers 用可选链访问, 兼容测试中的极简 fetch stub.
|
|
144
|
+
this.updateNotifier?.notifyIfOutdated(response.headers?.get(LATEST_CLIENT_VERSION_HEADER) ?? null);
|
|
101
145
|
return this.readResponse(response);
|
|
102
146
|
});
|
|
103
147
|
}
|
|
@@ -119,18 +163,68 @@ export class AiHiveApiClient {
|
|
|
119
163
|
async readResponse(response) {
|
|
120
164
|
const body = await response.json().catch(() => ({}));
|
|
121
165
|
if (!response.ok) {
|
|
122
|
-
|
|
166
|
+
const message = errorMessage(body);
|
|
167
|
+
const details = body &&
|
|
168
|
+
typeof body === "object" &&
|
|
169
|
+
typeof body.errorCode === "string" &&
|
|
170
|
+
body.errorCode.startsWith("ASSET_")
|
|
171
|
+
? {
|
|
172
|
+
errorCode: body.errorCode,
|
|
173
|
+
message,
|
|
174
|
+
retryable: body.retryable === true,
|
|
175
|
+
...(typeof body.recoveryAction === "string"
|
|
176
|
+
? { recoveryAction: body.recoveryAction }
|
|
177
|
+
: {}),
|
|
178
|
+
...(typeof body.helpUrl === "string"
|
|
179
|
+
? { helpUrl: body.helpUrl }
|
|
180
|
+
: {}),
|
|
181
|
+
...(typeof body.actionUrl === "string"
|
|
182
|
+
? { actionUrl: body.actionUrl }
|
|
183
|
+
: {}),
|
|
184
|
+
}
|
|
185
|
+
: undefined;
|
|
186
|
+
throw new AiHiveApiError(response.status, message, details);
|
|
123
187
|
}
|
|
124
188
|
return body;
|
|
125
189
|
}
|
|
126
190
|
getUserInfo() {
|
|
127
191
|
return this.request("/mcp/user-info");
|
|
128
192
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
193
|
+
getMaterialMode() {
|
|
194
|
+
return this.request("/mcp/material-mode");
|
|
195
|
+
}
|
|
196
|
+
quoteVideoMaterials(input) {
|
|
197
|
+
return this.request("/mcp/generation/video/material-quote", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
body: JSON.stringify(input),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async listModels(input) {
|
|
203
|
+
const query = new URLSearchParams();
|
|
204
|
+
if (typeof input.modelType === "string") {
|
|
205
|
+
query.set("modelType", input.modelType);
|
|
206
|
+
}
|
|
207
|
+
if (typeof input.detail === "string") {
|
|
208
|
+
query.set("detail", input.detail);
|
|
209
|
+
}
|
|
210
|
+
const serialized = query.toString();
|
|
211
|
+
return redactInternalPricing(await this.request(`/mcp/models${serialized ? `?${serialized}` : ""}`));
|
|
212
|
+
}
|
|
213
|
+
// getModel 与 listModels 的 detail 参数依赖服务端新部署; 旧服务端会 404 或被
|
|
214
|
+
// strict 入参校验拒绝, 所以 npm 0.3.0 必须在服务端升级之后发布.
|
|
215
|
+
async getModel(publicModelId, routingMode) {
|
|
216
|
+
const query = routingMode
|
|
217
|
+
? `?${new URLSearchParams({ routingMode }).toString()}`
|
|
132
218
|
: "";
|
|
133
|
-
|
|
219
|
+
try {
|
|
220
|
+
return redactInternalPricing(await this.request(`/mcp/models/${encodeURIComponent(publicModelId)}${query}`));
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof AiHiveApiError && error.status === 404) {
|
|
224
|
+
throw new Error(GET_MODEL_UNSUPPORTED_MESSAGE);
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
134
228
|
}
|
|
135
229
|
createMediaUpload(input) {
|
|
136
230
|
return this.request("/mcp/media/upload-token", {
|
|
@@ -143,23 +237,47 @@ export class AiHiveApiClient {
|
|
|
143
237
|
method: "POST",
|
|
144
238
|
});
|
|
145
239
|
}
|
|
146
|
-
chatText(input) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
240
|
+
async chatText(input) {
|
|
241
|
+
try {
|
|
242
|
+
return await this.request("/mcp/chat/text", {
|
|
243
|
+
method: "POST",
|
|
244
|
+
body: JSON.stringify(input),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (isTimeoutError(error)) {
|
|
249
|
+
throw new Error(PAID_TIMEOUT_MESSAGE);
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
151
253
|
}
|
|
152
254
|
generateImage(input) {
|
|
153
|
-
return this.
|
|
154
|
-
method: "POST",
|
|
155
|
-
body: JSON.stringify(input),
|
|
156
|
-
});
|
|
255
|
+
return this.requestGeneration("/mcp/generation/image", input);
|
|
157
256
|
}
|
|
158
257
|
generateVideo(input) {
|
|
159
|
-
return this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
258
|
+
return this.requestGeneration("/mcp/generation/video", input);
|
|
259
|
+
}
|
|
260
|
+
async requestGeneration(path, input) {
|
|
261
|
+
try {
|
|
262
|
+
return await this.request(path, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
body: JSON.stringify(input),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (isTimeoutError(error)) {
|
|
269
|
+
throw new Error(PAID_TIMEOUT_MESSAGE);
|
|
270
|
+
}
|
|
271
|
+
if (typeof input === "object" &&
|
|
272
|
+
input !== null &&
|
|
273
|
+
input.dryRun === true &&
|
|
274
|
+
error instanceof AiHiveApiError &&
|
|
275
|
+
error.status === 400 &&
|
|
276
|
+
/\bdryRun\b/.test(error.message)) {
|
|
277
|
+
throw new Error(DRY_RUN_UNSUPPORTED_MESSAGE);
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
163
281
|
}
|
|
164
282
|
getGenerationTask(input) {
|
|
165
283
|
return this.request(`/mcp/generation/tasks/${encodeURIComponent(input.taskId)}`);
|
package/dist/cli.js
CHANGED
|
@@ -295,8 +295,10 @@ async function runMcp(config, dependencies) {
|
|
|
295
295
|
try {
|
|
296
296
|
const resolved = await resolveServerAuth(config, dependencies.createStore(config.credentialsFile));
|
|
297
297
|
await dependencies.runServer({
|
|
298
|
+
...(config.verbose ? { verbose: true } : {}),
|
|
298
299
|
...resolved,
|
|
299
300
|
timeoutMs: config.timeoutMs,
|
|
301
|
+
webOrigin: config.webOrigin,
|
|
300
302
|
});
|
|
301
303
|
return 0;
|
|
302
304
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { resolveWorkBuddyCredentialsFile } from "./workbuddy-credentials.js";
|
|
2
2
|
const DEFAULT_ENDPOINT = "https://ai-hive.iclip.cn/api";
|
|
3
3
|
const DEFAULT_WEB_ORIGIN = "https://ai-hive.iclip.cn";
|
|
4
|
+
// 文本对话由上游同步返回, 长推理模型实测可达 157s+ (gpt-6/claude-sonnet 系列).
|
|
5
|
+
// 旧默认 120s 会在服务端仍在生成时先断开客户端连接, 用户看到"超时失败"、
|
|
6
|
+
// 服务端却成功扣费. 现取 300s, 仍小于网关 600s 读超时, 留足余量.
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
4
8
|
const CLI_COMMANDS = new Set([
|
|
5
9
|
"server",
|
|
6
10
|
"auth",
|
|
@@ -46,7 +50,9 @@ export function parseConfig(input) {
|
|
|
46
50
|
values.set(name, value);
|
|
47
51
|
index += 1;
|
|
48
52
|
}
|
|
49
|
-
const timeoutMs = Number.parseInt(values.get("timeout-ms") ??
|
|
53
|
+
const timeoutMs = Number.parseInt(values.get("timeout-ms") ??
|
|
54
|
+
input.env.AI_HIVE_MCP_TIMEOUT_MS ??
|
|
55
|
+
String(DEFAULT_TIMEOUT_MS), 10);
|
|
50
56
|
const endpointOverride = values.get("endpoint") ?? input.env.AI_HIVE_API_ENDPOINT;
|
|
51
57
|
return {
|
|
52
58
|
command,
|
|
@@ -58,7 +64,9 @@ export function parseConfig(input) {
|
|
|
58
64
|
key: values.get("key") ?? input.env.AI_HIVE_MCP_KEY ?? null,
|
|
59
65
|
credentialsFile: values.get("credentials-file") ??
|
|
60
66
|
resolveWorkBuddyCredentialsFile(input.env),
|
|
61
|
-
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
67
|
+
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
68
|
+
? timeoutMs
|
|
69
|
+
: DEFAULT_TIMEOUT_MS,
|
|
62
70
|
verbose: flags.has("verbose") || input.env.AI_HIVE_MCP_VERBOSE === "true",
|
|
63
71
|
};
|
|
64
72
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/local-media.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mcpCreateMediaUploadOutputSchema, mcpUploadMediaFromPathInputSchema, }
|
|
|
5
5
|
import { fileTypeFromBuffer } from "file-type";
|
|
6
6
|
const FILE_HEAD_BYTES = 4100;
|
|
7
7
|
const SMALL_MEDIA_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
const IMAGE_LIMIT_BYTES = 30 * 1024 * 1024;
|
|
8
9
|
const AUDIO_LIMIT_BYTES = 15 * 1024 * 1024;
|
|
9
10
|
const VIDEO_LIMIT_BYTES = 100 * 1024 * 1024;
|
|
10
11
|
const EXTENSION_MIME = new Map([
|
|
@@ -12,6 +13,12 @@ const EXTENSION_MIME = new Map([
|
|
|
12
13
|
[".jpg", "image/jpeg"],
|
|
13
14
|
[".jpeg", "image/jpeg"],
|
|
14
15
|
[".webp", "image/webp"],
|
|
16
|
+
[".bmp", "image/bmp"],
|
|
17
|
+
[".gif", "image/gif"],
|
|
18
|
+
[".tif", "image/tiff"],
|
|
19
|
+
[".tiff", "image/tiff"],
|
|
20
|
+
[".heic", "image/heic"],
|
|
21
|
+
[".heif", "image/heif"],
|
|
15
22
|
[".mp3", "audio/mpeg"],
|
|
16
23
|
[".wav", "audio/wav"],
|
|
17
24
|
[".mp4", "video/mp4"],
|
|
@@ -47,6 +54,9 @@ function mediaSizeLimit(contentType) {
|
|
|
47
54
|
if (contentType.startsWith("audio/")) {
|
|
48
55
|
return AUDIO_LIMIT_BYTES;
|
|
49
56
|
}
|
|
57
|
+
if (contentType.startsWith("image/")) {
|
|
58
|
+
return IMAGE_LIMIT_BYTES;
|
|
59
|
+
}
|
|
50
60
|
return SMALL_MEDIA_LIMIT_BYTES;
|
|
51
61
|
}
|
|
52
62
|
function mediaTypeLabel(contentType) {
|
|
@@ -56,6 +66,9 @@ function mediaTypeLabel(contentType) {
|
|
|
56
66
|
if (contentType.startsWith("audio/")) {
|
|
57
67
|
return "音频";
|
|
58
68
|
}
|
|
69
|
+
if (contentType.startsWith("image/")) {
|
|
70
|
+
return "图片";
|
|
71
|
+
}
|
|
59
72
|
return "图片/文档";
|
|
60
73
|
}
|
|
61
74
|
function assertFileSizeAllowed(sizeBytes, contentType) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -56,6 +56,11 @@ export declare const mcpUploadMediaFromPathInputSchema: z.ZodObject<{
|
|
|
56
56
|
}>;
|
|
57
57
|
export declare const listModelsShape: {
|
|
58
58
|
modelType: z.ZodOptional<z.ZodEnum<["TEXT", "IMAGE", "VIDEO"]>>;
|
|
59
|
+
detail: z.ZodOptional<z.ZodEnum<["lite", "full"]>>;
|
|
60
|
+
};
|
|
61
|
+
export declare const getModelShape: {
|
|
62
|
+
publicModelId: z.ZodString;
|
|
63
|
+
routingMode: z.ZodOptional<z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>>;
|
|
59
64
|
};
|
|
60
65
|
export declare const chatTextShape: {
|
|
61
66
|
publicModelId: z.ZodString;
|
|
@@ -63,7 +68,7 @@ export declare const chatTextShape: {
|
|
|
63
68
|
messages: z.ZodArray<z.ZodObject<{
|
|
64
69
|
role: z.ZodEnum<["system", "user", "assistant"]>;
|
|
65
70
|
content: z.ZodEffects<z.ZodString, string, string>;
|
|
66
|
-
mediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
71
|
+
mediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
67
72
|
}, "strict", z.ZodTypeAny, {
|
|
68
73
|
content: string;
|
|
69
74
|
role: "user" | "assistant" | "system";
|
|
@@ -71,31 +76,38 @@ export declare const chatTextShape: {
|
|
|
71
76
|
}, {
|
|
72
77
|
content: string;
|
|
73
78
|
role: "user" | "assistant" | "system";
|
|
74
|
-
mediaIds?: string[] | undefined;
|
|
79
|
+
mediaIds?: string[] | null | undefined;
|
|
75
80
|
}>, "many">;
|
|
76
81
|
thinkingEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
-
|
|
82
|
+
clientRequestId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
83
|
+
pricingSnapshot: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>, "many">, z.ZodString, z.ZodNull]>>;
|
|
78
84
|
};
|
|
79
85
|
export declare const generateImageShape: {
|
|
80
86
|
publicModelId: z.ZodString;
|
|
81
87
|
routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
|
|
82
88
|
prompt: z.ZodEffects<z.ZodString, string, string>;
|
|
83
|
-
batchSize: z.ZodDefault<z.ZodNumber
|
|
84
|
-
imageMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
85
|
-
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
86
|
-
|
|
89
|
+
batchSize: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodNumber, z.ZodNull]>, number, number | null>>;
|
|
90
|
+
imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
91
|
+
params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
|
|
92
|
+
dryRun: z.ZodDefault<z.ZodBoolean>;
|
|
93
|
+
clientRequestId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
94
|
+
pricingSnapshot: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>, "many">, z.ZodString, z.ZodNull]>>;
|
|
87
95
|
};
|
|
88
96
|
export declare const generateVideoShape: {
|
|
97
|
+
materialMode: z.ZodOptional<z.ZodEnum<["standard", "certified"]>>;
|
|
98
|
+
materialQuoteId: z.ZodOptional<z.ZodString>;
|
|
89
99
|
publicModelId: z.ZodString;
|
|
90
100
|
routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
|
|
91
101
|
prompt: z.ZodEffects<z.ZodString, string, string>;
|
|
92
|
-
imageMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
93
|
-
videoMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
94
|
-
audioMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
102
|
+
imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
103
|
+
videoMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
104
|
+
audioMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
95
105
|
firstFrameMediaId: z.ZodOptional<z.ZodString>;
|
|
96
106
|
lastFrameMediaId: z.ZodOptional<z.ZodString>;
|
|
97
|
-
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
98
|
-
|
|
107
|
+
params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
|
|
108
|
+
dryRun: z.ZodDefault<z.ZodBoolean>;
|
|
109
|
+
clientRequestId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
110
|
+
pricingSnapshot: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>, "many">, z.ZodString, z.ZodNull]>>;
|
|
99
111
|
};
|
|
100
112
|
export declare const getGenerationTaskShape: {
|
|
101
113
|
taskId: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -11,7 +11,24 @@ const routingModeSchema = z.enum([
|
|
|
11
11
|
"SUCCESS_FIRST",
|
|
12
12
|
]);
|
|
13
13
|
const pricingSnapshotSchema = z.record(z.unknown());
|
|
14
|
-
|
|
14
|
+
// 宿主常把可选数组/对象字段塞成 null, 归一到默认值避免服务端收到非法请求.
|
|
15
|
+
const nullableMediaIdsSchema = z
|
|
16
|
+
.union([z.array(mediaIdSchema), z.null()])
|
|
17
|
+
.transform((value) => value ?? [])
|
|
18
|
+
.default([]);
|
|
19
|
+
const paramsSchema = z
|
|
20
|
+
.union([z.record(z.unknown()), z.null()])
|
|
21
|
+
.transform((value) => value ?? {})
|
|
22
|
+
.default({});
|
|
23
|
+
// SDK 在 handler 之前先用 zod 校验入参, schema 必须放行畸形快照, server 层才有机会代为归一化.
|
|
24
|
+
const pricingSnapshotInputSchema = z
|
|
25
|
+
.union([
|
|
26
|
+
pricingSnapshotSchema,
|
|
27
|
+
z.array(pricingSnapshotSchema.nullable()),
|
|
28
|
+
z.string(),
|
|
29
|
+
z.null(),
|
|
30
|
+
])
|
|
31
|
+
.optional();
|
|
15
32
|
export const mcpCreateMediaUploadOutputSchema = z
|
|
16
33
|
.object({
|
|
17
34
|
mediaId: mediaIdSchema,
|
|
@@ -41,39 +58,62 @@ const messageSchema = z
|
|
|
41
58
|
.object({
|
|
42
59
|
role: z.enum(["system", "user", "assistant"]),
|
|
43
60
|
content: nonBlankStringSchema,
|
|
44
|
-
mediaIds:
|
|
61
|
+
mediaIds: nullableMediaIdsSchema,
|
|
45
62
|
})
|
|
46
63
|
.strict();
|
|
64
|
+
// 幂等键供付费调用去重, 空白值等同于没传, 所以沿用 nonBlank 校验但保持 optional.
|
|
65
|
+
const clientRequestIdSchema = z
|
|
66
|
+
.string()
|
|
67
|
+
.max(100)
|
|
68
|
+
.refine((value) => value.trim().length > 0, {
|
|
69
|
+
message: "Expected non-blank string",
|
|
70
|
+
})
|
|
71
|
+
.optional();
|
|
47
72
|
export const listModelsShape = {
|
|
48
73
|
modelType: z.enum(["TEXT", "IMAGE", "VIDEO"]).optional(),
|
|
74
|
+
detail: z.enum(["lite", "full"]).optional(),
|
|
75
|
+
};
|
|
76
|
+
export const getModelShape = {
|
|
77
|
+
publicModelId: z.string().min(1),
|
|
78
|
+
routingMode: routingModeSchema.optional(),
|
|
49
79
|
};
|
|
50
80
|
export const chatTextShape = {
|
|
51
81
|
publicModelId: z.string().min(1),
|
|
52
82
|
routingMode: routingModeSchema,
|
|
53
83
|
messages: z.array(messageSchema).min(1),
|
|
54
84
|
thinkingEnabled: z.boolean().optional(),
|
|
55
|
-
|
|
85
|
+
clientRequestId: clientRequestIdSchema,
|
|
86
|
+
pricingSnapshot: pricingSnapshotInputSchema,
|
|
56
87
|
};
|
|
57
88
|
export const generateImageShape = {
|
|
58
89
|
publicModelId: z.string().min(1),
|
|
59
90
|
routingMode: routingModeSchema,
|
|
60
91
|
prompt: nonBlankStringSchema,
|
|
61
|
-
batchSize: z
|
|
62
|
-
|
|
92
|
+
batchSize: z
|
|
93
|
+
.union([z.number().int().min(1).max(10), z.null()])
|
|
94
|
+
.transform((value) => value ?? 1)
|
|
95
|
+
.default(1),
|
|
96
|
+
imageMediaIds: nullableMediaIdsSchema,
|
|
63
97
|
params: paramsSchema,
|
|
64
|
-
|
|
98
|
+
dryRun: z.boolean().default(false),
|
|
99
|
+
clientRequestId: clientRequestIdSchema,
|
|
100
|
+
pricingSnapshot: pricingSnapshotInputSchema,
|
|
65
101
|
};
|
|
66
102
|
export const generateVideoShape = {
|
|
103
|
+
materialMode: z.enum(["standard", "certified"]).optional(),
|
|
104
|
+
materialQuoteId: z.string().min(1).optional(),
|
|
67
105
|
publicModelId: z.string().min(1),
|
|
68
106
|
routingMode: routingModeSchema,
|
|
69
107
|
prompt: nonBlankStringSchema,
|
|
70
|
-
imageMediaIds:
|
|
71
|
-
videoMediaIds:
|
|
72
|
-
audioMediaIds:
|
|
108
|
+
imageMediaIds: nullableMediaIdsSchema,
|
|
109
|
+
videoMediaIds: nullableMediaIdsSchema,
|
|
110
|
+
audioMediaIds: nullableMediaIdsSchema,
|
|
73
111
|
firstFrameMediaId: mediaIdSchema.optional(),
|
|
74
112
|
lastFrameMediaId: mediaIdSchema.optional(),
|
|
75
113
|
params: paramsSchema,
|
|
76
|
-
|
|
114
|
+
dryRun: z.boolean().default(false),
|
|
115
|
+
clientRequestId: clientRequestIdSchema,
|
|
116
|
+
pricingSnapshot: pricingSnapshotInputSchema,
|
|
77
117
|
};
|
|
78
118
|
export const getGenerationTaskShape = {
|
|
79
119
|
taskId: z.string().min(1),
|
package/dist/server.d.ts
CHANGED
|
@@ -4,16 +4,17 @@ import { AiHiveApiClient } from "./api-client.js";
|
|
|
4
4
|
import { type BearerTokenProvider } from "./auth-token-provider.js";
|
|
5
5
|
import type { McpClientConfig } from "./config.js";
|
|
6
6
|
import { WorkBuddyCredentialStore } from "./workbuddy-credentials.js";
|
|
7
|
-
export declare const SERVER_VERSION = "0.
|
|
7
|
+
export declare const SERVER_VERSION = "0.3.1";
|
|
8
8
|
declare function content(value: unknown): {
|
|
9
9
|
content: {
|
|
10
10
|
type: "text";
|
|
11
11
|
text: string;
|
|
12
12
|
}[];
|
|
13
13
|
};
|
|
14
|
-
type ApiClientLike = Pick<AiHiveApiClient, "getUserInfo" | "listModels" | "createMediaUpload" | "completeMediaUpload" | "chatText" | "generateImage" | "generateVideo" | "getGenerationTask" | "putUpload">;
|
|
14
|
+
type ApiClientLike = Pick<AiHiveApiClient, "getUserInfo" | "getMaterialMode" | "quoteVideoMaterials" | "listModels" | "getModel" | "createMediaUpload" | "completeMediaUpload" | "chatText" | "generateImage" | "generateVideo" | "getGenerationTask" | "putUpload">;
|
|
15
15
|
export type ToolDefinition = {
|
|
16
16
|
name: string;
|
|
17
|
+
description: string;
|
|
17
18
|
schema: z.ZodRawShape;
|
|
18
19
|
handler: (args: Record<string, unknown>) => Promise<ReturnType<typeof content>>;
|
|
19
20
|
};
|
|
@@ -23,14 +24,23 @@ export type ResolvedServerAuth = {
|
|
|
23
24
|
tokenProvider: BearerTokenProvider;
|
|
24
25
|
};
|
|
25
26
|
export declare function resolveServerAuth(config: McpClientConfig, store: WorkBuddyCredentialStore): Promise<ResolvedServerAuth>;
|
|
27
|
+
/**
|
|
28
|
+
* 与 @ai-hive/contracts 的 buildMcpServerInstructions 内容对应;
|
|
29
|
+
* mcp-client 独立发布不依赖 contracts, 修改时两边同步.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildServerInstructions(webOrigin?: string): string;
|
|
26
32
|
export declare function createServer(input: {
|
|
27
33
|
endpoint: string;
|
|
28
34
|
tokenProvider: BearerTokenProvider;
|
|
29
35
|
timeoutMs: number;
|
|
36
|
+
webOrigin?: string;
|
|
37
|
+
verbose?: boolean;
|
|
30
38
|
}): McpServer;
|
|
31
39
|
export declare function runServer(input: {
|
|
32
40
|
endpoint: string;
|
|
33
41
|
tokenProvider: BearerTokenProvider;
|
|
34
42
|
timeoutMs: number;
|
|
43
|
+
webOrigin?: string;
|
|
44
|
+
verbose?: boolean;
|
|
35
45
|
}): Promise<void>;
|
|
36
46
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, listModelsShape, } from "./schemas.js";
|
|
3
|
+
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, getModelShape, listModelsShape, } from "./schemas.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { AiHiveApiClient } from "./api-client.js";
|
|
5
|
+
import { AiHiveApiClient, AiHiveApiError } from "./api-client.js";
|
|
6
6
|
import { normalizeApiEndpoint, StaticBearerTokenProvider, WorkBuddyTokenProvider, } from "./auth-token-provider.js";
|
|
7
7
|
import { readMcpClientInfo } from "./client-info.js";
|
|
8
|
+
import { createUpdateNotifier } from "./update-notifier.js";
|
|
8
9
|
import { uploadMediaFromPath } from "./local-media.js";
|
|
9
10
|
import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
|
|
10
|
-
export const SERVER_VERSION = "0.
|
|
11
|
+
export const SERVER_VERSION = "0.3.1";
|
|
11
12
|
const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
|
|
13
|
+
const SNAPSHOT_PARSE_FAILURE_MESSAGE = "pricingSnapshot 格式不正确,应为 list_models 返回的快照对象,也可以省略该字段由客户端自动获取";
|
|
14
|
+
const SNAPSHOT_ARRAY_MISMATCH_MESSAGE = "pricingSnapshot 数组中没有匹配当前模型和 routingMode 的快照,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
15
|
+
const SNAPSHOT_IDENTITY_MISMATCH_MESSAGE = "pricingSnapshot 的 publicModelId 或 routingMode 与请求不一致,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
16
|
+
const MODEL_UNAVAILABLE_MESSAGE = "模型当前不可用或不支持该路由模式,请重新调用 list_models 获取最新模型信息";
|
|
12
17
|
function content(value) {
|
|
13
18
|
return {
|
|
14
19
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
@@ -19,20 +24,114 @@ function rejectDeprecatedAttachmentIds(args) {
|
|
|
19
24
|
throw new Error(DEPRECATED_ATTACHMENT_IDS_MESSAGE);
|
|
20
25
|
}
|
|
21
26
|
}
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function pickSnapshotForRequest(snapshots, publicModelId, routingMode) {
|
|
31
|
+
return snapshots.find((item) => isPlainObject(item) &&
|
|
32
|
+
item.publicModelId === publicModelId &&
|
|
33
|
+
item.routingMode === routingMode);
|
|
34
|
+
}
|
|
35
|
+
// LLM 回传 pricingSnapshot 时经常漏传或变形(数组/JSON 字符串/null), 由客户端代为归一化,
|
|
36
|
+
// 保证进入付费 API 的请求一定携带服务端期望的单个快照对象.
|
|
37
|
+
async function resolvePricingSnapshot(client, modelType, args) {
|
|
38
|
+
const requestArgs = { ...args };
|
|
39
|
+
if (requestArgs.dryRun === false) {
|
|
40
|
+
delete requestArgs.dryRun;
|
|
41
|
+
}
|
|
42
|
+
let snapshot = requestArgs.pricingSnapshot;
|
|
43
|
+
const isDryRun = args.dryRun === true;
|
|
44
|
+
const withoutPricingSnapshot = () => {
|
|
45
|
+
const request = { ...requestArgs };
|
|
46
|
+
delete request.pricingSnapshot;
|
|
47
|
+
return request;
|
|
48
|
+
};
|
|
49
|
+
const resolveCurrentSnapshot = async () => {
|
|
50
|
+
const models = await client().listModels({ modelType });
|
|
51
|
+
const model = Array.isArray(models)
|
|
52
|
+
? models.find((item) => isPlainObject(item) && item.publicModelId === args.publicModelId)
|
|
53
|
+
: undefined;
|
|
54
|
+
const snapshots = isPlainObject(model) ? model.pricingSnapshot : undefined;
|
|
55
|
+
const matched = Array.isArray(snapshots)
|
|
56
|
+
? pickSnapshotForRequest(snapshots, args.publicModelId, args.routingMode)
|
|
57
|
+
: undefined;
|
|
58
|
+
if (!matched) {
|
|
59
|
+
throw new Error(MODEL_UNAVAILABLE_MESSAGE);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...requestArgs,
|
|
63
|
+
pricingSnapshot: matched,
|
|
64
|
+
pricingSnapshotAutoResolved: true,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
if (snapshot === undefined || snapshot === null) {
|
|
68
|
+
return isDryRun ? withoutPricingSnapshot() : resolveCurrentSnapshot();
|
|
69
|
+
}
|
|
70
|
+
if (typeof snapshot === "string") {
|
|
71
|
+
try {
|
|
72
|
+
snapshot = JSON.parse(snapshot);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error(SNAPSHOT_PARSE_FAILURE_MESSAGE);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (snapshot === undefined || snapshot === null) {
|
|
79
|
+
return isDryRun ? withoutPricingSnapshot() : resolveCurrentSnapshot();
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(snapshot)) {
|
|
82
|
+
const matched = pickSnapshotForRequest(snapshot, args.publicModelId, args.routingMode);
|
|
83
|
+
if (!matched) {
|
|
84
|
+
throw new Error(SNAPSHOT_ARRAY_MISMATCH_MESSAGE);
|
|
85
|
+
}
|
|
86
|
+
return { ...requestArgs, pricingSnapshot: matched };
|
|
87
|
+
}
|
|
88
|
+
if (isPlainObject(snapshot)) {
|
|
89
|
+
// 单对象快照同样可能传错模型或档位, 与数组分支一致校验, 避免按错误价格计费;
|
|
90
|
+
// 键缺失时放行, 由服务端 strict schema 兜底.
|
|
91
|
+
if ((typeof snapshot.publicModelId === "string" &&
|
|
92
|
+
snapshot.publicModelId !== args.publicModelId) ||
|
|
93
|
+
(typeof snapshot.routingMode === "string" &&
|
|
94
|
+
snapshot.routingMode !== args.routingMode)) {
|
|
95
|
+
throw new Error(SNAPSHOT_IDENTITY_MISMATCH_MESSAGE);
|
|
96
|
+
}
|
|
97
|
+
return { ...requestArgs, pricingSnapshot: snapshot };
|
|
98
|
+
}
|
|
99
|
+
// 数字/布尔等原始类型同样是畸形回传, 不能原样透传给付费 API.
|
|
100
|
+
throw new Error(SNAPSHOT_PARSE_FAILURE_MESSAGE);
|
|
101
|
+
}
|
|
102
|
+
// 幂等键语义写在每个付费工具的描述里, 让宿主在选型阶段就能看到重试规则.
|
|
103
|
+
const CLIENT_REQUEST_ID_NOTE = "付费调用应传入稳定 UUID 作为 clientRequestId 幂等键;仅传输中断、REQUEST_IN_PROGRESS 或刷新过期 pricingSnapshot 时复用原值和相同业务参数。余额、参数、媒体或已缓存的模型调用失败在修正后必须使用新的 clientRequestId;REQUEST_OUTCOME_UNKNOWN 必须停止自动重试并人工核查。";
|
|
104
|
+
// 长耗时提醒同样前置到工具描述: 宿主据此设置超时, 避免"本端超时先断、
|
|
105
|
+
// 服务端跑完仍扣费"被误读成失败扣款.
|
|
106
|
+
const LONG_RUNNING_NOTE = "该调用同步返回,长推理模型单次可能耗时 1-3 分钟,客户端超时应设为 300 秒以上;等待超时只代表本端不再等待,服务端可能仍在生成,也可能已完成并扣费,超时后复用同一 clientRequestId 和相同业务参数重新调用即可取回首次结果,修正参数后换新值。";
|
|
22
107
|
export function createToolDefinitions(client) {
|
|
23
108
|
return [
|
|
24
109
|
{
|
|
25
110
|
name: "get_user_info",
|
|
111
|
+
description: "查询当前用户、余额,以及官网地址和充值入口;余额不足时引导用户打开 rechargeUrl 完成充值后重试。",
|
|
26
112
|
schema: {},
|
|
27
113
|
handler: async () => content(await client().getUserInfo()),
|
|
28
114
|
},
|
|
29
115
|
{
|
|
30
116
|
name: "list_models",
|
|
117
|
+
description: "按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格。detail 为 lite 时只返回各模型各路由的售价摘要(维度、单位、计费方式、内含数量及 saleMicros),用于快速选型;detail 为 full 时返回完整公开详情。saleMicros 是客户实际结算价,两种结果都不包含内部参考成本。",
|
|
31
118
|
schema: listModelsShape,
|
|
32
119
|
handler: async (args) => content(await client().listModels(args)),
|
|
33
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: "get_model",
|
|
123
|
+
description: "按 publicModelId 查询单个模型的参数 schema、媒体要求、计费因素和示例;传 routingMode 可获取线路专属契约。",
|
|
124
|
+
schema: getModelShape,
|
|
125
|
+
handler: async (args) => {
|
|
126
|
+
const publicModelId = String(args.publicModelId);
|
|
127
|
+
return content(typeof args.routingMode === "string"
|
|
128
|
+
? await client().getModel(publicModelId, args.routingMode)
|
|
129
|
+
: await client().getModel(publicModelId));
|
|
130
|
+
},
|
|
131
|
+
},
|
|
34
132
|
{
|
|
35
133
|
name: "upload_media_from_path",
|
|
134
|
+
description: "上传本地图片、视频、音频或文档并返回 mediaId。",
|
|
36
135
|
schema: {
|
|
37
136
|
path: z.string().min(1),
|
|
38
137
|
filename: z.string().min(1).optional(),
|
|
@@ -42,24 +141,65 @@ export function createToolDefinitions(client) {
|
|
|
42
141
|
},
|
|
43
142
|
{
|
|
44
143
|
name: "chat_text",
|
|
144
|
+
description: `使用指定模型生成文本。${LONG_RUNNING_NOTE}${CLIENT_REQUEST_ID_NOTE}`,
|
|
45
145
|
schema: chatTextShape,
|
|
46
146
|
handler: async (args) => {
|
|
47
147
|
rejectDeprecatedAttachmentIds(args);
|
|
48
|
-
|
|
148
|
+
const body = await resolvePricingSnapshot(client, "TEXT", args);
|
|
149
|
+
return content(await client().chatText(body));
|
|
49
150
|
},
|
|
50
151
|
},
|
|
51
152
|
{
|
|
52
153
|
name: "generate_image",
|
|
154
|
+
description: `创建图片生成任务;dryRun=true 时只校验参数、媒体和预估费用,不创建任务且可省略 pricingSnapshot 与 clientRequestId。${CLIENT_REQUEST_ID_NOTE}`,
|
|
53
155
|
schema: generateImageShape,
|
|
54
|
-
handler: async (args) =>
|
|
156
|
+
handler: async (args) => {
|
|
157
|
+
const body = await resolvePricingSnapshot(client, "IMAGE", args);
|
|
158
|
+
return content(await client().generateImage(body));
|
|
159
|
+
},
|
|
55
160
|
},
|
|
56
161
|
{
|
|
57
162
|
name: "generate_video",
|
|
163
|
+
description: `认证版接收普通素材,服务端按模型 ID 自动校验资格和准备素材。提交前先用 dryRun=true 做只读预检,不用真实付费提交试探资格。materialMode、materialQuoteId 和专用报价工具均为可选兼容能力;正式提交使用当前 pricingSnapshot 和稳定 clientRequestId。资格不足时明确说明差额或待授权事项,并在回复中展示返回的 helpUrl 或 actionUrl 为可点击链接;未返回链接时使用 https://ai-hive.iclip.cn/certified-materials#guide。保留用户指定模型,不擅自切换模型或继续付费提交。认证服务已包含在模型价格中;费用变化时重新确认。收到任务 ID 后查询原任务,不重复创建。dryRun 不创建任务、不冻结素材、不预占或扣费,也不执行供应商内容审核。${CLIENT_REQUEST_ID_NOTE}`,
|
|
58
164
|
schema: generateVideoShape,
|
|
59
|
-
handler: async (args) =>
|
|
165
|
+
handler: async (args) => {
|
|
166
|
+
const body = await resolvePricingSnapshot(client, "VIDEO", args);
|
|
167
|
+
return content(await client().generateVideo(body));
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "get_material_mode",
|
|
172
|
+
description: "查询认证版服务状态、授权与累计充值资格。已满足条件时直接继续;仅未授权时请用户本人打开 actionUrl 阅读并同意,不得代替用户授权。累计充值资格与钱包余额分别检查。",
|
|
173
|
+
schema: {},
|
|
174
|
+
handler: async () => content(await client().getMaterialMode()),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "quote_video_materials",
|
|
178
|
+
description: "上传普通素材后,由调用端用与生成相同的模型、参数及媒体 ID 获取报价,包含参考图片、视频、首帧和尾帧。无需用户手动认证素材。认证服务已包含在模型价格中,不另收素材费;向用户说明必要费用,费用或计费条件变化时重新确认。此工具可选,返回 id 可用作 materialQuoteId;未提供报价时服务端自动准备。显式报价过期时保留原素材重新获取。",
|
|
179
|
+
schema: {
|
|
180
|
+
publicModelId: generateVideoShape.publicModelId,
|
|
181
|
+
routingMode: generateVideoShape.routingMode,
|
|
182
|
+
prompt: generateVideoShape.prompt,
|
|
183
|
+
imageMediaIds: generateVideoShape.imageMediaIds,
|
|
184
|
+
videoMediaIds: generateVideoShape.videoMediaIds,
|
|
185
|
+
audioMediaIds: generateVideoShape.audioMediaIds,
|
|
186
|
+
firstFrameMediaId: generateVideoShape.firstFrameMediaId,
|
|
187
|
+
lastFrameMediaId: generateVideoShape.lastFrameMediaId,
|
|
188
|
+
pricingSnapshot: generateVideoShape.pricingSnapshot,
|
|
189
|
+
params: generateVideoShape.params,
|
|
190
|
+
},
|
|
191
|
+
handler: async (args) => {
|
|
192
|
+
const body = await resolvePricingSnapshot(client, "VIDEO", {
|
|
193
|
+
...args,
|
|
194
|
+
dryRun: true,
|
|
195
|
+
});
|
|
196
|
+
delete body.dryRun;
|
|
197
|
+
return content(await client().quoteVideoMaterials(body));
|
|
198
|
+
},
|
|
60
199
|
},
|
|
61
200
|
{
|
|
62
201
|
name: "get_generation_task",
|
|
202
|
+
description: "查询图片或视频任务状态和结果。",
|
|
63
203
|
schema: getGenerationTaskShape,
|
|
64
204
|
handler: async (args) => content(await client().getGenerationTask({
|
|
65
205
|
taskId: String(args.taskId),
|
|
@@ -98,16 +238,57 @@ export async function resolveServerAuth(config, store) {
|
|
|
98
238
|
}),
|
|
99
239
|
};
|
|
100
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* 与 @ai-hive/contracts 的 buildMcpServerInstructions 内容对应;
|
|
243
|
+
* mcp-client 独立发布不依赖 contracts, 修改时两边同步.
|
|
244
|
+
*/
|
|
245
|
+
export function buildServerInstructions(webOrigin) {
|
|
246
|
+
const lines = [
|
|
247
|
+
"AI Hive 是统一的大模型调用平台,按钱包余额扣费。",
|
|
248
|
+
"dryRun 的 readyToGenerate=true 与 mediaValidation=VERIFIED 仅表示参数和媒体预检结果,未执行人像或内容审核,不保证生成成功。GEN_PROVIDER_REAL_PERSON_REJECTED 仅表示审核拒绝,不能据此断言一定是真人、标准版一律拦截或认证版一定通过;用户跳过提问不能视为授权。价格以当前报价和 pricingSnapshot 为准,不按历史倍数计算;余额相同不能单独证明未扣费。",
|
|
249
|
+
"认证版接收普通素材,服务端按模型 ID 自动校验资格和准备素材。提交前先用 dryRun=true 做只读预检,不用真实付费提交试探资格。materialMode、materialQuoteId 和专用报价工具均为可选兼容能力;正式提交使用当前 pricingSnapshot 和稳定 clientRequestId。资格不足时明确说明差额或待授权事项,并在回复中展示返回的 helpUrl 或 actionUrl 为可点击链接;未返回链接时使用 https://ai-hive.iclip.cn/certified-materials#guide。保留用户指定模型,不擅自切换模型或继续付费提交。认证服务已包含在模型价格中;费用变化时重新确认。收到任务 ID 后查询原任务,不重复创建。",
|
|
250
|
+
"latestChange、changelog 和升级信息仅在用户主动询问或与当前任务直接相关时介绍。对用户只说明实际能力、费用、限制、结果和必要操作,不转述字段名、错误码、报价 ID、幂等机制、版本适配或内部排查过程;技术字段和 recoveryAction 供调用端处理,不能替代用户操作说明。用户明确排查接入问题时可以解释所需技术细节。",
|
|
251
|
+
"先用 get_user_info 查询余额;返回里带官网地址和充值入口,余额不足时引导用户打开 rechargeUrl 充值后重试。",
|
|
252
|
+
"调用 chat_text、generate_image、generate_video 前,用 get_model 确认参数契约;正式调用必须传稳定的 clientRequestId 幂等键,修正参数后换新值。",
|
|
253
|
+
"chat_text 同步返回,长推理模型单次可能耗时 1-3 分钟,客户端超时应设为 300 秒以上;等待超时只代表本端不再等待,服务端可能仍在生成,也可能已完成并扣费,超时后复用同一 clientRequestId 和相同业务参数重新调用即可取回首次结果,修正参数后换新值。",
|
|
254
|
+
];
|
|
255
|
+
lines.push(webOrigin
|
|
256
|
+
? `产品更新见 ${webOrigin}/changelog。`
|
|
257
|
+
: "产品更新见官网更新日志页。");
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
101
260
|
export function createServer(input) {
|
|
102
|
-
|
|
261
|
+
// instructions 随 initialize 握手注入宿主上下文, 是裸 MCP 缺少技能时的基础知识来源.
|
|
262
|
+
const server = new McpServer({ name: "ai-hive", version: SERVER_VERSION }, { instructions: buildServerInstructions(input.webOrigin) });
|
|
263
|
+
const updateNotifier = createUpdateNotifier({
|
|
264
|
+
currentVersion: SERVER_VERSION,
|
|
265
|
+
enabled: input.verbose === true,
|
|
266
|
+
endpoint: input.endpoint,
|
|
267
|
+
});
|
|
103
268
|
const client = () => new AiHiveApiClient({
|
|
104
269
|
endpoint: input.endpoint,
|
|
105
270
|
tokenProvider: input.tokenProvider,
|
|
106
271
|
timeoutMs: input.timeoutMs,
|
|
107
272
|
clientInfoProvider: () => readMcpClientInfo(server),
|
|
273
|
+
updateNotifier,
|
|
108
274
|
});
|
|
109
275
|
for (const tool of createToolDefinitions(client)) {
|
|
110
|
-
server.tool(tool.name, tool.
|
|
276
|
+
server.tool(tool.name, tool.description, tool.schema, async (args) => {
|
|
277
|
+
try {
|
|
278
|
+
return await tool.handler(args);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error instanceof AiHiveApiError && error.details)
|
|
282
|
+
return {
|
|
283
|
+
isError: true,
|
|
284
|
+
structuredContent: error.details,
|
|
285
|
+
content: [
|
|
286
|
+
{ type: "text", text: JSON.stringify(error.details) },
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
111
292
|
}
|
|
112
293
|
return server;
|
|
113
294
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 与服务端 contracts 的 LATEST_CLIENT_VERSION_HEADER 保持一致;
|
|
3
|
+
* mcp-client 独立发布到 npm, 不依赖 @ai-hive/contracts, 常量需手工同步.
|
|
4
|
+
*/
|
|
5
|
+
export declare const LATEST_CLIENT_VERSION_HEADER = "x-ai-hive-latest-client-version";
|
|
6
|
+
export type UpdateNotifier = {
|
|
7
|
+
notifyIfOutdated(latestVersion: string | null): void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 仅显式开启维护诊断时, 按服务端响应头在 stderr 一次性提醒可选升级.
|
|
11
|
+
* 只写 stderr, 不碰 stdout 的 MCP 协议通道; 提醒过就不再重复, 避免刷屏.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createUpdateNotifier(input: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
currentVersion: string;
|
|
16
|
+
endpoint: string;
|
|
17
|
+
write?: (line: string) => void;
|
|
18
|
+
}): UpdateNotifier;
|
|
19
|
+
/** 只比较 x.y.z 数字段; 非数字段按 0 处理, 不被预发布标签绊倒. */
|
|
20
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 与服务端 contracts 的 LATEST_CLIENT_VERSION_HEADER 保持一致;
|
|
3
|
+
* mcp-client 独立发布到 npm, 不依赖 @ai-hive/contracts, 常量需手工同步.
|
|
4
|
+
*/
|
|
5
|
+
export const LATEST_CLIENT_VERSION_HEADER = "x-ai-hive-latest-client-version";
|
|
6
|
+
/**
|
|
7
|
+
* 仅显式开启维护诊断时, 按服务端响应头在 stderr 一次性提醒可选升级.
|
|
8
|
+
* 只写 stderr, 不碰 stdout 的 MCP 协议通道; 提醒过就不再重复, 避免刷屏.
|
|
9
|
+
*/
|
|
10
|
+
export function createUpdateNotifier(input) {
|
|
11
|
+
const write = input.write ?? ((line) => console.error(line));
|
|
12
|
+
let notified = false;
|
|
13
|
+
return {
|
|
14
|
+
notifyIfOutdated(latestVersion) {
|
|
15
|
+
if (!input.enabled || notified || !latestVersion) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (compareVersions(latestVersion, input.currentVersion) <= 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
notified = true;
|
|
22
|
+
write(`[ai-hive-mcp] 新版本 ${latestVersion} 已发布(当前 ${input.currentVersion}),新工具和改进见更新日志:${changelogUrl(input.endpoint)}。升级命令:npm install -g @infimind-next/ai-hive-mcp@latest`);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** 只比较 x.y.z 数字段; 非数字段按 0 处理, 不被预发布标签绊倒. */
|
|
27
|
+
export function compareVersions(a, b) {
|
|
28
|
+
const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
29
|
+
const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
30
|
+
for (let index = 0; index < 3; index += 1) {
|
|
31
|
+
const diff = (pa[index] ?? 0) - (pb[index] ?? 0);
|
|
32
|
+
if (diff !== 0) {
|
|
33
|
+
return diff;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
/** API 与站点同域部署, 由 endpoint 推导更新日志页; 推导失败时回退正式环境. */
|
|
39
|
+
function changelogUrl(endpoint) {
|
|
40
|
+
try {
|
|
41
|
+
return `${new URL(endpoint).origin}/changelog`;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return "https://ai-hive.iclip.cn/changelog";
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infimind-next/ai-hive-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -12,13 +12,6 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc -p tsconfig.json",
|
|
17
|
-
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
18
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
19
|
-
"prepack": "pnpm run build",
|
|
20
|
-
"test": "vitest run"
|
|
21
|
-
},
|
|
22
15
|
"dependencies": {
|
|
23
16
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
24
17
|
"file-type": "^21.3.4",
|
|
@@ -29,5 +22,11 @@
|
|
|
29
22
|
"typescript": "^5.0.0",
|
|
30
23
|
"typescript-eslint": "^8.0.0",
|
|
31
24
|
"vitest": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
29
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
30
|
+
"test": "vitest run"
|
|
32
31
|
}
|
|
33
|
-
}
|
|
32
|
+
}
|