@infimind-next/ai-hive-mcp 0.3.0 → 0.3.2
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 +45 -3
- package/dist/api-client.d.ts +35 -8
- package/dist/api-client.js +114 -25
- package/dist/cli.js +6 -1
- package/dist/config.js +10 -2
- package/dist/index.js +0 -0
- package/dist/local-media.js +2 -2
- package/dist/schemas.d.ts +4 -2
- package/dist/schemas.js +2 -0
- package/dist/server.d.ts +23 -3
- package/dist/server.js +164 -20
- package/dist/update-notifier.d.ts +20 -0
- package/dist/update-notifier.js +46 -0
- package/dist/user-notices.d.ts +4 -0
- package/dist/user-notices.js +52 -0
- package/package.json +8 -9
package/README.md
CHANGED
|
@@ -11,25 +11,42 @@
|
|
|
11
11
|
"mcpServers": {
|
|
12
12
|
"ai-hive": {
|
|
13
13
|
"command": "npx",
|
|
14
|
-
"args": ["-y", "@infimind-next/ai-hive-mcp@0.3.
|
|
14
|
+
"args": ["-y", "@infimind-next/ai-hive-mcp@0.3.2", "--key", "<APIKEY>"]
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
要求 Node.js >= 20.19.0
|
|
20
|
+
要求 Node.js >= 20.19.0。默认请求超时为 300 秒,可通过 `--timeout-ms` 或 `AI_HIVE_MCP_TIMEOUT_MS` 覆盖;无效值回退为 300 秒。WorkBuddy Connector 的启动与认证命令保持引用 `@latest`,Connector 和 Skills 版本仍为 1.2.0。
|
|
21
|
+
|
|
22
|
+
本包通过标准 MCP stdio 与客户端通信,仅保证与支持 MCP 的客户端配合使用。
|
|
21
23
|
|
|
22
24
|
## 工具
|
|
23
25
|
|
|
24
|
-
- `get_user_info
|
|
26
|
+
- `get_user_info`:查询当前用户和普通蜂蜜余额
|
|
27
|
+
|
|
28
|
+
专属蜂蜜由服务端按实际模型自动选择抵扣,无需新增调用参数。不能仅凭普通蜂蜜余额为 0 拒绝模型调用;只有实际调用返回余额不足时,才使用 `rechargeUrl` 引导充值并重试。
|
|
29
|
+
|
|
25
30
|
- `list_models`:按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格
|
|
26
31
|
- `get_model`:查询单个模型的参数 schema、媒体要求、计费因素和调用示例
|
|
27
32
|
- `upload_media_from_path`:上传本地图片、视频或音频,返回 mediaId
|
|
28
33
|
- `chat_text`:文本对话
|
|
29
34
|
- `generate_image`:图片预检或正式生成
|
|
30
35
|
- `generate_video`:视频预检或正式生成
|
|
36
|
+
- `get_material_mode`:查询认证资格和网页授权入口(可选)
|
|
37
|
+
- `quote_video_materials`:获取素材报价(可选)
|
|
31
38
|
- `get_generation_task`:查询生成任务进度和结果
|
|
32
39
|
|
|
40
|
+
### 认证素材模式
|
|
41
|
+
|
|
42
|
+
已发布的 0.3.1 提供可选认证工具,其调用契约支持认证版接收普通素材,由服务端按模型 ID 自动识别并校验资格、准备素材,不要求新增参数或先调用报价工具;实际可用性以所连接服务端的响应为准。`materialMode`、`materialQuoteId` 与 `quote_video_materials` 均为可选兼容能力。先用 `dryRun: true` 预检资格与费用;预检不执行供应商内容审核。资格不足或需要授权时,展示返回的操作链接,或引导查看[认证资格指南](https://ai-hive.iclip.cn/certified-materials#guide)。用户本人在网页授权,MCP 不能代签;保留用户指定模型,费用变化时重新确认。费用以所选模型的当前报价为准。
|
|
43
|
+
|
|
44
|
+
## 0.3.2 活动与公告通知
|
|
45
|
+
|
|
46
|
+
支持服务端提供的独立充值活动通知。首段文本继续是原业务 JSON,后续文本包含活动摘要、截止时间、充值入口与档位;Agent 按握手中的展示规则简短转达,尊重拒绝促销和严格输出要求。
|
|
47
|
+
|
|
48
|
+
客户端自动使用可选查询参数请求通知,不增加工具必填参数,不改计费请求或自动重试次数。连接旧服务端且没有通知时正常返回业务结果。旧的 0.3.0/0.3.1 可继续使用新服务端,但不能保证旧 Agent 主动展示活动。支持管理员现有公告策略及无截止时间的公告;发布记录与实测范围见 [0.3.2 记录](../../docs/release/mcp-0.3.2-runbook.md)。
|
|
49
|
+
|
|
33
50
|
## 调用流程
|
|
34
51
|
|
|
35
52
|
图片和视频生成固定按 5 步走:
|
|
@@ -46,3 +63,28 @@
|
|
|
46
63
|
- 修正参数、余额或媒体问题后重试:必须使用新的 `clientRequestId`。
|
|
47
64
|
- 正式生成提示价格已更新:保持业务参数和原 `clientRequestId` 不变,重新预检获取最新 `pricingSnapshot` 后重试。
|
|
48
65
|
- 返回 `REQUEST_OUTCOME_UNKNOWN`:停止自动重试,联系管理员核查。
|
|
66
|
+
|
|
67
|
+
### 认证版的用户路径
|
|
68
|
+
|
|
69
|
+
用户提供普通素材即可,后台自动处理认证。使用对应认证模型与当前价格快照,先 dryRun 预检资格、素材和费用;通过后沿用标准版提交格式,认证字段和专用报价工具都可省略。收到任务 ID 后只查询原任务。
|
|
70
|
+
|
|
71
|
+
仅缺少授权、充值资格或余额时向用户说明必要操作,并提供[认证与充值入口](https://ai-hive.iclip.cn/certified-materials#guide);不擅自换模型提交。费用变化时重新确认,预检通过不代表内容审核通过。
|
|
72
|
+
|
|
73
|
+
更新信息仅在用户询问或与当前任务有关时介绍,不向用户转述内部参数、错误码、报价与重试机制。客户端可选升级提示默认静默,仅开启 `--verbose` 时输出维护诊断。技术维护记录见仓库 `docs/openapi/technical-changelog.md`。
|
|
74
|
+
|
|
75
|
+
`recoveryAction` 是给调用端的恢复指引,不要逐字转成用户待办。响应中断用原 `clientRequestId` 和相同参数恢复;明确失败后再次生成,或改变报价/输入时,使用新请求标识。结果未知时先核查,不自动重建付费任务。
|
|
76
|
+
|
|
77
|
+
上述已发布客户端部分依据已校验的 npm 0.3.1 tarball。发布时间为 2026-09-30 18:32(北京时间),核对日期为 2026-10-03。发布物、仓库差异与验证边界见 [0.3.1 核对记录](../../docs/release/mcp-0.3.1-runbook.md);下方另列本轮未发布扩展。
|
|
78
|
+
|
|
79
|
+
### 后续未发布增量:通知协商与直接上传
|
|
80
|
+
|
|
81
|
+
0.3.1 保留旧充值活动返回;下面的新公告扩展与直接上传工具需要先部署本轮服务端源码,再另行发布支持它们的客户端。0.3.1 的已发布工具不包含这些增量。
|
|
82
|
+
|
|
83
|
+
- `create_media_upload`:直接创建上传地址;`complete_media_upload`:直接确认上传完成。两者可在结果中返回通知。
|
|
84
|
+
- 支持通知的查询工具固定声明 `readOnlyHint=false`,因为服务端配置可启用返回状态写入。通知保留条件、截止时间和链接,结合对话转述,同一对话避免机械重复;实际展示由宿主决定。
|
|
85
|
+
- REST 请求级 `X-AI-Hive-Announcements: 1` 启用新的公告扩展;账户 POST 不要求该头。已发布的 0.3.1 不发送新请求头,仍可通过账户 GET 与原有付费请求接收旧充值活动,无需升级客户端或新增参数。SDK 直接工具请求协商,内部价格快照和自动上传初始化关闭;`upload_media_from_path` 展示完整上传完成结果。
|
|
86
|
+
- 正式付费通知请求必须有合法 `clientRequestId`,重放与恢复不重复通知,dryRun 无通知。这些是本地开发分支行为,尚未发布或完成真实宿主验收。
|
|
87
|
+
|
|
88
|
+
兼容模式(COMPAT)下,账户 GET 和 POST 常驻返回进行中的旧充值活动,不占用模型调用的每日活动通知次数。原有付费请求仍按用户每天首次成功调用返回活动;重放、恢复与 dryRun 不重复通知。管理员启用通知管理(MANAGED)后按入口设置返回;旧客户端保留 `activePromotion` 结构,功能公告通过支持新扩展的客户端返回。
|
|
89
|
+
|
|
90
|
+
本轮未发布增量中,CLI login/status 的凭证检查使用账户 GET 并明确发送 `X-AI-Hive-Announcements: 0`,完整验证凭证后仅返回账户核心字段、`activePromotion: null`,不写通知台账、共享次数或最短间隔状态。SDK 的 POST 404/405 后默认 GET 回退仍返回兼容活动。
|
package/dist/api-client.d.ts
CHANGED
|
@@ -1,36 +1,63 @@
|
|
|
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;
|
|
5
|
+
export type AnnouncementOptions = {
|
|
6
|
+
announcements?: boolean;
|
|
7
|
+
};
|
|
4
8
|
type AiHiveApiClientInput = {
|
|
5
9
|
endpoint: string;
|
|
6
10
|
tokenProvider: BearerTokenProvider;
|
|
7
11
|
timeoutMs: number;
|
|
8
12
|
fetchImpl?: FetchImpl;
|
|
9
13
|
clientInfoProvider?: () => McpClientInfo | undefined;
|
|
14
|
+
updateNotifier?: UpdateNotifier;
|
|
10
15
|
};
|
|
16
|
+
export declare class AiHiveApiError extends Error {
|
|
17
|
+
readonly status: number | undefined;
|
|
18
|
+
readonly details?: {
|
|
19
|
+
errorCode: string;
|
|
20
|
+
message: string;
|
|
21
|
+
helpUrl?: string;
|
|
22
|
+
actionUrl?: string;
|
|
23
|
+
retryable?: boolean;
|
|
24
|
+
recoveryAction?: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
constructor(status: number | undefined, message: string, details?: {
|
|
27
|
+
errorCode: string;
|
|
28
|
+
message: string;
|
|
29
|
+
helpUrl?: string;
|
|
30
|
+
actionUrl?: string;
|
|
31
|
+
retryable?: boolean;
|
|
32
|
+
recoveryAction?: string;
|
|
33
|
+
} | undefined);
|
|
34
|
+
}
|
|
11
35
|
export declare class AiHiveApiClient {
|
|
12
36
|
private readonly endpoint;
|
|
13
37
|
private readonly tokenProvider;
|
|
14
38
|
private readonly timeoutMs;
|
|
15
39
|
private readonly fetchImpl;
|
|
16
40
|
private readonly clientInfoProvider?;
|
|
41
|
+
private readonly updateNotifier?;
|
|
17
42
|
constructor(input: AiHiveApiClientInput);
|
|
18
43
|
private withTimeout;
|
|
19
44
|
private request;
|
|
20
45
|
private send;
|
|
21
46
|
private readResponse;
|
|
22
47
|
getUserInfo(): Promise<unknown>;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
getMaterialMode(options?: AnnouncementOptions): Promise<unknown>;
|
|
49
|
+
quoteVideoMaterials(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
|
|
50
|
+
listModels(input: Record<string, unknown>, options?: AnnouncementOptions): Promise<unknown>;
|
|
51
|
+
getModel(publicModelId: string, routingMode?: string, options?: AnnouncementOptions): Promise<unknown>;
|
|
52
|
+
createMediaUpload(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
|
|
53
|
+
completeMediaUpload(mediaId: string, options?: AnnouncementOptions): Promise<unknown>;
|
|
54
|
+
chatText(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
|
|
55
|
+
generateImage(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
|
|
56
|
+
generateVideo(input: unknown, options?: AnnouncementOptions): Promise<unknown>;
|
|
30
57
|
private requestGeneration;
|
|
31
58
|
getGenerationTask(input: {
|
|
32
59
|
taskId: string;
|
|
33
|
-
}): Promise<unknown>;
|
|
60
|
+
}, options?: AnnouncementOptions): Promise<unknown>;
|
|
34
61
|
putUpload(input: {
|
|
35
62
|
url: string;
|
|
36
63
|
method: "PUT";
|
package/dist/api-client.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { encodeMcpClientInfo, MCP_CLIENT_INFO_HEADER, } from "./client-info.js";
|
|
2
|
-
|
|
2
|
+
import { LATEST_CLIENT_VERSION_HEADER, } from "./update-notifier.js";
|
|
3
|
+
function announcementHeaders(options) {
|
|
4
|
+
return options?.announcements === true
|
|
5
|
+
? { "X-AI-Hive-Announcements": "1" }
|
|
6
|
+
: {};
|
|
7
|
+
}
|
|
8
|
+
export class AiHiveApiError extends Error {
|
|
3
9
|
status;
|
|
4
|
-
|
|
10
|
+
details;
|
|
11
|
+
constructor(status, message, details) {
|
|
5
12
|
super(message);
|
|
6
13
|
this.status = status;
|
|
14
|
+
this.details = details;
|
|
7
15
|
this.name = "AiHiveApiError";
|
|
8
16
|
}
|
|
9
17
|
}
|
|
@@ -41,8 +49,25 @@ function errorMessage(body) {
|
|
|
41
49
|
}
|
|
42
50
|
return "请求失败";
|
|
43
51
|
}
|
|
52
|
+
class AiHiveTimeoutError extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "AiHiveTimeoutError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// timeoutError 被 request/putUpload 共用, 覆盖只读接口与大文件上传,
|
|
59
|
+
// 文案必须保持通用: 这里只描述状态, 付费调用专属的幂等恢复口径
|
|
60
|
+
// 在 chatText/requestGeneration 处用 PAID_TIMEOUT_MESSAGE 替换.
|
|
61
|
+
const GENERIC_TIMEOUT_MESSAGE = "等待响应超时,服务端可能仍在处理,请稍后重试";
|
|
62
|
+
// 付费调用超时后服务端可能仍在生成(最终成功则已扣费), MCP/OpenAPI 没有
|
|
63
|
+
// "查询会话结果"的能力, 唯一安全的恢复手段是幂等重调; 改参数重发会被当成
|
|
64
|
+
// 新请求再次扣费, 与"同 prompt 提交 4 次扣 4 次"的客诉同因.
|
|
65
|
+
const PAID_TIMEOUT_MESSAGE = "等待响应超时,服务端可能仍在生成,也可能已完成并扣费。请复用同一 clientRequestId 和相同业务参数重新调用,由服务端幂等去重并返回首次结果;修正参数后必须更换新的 clientRequestId";
|
|
44
66
|
function timeoutError() {
|
|
45
|
-
return new
|
|
67
|
+
return new AiHiveTimeoutError(GENERIC_TIMEOUT_MESSAGE);
|
|
68
|
+
}
|
|
69
|
+
function isTimeoutError(error) {
|
|
70
|
+
return error instanceof AiHiveTimeoutError;
|
|
46
71
|
}
|
|
47
72
|
function redactInternalPricing(value) {
|
|
48
73
|
if (Array.isArray(value)) {
|
|
@@ -67,12 +92,14 @@ export class AiHiveApiClient {
|
|
|
67
92
|
timeoutMs;
|
|
68
93
|
fetchImpl;
|
|
69
94
|
clientInfoProvider;
|
|
95
|
+
updateNotifier;
|
|
70
96
|
constructor(input) {
|
|
71
97
|
this.endpoint = input.endpoint.replace(/\/+$/, "");
|
|
72
98
|
this.tokenProvider = input.tokenProvider;
|
|
73
99
|
this.timeoutMs = input.timeoutMs;
|
|
74
100
|
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
75
101
|
this.clientInfoProvider = input.clientInfoProvider;
|
|
102
|
+
this.updateNotifier = input.updateNotifier;
|
|
76
103
|
}
|
|
77
104
|
async withTimeout(operation) {
|
|
78
105
|
const controller = new AbortController();
|
|
@@ -117,6 +144,9 @@ export class AiHiveApiClient {
|
|
|
117
144
|
response = await this.send(path, init, signal, refreshedToken);
|
|
118
145
|
}
|
|
119
146
|
}
|
|
147
|
+
// 只看最终响应的版本头; 401 重试前的旧响应已废弃, 不重复参与判断.
|
|
148
|
+
// headers 用可选链访问, 兼容测试中的极简 fetch stub.
|
|
149
|
+
this.updateNotifier?.notifyIfOutdated(response.headers?.get(LATEST_CLIENT_VERSION_HEADER) ?? null);
|
|
120
150
|
return this.readResponse(response);
|
|
121
151
|
});
|
|
122
152
|
}
|
|
@@ -138,14 +168,58 @@ export class AiHiveApiClient {
|
|
|
138
168
|
async readResponse(response) {
|
|
139
169
|
const body = await response.json().catch(() => ({}));
|
|
140
170
|
if (!response.ok) {
|
|
141
|
-
|
|
171
|
+
const message = errorMessage(body);
|
|
172
|
+
const details = body &&
|
|
173
|
+
typeof body === "object" &&
|
|
174
|
+
typeof body.errorCode === "string" &&
|
|
175
|
+
body.errorCode.startsWith("ASSET_")
|
|
176
|
+
? {
|
|
177
|
+
errorCode: body.errorCode,
|
|
178
|
+
message,
|
|
179
|
+
retryable: body.retryable === true,
|
|
180
|
+
...(typeof body.recoveryAction === "string"
|
|
181
|
+
? { recoveryAction: body.recoveryAction }
|
|
182
|
+
: {}),
|
|
183
|
+
...(typeof body.helpUrl === "string"
|
|
184
|
+
? { helpUrl: body.helpUrl }
|
|
185
|
+
: {}),
|
|
186
|
+
...(typeof body.actionUrl === "string"
|
|
187
|
+
? { actionUrl: body.actionUrl }
|
|
188
|
+
: {}),
|
|
189
|
+
}
|
|
190
|
+
: undefined;
|
|
191
|
+
throw new AiHiveApiError(response.status, message, details);
|
|
142
192
|
}
|
|
143
193
|
return body;
|
|
144
194
|
}
|
|
145
|
-
getUserInfo() {
|
|
146
|
-
|
|
195
|
+
async getUserInfo() {
|
|
196
|
+
try {
|
|
197
|
+
return await this.request("/mcp/user-info?include=user-notices", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
body: "{}",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error instanceof AiHiveApiError &&
|
|
204
|
+
(error.status === 404 || error.status === 405)) {
|
|
205
|
+
return this.request("/mcp/user-info?include=user-notices");
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
getMaterialMode(options) {
|
|
211
|
+
return this.request("/mcp/material-mode", {
|
|
212
|
+
headers: announcementHeaders(options),
|
|
213
|
+
});
|
|
147
214
|
}
|
|
148
|
-
|
|
215
|
+
quoteVideoMaterials(input, options) {
|
|
216
|
+
return this.request("/mcp/generation/video/material-quote", {
|
|
217
|
+
method: "POST",
|
|
218
|
+
body: JSON.stringify(input),
|
|
219
|
+
headers: announcementHeaders(options),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async listModels(input, options) {
|
|
149
223
|
const query = new URLSearchParams();
|
|
150
224
|
if (typeof input.modelType === "string") {
|
|
151
225
|
query.set("modelType", input.modelType);
|
|
@@ -154,16 +228,16 @@ export class AiHiveApiClient {
|
|
|
154
228
|
query.set("detail", input.detail);
|
|
155
229
|
}
|
|
156
230
|
const serialized = query.toString();
|
|
157
|
-
return redactInternalPricing(await this.request(`/mcp/models${serialized ? `?${serialized}` : ""}
|
|
231
|
+
return redactInternalPricing(await this.request(`/mcp/models${serialized ? `?${serialized}` : ""}`, { headers: announcementHeaders(options) }));
|
|
158
232
|
}
|
|
159
233
|
// getModel 与 listModels 的 detail 参数依赖服务端新部署; 旧服务端会 404 或被
|
|
160
234
|
// strict 入参校验拒绝, 所以 npm 0.3.0 必须在服务端升级之后发布.
|
|
161
|
-
async getModel(publicModelId, routingMode) {
|
|
235
|
+
async getModel(publicModelId, routingMode, options) {
|
|
162
236
|
const query = routingMode
|
|
163
237
|
? `?${new URLSearchParams({ routingMode }).toString()}`
|
|
164
238
|
: "";
|
|
165
239
|
try {
|
|
166
|
-
return redactInternalPricing(await this.request(`/mcp/models/${encodeURIComponent(publicModelId)}${query}
|
|
240
|
+
return redactInternalPricing(await this.request(`/mcp/models/${encodeURIComponent(publicModelId)}${query}`, { headers: announcementHeaders(options) }));
|
|
167
241
|
}
|
|
168
242
|
catch (error) {
|
|
169
243
|
if (error instanceof AiHiveApiError && error.status === 404) {
|
|
@@ -172,37 +246,52 @@ export class AiHiveApiClient {
|
|
|
172
246
|
throw error;
|
|
173
247
|
}
|
|
174
248
|
}
|
|
175
|
-
createMediaUpload(input) {
|
|
249
|
+
createMediaUpload(input, options) {
|
|
176
250
|
return this.request("/mcp/media/upload-token", {
|
|
177
251
|
method: "POST",
|
|
178
252
|
body: JSON.stringify(input),
|
|
253
|
+
headers: announcementHeaders(options),
|
|
179
254
|
});
|
|
180
255
|
}
|
|
181
|
-
completeMediaUpload(mediaId) {
|
|
256
|
+
completeMediaUpload(mediaId, options) {
|
|
182
257
|
return this.request(`/mcp/media/${encodeURIComponent(mediaId)}/complete`, {
|
|
183
258
|
method: "POST",
|
|
259
|
+
headers: announcementHeaders(options),
|
|
184
260
|
});
|
|
185
261
|
}
|
|
186
|
-
chatText(input) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
262
|
+
async chatText(input, options) {
|
|
263
|
+
try {
|
|
264
|
+
return await this.request("/mcp/chat/text?include=user-notices", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
body: JSON.stringify(input),
|
|
267
|
+
headers: announcementHeaders(options),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (isTimeoutError(error)) {
|
|
272
|
+
throw new Error(PAID_TIMEOUT_MESSAGE);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
191
276
|
}
|
|
192
|
-
generateImage(input) {
|
|
193
|
-
return this.requestGeneration("/mcp/generation/image", input);
|
|
277
|
+
generateImage(input, options) {
|
|
278
|
+
return this.requestGeneration("/mcp/generation/image", input, options);
|
|
194
279
|
}
|
|
195
|
-
generateVideo(input) {
|
|
196
|
-
return this.requestGeneration("/mcp/generation/video", input);
|
|
280
|
+
generateVideo(input, options) {
|
|
281
|
+
return this.requestGeneration("/mcp/generation/video", input, options);
|
|
197
282
|
}
|
|
198
|
-
async requestGeneration(path, input) {
|
|
283
|
+
async requestGeneration(path, input, options) {
|
|
199
284
|
try {
|
|
200
|
-
return await this.request(path
|
|
285
|
+
return await this.request(`${path}?include=user-notices`, {
|
|
201
286
|
method: "POST",
|
|
202
287
|
body: JSON.stringify(input),
|
|
288
|
+
headers: announcementHeaders(options),
|
|
203
289
|
});
|
|
204
290
|
}
|
|
205
291
|
catch (error) {
|
|
292
|
+
if (isTimeoutError(error)) {
|
|
293
|
+
throw new Error(PAID_TIMEOUT_MESSAGE);
|
|
294
|
+
}
|
|
206
295
|
if (typeof input === "object" &&
|
|
207
296
|
input !== null &&
|
|
208
297
|
input.dryRun === true &&
|
|
@@ -214,8 +303,8 @@ export class AiHiveApiClient {
|
|
|
214
303
|
throw error;
|
|
215
304
|
}
|
|
216
305
|
}
|
|
217
|
-
getGenerationTask(input) {
|
|
218
|
-
return this.request(`/mcp/generation/tasks/${encodeURIComponent(input.taskId)}
|
|
306
|
+
getGenerationTask(input, options) {
|
|
307
|
+
return this.request(`/mcp/generation/tasks/${encodeURIComponent(input.taskId)}?include=user-notices`, { headers: announcementHeaders(options) });
|
|
219
308
|
}
|
|
220
309
|
async putUpload(input) {
|
|
221
310
|
await this.withTimeout(async (signal) => {
|
package/dist/cli.js
CHANGED
|
@@ -256,7 +256,10 @@ async function requestUserInfo(endpoint, token, deadlineAt) {
|
|
|
256
256
|
const timeoutMs = remainingMs(deadlineAt);
|
|
257
257
|
return withDeadline(fetch(`${endpoint.replace(/\/+$/, "")}/mcp/user-info`, {
|
|
258
258
|
method: "GET",
|
|
259
|
-
headers: {
|
|
259
|
+
headers: {
|
|
260
|
+
Authorization: `Bearer ${token}`,
|
|
261
|
+
"X-AI-Hive-Announcements": "0",
|
|
262
|
+
},
|
|
260
263
|
signal: controller.signal,
|
|
261
264
|
}), timeoutMs, () => controller.abort());
|
|
262
265
|
}
|
|
@@ -295,8 +298,10 @@ async function runMcp(config, dependencies) {
|
|
|
295
298
|
try {
|
|
296
299
|
const resolved = await resolveServerAuth(config, dependencies.createStore(config.credentialsFile));
|
|
297
300
|
await dependencies.runServer({
|
|
301
|
+
...(config.verbose ? { verbose: true } : {}),
|
|
298
302
|
...resolved,
|
|
299
303
|
timeoutMs: config.timeoutMs,
|
|
304
|
+
webOrigin: config.webOrigin,
|
|
300
305
|
});
|
|
301
306
|
return 0;
|
|
302
307
|
}
|
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
|
@@ -115,7 +115,7 @@ export async function uploadMediaFromPath(api, rawInput) {
|
|
|
115
115
|
filename,
|
|
116
116
|
contentType,
|
|
117
117
|
sizeBytes: stats.size,
|
|
118
|
-
}));
|
|
118
|
+
}, { announcements: false }));
|
|
119
119
|
const body = await openAsBlob(input.path, { type: contentType });
|
|
120
120
|
await api.putUpload({
|
|
121
121
|
url: upload.upload.url,
|
|
@@ -123,5 +123,5 @@ export async function uploadMediaFromPath(api, rawInput) {
|
|
|
123
123
|
headers: upload.upload.headers,
|
|
124
124
|
body,
|
|
125
125
|
});
|
|
126
|
-
return api.completeMediaUpload(upload.mediaId);
|
|
126
|
+
return api.completeMediaUpload(upload.mediaId, { announcements: true });
|
|
127
127
|
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
|
|
|
20
20
|
expiresAt: z.ZodString;
|
|
21
21
|
}, "strict", z.ZodTypeAny, {
|
|
22
22
|
mimeType: string;
|
|
23
|
+
expiresAt: string;
|
|
23
24
|
mediaId: string;
|
|
24
25
|
mediaType: "IMAGE" | "VIDEO" | "AUDIO" | "DOCUMENT";
|
|
25
26
|
sizeBytes: number;
|
|
@@ -28,9 +29,9 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
|
|
|
28
29
|
url: string;
|
|
29
30
|
headers: Record<string, string>;
|
|
30
31
|
};
|
|
31
|
-
expiresAt: string;
|
|
32
32
|
}, {
|
|
33
33
|
mimeType: string;
|
|
34
|
+
expiresAt: string;
|
|
34
35
|
mediaId: string;
|
|
35
36
|
mediaType: "IMAGE" | "VIDEO" | "AUDIO" | "DOCUMENT";
|
|
36
37
|
sizeBytes: number;
|
|
@@ -39,7 +40,6 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
|
|
|
39
40
|
url: string;
|
|
40
41
|
headers: Record<string, string>;
|
|
41
42
|
};
|
|
42
|
-
expiresAt: string;
|
|
43
43
|
}>;
|
|
44
44
|
export declare const mcpUploadMediaFromPathInputSchema: z.ZodObject<{
|
|
45
45
|
path: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -94,6 +94,8 @@ export declare const generateImageShape: {
|
|
|
94
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]>>;
|
|
95
95
|
};
|
|
96
96
|
export declare const generateVideoShape: {
|
|
97
|
+
materialMode: z.ZodOptional<z.ZodEnum<["standard", "certified"]>>;
|
|
98
|
+
materialQuoteId: z.ZodOptional<z.ZodString>;
|
|
97
99
|
publicModelId: z.ZodString;
|
|
98
100
|
routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
|
|
99
101
|
prompt: z.ZodEffects<z.ZodString, string, string>;
|
package/dist/schemas.js
CHANGED
|
@@ -100,6 +100,8 @@ export const generateImageShape = {
|
|
|
100
100
|
pricingSnapshot: pricingSnapshotInputSchema,
|
|
101
101
|
};
|
|
102
102
|
export const generateVideoShape = {
|
|
103
|
+
materialMode: z.enum(["standard", "certified"]).optional(),
|
|
104
|
+
materialQuoteId: z.string().min(1).optional(),
|
|
103
105
|
publicModelId: z.string().min(1),
|
|
104
106
|
routingMode: routingModeSchema,
|
|
105
107
|
prompt: nonBlankStringSchema,
|
package/dist/server.d.ts
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { AiHiveApiClient } from "./api-client.js";
|
|
4
5
|
import { type BearerTokenProvider } from "./auth-token-provider.js";
|
|
5
6
|
import type { McpClientConfig } from "./config.js";
|
|
6
7
|
import { WorkBuddyCredentialStore } from "./workbuddy-credentials.js";
|
|
7
|
-
export declare const SERVER_VERSION = "0.3.
|
|
8
|
+
export declare const SERVER_VERSION = "0.3.2";
|
|
8
9
|
declare function content(value: unknown): {
|
|
9
10
|
content: {
|
|
10
11
|
type: "text";
|
|
11
12
|
text: string;
|
|
12
13
|
}[];
|
|
14
|
+
structuredContent: Record<string, unknown>;
|
|
13
15
|
};
|
|
14
|
-
|
|
16
|
+
declare function userInfoContent(value: unknown): {
|
|
17
|
+
content: {
|
|
18
|
+
type: "text";
|
|
19
|
+
text: string;
|
|
20
|
+
}[];
|
|
21
|
+
structuredContent: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
type ApiClientLike = Pick<AiHiveApiClient, "getUserInfo" | "getMaterialMode" | "quoteVideoMaterials" | "listModels" | "getModel" | "createMediaUpload" | "completeMediaUpload" | "chatText" | "generateImage" | "generateVideo" | "getGenerationTask" | "putUpload">;
|
|
15
24
|
export type ToolDefinition = {
|
|
16
25
|
name: string;
|
|
26
|
+
title?: string;
|
|
17
27
|
description: string;
|
|
18
28
|
schema: z.ZodRawShape;
|
|
19
|
-
|
|
29
|
+
annotations?: ToolAnnotations;
|
|
30
|
+
handler: (args: Record<string, unknown>) => Promise<ReturnType<typeof content> | ReturnType<typeof userInfoContent>>;
|
|
20
31
|
};
|
|
21
32
|
export declare function createToolDefinitions(client: () => ApiClientLike): ToolDefinition[];
|
|
22
33
|
export type ResolvedServerAuth = {
|
|
@@ -24,14 +35,23 @@ export type ResolvedServerAuth = {
|
|
|
24
35
|
tokenProvider: BearerTokenProvider;
|
|
25
36
|
};
|
|
26
37
|
export declare function resolveServerAuth(config: McpClientConfig, store: WorkBuddyCredentialStore): Promise<ResolvedServerAuth>;
|
|
38
|
+
/**
|
|
39
|
+
* 与 @ai-hive/contracts 的 buildMcpServerInstructions 内容对应;
|
|
40
|
+
* mcp-client 独立发布不依赖 contracts, 修改时两边同步.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildServerInstructions(webOrigin?: string): string;
|
|
27
43
|
export declare function createServer(input: {
|
|
28
44
|
endpoint: string;
|
|
29
45
|
tokenProvider: BearerTokenProvider;
|
|
30
46
|
timeoutMs: number;
|
|
47
|
+
webOrigin?: string;
|
|
48
|
+
verbose?: boolean;
|
|
31
49
|
}): McpServer;
|
|
32
50
|
export declare function runServer(input: {
|
|
33
51
|
endpoint: string;
|
|
34
52
|
tokenProvider: BearerTokenProvider;
|
|
35
53
|
timeoutMs: number;
|
|
54
|
+
webOrigin?: string;
|
|
55
|
+
verbose?: boolean;
|
|
36
56
|
}): Promise<void>;
|
|
37
57
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
|
+
import { toolResultContent } from "./user-notices.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, getModelShape, listModelsShape, } from "./schemas.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
import { AiHiveApiClient } from "./api-client.js";
|
|
6
|
+
import { AiHiveApiClient, AiHiveApiError } from "./api-client.js";
|
|
6
7
|
import { normalizeApiEndpoint, StaticBearerTokenProvider, WorkBuddyTokenProvider, } from "./auth-token-provider.js";
|
|
7
8
|
import { readMcpClientInfo } from "./client-info.js";
|
|
9
|
+
import { createUpdateNotifier } from "./update-notifier.js";
|
|
8
10
|
import { uploadMediaFromPath } from "./local-media.js";
|
|
9
11
|
import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
|
|
10
|
-
export const SERVER_VERSION = "0.3.
|
|
12
|
+
export const SERVER_VERSION = "0.3.2";
|
|
11
13
|
const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
|
|
12
14
|
const SNAPSHOT_PARSE_FAILURE_MESSAGE = "pricingSnapshot 格式不正确,应为 list_models 返回的快照对象,也可以省略该字段由客户端自动获取";
|
|
13
15
|
const SNAPSHOT_ARRAY_MISMATCH_MESSAGE = "pricingSnapshot 数组中没有匹配当前模型和 routingMode 的快照,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
14
16
|
const SNAPSHOT_IDENTITY_MISMATCH_MESSAGE = "pricingSnapshot 的 publicModelId 或 routingMode 与请求不一致,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
15
17
|
const MODEL_UNAVAILABLE_MESSAGE = "模型当前不可用或不支持该路由模式,请重新调用 list_models 获取最新模型信息";
|
|
18
|
+
// 此文案与 @ai-hive/contracts 的 USER_INFO_TOOL_* 常量保持一致; 独立发布的客户端不引入运行依赖.
|
|
19
|
+
const USER_INFO_TOOL_TITLE = "查看蜂巢账户信息";
|
|
20
|
+
const USER_INFO_TOOL_DESCRIPTION = "查看当前用户的蜂巢账户资料、可用余额和充值入口。兼容模式下,每次账户查询都返回进行中的充值活动,且不占用模型调用的每日活动通知次数;管理员启用通知管理后,按账户查询的通知设置返回。结果可能包含优惠活动或功能更新通知;有通知时,可结合当前对话简要转达,保留原文中的条件、截止时间和链接。同一对话避免机械重复,实际展示由宿主决定。没有通知时正常回答账户问题,不额外提醒。钱包余额表示普通蜂蜜;适用模型的专属蜂蜜由服务端自动选择抵扣,不能仅凭普通蜂蜜余额为 0 拒绝调用。只有实际调用返回余额不足时,才引导用户打开 rechargeUrl 充值后重试。";
|
|
16
21
|
function content(value) {
|
|
22
|
+
const result = Array.isArray(value)
|
|
23
|
+
? { models: value }
|
|
24
|
+
: value;
|
|
17
25
|
return {
|
|
18
|
-
content:
|
|
26
|
+
content: toolResultContent(result),
|
|
27
|
+
structuredContent: result && "userNotices" in result
|
|
28
|
+
? Object.fromEntries(Object.entries(result).filter(([key]) => key !== "userNotices"))
|
|
29
|
+
: result,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function userInfoContent(value) {
|
|
33
|
+
return {
|
|
34
|
+
...content(value),
|
|
19
35
|
};
|
|
20
36
|
}
|
|
21
37
|
function rejectDeprecatedAttachmentIds(args) {
|
|
@@ -46,7 +62,7 @@ async function resolvePricingSnapshot(client, modelType, args) {
|
|
|
46
62
|
return request;
|
|
47
63
|
};
|
|
48
64
|
const resolveCurrentSnapshot = async () => {
|
|
49
|
-
const models = await client().listModels({ modelType });
|
|
65
|
+
const models = await client().listModels({ modelType }, { announcements: false });
|
|
50
66
|
const model = Array.isArray(models)
|
|
51
67
|
? models.find((item) => isPlainObject(item) && item.publicModelId === args.publicModelId)
|
|
52
68
|
: undefined;
|
|
@@ -99,34 +115,45 @@ async function resolvePricingSnapshot(client, modelType, args) {
|
|
|
99
115
|
throw new Error(SNAPSHOT_PARSE_FAILURE_MESSAGE);
|
|
100
116
|
}
|
|
101
117
|
// 幂等键语义写在每个付费工具的描述里, 让宿主在选型阶段就能看到重试规则.
|
|
102
|
-
const CLIENT_REQUEST_ID_NOTE = "
|
|
118
|
+
const CLIENT_REQUEST_ID_NOTE = "正式付费调用必须传入稳定 clientRequestId 幂等键(建议 UUID);仅传输中断、REQUEST_IN_PROGRESS 或刷新过期 pricingSnapshot 时复用原值和相同业务参数。余额、参数、媒体或已缓存的模型调用失败在修正后必须使用新的 clientRequestId;REQUEST_OUTCOME_UNKNOWN 必须停止自动重试并人工核查。";
|
|
119
|
+
// 长耗时提醒同样前置到工具描述: 宿主据此设置超时, 避免"本端超时先断、
|
|
120
|
+
// 服务端跑完仍扣费"被误读成失败扣款.
|
|
121
|
+
const LONG_RUNNING_NOTE = "该调用同步返回,长推理模型单次可能耗时 1-3 分钟,客户端超时应设为 300 秒以上;等待超时只代表本端不再等待,服务端可能仍在生成,也可能已完成并扣费,超时后复用同一 clientRequestId 和相同业务参数重新调用即可取回首次结果,修正参数后换新值。";
|
|
122
|
+
const CERTIFIED_VIDEO_NOTE = "认证版接收普通素材,服务端按模型 ID 自动校验资格和准备素材。提交前先用 dryRun=true 做只读预检,不用真实付费提交试探资格。materialMode、materialQuoteId 和专用报价工具均为可选兼容能力;正式提交使用当前 pricingSnapshot 和稳定 clientRequestId。资格不足时明确说明差额或待授权事项,并在回复中展示返回的 helpUrl 或 actionUrl 为可点击链接;未返回链接时使用 https://ai-hive.iclip.cn/certified-materials#guide。保留用户指定模型,不擅自切换模型或继续付费提交。费用变化时重新确认。收到任务 ID 后查询原任务,不重复创建。dryRun 不创建任务、不冻结素材、不预占或扣费,也不执行供应商内容审核。";
|
|
103
123
|
export function createToolDefinitions(client) {
|
|
104
|
-
|
|
124
|
+
const definitions = [
|
|
105
125
|
{
|
|
106
126
|
name: "get_user_info",
|
|
107
|
-
|
|
127
|
+
title: USER_INFO_TOOL_TITLE,
|
|
128
|
+
description: USER_INFO_TOOL_DESCRIPTION,
|
|
108
129
|
schema: {},
|
|
109
|
-
|
|
130
|
+
annotations: {
|
|
131
|
+
readOnlyHint: false,
|
|
132
|
+
destructiveHint: false,
|
|
133
|
+
openWorldHint: false,
|
|
134
|
+
},
|
|
135
|
+
handler: async () => userInfoContent(await client().getUserInfo()),
|
|
110
136
|
},
|
|
111
137
|
{
|
|
112
138
|
name: "list_models",
|
|
113
139
|
description: "按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格。detail 为 lite 时只返回各模型各路由的售价摘要(维度、单位、计费方式、内含数量及 saleMicros),用于快速选型;detail 为 full 时返回完整公开详情。saleMicros 是客户实际结算价,两种结果都不包含内部参考成本。",
|
|
114
140
|
schema: listModelsShape,
|
|
115
|
-
|
|
141
|
+
annotations: { readOnlyHint: false },
|
|
142
|
+
handler: async (args) => content(await client().listModels(args, { announcements: true })),
|
|
116
143
|
},
|
|
117
144
|
{
|
|
118
145
|
name: "get_model",
|
|
119
146
|
description: "按 publicModelId 查询单个模型的参数 schema、媒体要求、计费因素和示例;传 routingMode 可获取线路专属契约。",
|
|
120
147
|
schema: getModelShape,
|
|
148
|
+
annotations: { readOnlyHint: false },
|
|
121
149
|
handler: async (args) => {
|
|
122
150
|
const publicModelId = String(args.publicModelId);
|
|
123
|
-
return content(typeof args.routingMode === "string"
|
|
124
|
-
? await client().getModel(publicModelId, args.routingMode)
|
|
125
|
-
: await client().getModel(publicModelId));
|
|
151
|
+
return content(await client().getModel(publicModelId, typeof args.routingMode === "string" ? args.routingMode : undefined, { announcements: true }));
|
|
126
152
|
},
|
|
127
153
|
},
|
|
128
154
|
{
|
|
129
155
|
name: "upload_media_from_path",
|
|
156
|
+
annotations: { readOnlyHint: false },
|
|
130
157
|
description: "上传本地图片、视频、音频或文档并返回 mediaId。",
|
|
131
158
|
schema: {
|
|
132
159
|
path: z.string().min(1),
|
|
@@ -135,43 +162,106 @@ export function createToolDefinitions(client) {
|
|
|
135
162
|
},
|
|
136
163
|
handler: async (args) => content(await uploadMediaFromPath(client(), args)),
|
|
137
164
|
},
|
|
165
|
+
{
|
|
166
|
+
name: "create_media_upload",
|
|
167
|
+
description: "为媒体文件创建短期上传地址,按返回的 method 和 headers PUT 原始字节。",
|
|
168
|
+
schema: {
|
|
169
|
+
filename: z.string().min(1),
|
|
170
|
+
contentType: z.string().min(1),
|
|
171
|
+
sizeBytes: z.number().int().positive(),
|
|
172
|
+
},
|
|
173
|
+
annotations: { readOnlyHint: false },
|
|
174
|
+
handler: async (args) => content(await client().createMediaUpload(args, { announcements: true })),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "complete_media_upload",
|
|
178
|
+
description: "确认媒体上传完成,返回可用于模型调用的 mediaId。",
|
|
179
|
+
schema: { mediaId: z.string().min(1) },
|
|
180
|
+
annotations: { readOnlyHint: false },
|
|
181
|
+
handler: async (args) => content(await client().completeMediaUpload(String(args.mediaId), {
|
|
182
|
+
announcements: true,
|
|
183
|
+
})),
|
|
184
|
+
},
|
|
138
185
|
{
|
|
139
186
|
name: "chat_text",
|
|
140
|
-
description: `使用指定模型生成文本。${CLIENT_REQUEST_ID_NOTE}`,
|
|
187
|
+
description: `使用指定模型生成文本。${LONG_RUNNING_NOTE}${CLIENT_REQUEST_ID_NOTE}`,
|
|
141
188
|
schema: chatTextShape,
|
|
189
|
+
annotations: { readOnlyHint: false },
|
|
142
190
|
handler: async (args) => {
|
|
143
191
|
rejectDeprecatedAttachmentIds(args);
|
|
144
192
|
const body = await resolvePricingSnapshot(client, "TEXT", args);
|
|
145
|
-
return content(await client().chatText(body));
|
|
193
|
+
return content(await client().chatText(body, { announcements: true }));
|
|
146
194
|
},
|
|
147
195
|
},
|
|
148
196
|
{
|
|
149
197
|
name: "generate_image",
|
|
150
198
|
description: `创建图片生成任务;dryRun=true 时只校验参数、媒体和预估费用,不创建任务且可省略 pricingSnapshot 与 clientRequestId。${CLIENT_REQUEST_ID_NOTE}`,
|
|
151
199
|
schema: generateImageShape,
|
|
200
|
+
annotations: { readOnlyHint: false },
|
|
152
201
|
handler: async (args) => {
|
|
153
202
|
const body = await resolvePricingSnapshot(client, "IMAGE", args);
|
|
154
|
-
return content(await client().generateImage(body));
|
|
203
|
+
return content(await client().generateImage(body, { announcements: true }));
|
|
155
204
|
},
|
|
156
205
|
},
|
|
157
206
|
{
|
|
158
207
|
name: "generate_video",
|
|
159
|
-
description: `创建视频生成任务;dryRun=true
|
|
208
|
+
description: `创建视频生成任务;dryRun=true 时可省略 pricingSnapshot 与 clientRequestId。${CERTIFIED_VIDEO_NOTE}${CLIENT_REQUEST_ID_NOTE}`,
|
|
160
209
|
schema: generateVideoShape,
|
|
210
|
+
annotations: { readOnlyHint: false },
|
|
161
211
|
handler: async (args) => {
|
|
162
212
|
const body = await resolvePricingSnapshot(client, "VIDEO", args);
|
|
163
|
-
return content(await client().generateVideo(body));
|
|
213
|
+
return content(await client().generateVideo(body, { announcements: true }));
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "get_material_mode",
|
|
218
|
+
annotations: { readOnlyHint: false },
|
|
219
|
+
description: "查询认证版服务状态、授权与累计充值资格。已满足条件时直接继续;仅未授权时请用户本人打开 actionUrl 阅读并同意,不得代替用户授权。累计充值资格与钱包余额分别检查。",
|
|
220
|
+
schema: {},
|
|
221
|
+
handler: async () => content(await client().getMaterialMode({ announcements: true })),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "quote_video_materials",
|
|
225
|
+
annotations: { readOnlyHint: false },
|
|
226
|
+
description: "上传普通素材后,由调用端用与生成相同的模型、参数及媒体 ID 获取报价,包含参考图片、视频、首帧和尾帧。无需用户手动认证素材。向用户说明必要费用,费用或计费条件变化时重新确认。此工具可选,返回 id 可用作 materialQuoteId;未提供报价时服务端自动准备。显式报价过期时保留原素材重新获取。",
|
|
227
|
+
schema: {
|
|
228
|
+
publicModelId: generateVideoShape.publicModelId,
|
|
229
|
+
routingMode: generateVideoShape.routingMode,
|
|
230
|
+
prompt: generateVideoShape.prompt,
|
|
231
|
+
imageMediaIds: generateVideoShape.imageMediaIds,
|
|
232
|
+
videoMediaIds: generateVideoShape.videoMediaIds,
|
|
233
|
+
audioMediaIds: generateVideoShape.audioMediaIds,
|
|
234
|
+
firstFrameMediaId: generateVideoShape.firstFrameMediaId,
|
|
235
|
+
lastFrameMediaId: generateVideoShape.lastFrameMediaId,
|
|
236
|
+
pricingSnapshot: generateVideoShape.pricingSnapshot,
|
|
237
|
+
params: generateVideoShape.params,
|
|
238
|
+
},
|
|
239
|
+
handler: async (args) => {
|
|
240
|
+
const body = await resolvePricingSnapshot(client, "VIDEO", {
|
|
241
|
+
...args,
|
|
242
|
+
dryRun: true,
|
|
243
|
+
});
|
|
244
|
+
delete body.dryRun;
|
|
245
|
+
return content(await client().quoteVideoMaterials(body, { announcements: true }));
|
|
164
246
|
},
|
|
165
247
|
},
|
|
166
248
|
{
|
|
167
249
|
name: "get_generation_task",
|
|
168
250
|
description: "查询图片或视频任务状态和结果。",
|
|
169
251
|
schema: getGenerationTaskShape,
|
|
252
|
+
annotations: { readOnlyHint: false },
|
|
170
253
|
handler: async (args) => content(await client().getGenerationTask({
|
|
171
254
|
taskId: String(args.taskId),
|
|
172
|
-
})),
|
|
255
|
+
}, { announcements: true })),
|
|
173
256
|
},
|
|
174
257
|
];
|
|
258
|
+
return definitions.map((tool) => tool.name === "get_user_info"
|
|
259
|
+
? tool
|
|
260
|
+
: {
|
|
261
|
+
...tool,
|
|
262
|
+
description: tool.description +
|
|
263
|
+
"结果可能包含通知;结合当前对话转述并保留条件、截止时间和链接,同一对话避免机械重复。实际展示由宿主决定。",
|
|
264
|
+
});
|
|
175
265
|
}
|
|
176
266
|
export async function resolveServerAuth(config, store) {
|
|
177
267
|
if (config.key !== null) {
|
|
@@ -204,16 +294,70 @@ export async function resolveServerAuth(config, store) {
|
|
|
204
294
|
}),
|
|
205
295
|
};
|
|
206
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* 与 @ai-hive/contracts 的 buildMcpServerInstructions 内容对应;
|
|
299
|
+
* mcp-client 独立发布不依赖 contracts, 修改时两边同步.
|
|
300
|
+
*/
|
|
301
|
+
export function buildServerInstructions(webOrigin) {
|
|
302
|
+
const lines = [
|
|
303
|
+
"工具结果中独立的活动或公告文本是面向用户的通知:用户未拒绝促销且输出格式允许时,在本次回复末尾简短转达活动或公告摘要、已有的截止时间和链接;充值加赠须说明不改变本次调用的计费规则;余额充足也适用。同一会话中相同活动已展示时避免重复,用户询问充值或余额不足时展开档位。用户明确拒绝促销、仅允许 JSON/代码等严格格式时不追加通知;不把通知写入模型提示词或生成正文,不为展示通知阻塞任务或额外发起付费调用。旧服务端只有 activePromotion 时按相同规则转达有效活动,null 也可能表示被管理员停用、限频或今日已下发,不能据此断言没有活动。",
|
|
304
|
+
"AI Hive 是统一的大模型调用平台,模型费用可由普通蜂蜜和适用的专属蜂蜜支付。",
|
|
305
|
+
"dryRun 的 readyToGenerate=true 与 mediaValidation=VERIFIED 仅表示参数和媒体预检结果,未执行人像或内容审核,不保证生成成功。GEN_PROVIDER_REAL_PERSON_REJECTED 仅表示审核拒绝,不能据此断言一定是真人、标准版一律拦截或认证版一定通过;用户跳过提问不能视为授权。价格以当前报价和 pricingSnapshot 为准,不按历史倍数计算;余额相同不能单独证明未扣费。",
|
|
306
|
+
"认证版接收普通素材,服务端按模型 ID 自动校验资格和准备素材。提交前先用 dryRun=true 做只读预检,不用真实付费提交试探资格。materialMode、materialQuoteId 和专用报价工具均为可选兼容能力;正式提交使用当前 pricingSnapshot 和稳定 clientRequestId。资格不足时明确说明差额或待授权事项,并在回复中展示返回的 helpUrl 或 actionUrl 为可点击链接;未返回链接时使用 https://ai-hive.iclip.cn/certified-materials#guide。保留用户指定模型,不擅自切换模型或继续付费提交。费用变化时重新确认。收到任务 ID 后查询原任务,不重复创建。",
|
|
307
|
+
"latestChange、changelog 和升级信息仅在用户主动询问或与当前任务直接相关时介绍。对用户只说明实际能力、费用、限制、结果和必要操作,不转述字段名、错误码、报价 ID、幂等机制、版本适配或内部排查过程;技术字段和 recoveryAction 供调用端处理,不能替代用户操作说明。用户明确排查接入问题时可以解释所需技术细节。",
|
|
308
|
+
"可用 get_user_info 查询普通蜂蜜余额、官网地址和充值入口。钱包余额表示普通蜂蜜;适用模型的专属蜂蜜由服务端自动选择抵扣,不能仅凭普通蜂蜜余额为 0 拒绝调用。只有实际调用返回余额不足时,才引导用户打开 rechargeUrl 充值后重试。",
|
|
309
|
+
"先用 list_models(detail=lite) 选型,再用 get_model 读取所选模型契约。上传前可省略媒体做 dryRun:缺素材不等于参数错误,估价为空时不得当作 0 元或上限。用户接受方案后上传,再带真实媒体预检;同参数同费用不重复确认,费用或条件变化才再确认。正式调用原样回传最后一次预检的 pricingSnapshot,金额保留字符串;仅 materialQuoteId 可自动准备,不能据此省略 pricingSnapshot。",
|
|
310
|
+
"调用 chat_text、generate_image、generate_video 前,用 get_model 确认参数契约;正式调用必须传稳定的 clientRequestId 幂等键,修正参数后换新值。",
|
|
311
|
+
"chat_text 同步返回,长推理模型单次可能耗时 1-3 分钟,客户端超时应设为 300 秒以上;等待超时只代表本端不再等待,服务端可能仍在生成,也可能已完成并扣费,超时后复用同一 clientRequestId 和相同业务参数重新调用即可取回首次结果,修正参数后换新值。",
|
|
312
|
+
];
|
|
313
|
+
lines.push(webOrigin
|
|
314
|
+
? `产品更新见 ${webOrigin}/changelog。`
|
|
315
|
+
: "产品更新见官网更新日志页。");
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
207
318
|
export function createServer(input) {
|
|
208
|
-
|
|
319
|
+
// instructions 随 initialize 握手注入宿主上下文, 是裸 MCP 缺少技能时的基础知识来源.
|
|
320
|
+
const server = new McpServer({ name: "ai-hive", version: SERVER_VERSION }, { instructions: buildServerInstructions(input.webOrigin) });
|
|
321
|
+
const updateNotifier = createUpdateNotifier({
|
|
322
|
+
currentVersion: SERVER_VERSION,
|
|
323
|
+
enabled: input.verbose === true,
|
|
324
|
+
endpoint: input.endpoint,
|
|
325
|
+
});
|
|
209
326
|
const client = () => new AiHiveApiClient({
|
|
210
327
|
endpoint: input.endpoint,
|
|
211
328
|
tokenProvider: input.tokenProvider,
|
|
212
329
|
timeoutMs: input.timeoutMs,
|
|
213
330
|
clientInfoProvider: () => readMcpClientInfo(server),
|
|
331
|
+
updateNotifier,
|
|
214
332
|
});
|
|
215
333
|
for (const tool of createToolDefinitions(client)) {
|
|
216
|
-
|
|
334
|
+
const handler = async (args) => {
|
|
335
|
+
try {
|
|
336
|
+
return await tool.handler(args);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (error instanceof AiHiveApiError && error.details)
|
|
340
|
+
return {
|
|
341
|
+
isError: true,
|
|
342
|
+
structuredContent: error.details,
|
|
343
|
+
content: [
|
|
344
|
+
{ type: "text", text: JSON.stringify(error.details) },
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
if (tool.annotations) {
|
|
351
|
+
server.registerTool(tool.name, {
|
|
352
|
+
title: tool.title,
|
|
353
|
+
description: tool.description,
|
|
354
|
+
inputSchema: tool.schema,
|
|
355
|
+
annotations: tool.annotations,
|
|
356
|
+
}, handler);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
server.tool(tool.name, tool.description, tool.schema, handler);
|
|
360
|
+
}
|
|
217
361
|
}
|
|
218
362
|
return server;
|
|
219
363
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// 独立 npm 包不能依赖私有 workspace, 与 contracts 的通知文本通过跨包用例校验.
|
|
3
|
+
const noticeSchema = z.object({
|
|
4
|
+
id: z.string().min(1),
|
|
5
|
+
revision: z.string().min(1),
|
|
6
|
+
kind: z.enum(["RECHARGE_PROMOTION", "ANNOUNCEMENT"]),
|
|
7
|
+
title: z.string().min(1),
|
|
8
|
+
message: z.string().min(1),
|
|
9
|
+
details: z.string(),
|
|
10
|
+
expiresAt: z.string().datetime({ offset: true }).nullable(),
|
|
11
|
+
action: z
|
|
12
|
+
.object({
|
|
13
|
+
label: z.string().min(1),
|
|
14
|
+
url: z
|
|
15
|
+
.string()
|
|
16
|
+
.url()
|
|
17
|
+
.refine((url) => /^https?:\/\//.test(url)),
|
|
18
|
+
})
|
|
19
|
+
.optional(),
|
|
20
|
+
});
|
|
21
|
+
export function toolResultContent(value) {
|
|
22
|
+
if (!value ||
|
|
23
|
+
typeof value !== "object" ||
|
|
24
|
+
Array.isArray(value) ||
|
|
25
|
+
!("userNotices" in value)) {
|
|
26
|
+
return [{ type: "text", text: JSON.stringify(value) }];
|
|
27
|
+
}
|
|
28
|
+
const { userNotices, ...businessResult } = value;
|
|
29
|
+
const notices = Array.isArray(userNotices)
|
|
30
|
+
? userNotices.flatMap((raw) => {
|
|
31
|
+
const parsed = noticeSchema.safeParse(raw);
|
|
32
|
+
return parsed.success &&
|
|
33
|
+
(parsed.data.expiresAt === null ||
|
|
34
|
+
new Date(parsed.data.expiresAt).getTime() > Date.now())
|
|
35
|
+
? [parsed.data]
|
|
36
|
+
: [];
|
|
37
|
+
})
|
|
38
|
+
: [];
|
|
39
|
+
return [
|
|
40
|
+
{ type: "text", text: JSON.stringify(businessResult) },
|
|
41
|
+
...notices.map((notice) => ({
|
|
42
|
+
type: "text",
|
|
43
|
+
text: [
|
|
44
|
+
`${notice.title}:${notice.message}`,
|
|
45
|
+
...(notice.action
|
|
46
|
+
? [`${notice.action.label}:${notice.action.url}`]
|
|
47
|
+
: []),
|
|
48
|
+
...(notice.details ? [`充值档位:\n${notice.details}`] : []),
|
|
49
|
+
].join("\n"),
|
|
50
|
+
})),
|
|
51
|
+
];
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infimind-next/ai-hive-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
+
}
|