@infimind-next/ai-hive-mcp 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -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,332 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, stat, unlink, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { detectTarget } from "./detect.js";
|
|
5
|
+
import { mergeJsonMcpServers, mergeOpenCodeServer, readJsonServerEnv, upsertCodexServer, } from "./merge.js";
|
|
6
|
+
import { readBundledSkills } from "./skills.js";
|
|
7
|
+
import { resolveTargets } from "./targets.js";
|
|
8
|
+
export const MCP_SERVER_NAME = "ai-hive";
|
|
9
|
+
export const MCP_CODEX_SERVER_NAME = "ai_hive";
|
|
10
|
+
export const MCP_PACKAGE_SPEC = "@infimind-next/ai-hive-mcp@latest";
|
|
11
|
+
export async function runSetup(config, dependencies) {
|
|
12
|
+
const options = config.setup ?? {
|
|
13
|
+
clients: null,
|
|
14
|
+
dryRun: false,
|
|
15
|
+
skipSkills: false,
|
|
16
|
+
};
|
|
17
|
+
const key = config.key;
|
|
18
|
+
// 无密钥又无本机登录凭证时, 写进客户端的配置无法通过鉴权; 此时成功退出等于假成功.
|
|
19
|
+
const hasStoredCredentials = key === null && (await readOptionalFile(config.credentialsFile)) !== null;
|
|
20
|
+
if (key === null && !hasStoredCredentials) {
|
|
21
|
+
dependencies.stderr("缺少 MCP 密钥。请在控制台创建后设置 AI_HIVE_MCP_KEY,或先运行 ai-hive-mcp auth 登录。");
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
if (key !== null && !/^[A-Za-z0-9._~-]+$/.test(key)) {
|
|
25
|
+
dependencies.stderr("MCP 密钥格式不正确,请重新复制完整密钥。");
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const targets = resolveTargets(dependencies);
|
|
29
|
+
const requested = options.clients
|
|
30
|
+
? new Set(options.clients)
|
|
31
|
+
: null;
|
|
32
|
+
const candidates = requested
|
|
33
|
+
? targets.filter((target) => requested.has(target.id))
|
|
34
|
+
: targets;
|
|
35
|
+
const detected = requested !== null ? candidates : await detectAll(candidates, dependencies);
|
|
36
|
+
if (detected.length === 0) {
|
|
37
|
+
dependencies.stderr("未检测到支持的客户端。请先安装 Claude Code、Cursor、Codex、Claude Desktop 或 OpenCode;检测不到时可以用 --client 指定。");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
dependencies.stdout(`检测到:${detected.map((target) => target.label).join("、")}`);
|
|
41
|
+
const env = buildServerEnv(config, key);
|
|
42
|
+
const skills = options.skipSkills
|
|
43
|
+
? []
|
|
44
|
+
: await readSkillsSafely(dependencies.skillsRoot, dependencies);
|
|
45
|
+
let configured = 0;
|
|
46
|
+
const errors = [];
|
|
47
|
+
for (const target of detected) {
|
|
48
|
+
const result = await configureTarget({
|
|
49
|
+
target,
|
|
50
|
+
env,
|
|
51
|
+
dryRun: options.dryRun,
|
|
52
|
+
dependencies,
|
|
53
|
+
});
|
|
54
|
+
for (const line of result.lines)
|
|
55
|
+
dependencies.stdout(line);
|
|
56
|
+
for (const error of result.errors) {
|
|
57
|
+
dependencies.stderr(error);
|
|
58
|
+
errors.push(error);
|
|
59
|
+
}
|
|
60
|
+
if (result.configured)
|
|
61
|
+
configured += 1;
|
|
62
|
+
if (result.configured && target.skillsDir && skills.length > 0) {
|
|
63
|
+
const skillErrors = await installSkills({
|
|
64
|
+
directory: target.skillsDir,
|
|
65
|
+
skills,
|
|
66
|
+
dryRun: options.dryRun,
|
|
67
|
+
label: target.label,
|
|
68
|
+
dependencies,
|
|
69
|
+
});
|
|
70
|
+
for (const error of skillErrors) {
|
|
71
|
+
dependencies.stderr(error);
|
|
72
|
+
errors.push(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (configured === 0) {
|
|
77
|
+
dependencies.stderr("没有写入任何配置,请检查上面的提示后重试。");
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (options.dryRun) {
|
|
81
|
+
dependencies.stdout("仅预览,未写入任何文件。去掉 --dry-run 后执行。");
|
|
82
|
+
return errors.length > 0 ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
if (!key) {
|
|
85
|
+
dependencies.stdout("未提供 MCP 密钥:客户端会使用本机已有的登录凭证。需要重新登录时先运行 ai-hive-mcp auth。");
|
|
86
|
+
return errors.length > 0 ? 1 : 0;
|
|
87
|
+
}
|
|
88
|
+
const verified = await verifyConnection(config.endpoint, key, config.timeoutMs, dependencies);
|
|
89
|
+
if (!verified) {
|
|
90
|
+
dependencies.stderr("配置已写入,但连接验证失败。请确认 MCP 密钥有效,或重新运行 setup。");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
dependencies.stdout("连接验证通过。重启客户端后即可调用 list_models。");
|
|
94
|
+
return errors.length > 0 ? 1 : 0;
|
|
95
|
+
}
|
|
96
|
+
async function readSkillsSafely(skillsRoot, dependencies) {
|
|
97
|
+
try {
|
|
98
|
+
return await readBundledSkills(skillsRoot);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// 老版本发布物或从源码运行时可能没有 skills 目录; 不因此阻断 MCP 配置写入.
|
|
102
|
+
dependencies.stderr("未找到随包技能,跳过技能安装。");
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function detectAll(targets, dependencies) {
|
|
107
|
+
const detected = [];
|
|
108
|
+
for (const target of targets) {
|
|
109
|
+
if (await detectTarget(target, {
|
|
110
|
+
home: dependencies.home,
|
|
111
|
+
platform: dependencies.platform,
|
|
112
|
+
env: dependencies.env,
|
|
113
|
+
exists: async (path) => {
|
|
114
|
+
try {
|
|
115
|
+
await stat(path);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
})) {
|
|
123
|
+
detected.push(target);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return detected;
|
|
127
|
+
}
|
|
128
|
+
function buildServerEnv(config, key) {
|
|
129
|
+
return {
|
|
130
|
+
...(key ? { AI_HIVE_MCP_KEY: key } : {}),
|
|
131
|
+
...(config.endpointExplicit
|
|
132
|
+
? { AI_HIVE_API_ENDPOINT: config.endpoint }
|
|
133
|
+
: {}),
|
|
134
|
+
...(config.webOriginExplicit
|
|
135
|
+
? { AI_HIVE_WEB_ORIGIN: config.webOrigin }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function configureTarget(input) {
|
|
140
|
+
const { target, env, dryRun, dependencies } = input;
|
|
141
|
+
if (target.format === "claude-cli") {
|
|
142
|
+
return configureClaudeCode({ env, dryRun, dependencies });
|
|
143
|
+
}
|
|
144
|
+
const configPath = target.configPath;
|
|
145
|
+
if (!configPath) {
|
|
146
|
+
return {
|
|
147
|
+
configured: false,
|
|
148
|
+
lines: [],
|
|
149
|
+
errors: [`${target.label}:没有可写入的配置路径`],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const existing = await readOptionalFile(configPath);
|
|
154
|
+
const next = renderConfig(target, existing, env);
|
|
155
|
+
if (next === existing) {
|
|
156
|
+
return {
|
|
157
|
+
configured: true,
|
|
158
|
+
lines: [`${target.label}:配置已是最新,跳过`],
|
|
159
|
+
errors: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!dryRun) {
|
|
163
|
+
await writePrivateFile(configPath, next);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
configured: true,
|
|
167
|
+
lines: [`${target.label}:${dryRun ? "将写入" : "已写入"} ${configPath}`],
|
|
168
|
+
errors: [],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
configured: false,
|
|
174
|
+
lines: [],
|
|
175
|
+
errors: [
|
|
176
|
+
`${target.label}:${error instanceof Error ? error.message : "写入配置失败"}`,
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function renderConfig(target, existing, env) {
|
|
182
|
+
if (target.format === "json-mcpServers") {
|
|
183
|
+
const mergedEnv = {
|
|
184
|
+
...readJsonServerEnv(existing, "mcpServers", MCP_SERVER_NAME),
|
|
185
|
+
...env,
|
|
186
|
+
};
|
|
187
|
+
return mergeJsonMcpServers(existing, MCP_SERVER_NAME, {
|
|
188
|
+
command: "npx",
|
|
189
|
+
args: ["-y", MCP_PACKAGE_SPEC],
|
|
190
|
+
...(Object.keys(mergedEnv).length > 0 ? { env: mergedEnv } : {}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (target.format === "json-opencode") {
|
|
194
|
+
const mergedEnv = {
|
|
195
|
+
...readJsonServerEnv(existing, "mcp", MCP_SERVER_NAME),
|
|
196
|
+
...env,
|
|
197
|
+
};
|
|
198
|
+
return mergeOpenCodeServer(existing, MCP_SERVER_NAME, {
|
|
199
|
+
type: "local",
|
|
200
|
+
command: ["npx", "-y", MCP_PACKAGE_SPEC],
|
|
201
|
+
enabled: true,
|
|
202
|
+
...(Object.keys(mergedEnv).length > 0 ? { environment: mergedEnv } : {}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return upsertCodexServer(existing, MCP_CODEX_SERVER_NAME, MCP_PACKAGE_SPEC, env);
|
|
206
|
+
}
|
|
207
|
+
// 临时文件 + rename 保证中断时不会截断用户配置; chmod 兜住已存在文件的 0644.
|
|
208
|
+
async function writePrivateFile(path, content) {
|
|
209
|
+
const directory = dirname(path);
|
|
210
|
+
await mkdir(directory, { recursive: true });
|
|
211
|
+
const temp = join(directory, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
212
|
+
await writeFile(temp, content, { mode: 0o600 });
|
|
213
|
+
try {
|
|
214
|
+
await rename(temp, path);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
await unlink(temp).catch(() => undefined);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (process.platform !== "win32") {
|
|
221
|
+
await chmod(path, 0o600);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function configureClaudeCode(input) {
|
|
225
|
+
const args = [
|
|
226
|
+
"mcp",
|
|
227
|
+
"add",
|
|
228
|
+
MCP_SERVER_NAME,
|
|
229
|
+
"--scope",
|
|
230
|
+
"user",
|
|
231
|
+
...Object.entries(input.env).flatMap(([name, value]) => [
|
|
232
|
+
"-e",
|
|
233
|
+
`${name}=${value}`,
|
|
234
|
+
]),
|
|
235
|
+
"--",
|
|
236
|
+
"npx",
|
|
237
|
+
"-y",
|
|
238
|
+
MCP_PACKAGE_SPEC,
|
|
239
|
+
];
|
|
240
|
+
if (input.dryRun) {
|
|
241
|
+
return {
|
|
242
|
+
configured: true,
|
|
243
|
+
lines: [`Claude Code:将运行 ${maskedCommand(args)}`],
|
|
244
|
+
errors: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// 重复执行时 claude 会因为同名服务器报错; 先移除再添加, 保证 setup 可重复运行.
|
|
248
|
+
const runner = input.dependencies.runCommand ?? defaultRunner;
|
|
249
|
+
await runner("claude", ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
250
|
+
const result = await runner("claude", args);
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
return {
|
|
253
|
+
configured: false,
|
|
254
|
+
lines: [],
|
|
255
|
+
errors: [
|
|
256
|
+
"Claude Code:自动写入失败,请运行 claude mcp add ai-hive --scope user -e AI_HIVE_MCP_KEY=<你的 MCP 密钥> -- npx -y " +
|
|
257
|
+
MCP_PACKAGE_SPEC +
|
|
258
|
+
" 手动添加",
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
configured: true,
|
|
264
|
+
lines: ["Claude Code:已写入 MCP 配置"],
|
|
265
|
+
errors: [],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function maskedCommand(args) {
|
|
269
|
+
return ["claude", ...args]
|
|
270
|
+
.map((part) => part.startsWith("AI_HIVE_MCP_KEY=") ? "AI_HIVE_MCP_KEY=***" : part)
|
|
271
|
+
.join(" ");
|
|
272
|
+
}
|
|
273
|
+
async function defaultRunner(command, args) {
|
|
274
|
+
const result = spawnSync(command, args, {
|
|
275
|
+
stdio: "ignore",
|
|
276
|
+
// Windows 上 npm 安装的是 claude.cmd, Node 必须经 shell 才能执行; 参数只含常量和已校验的密钥.
|
|
277
|
+
shell: process.platform === "win32",
|
|
278
|
+
timeout: 30_000,
|
|
279
|
+
});
|
|
280
|
+
return { ok: result.status === 0 };
|
|
281
|
+
}
|
|
282
|
+
async function installSkills(input) {
|
|
283
|
+
const errors = [];
|
|
284
|
+
let changed = 0;
|
|
285
|
+
for (const skill of input.skills) {
|
|
286
|
+
for (const file of skill.files) {
|
|
287
|
+
const path = join(input.directory, skill.name, file.relativePath);
|
|
288
|
+
try {
|
|
289
|
+
const existing = await readOptionalFile(path);
|
|
290
|
+
if (existing === file.content)
|
|
291
|
+
continue;
|
|
292
|
+
if (!input.dryRun) {
|
|
293
|
+
await mkdir(dirname(path), { recursive: true });
|
|
294
|
+
await writeFile(path, file.content, "utf8");
|
|
295
|
+
}
|
|
296
|
+
changed += 1;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
errors.push(`${input.label}:安装技能 ${skill.name} 失败`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (errors.length === 0) {
|
|
304
|
+
input.dependencies.stdout(`${input.label}:${input.dryRun ? "将安装" : "已安装"} ${input.skills.length} 个技能(${changed} 个文件有更新)到 ${input.directory}`);
|
|
305
|
+
}
|
|
306
|
+
return errors;
|
|
307
|
+
}
|
|
308
|
+
async function verifyConnection(endpoint, key, timeoutMs, dependencies) {
|
|
309
|
+
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetchImpl(`${endpoint.replace(/\/+$/, "")}/mcp/user-info`, {
|
|
312
|
+
headers: {
|
|
313
|
+
Authorization: `Bearer ${key}`,
|
|
314
|
+
"X-AI-Hive-Announcements": "0",
|
|
315
|
+
},
|
|
316
|
+
signal: AbortSignal.timeout(Math.min(timeoutMs, 15_000)),
|
|
317
|
+
});
|
|
318
|
+
await response.body?.cancel();
|
|
319
|
+
return response.ok;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function readOptionalFile(path) {
|
|
326
|
+
try {
|
|
327
|
+
return await readFile(path, "utf8");
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
// 只收 Markdown; 技能里不放二进制, 保证包体和逐文件写入都可预期.
|
|
4
|
+
export async function readBundledSkills(skillsRoot) {
|
|
5
|
+
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
6
|
+
const skills = [];
|
|
7
|
+
for (const entry of entries) {
|
|
8
|
+
if (!entry.isDirectory())
|
|
9
|
+
continue;
|
|
10
|
+
const directory = join(skillsRoot, entry.name);
|
|
11
|
+
const files = await readMarkdownFiles(directory);
|
|
12
|
+
if (!files.some((file) => file.relativePath === "SKILL.md"))
|
|
13
|
+
continue;
|
|
14
|
+
skills.push({ name: entry.name, files });
|
|
15
|
+
}
|
|
16
|
+
return skills.sort((left, right) => left.name.localeCompare(right.name));
|
|
17
|
+
}
|
|
18
|
+
async function readMarkdownFiles(root) {
|
|
19
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const path = join(root, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
files.push(...(await readMarkdownFiles(path)));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!entry.name.endsWith(".md"))
|
|
28
|
+
continue;
|
|
29
|
+
files.push({
|
|
30
|
+
relativePath: relative(root, path),
|
|
31
|
+
content: await readFile(path, "utf8"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SetupClientId } from "../config.js";
|
|
2
|
+
export type ClientFormat = "claude-cli" | "json-mcpServers" | "toml-mcp-servers" | "json-opencode";
|
|
3
|
+
export type ClientTarget = {
|
|
4
|
+
id: SetupClientId;
|
|
5
|
+
label: string;
|
|
6
|
+
format: ClientFormat;
|
|
7
|
+
configPath: string | null;
|
|
8
|
+
skillsDir: string | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function resolveTargets(input: {
|
|
11
|
+
home: string;
|
|
12
|
+
platform: NodeJS.Platform;
|
|
13
|
+
env: Record<string, string | undefined>;
|
|
14
|
+
}): ClientTarget[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
export function resolveTargets(input) {
|
|
3
|
+
const { home, platform, env } = input;
|
|
4
|
+
const claudeDesktopConfig = platform === "darwin"
|
|
5
|
+
? join(home, "Library/Application Support/Claude/claude_desktop_config.json")
|
|
6
|
+
: platform === "win32"
|
|
7
|
+
? join(env.APPDATA ?? join(home, "AppData/Roaming"), "Claude/claude_desktop_config.json")
|
|
8
|
+
: join(home, ".config/Claude/claude_desktop_config.json");
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
id: "claude-code",
|
|
12
|
+
label: "Claude Code",
|
|
13
|
+
format: "claude-cli",
|
|
14
|
+
configPath: null,
|
|
15
|
+
skillsDir: join(home, ".claude/skills"),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "cursor",
|
|
19
|
+
label: "Cursor",
|
|
20
|
+
format: "json-mcpServers",
|
|
21
|
+
configPath: join(home, ".cursor/mcp.json"),
|
|
22
|
+
skillsDir: null,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "codex",
|
|
26
|
+
label: "Codex",
|
|
27
|
+
format: "toml-mcp-servers",
|
|
28
|
+
configPath: join(home, ".codex/config.toml"),
|
|
29
|
+
skillsDir: join(home, ".codex/skills"),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "claude-desktop",
|
|
33
|
+
label: "Claude Desktop",
|
|
34
|
+
format: "json-mcpServers",
|
|
35
|
+
configPath: claudeDesktopConfig,
|
|
36
|
+
skillsDir: null,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "opencode",
|
|
40
|
+
label: "OpenCode",
|
|
41
|
+
format: "json-opencode",
|
|
42
|
+
configPath: join(home, ".config/opencode/opencode.json"),
|
|
43
|
+
skillsDir: null,
|
|
44
|
+
},
|
|
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.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
"ai-hive-mcp": "dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"dist"
|
|
13
|
+
"dist",
|
|
14
|
+
"skills"
|
|
14
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
19
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
20
|
+
"prepack": "pnpm run build",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
15
23
|
"dependencies": {
|
|
16
24
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
17
25
|
"file-type": "^21.3.4",
|
|
@@ -22,11 +30,5 @@
|
|
|
22
30
|
"typescript": "^5.0.0",
|
|
23
31
|
"typescript-eslint": "^8.0.0",
|
|
24
32
|
"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"
|
|
31
33
|
}
|
|
32
|
-
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-hive-billing
|
|
3
|
+
description: 查询蜂巢账户的积分、消费和账单明细。用户问"还剩多少""这次花了多少""今天用了多少""为什么扣费"时使用。只读,不会产生费用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 查积分与账单
|
|
7
|
+
|
|
8
|
+
## 命令
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
ai-hive-mcp usage --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
如果返回"当前服务端暂不支持账单查询",直接引导用户去控制台查看积分与消费,不要重试或改用其他工具猜测金额。
|
|
15
|
+
|
|
16
|
+
MCP 工具:`get_billing_summary`、`list_billing_records`、`get_billing_record`。
|
|
17
|
+
|
|
18
|
+
## 回答用户时
|
|
19
|
+
|
|
20
|
+
1. 积分和消费统一换算成蜂蜜(微单位除以 1,000,000),最多保留 6 位小数。只有充值实付金额才是元。
|
|
21
|
+
2. 说明时间范围。不传时间时默认是北京时间今日,跨天要显式传 `from` / `to`。
|
|
22
|
+
3. 用户问单次调用时,用 `list_billing_records` 按 `publicModelId` 或任务 ID 过滤,再用 `get_billing_record` 看明细。
|
|
23
|
+
4. 积分不足时给出充值入口,不要把积分不足说成调用失败。
|
|
24
|
+
|
|
25
|
+
## 注意
|
|
26
|
+
|
|
27
|
+
- 账单记录包含结算、预占、释放三类;预占和释放不算消费,解释时区分清楚。
|
|
28
|
+
- 不要暴露内部成本价、上游供应商名称或渠道信息。
|
|
29
|
+
- 不要根据本地估算回答消费问题,一律以接口返回为准。
|
|
30
|
+
|
|
31
|
+
## 账户信息
|
|
32
|
+
|
|
33
|
+
需要账户资料或充值入口时用 `get_user_info`。该工具同时返回当前活动,有活动时可以把活动信息转达给用户。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-hive-image-video-generation
|
|
3
|
+
description: 用蜂巢生成图片或视频。用户要出图、图生图、文生视频、图生视频或首尾帧视频时使用。必须先 dryRun 预检报价,确认后再正式生成,并轮询任务直到完成或失败。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 生成图片与视频
|
|
7
|
+
|
|
8
|
+
## 强制流程
|
|
9
|
+
|
|
10
|
+
1. 选型:见 `ai-hive-model-selection`。
|
|
11
|
+
2. 预检:`generate_image` 或 `generate_video`,传 `dryRun: true`。
|
|
12
|
+
3. 把预检返回的预计费用告诉用户。费用超出用户预期时先确认,不要直接正式生成。
|
|
13
|
+
4. 正式生成:同一组参数、同一个 `clientRequestId`,去掉 `dryRun`。
|
|
14
|
+
5. 轮询:用 `get_generation_task` 查 `taskId`,看 `items[].status`,直到 `COMPLETED` 或 `FAILED`。
|
|
15
|
+
|
|
16
|
+
## 参数
|
|
17
|
+
|
|
18
|
+
- 图片:`publicModelId`、`routingMode`、`prompt`,可选 `batchSize`、`imageMediaIds`、`params`。
|
|
19
|
+
- 视频:`publicModelId`、`routingMode`、`prompt`,可选 `imageMediaIds`、`videoMediaIds`、`audioMediaIds`、`firstFrameMediaId`、`lastFrameMediaId`、`params`。
|
|
20
|
+
- `params` 必须以 `get_model` 返回的 schema 为准。
|
|
21
|
+
|
|
22
|
+
## 认证素材(仅视频)
|
|
23
|
+
|
|
24
|
+
用户要用认证素材时:
|
|
25
|
+
|
|
26
|
+
1. `get_material_mode` 确认当前账户是否有资格。
|
|
27
|
+
2. `quote_video_materials` 冻结素材,返回的字段名是 `id`。
|
|
28
|
+
3. `generate_video` 传 `materialMode: "certified"`,并把上一步的 `id` 作为 `materialQuoteId` 传入。
|
|
29
|
+
|
|
30
|
+
没有资格时如实说明,不要改用普通素材冒充。
|
|
31
|
+
|
|
32
|
+
## 失败与重试
|
|
33
|
+
|
|
34
|
+
- `FAILED`:读取 `errorMessage`,转述可执行的下一步;不要用相同参数无限重试。
|
|
35
|
+
- 超时且服务端仍在处理:用任务 ID 继续查,不要重新提交。
|
|
36
|
+
- 正式生成请求超时又查不到任务时,复用同一个 `clientRequestId` 重新提交,由服务端幂等去重。
|
|
37
|
+
|
|
38
|
+
## 回复用户
|
|
39
|
+
|
|
40
|
+
- 成功:给结果链接或 mediaId,并说明本次消费(单位是蜂蜜,充值实付才是元)。
|
|
41
|
+
- 失败:说明失败原因和用户能做什么,不展示上游响应体。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-hive-media-upload
|
|
3
|
+
description: 把本机图片、视频、音频或文档传给蜂巢,换取可复用的 mediaId。图片理解、图生图、图生视频、首尾帧和参考素材之前都要用。包含本机直传和"创建上传地址再 PUT"两种方式和大小限制处理。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 上传媒体
|
|
7
|
+
|
|
8
|
+
## 选哪种方式
|
|
9
|
+
|
|
10
|
+
- Agent 能读本机文件:用 `upload_media_from_path`,一步完成。
|
|
11
|
+
- 只能拿到字节流或远程文件:先 `create_media_upload`,再按返回的 `method` 和 `headers` 把原始字节 PUT 到 `upload.url`,最后 `complete_media_upload`。
|
|
12
|
+
|
|
13
|
+
## 步骤(本机文件)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
工具:upload_media_from_path
|
|
17
|
+
参数:{ "path": "/绝对路径/图片.png" }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
返回 `mediaId`,后续放进 `messages[].mediaIds` 或 `imageMediaIds`、`videoMediaIds`、`firstFrameMediaId`、`lastFrameMediaId`。
|
|
21
|
+
|
|
22
|
+
## 步骤(创建上传地址)
|
|
23
|
+
|
|
24
|
+
1. `create_media_upload`,必须传 `filename`、`contentType`、`sizeBytes`(原始字节数),缺一个都会被拒绝。
|
|
25
|
+
2. PUT 文件原始字节到返回的 `upload.url`,请求头使用返回的 `upload.headers`,不要改成 multipart。
|
|
26
|
+
3. `complete_media_upload`,传 `mediaId`。成功后该 `mediaId` 才可用。
|
|
27
|
+
|
|
28
|
+
## 注意
|
|
29
|
+
|
|
30
|
+
- 上传地址有有效期,超时后重新创建,不要复用。
|
|
31
|
+
- `complete_media_upload` 失败时不要直接拿去生成,先重试确认或重新上传。
|
|
32
|
+
- 单文件大小和格式限制以 `get_model` 返回的媒体要求为准。
|
|
33
|
+
- 不要把文件内容贴进对话文本,必须走上传换取 `mediaId`。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-hive-model-selection
|
|
3
|
+
description: 在蜂巢 AI 开放平台选择模型。用户要调用文字、图片或视频模型,但还没指定模型时使用;先列出可用模型和售价,再读取目标模型的参数契约,避免按记忆猜参数。也可用于比较同类模型的价格和线路。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 选择模型
|
|
7
|
+
|
|
8
|
+
## 何时用
|
|
9
|
+
|
|
10
|
+
- 用户说"用蜂巢生成一张图/一段视频/写一段文字",但没有给模型 ID。
|
|
11
|
+
- 需要在多个模型之间比价格、比能力、比线路。
|
|
12
|
+
|
|
13
|
+
不要跳过这一步直接调用生成工具。模型 ID、参数和线路必须以当前账户可见的契约返回值为准。
|
|
14
|
+
|
|
15
|
+
## 步骤
|
|
16
|
+
|
|
17
|
+
1. 列出候选模型(`detail=lite` 只返回选型需要的字段):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
ai-hive-mcp models --type IMAGE --json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
已连接 MCP 时调用 `list_models`,参数相同。
|
|
24
|
+
|
|
25
|
+
2. 按任务筛选:
|
|
26
|
+
- 文字:`TEXT`
|
|
27
|
+
- 图片:`IMAGE`
|
|
28
|
+
- 视频:`VIDEO`
|
|
29
|
+
|
|
30
|
+
3. 比较 `pricingSummary` 里的 `saleMicros`。这是客户实际结算价,单位是蜂蜜微单位(1,000,000 微单位 = 1 蜂蜜),不要引用 `originalMicros`。只有充值实付金额才是元。
|
|
31
|
+
4. 用 `get_model` / `ai-hive-mcp model <模型 ID> --json` 读取:
|
|
32
|
+
- 支持的 `routingModes`
|
|
33
|
+
- 参数 schema
|
|
34
|
+
- 媒体要求
|
|
35
|
+
- 计费因素
|
|
36
|
+
5. 把选中的 `publicModelId` 和 `routingMode` 交给下一步技能。
|
|
37
|
+
|
|
38
|
+
## 线路怎么选
|
|
39
|
+
|
|
40
|
+
- `COST_FIRST`:默认,价格优先。
|
|
41
|
+
- `SPEED_FIRST`:速度优先。
|
|
42
|
+
- `SUCCESS_FIRST`:成功率优先。
|
|
43
|
+
|
|
44
|
+
用户没有偏好时用 `COST_FIRST`,并在回复里说明选择的线路。
|
|
45
|
+
|
|
46
|
+
## 失败处理
|
|
47
|
+
|
|
48
|
+
- 返回"模型不存在或当前不可用":重新列一遍模型,不要重试同一个 ID。
|
|
49
|
+
- 返回没有价格:该模型暂不可售,换一个模型。
|
|
50
|
+
- 不要猜测参数名。`get_model` 没有给出的字段一律不要传。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-hive-text-generation
|
|
3
|
+
description: 用蜂巢的 Hive Chat API 发起一次付费文字调用。用户要写文案、总结、翻译、问答,且已经确定模型时使用。包含幂等键、报价快照和超时恢复规则,避免重复扣费。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 文字生成
|
|
7
|
+
|
|
8
|
+
## 前置
|
|
9
|
+
|
|
10
|
+
先按 `ai-hive-model-selection` 确定 `publicModelId` 和 `routingMode`。
|
|
11
|
+
|
|
12
|
+
## 调用
|
|
13
|
+
|
|
14
|
+
MCP 工具:`chat_text`
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ai-hive-mcp run <模型 ID> "你的提示词" --json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 必须遵守
|
|
21
|
+
|
|
22
|
+
1. **每次付费调用都带 `clientRequestId`**,同一次业务请求重试必须复用同一个值。改参数后必须换新值,否则会被当成同一次请求去重。
|
|
23
|
+
2. `pricingSnapshot` 可选。省略时客户端会自动向服务端取当前快照;手动传时必须是 `list_models` 返回的对应对象,且 `publicModelId`、`routingMode` 一致。
|
|
24
|
+
3. 多轮对话时把历史消息按 `system` / `user` / `assistant` 顺序传入。
|
|
25
|
+
4. 图片理解:先把图片上传为 `mediaId`(见 `ai-hive-media-upload`),再放进 `messages[].mediaIds`。
|
|
26
|
+
|
|
27
|
+
## 超时怎么办
|
|
28
|
+
|
|
29
|
+
返回"等待响应超时,服务端可能仍在生成,也可能已完成并扣费"时:
|
|
30
|
+
|
|
31
|
+
- 复用同一个 `clientRequestId` 和完全相同的参数重新调用;
|
|
32
|
+
- 不要换参数重发,那会被当成新请求再次扣费;
|
|
33
|
+
- 不要为了"确认结果"而重复提交。
|
|
34
|
+
|
|
35
|
+
## 回复用户
|
|
36
|
+
|
|
37
|
+
- 给出生成结果。
|
|
38
|
+
- 结果里包含 `chargeMicros` 时,换算成蜂蜜(除以 1,000,000)告诉用户本次消费;只有充值实付金额才是元。
|
|
39
|
+
- 不要展示内部错误码或上游响应体。
|