@infimind-next/ai-hive-mcp 0.3.3 → 0.4.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 +78 -12
- package/dist/api-client.d.ts +15 -0
- package/dist/api-client.js +89 -9
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +34 -0
- package/dist/config.d.ts +19 -1
- package/dist/config.js +104 -7
- package/dist/index.js +9 -1
- package/dist/managed-config.d.ts +75 -0
- package/dist/managed-config.js +71 -0
- package/dist/managed-download.d.ts +13 -0
- package/dist/managed-download.js +138 -0
- package/dist/managed-files.d.ts +35 -0
- package/dist/managed-files.js +240 -0
- package/dist/managed-images.d.ts +70 -0
- package/dist/managed-images.js +449 -0
- package/dist/managed-server.d.ts +15 -0
- package/dist/managed-server.js +195 -0
- package/dist/managed-token-provider.d.ts +11 -0
- package/dist/managed-token-provider.js +31 -0
- package/dist/rest-commands.d.ts +14 -0
- package/dist/rest-commands.js +164 -0
- package/dist/schemas.d.ts +118 -10
- package/dist/schemas.js +79 -0
- package/dist/server.d.ts +4 -3
- package/dist/server.js +116 -34
- package/dist/setup/detect.d.ts +9 -0
- package/dist/setup/detect.js +34 -0
- package/dist/setup/merge.d.ts +5 -0
- package/dist/setup/merge.js +142 -0
- package/dist/setup/run-setup.d.ts +20 -0
- package/dist/setup/run-setup.js +332 -0
- package/dist/setup/skills.d.ts +8 -0
- package/dist/setup/skills.js +35 -0
- package/dist/setup/targets.d.ts +14 -0
- package/dist/setup/targets.js +46 -0
- package/package.json +11 -9
- package/skills/ai-hive-billing/SKILL.md +33 -0
- package/skills/ai-hive-image-video-generation/SKILL.md +41 -0
- package/skills/ai-hive-media-upload/SKILL.md +33 -0
- package/skills/ai-hive-model-selection/SKILL.md +50 -0
- package/skills/ai-hive-text-generation/SKILL.md +39 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { link, lstat, open, readdir, realpath, unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { AiHiveApiError } from "./api-client.js";
|
|
7
|
+
import { downloadGeneratedImage } from "./managed-download.js";
|
|
8
|
+
import { ManagedDirectory, ManagedFiles } from "./managed-files.js";
|
|
9
|
+
import { generateImageShape, mcpCreateMediaUploadOutputSchema, } from "./schemas.js";
|
|
10
|
+
export const managedPrepareShape = {
|
|
11
|
+
publicModelId: generateImageShape.publicModelId,
|
|
12
|
+
routingMode: generateImageShape.routingMode,
|
|
13
|
+
prompt: generateImageShape.prompt,
|
|
14
|
+
batchSize: generateImageShape.batchSize,
|
|
15
|
+
imageMediaIds: generateImageShape.imageMediaIds,
|
|
16
|
+
params: generateImageShape.params,
|
|
17
|
+
};
|
|
18
|
+
const prepareSchema = z.object(managedPrepareShape).strict();
|
|
19
|
+
const pricingSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
publicModelId: z.string(),
|
|
22
|
+
routingMode: z.string(),
|
|
23
|
+
billingRuleSnapshot: z.record(z.unknown()),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
const formalSchema = prepareSchema
|
|
27
|
+
.extend({
|
|
28
|
+
clientRequestId: z.string().uuid(),
|
|
29
|
+
pricingSnapshot: pricingSchema,
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
const operationSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
version: z.literal(1),
|
|
35
|
+
userId: z.string(),
|
|
36
|
+
operationId: z.string().uuid(),
|
|
37
|
+
phase: z.enum(["PREPARED", "SUBMITTING", "UNKNOWN", "SUBMITTED", "FAILED"]),
|
|
38
|
+
request: formalSchema,
|
|
39
|
+
preflight: z.record(z.unknown()),
|
|
40
|
+
createdAt: z.string(),
|
|
41
|
+
updatedAt: z.string(),
|
|
42
|
+
taskId: z.string().optional(),
|
|
43
|
+
savedResults: z
|
|
44
|
+
.array(z.object({ itemId: z.string(), path: z.string() }).strict())
|
|
45
|
+
.default([]),
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
const saveSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
taskId: z.string().min(1),
|
|
51
|
+
itemId: z.string().min(1),
|
|
52
|
+
relativePath: z.string().min(1).optional(),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
const bindingSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
version: z.literal(1),
|
|
58
|
+
userId: z.string(),
|
|
59
|
+
projectRoot: z.string(),
|
|
60
|
+
apiBaseUrl: z.string(),
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
const TASK_ERROR = "这项结果不是当前账户的图片任务,请查询原图片任务";
|
|
64
|
+
const UNKNOWN_ERROR = "提交结果暂未确认,请恢复原任务,不要重新生成";
|
|
65
|
+
const STATE_ERROR = "图片任务记录不可用,请从 AI-HIVE 启动器检查原任务";
|
|
66
|
+
function object(value) {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
68
|
+
throw new Error("图片服务暂不可用,请稍后再试");
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
export class ManagedImages {
|
|
72
|
+
config;
|
|
73
|
+
client;
|
|
74
|
+
tokenProvider;
|
|
75
|
+
files;
|
|
76
|
+
state;
|
|
77
|
+
download;
|
|
78
|
+
constructor(config, client, tokenProvider, files, state, download) {
|
|
79
|
+
this.config = config;
|
|
80
|
+
this.client = client;
|
|
81
|
+
this.tokenProvider = tokenProvider;
|
|
82
|
+
this.files = files;
|
|
83
|
+
this.state = state;
|
|
84
|
+
this.download = download;
|
|
85
|
+
}
|
|
86
|
+
static async create(input) {
|
|
87
|
+
const files = await ManagedFiles.create(input.config);
|
|
88
|
+
const state = await ManagedDirectory.create(input.config.stateDirectory);
|
|
89
|
+
const binding = {
|
|
90
|
+
version: 1,
|
|
91
|
+
userId: input.config.userId,
|
|
92
|
+
projectRoot: await realpath(input.config.projectRoot),
|
|
93
|
+
apiBaseUrl: input.config.apiBaseUrl.replace(/\/+$/, ""),
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await state.writeAtomic("binding.json", Buffer.from(JSON.stringify(binding)));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!(error instanceof Error) ||
|
|
100
|
+
error.message !== "文件已存在,请换一个文件名")
|
|
101
|
+
throw error;
|
|
102
|
+
try {
|
|
103
|
+
const existing = bindingSchema.parse(JSON.parse((await state.read("binding.json")).toString()));
|
|
104
|
+
if (JSON.stringify(existing) !== JSON.stringify(binding))
|
|
105
|
+
throw new Error();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error("图片工具连接已变化,请从 AI-HIVE 启动器重新打开项目");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return new ManagedImages(input.config, input.client, input.tokenProvider, files, state, input.download ?? downloadGeneratedImage);
|
|
112
|
+
}
|
|
113
|
+
async getUserInfo() {
|
|
114
|
+
const result = object(await this.client.getUserInfo());
|
|
115
|
+
if (result.userId !== this.config.userId)
|
|
116
|
+
throw new Error("图片工具账户已变化,请回到 AI-HIVE 启动器重新登录");
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
async listModels(input) {
|
|
120
|
+
await this.getUserInfo();
|
|
121
|
+
const result = await this.client.listModels({ modelType: "IMAGE", ...input }, { announcements: true });
|
|
122
|
+
if (!Array.isArray(result))
|
|
123
|
+
throw new Error("图片模型暂不可用,请稍后再试");
|
|
124
|
+
return result.filter((model) => object(model).modelType === "IMAGE");
|
|
125
|
+
}
|
|
126
|
+
async getModel(publicModelId, routingMode) {
|
|
127
|
+
await this.getUserInfo();
|
|
128
|
+
const result = object(await this.client.getModel(publicModelId, routingMode, {
|
|
129
|
+
announcements: true,
|
|
130
|
+
}));
|
|
131
|
+
if (result.modelType !== "IMAGE")
|
|
132
|
+
throw new Error("请从图片模型列表选择模型");
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
async upload(path) {
|
|
136
|
+
await this.getUserInfo();
|
|
137
|
+
const image = await this.files.readImage(path);
|
|
138
|
+
const upload = mcpCreateMediaUploadOutputSchema.parse(await this.client.createMediaUpload({
|
|
139
|
+
filename: image.filename,
|
|
140
|
+
contentType: image.contentType,
|
|
141
|
+
sizeBytes: image.sizeBytes,
|
|
142
|
+
}, { announcements: false }));
|
|
143
|
+
if (upload.mediaType !== "IMAGE" ||
|
|
144
|
+
upload.mimeType !== image.contentType ||
|
|
145
|
+
upload.sizeBytes !== image.sizeBytes ||
|
|
146
|
+
new URL(upload.upload.url).protocol !== "https:" ||
|
|
147
|
+
Object.keys(upload.upload.headers).some((key) => /authorization|cookie/i.test(key))) {
|
|
148
|
+
throw new Error("图片上传暂不可用,请稍后重试");
|
|
149
|
+
}
|
|
150
|
+
await this.tokenProvider.getToken();
|
|
151
|
+
await this.client.putUpload({ ...upload.upload, body: image.body });
|
|
152
|
+
return this.client.completeMediaUpload(upload.mediaId, {
|
|
153
|
+
announcements: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async prepare(input) {
|
|
157
|
+
const request = prepareSchema.parse(input);
|
|
158
|
+
await this.getUserInfo();
|
|
159
|
+
const preflight = object(await this.client.generateImage({ ...request, dryRun: true }, { announcements: false }));
|
|
160
|
+
const snapshot = pricingSchema.parse(preflight.pricingSnapshot);
|
|
161
|
+
const resolvedParams = z
|
|
162
|
+
.record(z.unknown())
|
|
163
|
+
.safeParse(preflight.resolvedParams);
|
|
164
|
+
if (!resolvedParams.success)
|
|
165
|
+
throw new Error("图片预检暂不可用,请重新检查原方案");
|
|
166
|
+
if (snapshot.publicModelId !== request.publicModelId ||
|
|
167
|
+
snapshot.routingMode !== request.routingMode ||
|
|
168
|
+
preflight.mode !== "DRY_RUN" ||
|
|
169
|
+
preflight.wouldCreateTask !== false ||
|
|
170
|
+
(request.imageMediaIds.length > 0 &&
|
|
171
|
+
Object.prototype.hasOwnProperty.call(resolvedParams.data, "imageUrls")))
|
|
172
|
+
throw new Error("图片预检暂不可用,请重新检查原方案");
|
|
173
|
+
const operationId = randomUUID();
|
|
174
|
+
const timestamp = new Date().toISOString();
|
|
175
|
+
const operation = {
|
|
176
|
+
version: 1,
|
|
177
|
+
userId: this.config.userId,
|
|
178
|
+
operationId,
|
|
179
|
+
phase: "PREPARED",
|
|
180
|
+
request: {
|
|
181
|
+
...request,
|
|
182
|
+
// 模型默认值可能在确认后变化; 正式提交与恢复沿用已预检的非瞬态参数.
|
|
183
|
+
params: resolvedParams.data,
|
|
184
|
+
clientRequestId: operationId,
|
|
185
|
+
pricingSnapshot: snapshot,
|
|
186
|
+
},
|
|
187
|
+
preflight,
|
|
188
|
+
createdAt: timestamp,
|
|
189
|
+
updatedAt: timestamp,
|
|
190
|
+
savedResults: [],
|
|
191
|
+
};
|
|
192
|
+
await this.writeOperation(operation, false);
|
|
193
|
+
return { ...preflight, operationId, clientRequestId: operationId };
|
|
194
|
+
}
|
|
195
|
+
async generate(operationId) {
|
|
196
|
+
await this.getUserInfo();
|
|
197
|
+
return this.withOperationLock(operationId, async () => {
|
|
198
|
+
const operation = await this.readOperation(operationId);
|
|
199
|
+
if (operation.taskId)
|
|
200
|
+
return { ...object(await this.getTask(operation.taskId)), operationId };
|
|
201
|
+
if (operation.phase === "FAILED")
|
|
202
|
+
throw new Error("原任务未能提交,请确认原记录后重新预检");
|
|
203
|
+
if (operation.preflight.readyToGenerate !== true ||
|
|
204
|
+
operation.preflight.estimatedReservationMicros == null)
|
|
205
|
+
throw new Error("图片预检尚未完成,请补齐素材后重新检查");
|
|
206
|
+
await this.writeOperation({ ...operation, phase: "SUBMITTING" });
|
|
207
|
+
let result;
|
|
208
|
+
try {
|
|
209
|
+
result = object(await this.client.generateImage(operation.request, {
|
|
210
|
+
announcements: true,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const definitive = error instanceof AiHiveApiError &&
|
|
215
|
+
error.status !== undefined &&
|
|
216
|
+
error.status >= 400 &&
|
|
217
|
+
error.status < 500 &&
|
|
218
|
+
![401, 403, 408, 409, 429].includes(error.status);
|
|
219
|
+
await this.writeOperation({
|
|
220
|
+
...operation,
|
|
221
|
+
phase: definitive ? "FAILED" : "UNKNOWN",
|
|
222
|
+
});
|
|
223
|
+
if (definitive ||
|
|
224
|
+
(error instanceof AiHiveApiError &&
|
|
225
|
+
[401, 403].includes(error.status ?? 0)))
|
|
226
|
+
throw error;
|
|
227
|
+
throw new Error(UNKNOWN_ERROR);
|
|
228
|
+
}
|
|
229
|
+
if (typeof result.taskId !== "string" || result.taskType !== "IMAGE") {
|
|
230
|
+
await this.writeOperation({ ...operation, phase: "UNKNOWN" });
|
|
231
|
+
throw new Error(UNKNOWN_ERROR);
|
|
232
|
+
}
|
|
233
|
+
await this.writeOperation({
|
|
234
|
+
...operation,
|
|
235
|
+
phase: "SUBMITTED",
|
|
236
|
+
taskId: result.taskId,
|
|
237
|
+
});
|
|
238
|
+
return { ...result, operationId };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async recover(operationId) {
|
|
242
|
+
await this.getUserInfo();
|
|
243
|
+
const operation = await this.readOperation(operationId);
|
|
244
|
+
if (operation.phase === "PREPARED")
|
|
245
|
+
throw new Error("这项图片方案尚未提交,请确认方案和费用后生成");
|
|
246
|
+
return this.generate(operationId);
|
|
247
|
+
}
|
|
248
|
+
async getTask(taskId) {
|
|
249
|
+
await this.getUserInfo();
|
|
250
|
+
const task = object(await this.client.getGenerationTask({ taskId }, { announcements: true }));
|
|
251
|
+
if (task.taskType !== "IMAGE" || task.taskId !== taskId)
|
|
252
|
+
throw new Error(TASK_ERROR);
|
|
253
|
+
return task;
|
|
254
|
+
}
|
|
255
|
+
async list() {
|
|
256
|
+
await this.getUserInfo();
|
|
257
|
+
await this.state.assertUnchanged();
|
|
258
|
+
const names = await readdir(this.state.path);
|
|
259
|
+
const operations = await Promise.all(names
|
|
260
|
+
.filter((name) => /^operation-[0-9a-f-]{36}\.json$/.test(name))
|
|
261
|
+
.map((name) => this.readOperation(name.slice(10, -5))));
|
|
262
|
+
return operations
|
|
263
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
264
|
+
.map((operation) => ({
|
|
265
|
+
operationId: operation.operationId,
|
|
266
|
+
phase: operation.phase,
|
|
267
|
+
taskId: operation.taskId,
|
|
268
|
+
prompt: operation.request.prompt,
|
|
269
|
+
publicModelId: operation.request.publicModelId,
|
|
270
|
+
createdAt: operation.createdAt,
|
|
271
|
+
estimatedReservationMicros: operation.preflight.estimatedReservationMicros,
|
|
272
|
+
savedResults: operation.savedResults,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
async save(input) {
|
|
276
|
+
const { taskId, itemId, relativePath } = saveSchema.parse(input);
|
|
277
|
+
const task = await this.getTask(taskId);
|
|
278
|
+
const item = Array.isArray(task.items)
|
|
279
|
+
? task.items.find((item) => object(item).id === itemId)
|
|
280
|
+
: undefined;
|
|
281
|
+
const result = item ? object(item) : undefined;
|
|
282
|
+
if (!result ||
|
|
283
|
+
result.status !== "COMPLETED" ||
|
|
284
|
+
typeof result.resultUrl !== "string" ||
|
|
285
|
+
result.settlementPending === true)
|
|
286
|
+
throw new Error("图片结果尚不可用,请稍后查询原任务");
|
|
287
|
+
await this.tokenProvider.getToken();
|
|
288
|
+
const saved = await this.files.saveImage(await this.download(result.resultUrl), relativePath);
|
|
289
|
+
await this.recordSavedResult(taskId, itemId, saved.path);
|
|
290
|
+
return { taskId, itemId, ...saved };
|
|
291
|
+
}
|
|
292
|
+
async recordSavedResult(taskId, itemId, path) {
|
|
293
|
+
// 图片已确认落盘; 后续本地记录失败不能让调用方误以为保存失败而重试.
|
|
294
|
+
try {
|
|
295
|
+
await this.state.assertUnchanged();
|
|
296
|
+
const names = await readdir(this.state.path);
|
|
297
|
+
for (const name of names) {
|
|
298
|
+
if (!/^operation-[0-9a-f-]{36}\.json$/.test(name))
|
|
299
|
+
continue;
|
|
300
|
+
try {
|
|
301
|
+
const operation = await this.readOperation(name.slice(10, -5));
|
|
302
|
+
if (operation.taskId !== taskId)
|
|
303
|
+
continue;
|
|
304
|
+
await this.withOperationLock(operation.operationId, async () => {
|
|
305
|
+
const current = await this.readOperation(operation.operationId);
|
|
306
|
+
if (current.taskId !== taskId)
|
|
307
|
+
return;
|
|
308
|
+
await this.writeOperation({
|
|
309
|
+
...current,
|
|
310
|
+
savedResults: [...current.savedResults, { itemId, path }],
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// 跳过单条不可用记录, 保留原任务用于查询和恢复.
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// 记录目录暂不可用时仍返回已保存的路径, 不改变文件和任务状态.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async readOperation(operationId) {
|
|
324
|
+
try {
|
|
325
|
+
z.string().uuid().parse(operationId);
|
|
326
|
+
const operation = operationSchema.parse(JSON.parse((await this.state.read(`operation-${operationId}.json`)).toString()));
|
|
327
|
+
if (operation.userId !== this.config.userId ||
|
|
328
|
+
operation.operationId !== operationId ||
|
|
329
|
+
operation.request.clientRequestId !== operationId ||
|
|
330
|
+
JSON.stringify(operation.preflight.pricingSnapshot) !==
|
|
331
|
+
JSON.stringify(operation.request.pricingSnapshot))
|
|
332
|
+
throw new Error();
|
|
333
|
+
return operation;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
throw new Error(STATE_ERROR);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
writeOperation(operation, overwrite = true) {
|
|
340
|
+
return this.state.writeAtomic(`operation-${operation.operationId}.json`, Buffer.from(JSON.stringify({ ...operation, updatedAt: new Date().toISOString() })), overwrite);
|
|
341
|
+
}
|
|
342
|
+
async withOperationLock(operationId, action) {
|
|
343
|
+
z.string().uuid().parse(operationId);
|
|
344
|
+
await this.state.assertUnchanged();
|
|
345
|
+
const lockPath = join(this.state.path, `operation-${operationId}.lock`);
|
|
346
|
+
const temporaryPath = await this.state.writeAtomic(`.operation-${operationId}-${randomUUID()}.lock.tmp`, Buffer.from(JSON.stringify({ pid: process.pid })));
|
|
347
|
+
const temporary = await open(temporaryPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
348
|
+
const identity = await temporary.stat();
|
|
349
|
+
const matchesIdentity = (value) => value.isFile() &&
|
|
350
|
+
!value.isSymbolicLink() &&
|
|
351
|
+
value.dev === identity.dev &&
|
|
352
|
+
value.ino === identity.ino;
|
|
353
|
+
let ownsLock = false;
|
|
354
|
+
const removeOwned = async (path) => {
|
|
355
|
+
await this.state.assertUnchanged();
|
|
356
|
+
const current = await lstat(path).catch((error) => {
|
|
357
|
+
if (error &&
|
|
358
|
+
typeof error === "object" &&
|
|
359
|
+
"code" in error &&
|
|
360
|
+
error.code === "ENOENT")
|
|
361
|
+
return null;
|
|
362
|
+
throw error;
|
|
363
|
+
});
|
|
364
|
+
if (current && matchesIdentity(current))
|
|
365
|
+
await unlink(path);
|
|
366
|
+
};
|
|
367
|
+
const acquire = async () => {
|
|
368
|
+
await this.state.assertUnchanged();
|
|
369
|
+
if (!matchesIdentity(await lstat(temporaryPath)))
|
|
370
|
+
throw new Error(STATE_ERROR);
|
|
371
|
+
// 先写完整 owner, 再发布硬链接; 崩溃不会留下无法判断归属的空锁.
|
|
372
|
+
await link(temporaryPath, lockPath);
|
|
373
|
+
ownsLock = true;
|
|
374
|
+
await this.state.assertUnchanged();
|
|
375
|
+
if (!matchesIdentity(await lstat(lockPath)))
|
|
376
|
+
throw new Error(STATE_ERROR);
|
|
377
|
+
};
|
|
378
|
+
try {
|
|
379
|
+
try {
|
|
380
|
+
await acquire();
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (!(error &&
|
|
384
|
+
typeof error === "object" &&
|
|
385
|
+
"code" in error &&
|
|
386
|
+
error.code === "EEXIST"))
|
|
387
|
+
throw new Error(STATE_ERROR);
|
|
388
|
+
try {
|
|
389
|
+
const before = await lstat(lockPath);
|
|
390
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
391
|
+
throw new Error();
|
|
392
|
+
const stale = await open(lockPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
393
|
+
try {
|
|
394
|
+
const information = await stale.stat();
|
|
395
|
+
if (information.dev !== before.dev ||
|
|
396
|
+
information.ino !== before.ino ||
|
|
397
|
+
information.size > 4096)
|
|
398
|
+
throw new Error();
|
|
399
|
+
const value = JSON.parse((await stale.readFile()).toString());
|
|
400
|
+
if (typeof value.pid !== "number" ||
|
|
401
|
+
!Number.isSafeInteger(value.pid) ||
|
|
402
|
+
value.pid <= 0)
|
|
403
|
+
throw new Error();
|
|
404
|
+
let alive = true;
|
|
405
|
+
try {
|
|
406
|
+
process.kill(value.pid, 0);
|
|
407
|
+
}
|
|
408
|
+
catch (cause) {
|
|
409
|
+
if (cause &&
|
|
410
|
+
typeof cause === "object" &&
|
|
411
|
+
"code" in cause &&
|
|
412
|
+
cause.code === "ESRCH")
|
|
413
|
+
alive = false;
|
|
414
|
+
}
|
|
415
|
+
if (alive)
|
|
416
|
+
throw new Error();
|
|
417
|
+
await this.state.assertUnchanged();
|
|
418
|
+
const after = await lstat(lockPath);
|
|
419
|
+
if (!after.isFile() ||
|
|
420
|
+
after.isSymbolicLink() ||
|
|
421
|
+
before.ino !== after.ino ||
|
|
422
|
+
before.dev !== after.dev)
|
|
423
|
+
throw new Error();
|
|
424
|
+
await unlink(lockPath);
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
await stale.close();
|
|
428
|
+
}
|
|
429
|
+
await acquire();
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
throw new Error("原任务正在处理,请稍后查询原记录");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await removeOwned(temporaryPath);
|
|
436
|
+
return await action();
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
try {
|
|
440
|
+
if (ownsLock)
|
|
441
|
+
await removeOwned(lockPath);
|
|
442
|
+
await removeOwned(temporaryPath);
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
await temporary.close();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { BearerTokenProvider } from "./auth-token-provider.js";
|
|
3
|
+
import type { McpManagedConfig } from "./managed-config.js";
|
|
4
|
+
export declare function createManagedServer(input: {
|
|
5
|
+
config: McpManagedConfig;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
tokenProvider?: BearerTokenProvider;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
}): Promise<McpServer>;
|
|
10
|
+
export declare function registerManagedProcess(config: McpManagedConfig, managedConfigPath: string): Promise<() => Promise<void>>;
|
|
11
|
+
export declare function runManagedServer(input: {
|
|
12
|
+
config: McpManagedConfig;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
managedConfigPath: string;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { AiHiveApiClient, AiHiveApiError } from "./api-client.js";
|
|
7
|
+
import { ManagedDirectory } from "./managed-files.js";
|
|
8
|
+
import { ManagedImages, managedPrepareShape } from "./managed-images.js";
|
|
9
|
+
import { ManagedBearerTokenProvider } from "./managed-token-provider.js";
|
|
10
|
+
import { readMcpClientInfo } from "./client-info.js";
|
|
11
|
+
import { SERVER_VERSION } from "./server.js";
|
|
12
|
+
import { toolResultContent } from "./user-notices.js";
|
|
13
|
+
const INSTRUCTIONS = [
|
|
14
|
+
"AI-HIVE 图片工具仅用于当前蜂巢账户和当前项目。先选择图片模型并读取模型契约,再免费预检准备方案。",
|
|
15
|
+
"prepare_image_generation 返回的 operationId 代表已持久化的原方案和费用。用户确认方案和费用后再调用 generate_image;正式生成只传原 operationId,不更换请求编号或修改原方案。费用或素材变化时重新预检并取得用户确认。",
|
|
16
|
+
"预检仅校验参数和素材,不执行内容审核,也不保证生成成功。估价为空或 readyToGenerate=false 时补齐素材再预检。",
|
|
17
|
+
"提交结果未知时调用 recover_image_generation 恢复原请求;已收到 taskId 时仅查询原任务。FAILED 记录不自动重新付费;先核查原任务。停止工具或关闭启动器只停止本地等待,已受理的服务器任务可能继续生成和结算。",
|
|
18
|
+
"本地图片只能来自当前项目或启动器明确选择的单个图片。结果通过 save_generation_result(taskId,itemId,relativePath?) 保存,relativePath 仅为文件名,目录固定为项目的 ai-hive-images,已有文件不覆盖。不要传入下载 URL。",
|
|
19
|
+
"同账户重新登录后可查询 list_image_operations 恢复原记录。新登录会使旧连接失效;账户变化后返回启动器重新打开项目。",
|
|
20
|
+
"向用户说明结果、费用和下一步;不要展示密钥、内部诊断或重试机制。余额不足时使用账户结果的充值链接;不擅自切换模型或继续付费。",
|
|
21
|
+
].join("\n");
|
|
22
|
+
function resultContent(value) {
|
|
23
|
+
return {
|
|
24
|
+
content: toolResultContent(value),
|
|
25
|
+
structuredContent: Object.fromEntries(Object.entries(value).filter(([key]) => key !== "userNotices")),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function createManagedServer(input) {
|
|
29
|
+
const server = new McpServer({ name: "ai-hive-images", version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
|
|
30
|
+
const tokenProvider = input.tokenProvider ??
|
|
31
|
+
new ManagedBearerTokenProvider(input.config.credentialHelper);
|
|
32
|
+
const client = new AiHiveApiClient({
|
|
33
|
+
endpoint: input.config.apiBaseUrl,
|
|
34
|
+
clientKind: "codex-launcher",
|
|
35
|
+
tokenProvider,
|
|
36
|
+
timeoutMs: input.timeoutMs,
|
|
37
|
+
fetchImpl: input.fetchImpl,
|
|
38
|
+
clientInfoProvider: () => readMcpClientInfo(server),
|
|
39
|
+
});
|
|
40
|
+
const images = await ManagedImages.create({
|
|
41
|
+
config: input.config,
|
|
42
|
+
client,
|
|
43
|
+
tokenProvider,
|
|
44
|
+
});
|
|
45
|
+
const tools = [
|
|
46
|
+
{
|
|
47
|
+
name: "get_user_info",
|
|
48
|
+
description: "查看你的蜂巢账户资料、蜂蜜余额和充值入口。",
|
|
49
|
+
schema: {},
|
|
50
|
+
handler: () => images.getUserInfo(),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "list_models",
|
|
54
|
+
description: "查询可用图片模型和价格;detail=lite 用于选型,full 返回完整信息。",
|
|
55
|
+
schema: { detail: z.enum(["lite", "full"]).optional() },
|
|
56
|
+
handler: async (args) => ({
|
|
57
|
+
models: await images.listModels(args),
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "get_model",
|
|
62
|
+
description: "查询所选图片模型的参数、素材要求和费用。",
|
|
63
|
+
schema: {
|
|
64
|
+
publicModelId: z.string().min(1),
|
|
65
|
+
routingMode: z
|
|
66
|
+
.enum(["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"])
|
|
67
|
+
.optional(),
|
|
68
|
+
},
|
|
69
|
+
handler: (args) => images.getModel(String(args.publicModelId), args.routingMode),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "upload_media_from_path",
|
|
73
|
+
description: "上传项目或启动器明确选择的本地图片,返回 mediaId。仅接收绝对路径,按实际图片内容识别格式,最大 30MiB;同时受模型与服务端限制。",
|
|
74
|
+
schema: { path: z.string().min(1) },
|
|
75
|
+
handler: async (args) => (await images.upload(String(args.path))),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "prepare_image_generation",
|
|
79
|
+
description: "免费检查图片方案和已上传素材,返回预估费用及 operationId,不创建生成任务;保留原方案供用户确认后提交。",
|
|
80
|
+
schema: managedPrepareShape,
|
|
81
|
+
handler: (args) => images.prepare(args),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "generate_image",
|
|
85
|
+
description: "用户确认方案和费用后,按免费预检保存的 operationId 正式生成图片。仅接受原编号,不接收新参数。提交未知时恢复原请求,避免重复创建。",
|
|
86
|
+
schema: { operationId: z.string().uuid() },
|
|
87
|
+
handler: (args) => images.generate(String(args.operationId)),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "get_generation_task",
|
|
91
|
+
description: "查询当前账户的原图片任务。已获得 taskId 后查询结果,不重复生成。",
|
|
92
|
+
schema: { taskId: z.string().min(1) },
|
|
93
|
+
handler: (args) => images.getTask(String(args.taskId)),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "recover_image_generation",
|
|
97
|
+
description: "恢复提交结果未知的原图片请求,保持原方案、报价和幂等编号;已有 taskId 时只查询,不重新生成。",
|
|
98
|
+
schema: { operationId: z.string().uuid() },
|
|
99
|
+
handler: (args) => images.recover(String(args.operationId)),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "list_image_operations",
|
|
103
|
+
description: "列出当前账户和项目已保存的图片方案、提交状态、原任务和本地结果。",
|
|
104
|
+
schema: {},
|
|
105
|
+
handler: async () => ({ operations: await images.list() }),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "save_generation_result",
|
|
109
|
+
description: "查询原图片任务并将已完成的指定结果安全保存到项目 ai-hive-images。relativePath 只接收文件名,已有文件不覆盖;不接收 URL。",
|
|
110
|
+
schema: {
|
|
111
|
+
taskId: z.string().min(1),
|
|
112
|
+
itemId: z.string().min(1),
|
|
113
|
+
relativePath: z.string().min(1).optional(),
|
|
114
|
+
},
|
|
115
|
+
handler: (args) => images.save(args),
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
for (const tool of tools) {
|
|
119
|
+
server.registerTool(tool.name, {
|
|
120
|
+
description: tool.description,
|
|
121
|
+
inputSchema: z.object(tool.schema).strict(),
|
|
122
|
+
annotations: {
|
|
123
|
+
readOnlyHint: false,
|
|
124
|
+
destructiveHint: false,
|
|
125
|
+
openWorldHint: true,
|
|
126
|
+
},
|
|
127
|
+
}, async (args) => {
|
|
128
|
+
try {
|
|
129
|
+
return resultContent(await tool.handler(args));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof AiHiveApiError
|
|
133
|
+
? error.message
|
|
134
|
+
: error instanceof z.ZodError
|
|
135
|
+
? "图片方案暂不可用,请检查所选模型和素材"
|
|
136
|
+
: error instanceof Error &&
|
|
137
|
+
/^[\u4e00-\u9fff]/.test(error.message) &&
|
|
138
|
+
!/[\r\n]/.test(error.message)
|
|
139
|
+
? error.message
|
|
140
|
+
: "图片操作暂不可用,请稍后查询原任务";
|
|
141
|
+
return {
|
|
142
|
+
isError: true,
|
|
143
|
+
content: [{ type: "text", text: message }],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return server;
|
|
149
|
+
}
|
|
150
|
+
export async function registerManagedProcess(config, managedConfigPath) {
|
|
151
|
+
const state = await ManagedDirectory.create(config.stateDirectory);
|
|
152
|
+
const filename = `process-${process.pid}.json`;
|
|
153
|
+
await state.writeAtomic(filename, Buffer.from(JSON.stringify({
|
|
154
|
+
version: 1,
|
|
155
|
+
activationId: config.activationId,
|
|
156
|
+
userId: config.userId,
|
|
157
|
+
pid: process.pid,
|
|
158
|
+
startedAt: new Date().toISOString(),
|
|
159
|
+
managedConfigPath,
|
|
160
|
+
})), true);
|
|
161
|
+
return async () => {
|
|
162
|
+
await state.assertUnchanged();
|
|
163
|
+
await unlink(join(state.path, filename)).catch(() => { });
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export async function runManagedServer(input) {
|
|
167
|
+
const server = await createManagedServer(input);
|
|
168
|
+
const unregister = await registerManagedProcess(input.config, input.managedConfigPath);
|
|
169
|
+
let cleanupPromise;
|
|
170
|
+
// close 与 signal 回调可能同时开始清理; 两条路径都必须等待同一个删除操作.
|
|
171
|
+
const cleanup = () => {
|
|
172
|
+
cleanupPromise ??= unregister().catch(() => { });
|
|
173
|
+
return cleanupPromise;
|
|
174
|
+
};
|
|
175
|
+
const stop = () => {
|
|
176
|
+
void server.close().finally(async () => {
|
|
177
|
+
await cleanup();
|
|
178
|
+
process.exit(0);
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
process.once("SIGTERM", stop);
|
|
182
|
+
process.once("SIGINT", stop);
|
|
183
|
+
server.server.onclose = () => {
|
|
184
|
+
void cleanup();
|
|
185
|
+
process.removeListener("SIGTERM", stop);
|
|
186
|
+
process.removeListener("SIGINT", stop);
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
await server.connect(new StdioServerTransport());
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await cleanup();
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BearerTokenProvider } from "./auth-token-provider.js";
|
|
2
|
+
export declare const MANAGED_AUTH_ERROR = "\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u56DE\u5230 AI-HIVE \u542F\u52A8\u5668\u91CD\u65B0\u767B\u5F55";
|
|
3
|
+
export declare class ManagedBearerTokenProvider implements BearerTokenProvider {
|
|
4
|
+
private readonly helper;
|
|
5
|
+
constructor(helper: {
|
|
6
|
+
command: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
});
|
|
9
|
+
getToken(): Promise<string>;
|
|
10
|
+
refreshAfterUnauthorized(): Promise<string>;
|
|
11
|
+
}
|