focalapi-cli 0.1.0 → 0.2.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/CHANGELOG.md +10 -0
- package/LICENSE +202 -202
- package/README.md +155 -190
- package/dist/cli.js +1203 -331
- package/package.json +53 -50
- package/scripts/postinstall.cjs +41 -0
- package/skills/focalapi/SKILL.md +54 -43
- package/skills/focalapi-auth/SKILL.md +30 -50
- package/skills/focalapi-chat/SKILL.md +32 -51
- package/skills/focalapi-gen/SKILL.md +53 -43
- package/skills/focalapi-models/SKILL.md +49 -0
- package/skills/focalapi-task/SKILL.md +25 -0
- package/skills/focalapi-usage/SKILL.md +27 -36
- package/skills/focalapi-search/SKILL.md +0 -31
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,10 @@ var ApiError = class extends Error {
|
|
|
8
8
|
code;
|
|
9
9
|
status;
|
|
10
10
|
hint;
|
|
11
|
+
/** 上游返回的稳定错误代码或类型;不包含密钥等敏感请求信息。 */
|
|
12
|
+
upstreamCode;
|
|
13
|
+
/** 可用于向服务方关联日志的请求 ID。 */
|
|
14
|
+
requestId;
|
|
11
15
|
/** 上游原始响应体(已截断),仅调试用途,打印前需脱敏。 */
|
|
12
16
|
body;
|
|
13
17
|
constructor(code, message, opts) {
|
|
@@ -17,6 +21,8 @@ var ApiError = class extends Error {
|
|
|
17
21
|
this.status = opts?.status;
|
|
18
22
|
this.hint = opts?.hint;
|
|
19
23
|
this.body = opts?.body;
|
|
24
|
+
this.upstreamCode = opts?.upstreamCode;
|
|
25
|
+
this.requestId = opts?.requestId;
|
|
20
26
|
}
|
|
21
27
|
toJSON() {
|
|
22
28
|
return {
|
|
@@ -24,12 +30,14 @@ var ApiError = class extends Error {
|
|
|
24
30
|
code: this.code,
|
|
25
31
|
message: this.message,
|
|
26
32
|
...this.hint ? { hint: this.hint } : {},
|
|
27
|
-
...this.status !== void 0 ? { status: this.status } : {}
|
|
33
|
+
...this.status !== void 0 ? { status: this.status } : {},
|
|
34
|
+
...this.upstreamCode ? { upstream_code: this.upstreamCode } : {},
|
|
35
|
+
...this.requestId ? { request_id: this.requestId } : {}
|
|
28
36
|
}
|
|
29
37
|
};
|
|
30
38
|
}
|
|
31
39
|
};
|
|
32
|
-
function refineErrorCode(status, message) {
|
|
40
|
+
function refineErrorCode(status, message, opts) {
|
|
33
41
|
const m = message.toLowerCase();
|
|
34
42
|
if (m.includes("quota") || m.includes("\u989D\u5EA6") || m.includes("insufficient")) {
|
|
35
43
|
return "insufficient_quota";
|
|
@@ -38,14 +46,17 @@ function refineErrorCode(status, message) {
|
|
|
38
46
|
return "model_not_found";
|
|
39
47
|
}
|
|
40
48
|
if (m.includes("key") || m.includes("token") || m.includes("auth")) {
|
|
41
|
-
|
|
49
|
+
if (status === 401 || status === 403) {
|
|
50
|
+
return opts?.authFailureIsInvalidApiKey ? "invalid_api_key" : "upstream_auth_failed";
|
|
51
|
+
}
|
|
52
|
+
return "invalid_request";
|
|
42
53
|
}
|
|
43
54
|
switch (status) {
|
|
44
55
|
case 400:
|
|
45
56
|
return "invalid_request";
|
|
46
57
|
case 401:
|
|
47
58
|
case 403:
|
|
48
|
-
return "invalid_api_key";
|
|
59
|
+
return opts?.authFailureIsInvalidApiKey ? "invalid_api_key" : "authentication_failed";
|
|
49
60
|
case 404:
|
|
50
61
|
return "model_not_found";
|
|
51
62
|
case 429:
|
|
@@ -103,6 +114,14 @@ function printError(err, opts) {
|
|
|
103
114
|
const hint = err.hint;
|
|
104
115
|
if (hint) {
|
|
105
116
|
process.stderr.write(`\u63D0\u793A\uFF1A${hint}
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
if (err.upstreamCode) {
|
|
120
|
+
process.stderr.write(`\u4E0A\u6E38\u4EE3\u7801\uFF1A${err.upstreamCode}
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
123
|
+
if (err.requestId) {
|
|
124
|
+
process.stderr.write(`\u8BF7\u6C42 ID\uFF1A${err.requestId}
|
|
106
125
|
`);
|
|
107
126
|
}
|
|
108
127
|
}
|
|
@@ -136,7 +155,7 @@ function displayWidth(s) {
|
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
// src/lib/version.ts
|
|
139
|
-
var VERSION = true ? "0.
|
|
158
|
+
var VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
140
159
|
|
|
141
160
|
// src/commands/auth.ts
|
|
142
161
|
import { createInterface } from "readline/promises";
|
|
@@ -248,7 +267,8 @@ function extractErrorMessage(raw) {
|
|
|
248
267
|
const obj = parsed;
|
|
249
268
|
const errObj = obj?.error;
|
|
250
269
|
const message = typeof errObj?.message === "string" && errObj.message || typeof obj?.message === "string" && obj.message || JSON.stringify(parsed).slice(0, 500);
|
|
251
|
-
|
|
270
|
+
const upstreamCode = [errObj?.code, errObj?.type, obj?.code, obj?.type].find((value) => typeof value === "string");
|
|
271
|
+
return { message, body: parsed, upstreamCode: typeof upstreamCode === "string" ? upstreamCode : void 0 };
|
|
252
272
|
}
|
|
253
273
|
async function request(opts) {
|
|
254
274
|
const res = await rawRequest(opts);
|
|
@@ -298,9 +318,10 @@ async function rawRequest(opts) {
|
|
|
298
318
|
}
|
|
299
319
|
if (!res.ok) {
|
|
300
320
|
const text = await res.text().catch(() => "");
|
|
301
|
-
const { message, body } = extractErrorMessage(text);
|
|
302
|
-
const code = refineErrorCode(res.status, message);
|
|
303
|
-
|
|
321
|
+
const { message, body, upstreamCode } = extractErrorMessage(text);
|
|
322
|
+
const code = refineErrorCode(res.status, message, { authFailureIsInvalidApiKey: opts.authFailureIsInvalidApiKey });
|
|
323
|
+
const requestId = res.headers.get("x-request-id") ?? res.headers.get("request-id") ?? res.headers.get("x-requestid") ?? void 0;
|
|
324
|
+
throw new ApiError(code, message, { status: res.status, body, upstreamCode, requestId });
|
|
304
325
|
}
|
|
305
326
|
return res;
|
|
306
327
|
}
|
|
@@ -342,7 +363,8 @@ async function fetchTokenUsage(baseUrl, apiKey) {
|
|
|
342
363
|
baseUrl,
|
|
343
364
|
path: "/api/usage/token/",
|
|
344
365
|
apiKey,
|
|
345
|
-
timeoutMs: 15e3
|
|
366
|
+
timeoutMs: 15e3,
|
|
367
|
+
authFailureIsInvalidApiKey: true
|
|
346
368
|
});
|
|
347
369
|
if (!res.data) {
|
|
348
370
|
throw new ApiError("bad_response", "\u7528\u91CF\u63A5\u53E3\u54CD\u5E94\u7F3A\u5C11 data \u5B57\u6BB5");
|
|
@@ -443,42 +465,183 @@ function registerAuth(program) {
|
|
|
443
465
|
});
|
|
444
466
|
}
|
|
445
467
|
|
|
468
|
+
// src/lib/model-selection.ts
|
|
469
|
+
var RECOMMENDED_MODELS = {
|
|
470
|
+
image: [
|
|
471
|
+
"seedream-5-0-260128",
|
|
472
|
+
"gpt-image-2",
|
|
473
|
+
"gemini-3.1-flash-image",
|
|
474
|
+
"grok-imagine-image-quality",
|
|
475
|
+
"seedream-4-5-251128"
|
|
476
|
+
],
|
|
477
|
+
video: [
|
|
478
|
+
"dreamina-seedance-2-5-260628",
|
|
479
|
+
"veo-3.1-generate-preview",
|
|
480
|
+
"grok-imagine-video-1.5",
|
|
481
|
+
"dreamina-seedance-2-0-260128",
|
|
482
|
+
"veo-3.1-fast-generate-preview"
|
|
483
|
+
]
|
|
484
|
+
};
|
|
485
|
+
function parseCreativeCapability(value) {
|
|
486
|
+
const normalized = value.trim().toLowerCase();
|
|
487
|
+
if (normalized === "image" || normalized === "video") return normalized;
|
|
488
|
+
throw new ApiError("invalid_request", `\u4E0D\u652F\u6301\u7684\u521B\u4F5C\u80FD\u529B\uFF1A${value}`, {
|
|
489
|
+
hint: "\u53EF\u9009\uFF1Aimage | video\u3002"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
async function resolveCreativeModel(auth, capability) {
|
|
493
|
+
const endpointType = capability === "image" ? "image-generation" : "video-generation";
|
|
494
|
+
const listed = await request({
|
|
495
|
+
baseUrl: auth.baseUrl,
|
|
496
|
+
path: "/v1/models",
|
|
497
|
+
apiKey: auth.apiKey
|
|
498
|
+
});
|
|
499
|
+
const available = new Map((listed.data ?? []).map((model) => [model.id, model]));
|
|
500
|
+
const ranked = RECOMMENDED_MODELS[capability].filter((id) => available.has(id));
|
|
501
|
+
const discovered = (listed.data ?? []).filter((model) => model.supported_endpoint_types?.includes(endpointType)).map((model) => model.id).filter((id) => !ranked.includes(id));
|
|
502
|
+
const candidates = [...ranked, ...discovered];
|
|
503
|
+
for (const id of candidates) {
|
|
504
|
+
const contract = await request({
|
|
505
|
+
baseUrl: auth.baseUrl,
|
|
506
|
+
path: `/v1/models/${encodeURIComponent(id)}`,
|
|
507
|
+
apiKey: auth.apiKey
|
|
508
|
+
});
|
|
509
|
+
if (contract.error || !contract.id || !contract.supported_endpoint_types?.includes(endpointType)) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const alternatives = candidates.filter((candidate) => candidate !== id && available.has(candidate)).slice(0, 4);
|
|
513
|
+
return {
|
|
514
|
+
capability,
|
|
515
|
+
endpoint_type: endpointType,
|
|
516
|
+
model: contract,
|
|
517
|
+
selected_by: "focalapi-default",
|
|
518
|
+
next_command: capability === "image" ? `focalapi gen image "<prompt>" -m ${id} --json` : `focalapi gen video "<prompt>" -m ${id} --no-wait --json`,
|
|
519
|
+
alternatives
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
throw new ApiError("model_not_found", `\u5F53\u524D Key \u6CA1\u6709\u53EF\u76F4\u63A5\u8C03\u7528\u7684${capability === "image" ? "\u56FE\u50CF" : "\u89C6\u9891"}\u751F\u6210\u6A21\u578B`, {
|
|
523
|
+
hint: "\u8FD0\u884C focalapi models list --json \u67E5\u770B\u5F53\u524D\u6A21\u578B\u6C60\uFF1B\u4E0D\u8981\u731C\u6D4B\u6216\u53CD\u590D\u8BD5\u6A21\u578B ID\u3002"
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
446
527
|
// src/commands/models.ts
|
|
528
|
+
function textValue(value) {
|
|
529
|
+
if (value === null || value === void 0 || value === "") return "-";
|
|
530
|
+
if (Array.isArray(value)) return value.length === 0 ? "-" : value.map(textValue).join(", ");
|
|
531
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
532
|
+
return String(value);
|
|
533
|
+
}
|
|
534
|
+
function parameterConstraint(parameter) {
|
|
535
|
+
const constraints = [];
|
|
536
|
+
if (parameter.required) constraints.push("\u5FC5\u586B");
|
|
537
|
+
if (parameter.default !== void 0) constraints.push(`\u9ED8\u8BA4 ${String(parameter.default)}`);
|
|
538
|
+
if (parameter.values?.length) constraints.push(`\u53EF\u9009 ${parameter.values.join(" / ")}`);
|
|
539
|
+
if (parameter.minimum !== void 0 || parameter.maximum !== void 0) {
|
|
540
|
+
constraints.push(`${parameter.minimum ?? "-"}\u2013${parameter.maximum ?? "-"}`);
|
|
541
|
+
}
|
|
542
|
+
return [parameter.type, ...constraints, parameter.description].join("\uFF1B");
|
|
543
|
+
}
|
|
544
|
+
function printModelDetails(model) {
|
|
545
|
+
const { supported_params: supportedParams, ...summary } = model;
|
|
546
|
+
printTable(
|
|
547
|
+
["\u5B57\u6BB5", "\u503C"],
|
|
548
|
+
Object.entries(summary).map(([key, value]) => [key, textValue(value)])
|
|
549
|
+
);
|
|
550
|
+
if (Array.isArray(supportedParams) && supportedParams.length > 0) {
|
|
551
|
+
process.stdout.write("\n\u652F\u6301\u53C2\u6570\n");
|
|
552
|
+
printTable(
|
|
553
|
+
["\u53C2\u6570", "\u7EA6\u675F"],
|
|
554
|
+
supportedParams.map((parameter) => {
|
|
555
|
+
const item = parameter;
|
|
556
|
+
return [item.name, parameterConstraint(item)];
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function filterModels(models, query, endpoint) {
|
|
562
|
+
const normalizedQuery = query?.trim().toLowerCase();
|
|
563
|
+
const normalizedEndpoint = endpoint?.trim().toLowerCase();
|
|
564
|
+
return models.filter((model) => {
|
|
565
|
+
if (normalizedQuery && !model.id.toLowerCase().includes(normalizedQuery)) return false;
|
|
566
|
+
return !normalizedEndpoint || (model.supported_endpoint_types ?? []).some((item) => item.toLowerCase() === normalizedEndpoint);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
function printModelList(models, json) {
|
|
570
|
+
if (json) {
|
|
571
|
+
printJson({ data: models });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
printTable(
|
|
575
|
+
["\u6A21\u578B ID", "\u63D0\u4F9B\u65B9", "\u652F\u6301\u7AEF\u70B9"],
|
|
576
|
+
models.map((model) => [model.id, model.owned_by ?? "-", textValue(model.supported_endpoint_types)])
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
async function fetchModels(g) {
|
|
580
|
+
const auth = resolveAuth(g);
|
|
581
|
+
const response = await request({ baseUrl: auth.baseUrl, path: "/v1/models", apiKey: auth.apiKey });
|
|
582
|
+
return response.data ?? [];
|
|
583
|
+
}
|
|
447
584
|
function registerModels(program) {
|
|
448
585
|
const models = program.command("models").description("\u53EF\u7528\u6A21\u578B\u67E5\u8BE2");
|
|
449
|
-
models.command("
|
|
586
|
+
models.command("resolve").description("\u4E3A\u521B\u4F5C\u4EFB\u52A1\u9009\u62E9\u5F53\u524D Key \u53EF\u7528\u7684\u9ED8\u8BA4\u6A21\u578B\uFF0C\u5E76\u8FD4\u56DE\u5B8C\u6574\u5B9E\u65F6\u5951\u7EA6").argument("<capability>", "\u521B\u4F5C\u80FD\u529B\uFF1Aimage | video").action(async (capability, _opts, cmd) => {
|
|
450
587
|
const g = cmd.optsWithGlobals();
|
|
451
588
|
const auth = resolveAuth(g);
|
|
452
|
-
const
|
|
453
|
-
let list = res.data ?? [];
|
|
454
|
-
if (opts.filter) {
|
|
455
|
-
const kw = opts.filter.toLowerCase();
|
|
456
|
-
list = list.filter((m) => m.id.toLowerCase().includes(kw));
|
|
457
|
-
}
|
|
589
|
+
const resolved = await resolveCreativeModel(auth, parseCreativeCapability(capability));
|
|
458
590
|
if (g.json) {
|
|
459
|
-
printJson(
|
|
460
|
-
|
|
591
|
+
printJson(resolved);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
printTable(
|
|
595
|
+
["\u5B57\u6BB5", "\u503C"],
|
|
596
|
+
[
|
|
597
|
+
["\u80FD\u529B", resolved.capability],
|
|
598
|
+
["\u6A21\u578B", resolved.model.id],
|
|
599
|
+
["\u63D0\u4F9B\u65B9", resolved.model.owned_by ?? "-"],
|
|
600
|
+
["\u7AEF\u70B9", resolved.endpoint_type],
|
|
601
|
+
["\u4E0B\u4E00\u6B65", resolved.next_command]
|
|
602
|
+
]
|
|
603
|
+
);
|
|
604
|
+
if (Array.isArray(resolved.model.supported_params) && resolved.model.supported_params.length > 0) {
|
|
605
|
+
process.stdout.write("\n\u5B9E\u65F6\u53C2\u6570\u5951\u7EA6\n");
|
|
461
606
|
printTable(
|
|
462
|
-
["\
|
|
463
|
-
|
|
607
|
+
["\u53C2\u6570", "\u7EA6\u675F"],
|
|
608
|
+
resolved.model.supported_params.map((parameter) => {
|
|
609
|
+
const item = parameter;
|
|
610
|
+
return [item.name, parameterConstraint(item)];
|
|
611
|
+
})
|
|
464
612
|
);
|
|
465
613
|
}
|
|
466
614
|
});
|
|
467
|
-
models.command("
|
|
615
|
+
models.command("list").description("\u5217\u51FA\u5F53\u524D Key \u53EF\u7528\u7684\u5168\u90E8\u6A21\u578B").option("--filter <keyword>", "\u6309 ID \u5173\u952E\u5B57\u8FC7\u6EE4\uFF08\u4E0D\u533A\u5206\u5927\u5C0F\u5199\uFF09").option("--endpoint <type>", "\u6309\u7AEF\u70B9\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 image-generation").action(async (opts, cmd) => {
|
|
616
|
+
const g = cmd.optsWithGlobals();
|
|
617
|
+
printModelList(filterModels(await fetchModels(g), opts.filter, opts.endpoint), Boolean(g.json));
|
|
618
|
+
});
|
|
619
|
+
models.command("search").description("\u6309\u5173\u952E\u5B57\u641C\u7D22\u5F53\u524D Key \u53EF\u7528\u7684\u6A21\u578B").argument("<query>", "\u6A21\u578B ID \u5173\u952E\u5B57").option("--endpoint <type>", "\u6309\u7AEF\u70B9\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 video-generation").action(async (query, opts, cmd) => {
|
|
620
|
+
const g = cmd.optsWithGlobals();
|
|
621
|
+
printModelList(filterModels(await fetchModels(g), query, opts.endpoint), Boolean(g.json));
|
|
622
|
+
});
|
|
623
|
+
models.command("get").description("\u67E5\u770B\u5355\u4E2A\u6A21\u578B\u8BE6\u60C5\u4E0E\u5B9E\u65F6\u53C2\u6570\u5951\u7EA6").argument("<model>", "\u6A21\u578B ID").action(async (model, _opts, cmd) => {
|
|
468
624
|
const g = cmd.optsWithGlobals();
|
|
469
625
|
const auth = resolveAuth(g);
|
|
470
|
-
const
|
|
626
|
+
const response = await request({
|
|
471
627
|
baseUrl: auth.baseUrl,
|
|
472
628
|
path: `/v1/models/${encodeURIComponent(model)}`,
|
|
473
629
|
apiKey: auth.apiKey
|
|
474
630
|
});
|
|
631
|
+
if (response.error) {
|
|
632
|
+
throw new ApiError(
|
|
633
|
+
"invalid_request",
|
|
634
|
+
response.error.message ?? `\u6A21\u578B ${model} \u4E0D\u53EF\u7528`,
|
|
635
|
+
{ hint: "\u8FD0\u884C focalapi models list --json\uFF0C\u4F7F\u7528\u5217\u8868\u4E2D\u7684\u6A21\u578B ID\u3002" }
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (!response.id) {
|
|
639
|
+
throw new ApiError("bad_response", `\u6A21\u578B ${model} \u7684\u5951\u7EA6\u54CD\u5E94\u7F3A\u5C11 id`, { body: response });
|
|
640
|
+
}
|
|
475
641
|
if (g.json) {
|
|
476
|
-
printJson(
|
|
642
|
+
printJson(response);
|
|
477
643
|
} else {
|
|
478
|
-
|
|
479
|
-
["\u5B57\u6BB5", "\u503C"],
|
|
480
|
-
Object.entries(res).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
|
|
481
|
-
);
|
|
644
|
+
printModelDetails(response);
|
|
482
645
|
}
|
|
483
646
|
});
|
|
484
647
|
}
|
|
@@ -551,7 +714,7 @@ function extractText(content) {
|
|
|
551
714
|
return "";
|
|
552
715
|
}
|
|
553
716
|
function registerChat(program) {
|
|
554
|
-
program.command("chat").description("\
|
|
717
|
+
program.command("chat").description("\u8865\u5145\u6587\u672C\u8F85\u52A9\uFF08\u6A21\u578B\u5FC5\u987B\u6765\u81EA\u5F53\u524D Key \u7684\u5B9E\u65F6\u5217\u8868\uFF09").argument("[prompt...]", "\u63D0\u793A\u8BCD\uFF1B\u7701\u7565\u4E14 stdin \u4E3A\u7BA1\u9053\u65F6\u4ECE stdin \u8BFB\u53D6").option("-m, --model <model>", "\u6A21\u578B ID\uFF08\u6216\u8BBE FOCALAPI_MODEL\uFF09").option("--system <text>", "system \u63D0\u793A\u8BCD").option("--input <file...>", "\u8F93\u5165\u6587\u4EF6\uFF08\u56FE\u7247\u8F6C data URL\uFF0C\u5982 --input @photo.jpg\uFF1B@ \u524D\u7F00\u53EF\u9009\uFF09").option("--max-tokens <n>", "max_tokens", (v) => Number.parseInt(v, 10)).option("--stream", "\u5F3A\u5236\u6D41\u5F0F\u8F93\u51FA").option("--no-stream", "\u5F3A\u5236\u975E\u6D41\u5F0F").action(
|
|
555
718
|
async (promptParts, opts, cmd) => {
|
|
556
719
|
const g = cmd.optsWithGlobals();
|
|
557
720
|
const auth = resolveAuth(g);
|
|
@@ -724,6 +887,350 @@ import { join as join3, resolve as resolve2 } from "path";
|
|
|
724
887
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
725
888
|
import { Readable as Readable2 } from "stream";
|
|
726
889
|
|
|
890
|
+
// src/lib/model-capabilities.ts
|
|
891
|
+
var SEEDREAM_OUTPUT_FORMATS = ["png", "jpeg"];
|
|
892
|
+
var SEEDREAM_OPTIMIZE_PROMPT_MODES = ["auto", "enabled", "disabled"];
|
|
893
|
+
var GROK_IMAGE_ASPECT_RATIOS = [
|
|
894
|
+
"auto",
|
|
895
|
+
"1:1",
|
|
896
|
+
"2:3",
|
|
897
|
+
"3:2",
|
|
898
|
+
"3:4",
|
|
899
|
+
"4:3",
|
|
900
|
+
"9:16",
|
|
901
|
+
"16:9",
|
|
902
|
+
"9:19.5",
|
|
903
|
+
"19.5:9",
|
|
904
|
+
"1:2",
|
|
905
|
+
"2:1",
|
|
906
|
+
"1:3",
|
|
907
|
+
"3:1"
|
|
908
|
+
];
|
|
909
|
+
var IMAGE_CONSTRAINTS = {
|
|
910
|
+
"gpt-image-2": {
|
|
911
|
+
defaultSize: "1024x1024",
|
|
912
|
+
maxN: 8,
|
|
913
|
+
maxReferenceImages: 16,
|
|
914
|
+
minMegapixels: 0.65536,
|
|
915
|
+
maxMegapixels: 8.2944,
|
|
916
|
+
minEdge: 1024,
|
|
917
|
+
maxEdge: 3840,
|
|
918
|
+
edgeMultiple: 16,
|
|
919
|
+
maxAspectRatio: 3,
|
|
920
|
+
qualities: ["low", "medium", "high"],
|
|
921
|
+
backgrounds: ["auto", "opaque"]
|
|
922
|
+
},
|
|
923
|
+
"seedream-4-0-250828": {
|
|
924
|
+
defaultSize: "1k",
|
|
925
|
+
sizeTiers: ["1k", "2k", "4k"],
|
|
926
|
+
maxN: 10,
|
|
927
|
+
maxReferenceImages: 10,
|
|
928
|
+
maxTotalImages: 15,
|
|
929
|
+
minMegapixels: 0.92,
|
|
930
|
+
maxMegapixels: 16.777216,
|
|
931
|
+
supportsWatermark: true,
|
|
932
|
+
outputFormats: SEEDREAM_OUTPUT_FORMATS,
|
|
933
|
+
optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
|
|
934
|
+
},
|
|
935
|
+
"seedream-4-5-251128": {
|
|
936
|
+
defaultSize: "2k",
|
|
937
|
+
sizeTiers: ["2k", "4k"],
|
|
938
|
+
maxN: 10,
|
|
939
|
+
maxReferenceImages: 10,
|
|
940
|
+
maxTotalImages: 15,
|
|
941
|
+
minMegapixels: 3.6864,
|
|
942
|
+
maxMegapixels: 16.777216,
|
|
943
|
+
supportsWatermark: true,
|
|
944
|
+
outputFormats: SEEDREAM_OUTPUT_FORMATS,
|
|
945
|
+
optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
|
|
946
|
+
},
|
|
947
|
+
"dola-seedream-5-0-pro-260628": {
|
|
948
|
+
defaultSize: "1k",
|
|
949
|
+
sizeTiers: ["1k", "1.5k", "2k"],
|
|
950
|
+
maxN: 1,
|
|
951
|
+
maxReferenceImages: 10,
|
|
952
|
+
minMegapixels: 0.92,
|
|
953
|
+
maxMegapixels: 4.194304,
|
|
954
|
+
supportsWatermark: true,
|
|
955
|
+
outputFormats: SEEDREAM_OUTPUT_FORMATS,
|
|
956
|
+
optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
|
|
957
|
+
},
|
|
958
|
+
"seedream-5-0-260128": {
|
|
959
|
+
defaultSize: "2k",
|
|
960
|
+
sizeTiers: ["2k", "3k", "4k"],
|
|
961
|
+
maxN: 14,
|
|
962
|
+
maxReferenceImages: 14,
|
|
963
|
+
maxTotalImages: 15,
|
|
964
|
+
minMegapixels: 3.6864,
|
|
965
|
+
maxMegapixels: 16.777216,
|
|
966
|
+
supportsWatermark: true,
|
|
967
|
+
outputFormats: SEEDREAM_OUTPUT_FORMATS,
|
|
968
|
+
optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
|
|
969
|
+
},
|
|
970
|
+
"grok-imagine-image-quality": {
|
|
971
|
+
defaultSize: "1024x1024",
|
|
972
|
+
maxN: 10,
|
|
973
|
+
maxReferenceImages: 3,
|
|
974
|
+
aspectRatios: GROK_IMAGE_ASPECT_RATIOS,
|
|
975
|
+
resolutions: ["1k", "2k"],
|
|
976
|
+
supportsSeed: true
|
|
977
|
+
},
|
|
978
|
+
"grok-imagine-image": {
|
|
979
|
+
defaultSize: "1024x1024",
|
|
980
|
+
maxN: 10,
|
|
981
|
+
maxReferenceImages: 3,
|
|
982
|
+
aspectRatios: GROK_IMAGE_ASPECT_RATIOS,
|
|
983
|
+
resolutions: ["1k", "2k"],
|
|
984
|
+
supportsSeed: true
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
var SEEDANCE_RATIOS = ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"];
|
|
988
|
+
var GROK_VIDEO_ASPECT_RATIOS = ["auto", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"];
|
|
989
|
+
var VIDEO_CONSTRAINTS = {
|
|
990
|
+
"veo-3.1-generate-preview": {
|
|
991
|
+
resolutions: ["720p", "1080p", "4k"],
|
|
992
|
+
ratios: ["16:9", "9:16"],
|
|
993
|
+
minSeconds: 4,
|
|
994
|
+
maxSeconds: 8,
|
|
995
|
+
allowedSeconds: [4, 6, 8],
|
|
996
|
+
requiredSecondsByResolution: { "1080p": 8, "4k": 8 },
|
|
997
|
+
supportsSeed: true
|
|
998
|
+
},
|
|
999
|
+
"veo-3.1-fast-generate-preview": {
|
|
1000
|
+
resolutions: ["720p", "1080p", "4k"],
|
|
1001
|
+
ratios: ["16:9", "9:16"],
|
|
1002
|
+
minSeconds: 4,
|
|
1003
|
+
maxSeconds: 8,
|
|
1004
|
+
allowedSeconds: [4, 6, 8],
|
|
1005
|
+
requiredSecondsByResolution: { "1080p": 8, "4k": 8 },
|
|
1006
|
+
supportsSeed: true
|
|
1007
|
+
},
|
|
1008
|
+
"veo-3.1-lite-generate-preview": {
|
|
1009
|
+
resolutions: ["720p", "1080p"],
|
|
1010
|
+
ratios: ["16:9", "9:16"],
|
|
1011
|
+
minSeconds: 4,
|
|
1012
|
+
maxSeconds: 8,
|
|
1013
|
+
allowedSeconds: [4, 6, 8],
|
|
1014
|
+
requiredSecondsByResolution: { "1080p": 8 },
|
|
1015
|
+
supportsSeed: true
|
|
1016
|
+
},
|
|
1017
|
+
"dreamina-seedance-2-0-260128": {
|
|
1018
|
+
resolutions: ["480p", "720p", "1080p", "4k"],
|
|
1019
|
+
ratios: SEEDANCE_RATIOS,
|
|
1020
|
+
minSeconds: 4,
|
|
1021
|
+
maxSeconds: 15,
|
|
1022
|
+
supportsPriority: true
|
|
1023
|
+
},
|
|
1024
|
+
"dreamina-seedance-2-0-fast-260128": {
|
|
1025
|
+
resolutions: ["480p", "720p"],
|
|
1026
|
+
ratios: SEEDANCE_RATIOS,
|
|
1027
|
+
minSeconds: 4,
|
|
1028
|
+
maxSeconds: 15,
|
|
1029
|
+
supportsPriority: true
|
|
1030
|
+
},
|
|
1031
|
+
"dreamina-seedance-2-0-mini-260615": {
|
|
1032
|
+
resolutions: ["480p", "720p"],
|
|
1033
|
+
ratios: SEEDANCE_RATIOS,
|
|
1034
|
+
minSeconds: 4,
|
|
1035
|
+
maxSeconds: 15,
|
|
1036
|
+
supportsPriority: true
|
|
1037
|
+
},
|
|
1038
|
+
"dreamina-seedance-2-5-260628": {
|
|
1039
|
+
resolutions: ["480p", "720p"],
|
|
1040
|
+
ratios: SEEDANCE_RATIOS,
|
|
1041
|
+
minSeconds: 4,
|
|
1042
|
+
maxSeconds: 30
|
|
1043
|
+
},
|
|
1044
|
+
"grok-imagine-video": {
|
|
1045
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
1046
|
+
aspectRatios: GROK_VIDEO_ASPECT_RATIOS,
|
|
1047
|
+
minSeconds: 1,
|
|
1048
|
+
maxSeconds: 15,
|
|
1049
|
+
supportsSeed: true
|
|
1050
|
+
},
|
|
1051
|
+
"grok-imagine-video-1.5": {
|
|
1052
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
1053
|
+
aspectRatios: GROK_VIDEO_ASPECT_RATIOS,
|
|
1054
|
+
minSeconds: 1,
|
|
1055
|
+
maxSeconds: 15,
|
|
1056
|
+
supportsSeed: true
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
var COMMON_GEMINI_RATIOS = ["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
1060
|
+
var GEMINI_IMAGE_CONSTRAINTS = {
|
|
1061
|
+
"gemini-2.5-flash-image": { aspectRatios: COMMON_GEMINI_RATIOS, supportsSampling: false },
|
|
1062
|
+
"gemini-3-pro-image": { aspectRatios: COMMON_GEMINI_RATIOS, imageSizes: ["1K", "2K", "4K"], supportsSampling: false },
|
|
1063
|
+
"gemini-3.1-flash-image": { aspectRatios: COMMON_GEMINI_RATIOS, imageSizes: ["1K", "2K", "4K"], supportsSampling: false },
|
|
1064
|
+
"gemini-3.1-flash-lite-image": {
|
|
1065
|
+
aspectRatios: [...COMMON_GEMINI_RATIOS, "1:4", "4:1", "1:8", "8:1"],
|
|
1066
|
+
imageSizes: ["1K"],
|
|
1067
|
+
supportsSampling: true
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
function parseSize(size, model) {
|
|
1071
|
+
const match = /^(\d+)x(\d+)$/i.exec(size.trim());
|
|
1072
|
+
if (!match) {
|
|
1073
|
+
throw new ApiError("invalid_request", `${model} size must be a supported tier or WIDTHxHEIGHT (received: ${size})`);
|
|
1074
|
+
}
|
|
1075
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
1076
|
+
}
|
|
1077
|
+
function megapixels(width, height) {
|
|
1078
|
+
return width * height / 1e6;
|
|
1079
|
+
}
|
|
1080
|
+
function formatMegapixels(value) {
|
|
1081
|
+
return value.toFixed(2).replace(/\.00$/, "");
|
|
1082
|
+
}
|
|
1083
|
+
function validateOptionalChoice(model, name, value, supported) {
|
|
1084
|
+
if (!value) return;
|
|
1085
|
+
if (!supported) {
|
|
1086
|
+
throw new ApiError("invalid_request", `${model} does not support ${name}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (!supported.includes(value.toLowerCase())) {
|
|
1089
|
+
throw new ApiError("invalid_request", `${model} ${name} must be one of ${supported.join(", ")} (received: ${value})`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function validateImageGeneration(model, input) {
|
|
1093
|
+
if (input.responseFormat && input.responseFormat !== "url" && input.responseFormat !== "b64_json") {
|
|
1094
|
+
throw new ApiError("invalid_request", `response_format must be url or b64_json (received: ${input.responseFormat})`);
|
|
1095
|
+
}
|
|
1096
|
+
const constraint = IMAGE_CONSTRAINTS[model.trim()];
|
|
1097
|
+
if (!constraint) return;
|
|
1098
|
+
if (input.n < 1 || input.n > constraint.maxN) {
|
|
1099
|
+
throw new ApiError("invalid_request", `${model} n must be 1-${constraint.maxN} (received: ${input.n})`);
|
|
1100
|
+
}
|
|
1101
|
+
if (input.imageCount !== void 0 && constraint.maxReferenceImages !== void 0 && input.imageCount > constraint.maxReferenceImages) {
|
|
1102
|
+
throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxReferenceImages} reference images`);
|
|
1103
|
+
}
|
|
1104
|
+
if (input.imageCount !== void 0 && constraint.maxTotalImages !== void 0 && input.imageCount + input.n > constraint.maxTotalImages) {
|
|
1105
|
+
throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxTotalImages} input plus generated images`);
|
|
1106
|
+
}
|
|
1107
|
+
if (input.hasMask && model === "gpt-image-2" && input.imageCount !== 1) {
|
|
1108
|
+
throw new ApiError("invalid_request", "gpt-image-2 mask requires exactly one reference image");
|
|
1109
|
+
}
|
|
1110
|
+
validateOptionalChoice(model, "quality", input.quality, constraint.qualities);
|
|
1111
|
+
validateOptionalChoice(model, "background", input.background, constraint.backgrounds);
|
|
1112
|
+
validateOptionalChoice(model, "aspect_ratio", input.aspectRatio, constraint.aspectRatios);
|
|
1113
|
+
validateOptionalChoice(model, "resolution", input.resolution, constraint.resolutions);
|
|
1114
|
+
validateOptionalChoice(model, "output_format", input.outputFormat, constraint.outputFormats);
|
|
1115
|
+
validateOptionalChoice(model, "optimize_prompt", input.optimizePrompt, constraint.optimizePromptModes);
|
|
1116
|
+
if (input.seed !== void 0) {
|
|
1117
|
+
if (!constraint.supportsSeed) {
|
|
1118
|
+
throw new ApiError("invalid_request", `${model} does not support seed`);
|
|
1119
|
+
}
|
|
1120
|
+
if (!Number.isInteger(input.seed) || input.seed < 0) {
|
|
1121
|
+
throw new ApiError("invalid_request", "seed must be a non-negative integer");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (input.watermark !== void 0 && !constraint.supportsWatermark) {
|
|
1125
|
+
throw new ApiError("invalid_request", `${model} does not support watermark`);
|
|
1126
|
+
}
|
|
1127
|
+
const suppliedSize = input.size ?? constraint.defaultSize;
|
|
1128
|
+
if (constraint.sizeTiers?.includes(suppliedSize.toLowerCase())) return;
|
|
1129
|
+
if (constraint.sizeTiers && !/^\d+x\d+$/i.test(suppliedSize)) {
|
|
1130
|
+
throw new ApiError("invalid_request", `${model} size must be one of ${constraint.sizeTiers.join(", ")} or WIDTHxHEIGHT (received: ${suppliedSize})`);
|
|
1131
|
+
}
|
|
1132
|
+
const { width, height } = parseSize(suppliedSize, model);
|
|
1133
|
+
const pixels = megapixels(width, height);
|
|
1134
|
+
if (constraint.minEdge && (width < constraint.minEdge || height < constraint.minEdge) || constraint.maxEdge && (width > constraint.maxEdge || height > constraint.maxEdge) || constraint.edgeMultiple && (width % constraint.edgeMultiple !== 0 || height % constraint.edgeMultiple !== 0) || constraint.minMegapixels && pixels < constraint.minMegapixels || constraint.maxMegapixels && pixels > constraint.maxMegapixels) {
|
|
1135
|
+
const edge = constraint.minEdge && constraint.maxEdge ? `${constraint.minEdge}-${constraint.maxEdge}px per edge` : "";
|
|
1136
|
+
const step = constraint.edgeMultiple ? `, edge multiples of ${constraint.edgeMultiple}` : "";
|
|
1137
|
+
const mp = constraint.minMegapixels && constraint.maxMegapixels ? `, ${formatMegapixels(constraint.minMegapixels)}-${formatMegapixels(constraint.maxMegapixels)} MP` : "";
|
|
1138
|
+
throw new ApiError("invalid_request", `${model} does not support size=${width}x${height}; supported: ${edge}${step}${mp}`);
|
|
1139
|
+
}
|
|
1140
|
+
if (constraint.maxAspectRatio) {
|
|
1141
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
1142
|
+
if (ratio > constraint.maxAspectRatio) {
|
|
1143
|
+
throw new ApiError("invalid_request", `${model} longest-to-shortest edge ratio must not exceed ${constraint.maxAspectRatio}:1 (received: ${width}x${height})`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function validateGeminiImageGeneration(model, input) {
|
|
1148
|
+
const constraint = GEMINI_IMAGE_CONSTRAINTS[model.trim()];
|
|
1149
|
+
if (!constraint) {
|
|
1150
|
+
throw new ApiError("invalid_request", `${model} is not a supported Gemini image model; run focalapi models get ${model} first`);
|
|
1151
|
+
}
|
|
1152
|
+
if (input.aspectRatio && !constraint.aspectRatios.includes(input.aspectRatio)) {
|
|
1153
|
+
throw new ApiError("invalid_request", `${model} aspectRatio must be one of ${constraint.aspectRatios.join(", ")} (received: ${input.aspectRatio})`);
|
|
1154
|
+
}
|
|
1155
|
+
if (input.imageSize && !constraint.imageSizes?.includes(input.imageSize.toUpperCase())) {
|
|
1156
|
+
const supported = constraint.imageSizes?.join(", ") ?? "none";
|
|
1157
|
+
throw new ApiError("invalid_request", `${model} imageSize must be one of ${supported} (received: ${input.imageSize})`);
|
|
1158
|
+
}
|
|
1159
|
+
if (input.seed !== void 0 && (!Number.isInteger(input.seed) || input.seed < 0)) {
|
|
1160
|
+
throw new ApiError("invalid_request", "seed must be a non-negative integer");
|
|
1161
|
+
}
|
|
1162
|
+
if (!constraint.supportsSampling && (input.thinkingLevel || input.temperature !== void 0 || input.topP !== void 0)) {
|
|
1163
|
+
throw new ApiError("invalid_request", `${model} supports thinkingLevel, temperature, and topP only on gemini-3.1-flash-lite-image`);
|
|
1164
|
+
}
|
|
1165
|
+
if (input.thinkingLevel && !["MINIMAL", "HIGH"].includes(input.thinkingLevel.toUpperCase())) {
|
|
1166
|
+
throw new ApiError("invalid_request", "thinkingLevel must be MINIMAL or HIGH");
|
|
1167
|
+
}
|
|
1168
|
+
if (input.temperature !== void 0 && (!Number.isFinite(input.temperature) || input.temperature < 0 || input.temperature > 2)) {
|
|
1169
|
+
throw new ApiError("invalid_request", "temperature must be between 0 and 2");
|
|
1170
|
+
}
|
|
1171
|
+
if (input.topP !== void 0 && (!Number.isFinite(input.topP) || input.topP < 0 || input.topP > 1)) {
|
|
1172
|
+
throw new ApiError("invalid_request", "topP must be between 0 and 1");
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function validateVideoGeneration(model, input) {
|
|
1176
|
+
const constraint = VIDEO_CONSTRAINTS[model.trim()];
|
|
1177
|
+
if (!constraint) return;
|
|
1178
|
+
if (input.seconds !== void 0 && (input.seconds < constraint.minSeconds || input.seconds > constraint.maxSeconds)) {
|
|
1179
|
+
throw new ApiError("invalid_request", `${model} seconds must be ${constraint.minSeconds}-${constraint.maxSeconds} (received: ${input.seconds})`);
|
|
1180
|
+
}
|
|
1181
|
+
if (input.seconds !== void 0 && constraint.allowedSeconds && !constraint.allowedSeconds.includes(input.seconds)) {
|
|
1182
|
+
throw new ApiError("invalid_request", `${model} seconds must be one of ${constraint.allowedSeconds.join(", ")} (received: ${input.seconds})`);
|
|
1183
|
+
}
|
|
1184
|
+
if (input.resolution && !constraint.resolutions.includes(input.resolution.toLowerCase())) {
|
|
1185
|
+
throw new ApiError("invalid_request", `${model} resolution must be one of ${constraint.resolutions.join(", ")} (received: ${input.resolution})`);
|
|
1186
|
+
}
|
|
1187
|
+
if (input.seconds !== void 0 && input.resolution) {
|
|
1188
|
+
const required = constraint.requiredSecondsByResolution?.[input.resolution.toLowerCase()];
|
|
1189
|
+
if (required !== void 0 && input.seconds !== required) {
|
|
1190
|
+
throw new ApiError("invalid_request", `${model} resolution ${input.resolution} requires ${required} seconds`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (input.ratio) {
|
|
1194
|
+
if (!constraint.ratios) {
|
|
1195
|
+
throw new ApiError("invalid_request", `${model} uses --aspect-ratio instead of --ratio`);
|
|
1196
|
+
}
|
|
1197
|
+
if (!constraint.ratios.includes(input.ratio)) {
|
|
1198
|
+
throw new ApiError("invalid_request", `${model} ratio must be one of ${constraint.ratios.join(", ")} (received: ${input.ratio})`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (input.aspectRatio) {
|
|
1202
|
+
if (!constraint.aspectRatios) {
|
|
1203
|
+
throw new ApiError("invalid_request", `${model} uses --ratio instead of --aspect-ratio`);
|
|
1204
|
+
}
|
|
1205
|
+
if (!constraint.aspectRatios.includes(input.aspectRatio)) {
|
|
1206
|
+
throw new ApiError("invalid_request", `${model} aspect_ratio must be one of ${constraint.aspectRatios.join(", ")} (received: ${input.aspectRatio})`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (input.seed !== void 0) {
|
|
1210
|
+
if (!constraint.supportsSeed) {
|
|
1211
|
+
throw new ApiError("invalid_request", `${model} does not support seed`);
|
|
1212
|
+
}
|
|
1213
|
+
if (!Number.isInteger(input.seed) || input.seed < 0) {
|
|
1214
|
+
throw new ApiError("invalid_request", "seed must be a non-negative integer");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (input.priority !== void 0 && !constraint.supportsPriority) {
|
|
1218
|
+
throw new ApiError("invalid_request", `${model} does not support priority`);
|
|
1219
|
+
}
|
|
1220
|
+
if (input.priority !== void 0 && (input.priority < 0 || input.priority > 9)) {
|
|
1221
|
+
throw new ApiError("invalid_request", `${model} priority must be 0-9 (received: ${input.priority})`);
|
|
1222
|
+
}
|
|
1223
|
+
if (input.serviceTier && input.serviceTier !== "default") {
|
|
1224
|
+
throw new ApiError("invalid_request", `${model} service_tier must be default (received: ${input.serviceTier})`);
|
|
1225
|
+
}
|
|
1226
|
+
if (input.executionExpiresAfter !== void 0 && (input.executionExpiresAfter < 3600 || input.executionExpiresAfter > 259200)) {
|
|
1227
|
+
throw new ApiError("invalid_request", `${model} execution_expires_after must be 3600-259200 (received: ${input.executionExpiresAfter})`);
|
|
1228
|
+
}
|
|
1229
|
+
if (input.safetyIdentifier !== void 0 && !/^[\x21-\x7e]{1,64}$/.test(input.safetyIdentifier)) {
|
|
1230
|
+
throw new ApiError("invalid_request", `${model} safety_identifier must contain 1-64 printable ASCII characters`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
727
1234
|
// src/lib/tasks.ts
|
|
728
1235
|
import { createWriteStream } from "fs";
|
|
729
1236
|
import { mkdir } from "fs/promises";
|
|
@@ -767,11 +1274,21 @@ function extractProgress(body) {
|
|
|
767
1274
|
return void 0;
|
|
768
1275
|
}
|
|
769
1276
|
async function fetchTask(baseUrl, apiKey, taskId) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1277
|
+
let raw;
|
|
1278
|
+
try {
|
|
1279
|
+
raw = await request({
|
|
1280
|
+
baseUrl,
|
|
1281
|
+
path: `/v1/video/generations/${encodeURIComponent(taskId)}`,
|
|
1282
|
+
apiKey
|
|
1283
|
+
});
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
if (!(err instanceof ApiError) || err.status !== 404) throw err;
|
|
1286
|
+
raw = await request({
|
|
1287
|
+
baseUrl,
|
|
1288
|
+
path: `/v1/images/generations/${encodeURIComponent(taskId)}`,
|
|
1289
|
+
apiKey
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
775
1292
|
const obj = raw;
|
|
776
1293
|
const rawStatus = String(obj?.status ?? obj?.data?.status ?? "");
|
|
777
1294
|
return {
|
|
@@ -843,9 +1360,34 @@ function clampInt(value, min, max, name) {
|
|
|
843
1360
|
}
|
|
844
1361
|
return value;
|
|
845
1362
|
}
|
|
1363
|
+
function parseBooleanOption(value, name) {
|
|
1364
|
+
switch (value.trim().toLowerCase()) {
|
|
1365
|
+
case "true":
|
|
1366
|
+
return true;
|
|
1367
|
+
case "false":
|
|
1368
|
+
return false;
|
|
1369
|
+
default:
|
|
1370
|
+
throw new ApiError("invalid_request", `--${name} must be true or false (received: ${value})`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
async function withProgress(label, operation) {
|
|
1374
|
+
const startedAt = Date.now();
|
|
1375
|
+
info(`${label}\u2026`);
|
|
1376
|
+
const timer = setInterval(() => {
|
|
1377
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1e3));
|
|
1378
|
+
info(`${label}\uFF0C\u5DF2\u7B49\u5F85 ${elapsedSeconds} \u79D2\u2026`);
|
|
1379
|
+
}, 1e4);
|
|
1380
|
+
timer.unref();
|
|
1381
|
+
try {
|
|
1382
|
+
return await operation();
|
|
1383
|
+
} finally {
|
|
1384
|
+
clearInterval(timer);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
846
1387
|
async function saveImageItem(item, dir, base, apiKey) {
|
|
847
1388
|
if (item.b64_json) {
|
|
848
|
-
const
|
|
1389
|
+
const ext = item.mime_type?.includes("jpeg") ? ".jpg" : item.mime_type?.includes("webp") ? ".webp" : ".png";
|
|
1390
|
+
const filePath = join3(dir, `${base}${ext}`);
|
|
849
1391
|
await writeFile(filePath, Buffer.from(item.b64_json, "base64"));
|
|
850
1392
|
return filePath;
|
|
851
1393
|
}
|
|
@@ -864,21 +1406,128 @@ async function saveImageItem(item, dir, base, apiKey) {
|
|
|
864
1406
|
}
|
|
865
1407
|
throw new ApiError("bad_response", "\u56FE\u50CF\u7ED3\u679C\u65E2\u6CA1\u6709 url \u4E5F\u6CA1\u6709 b64_json");
|
|
866
1408
|
}
|
|
1409
|
+
function parseGenerationConfig(raw) {
|
|
1410
|
+
if (!raw) return {};
|
|
1411
|
+
try {
|
|
1412
|
+
const parsed = JSON.parse(raw);
|
|
1413
|
+
if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
1414
|
+
throw new Error("not an object");
|
|
1415
|
+
}
|
|
1416
|
+
return parsed;
|
|
1417
|
+
} catch {
|
|
1418
|
+
throw new ApiError("invalid_request", "--config \u5FC5\u987B\u662F\u5408\u6CD5\u7684 generationConfig JSON \u5BF9\u8C61\u3002");
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function parseJsonArray(raw, option) {
|
|
1422
|
+
try {
|
|
1423
|
+
const parsed = JSON.parse(raw);
|
|
1424
|
+
if (!Array.isArray(parsed)) throw new Error("not an array");
|
|
1425
|
+
return parsed;
|
|
1426
|
+
} catch {
|
|
1427
|
+
throw new ApiError("invalid_request", `--${option} must be a JSON array`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function parseGeminiResponseModalities(raw) {
|
|
1431
|
+
const modalities = raw.split(",").map((value) => value.trim().toUpperCase()).filter(Boolean);
|
|
1432
|
+
const unique = new Set(modalities);
|
|
1433
|
+
if (unique.size !== modalities.length || !unique.has("IMAGE") || [...unique].some((value) => value !== "IMAGE" && value !== "TEXT")) {
|
|
1434
|
+
throw new ApiError("invalid_request", "--response-modalities must be IMAGE or IMAGE,TEXT");
|
|
1435
|
+
}
|
|
1436
|
+
return unique.has("TEXT") ? ["IMAGE", "TEXT"] : ["IMAGE"];
|
|
1437
|
+
}
|
|
1438
|
+
function geminiImagePart(source) {
|
|
1439
|
+
const dataUri = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(source.trim());
|
|
1440
|
+
if (dataUri) {
|
|
1441
|
+
const [, mimeType = "", data = ""] = dataUri;
|
|
1442
|
+
return { inlineData: { mimeType, data: data.replace(/[\r\n]/g, "") } };
|
|
1443
|
+
}
|
|
1444
|
+
return { fileData: { fileUri: source } };
|
|
1445
|
+
}
|
|
1446
|
+
function geminiOmniImageInput(source) {
|
|
1447
|
+
const dataUri = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(source.trim());
|
|
1448
|
+
if (!dataUri) {
|
|
1449
|
+
throw new ApiError("invalid_request", "--image for Gemini Omni must be a base64 data URI");
|
|
1450
|
+
}
|
|
1451
|
+
const [, mimeType = "", data = ""] = dataUri;
|
|
1452
|
+
return { type: "image", mime_type: mimeType, data: data.replace(/[\r\n]/g, "") };
|
|
1453
|
+
}
|
|
1454
|
+
function extractGeminiOmniVideo(response) {
|
|
1455
|
+
for (const step of response.steps ?? []) {
|
|
1456
|
+
for (const content of step.content ?? []) {
|
|
1457
|
+
if (content.type === "video" && content.data) {
|
|
1458
|
+
return { data: content.data, mimeType: content.mime_type };
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return void 0;
|
|
1463
|
+
}
|
|
1464
|
+
function extractGeminiImageItems(response) {
|
|
1465
|
+
return (response.candidates ?? []).flatMap(
|
|
1466
|
+
(candidate) => (candidate.content?.parts ?? []).flatMap(
|
|
1467
|
+
(part) => part.inlineData?.data ? [{ b64_json: part.inlineData.data, mime_type: part.inlineData.mimeType }] : []
|
|
1468
|
+
)
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
867
1471
|
function registerGen(program) {
|
|
868
1472
|
const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
|
|
869
|
-
gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\
|
|
1473
|
+
gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u56FE\u50CF\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--size <size>", "\u5C3A\u5BF8\uFF0C\u5982 1024x1024").option("--aspect-ratio <ratio>", "\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF08\u4EC5 Grok \u56FE\u50CF\u6A21\u578B\uFF09\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF08\u4EC5 Grok \u56FE\u50CF\u6A21\u578B\uFF09\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u968F\u673A\u79CD\u5B50\uFF08\u4EC5 Grok \u56FE\u50CF\u6A21\u578B\uFF0C\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--quality <quality>", "\u56FE\u50CF\u8D28\u91CF\u6863\u4F4D\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--background <background>", "\u80CC\u666F\u6A21\u5F0F\uFF08\u4EC5 gpt-image-2 \u652F\u6301 auto/opaque\uFF09").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5 Seedream\uFF0Ctrue \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--output-format <format>", "\u8F93\u51FA\u683C\u5F0F\uFF08\u4EC5 Seedream\uFF1Apng \u6216 jpeg\uFF09").option("--optimize-prompt <mode>", "\u63D0\u793A\u8BCD\u4F18\u5316\uFF08\u4EC5 Seedream\uFF1Aauto\u3001enabled \u6216 disabled\uFF09").option("--image <url...>", "\u53C2\u8003\u56FE\u6216\u7F16\u8F91\u56FE URL\uFF0C\u53EF\u591A\u4E2A").option("--mask <url>", "\u7F16\u8F91 mask URL\uFF08gpt-image-2 \u9700\u8981\u5355\u5F20\u53C2\u8003\u56FE\uFF09").option("--response-format <format>", "\u56FE\u50CF\u54CD\u5E94\u683C\u5F0F\uFF1Aurl \u6216 b64_json").option("--n <count>", "\u5F20\u6570\uFF081\u2013128\uFF09", (v) => Number.parseInt(v, 10), 1).option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u56FE\u50CF\u751F\u6210\u5B8C\u6210").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
|
|
870
1474
|
const g = cmd.optsWithGlobals();
|
|
871
1475
|
const auth = resolveAuth(g);
|
|
1476
|
+
const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
|
|
1477
|
+
if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u56FE\u50CF\u6A21\u578B\uFF1A${model}`);
|
|
872
1478
|
const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
|
|
873
|
-
|
|
1479
|
+
validateImageGeneration(model, {
|
|
1480
|
+
n,
|
|
1481
|
+
size: opts.size,
|
|
1482
|
+
quality: opts.quality,
|
|
1483
|
+
background: opts.background,
|
|
1484
|
+
aspectRatio: opts.aspectRatio,
|
|
1485
|
+
resolution: opts.resolution,
|
|
1486
|
+
seed: opts.seed,
|
|
1487
|
+
watermark: opts.watermark,
|
|
1488
|
+
outputFormat: opts.outputFormat,
|
|
1489
|
+
optimizePrompt: opts.optimizePrompt,
|
|
1490
|
+
responseFormat: opts.responseFormat,
|
|
1491
|
+
imageCount: opts.image?.length,
|
|
1492
|
+
hasMask: Boolean(opts.mask)
|
|
1493
|
+
});
|
|
1494
|
+
if (opts.wait === false && opts.responseFormat === "b64_json") {
|
|
1495
|
+
throw new ApiError("invalid_request", "--response-format b64_json cannot be used with --no-wait; use url");
|
|
1496
|
+
}
|
|
1497
|
+
const body = { model, prompt: promptParts.join(" "), n };
|
|
874
1498
|
if (opts.size) body.size = opts.size;
|
|
875
|
-
|
|
1499
|
+
if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
|
|
1500
|
+
if (opts.resolution) body.resolution = opts.resolution.toLowerCase();
|
|
1501
|
+
if (opts.seed !== void 0) body.seed = opts.seed;
|
|
1502
|
+
if (opts.quality) body.quality = opts.quality;
|
|
1503
|
+
if (opts.background) body.background = opts.background;
|
|
1504
|
+
if (opts.watermark !== void 0) body.watermark = opts.watermark;
|
|
1505
|
+
if (opts.outputFormat) body.output_format = opts.outputFormat.toLowerCase();
|
|
1506
|
+
if (opts.optimizePrompt) body.optimize_prompt_options = { thinking: opts.optimizePrompt.toLowerCase() };
|
|
1507
|
+
if (opts.image) body.image = opts.image;
|
|
1508
|
+
if (opts.mask) body.mask = opts.mask;
|
|
1509
|
+
if (opts.responseFormat) body.response_format = opts.responseFormat;
|
|
1510
|
+
const res = await withProgress(opts.wait === false ? "\u6B63\u5728\u63D0\u4EA4\u56FE\u50CF\u4EFB\u52A1" : "\u6B63\u5728\u751F\u6210\u56FE\u50CF", () => request({
|
|
876
1511
|
baseUrl: auth.baseUrl,
|
|
877
1512
|
path: "/v1/images/generations",
|
|
878
1513
|
apiKey: auth.apiKey,
|
|
879
1514
|
body,
|
|
1515
|
+
headers: opts.wait === false ? { Prefer: "respond-async" } : void 0,
|
|
880
1516
|
timeoutMs: 6e5
|
|
881
|
-
});
|
|
1517
|
+
}));
|
|
1518
|
+
if (opts.wait === false) {
|
|
1519
|
+
const taskId = extractTaskId(res);
|
|
1520
|
+
if (!taskId) {
|
|
1521
|
+
throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
|
|
1522
|
+
}
|
|
1523
|
+
if (g.json) {
|
|
1524
|
+
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
1525
|
+
} else {
|
|
1526
|
+
process.stdout.write(taskId + "\n");
|
|
1527
|
+
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u67E5\u8BE2\uFF1Afocalapi task status ${taskId}`);
|
|
1528
|
+
}
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
882
1531
|
const items = res.data ?? [];
|
|
883
1532
|
if (items.length === 0) {
|
|
884
1533
|
throw new ApiError("bad_response", "\u56FE\u50CF\u751F\u6210\u54CD\u5E94\u4E3A\u7A7A", { body: res });
|
|
@@ -891,34 +1540,163 @@ function registerGen(program) {
|
|
|
891
1540
|
files.push(await saveImageItem(item, dir, `image-${ts}-${i + 1}`, auth.apiKey));
|
|
892
1541
|
}
|
|
893
1542
|
if (g.json) {
|
|
894
|
-
printJson({ files, count: files.length });
|
|
1543
|
+
printJson({ model, files, count: files.length });
|
|
895
1544
|
} else {
|
|
896
1545
|
for (const f of files) info(`\u2713 ${f}`);
|
|
897
1546
|
}
|
|
898
1547
|
});
|
|
899
|
-
gen.command("
|
|
1548
|
+
gen.command("gemini-image").description("\u4F7F\u7528 Gemini \u539F\u751F generateContent \u63A5\u53E3\u751F\u6210\u56FE\u50CF").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "Gemini \u56FE\u50CF\u6A21\u578B ID\uFF1B\u5148\u7528 focalapi models get \u786E\u8BA4").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF0C\u4F8B\u5982 1:1\u300116:9\u3001auto").option("--image-size <size>", "\u8F93\u51FA\u5C3A\u5BF8\uFF0C\u4F8B\u5982 1K\u30012K\u30014K").option("--response-modalities <modalities>", "\u8F93\u51FA\u7C7B\u578B\uFF1AIMAGE \u6216 IMAGE,TEXT\uFF1B\u672A\u4F20\u65F6\u7531\u670D\u52A1\u7AEF\u9ED8\u8BA4 IMAGE,TEXT").option("--config <json>", "\u9644\u52A0 Gemini generationConfig JSON\uFF1B\u547D\u4EE4\u56FA\u5B9A responseFormat.image \u548C\u5355\u5019\u9009").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--image <url...>", "Gemini reference image URL or data URI; repeatable").option("--system <text>", "Gemini systemInstruction text").option("--seed <n>", "Non-negative Gemini generation seed", (v) => Number.parseInt(v, 10)).option("--thinking-level <level>", "Nano Banana 2 Lite: MINIMAL or HIGH").option("--temperature <n>", "Nano Banana 2 Lite: 0 through 2", (v) => Number.parseFloat(v)).option("--top-p <n>", "Nano Banana 2 Lite: 0 through 1", (v) => Number.parseFloat(v)).action(async (promptParts, opts, cmd) => {
|
|
1549
|
+
const g = cmd.optsWithGlobals();
|
|
1550
|
+
const auth = resolveAuth(g);
|
|
1551
|
+
validateGeminiImageGeneration(opts.model, {
|
|
1552
|
+
aspectRatio: opts.aspectRatio,
|
|
1553
|
+
imageSize: opts.imageSize,
|
|
1554
|
+
seed: opts.seed,
|
|
1555
|
+
thinkingLevel: opts.thinkingLevel,
|
|
1556
|
+
temperature: opts.temperature,
|
|
1557
|
+
topP: opts.topP
|
|
1558
|
+
});
|
|
1559
|
+
const suppliedConfig = parseGenerationConfig(opts.config);
|
|
1560
|
+
const suppliedResponseFormat = suppliedConfig.responseFormat;
|
|
1561
|
+
const suppliedImageConfig = suppliedResponseFormat !== null && typeof suppliedResponseFormat === "object" && !Array.isArray(suppliedResponseFormat) ? suppliedResponseFormat.image : void 0;
|
|
1562
|
+
const imageConfig = {
|
|
1563
|
+
...suppliedImageConfig !== null && typeof suppliedImageConfig === "object" && !Array.isArray(suppliedImageConfig) ? suppliedImageConfig : {},
|
|
1564
|
+
...opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {},
|
|
1565
|
+
...opts.imageSize ? { imageSize: opts.imageSize.toUpperCase() } : {}
|
|
1566
|
+
};
|
|
1567
|
+
const generationConfig = {
|
|
1568
|
+
...suppliedConfig,
|
|
1569
|
+
candidateCount: 1,
|
|
1570
|
+
responseFormat: { image: imageConfig },
|
|
1571
|
+
...opts.responseModalities ? { responseModalities: parseGeminiResponseModalities(opts.responseModalities) } : {},
|
|
1572
|
+
...opts.seed !== void 0 ? { seed: opts.seed } : {},
|
|
1573
|
+
...opts.thinkingLevel ? { thinkingConfig: { ...suppliedConfig.thinkingConfig ?? {}, thinkingLevel: opts.thinkingLevel.toUpperCase() } } : {},
|
|
1574
|
+
...opts.temperature !== void 0 ? { temperature: opts.temperature } : {},
|
|
1575
|
+
...opts.topP !== void 0 ? { topP: opts.topP } : {}
|
|
1576
|
+
};
|
|
1577
|
+
const res = await withProgress("\u6B63\u5728\u751F\u6210 Gemini \u56FE\u50CF", () => request({
|
|
1578
|
+
baseUrl: auth.baseUrl,
|
|
1579
|
+
path: `/v1beta/models/${encodeURIComponent(opts.model)}:generateContent`,
|
|
1580
|
+
apiKey: auth.apiKey,
|
|
1581
|
+
body: {
|
|
1582
|
+
contents: [{ role: "user", parts: [{ text: promptParts.join(" ") }, ...(opts.image ?? []).map(geminiImagePart)] }],
|
|
1583
|
+
...opts.system ? { systemInstruction: { parts: [{ text: opts.system }] } } : {},
|
|
1584
|
+
generationConfig
|
|
1585
|
+
},
|
|
1586
|
+
timeoutMs: 6e5
|
|
1587
|
+
}));
|
|
1588
|
+
const items = extractGeminiImageItems(res);
|
|
1589
|
+
if (items.length === 0) {
|
|
1590
|
+
throw new ApiError("bad_response", "Gemini \u56FE\u50CF\u54CD\u5E94\u4E2D\u672A\u627E\u5230 inlineData \u56FE\u50CF\u3002", { body: res });
|
|
1591
|
+
}
|
|
1592
|
+
const dir = resolve2(opts.out);
|
|
1593
|
+
await mkdir2(dir, { recursive: true });
|
|
1594
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1595
|
+
const files = [];
|
|
1596
|
+
for (const [i, item] of items.entries()) {
|
|
1597
|
+
files.push(await saveImageItem(item, dir, `gemini-image-${ts}-${i + 1}`, auth.apiKey));
|
|
1598
|
+
}
|
|
1599
|
+
if (g.json) {
|
|
1600
|
+
printJson({ files, count: files.length });
|
|
1601
|
+
} else {
|
|
1602
|
+
for (const file of files) info(`\u2713 ${file}`);
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
gen.command("omni-video").description("\u4F7F\u7528 Gemini Omni Flash \u539F\u751F Interactions API \u751F\u6210\u6216\u7F16\u8F91\u89C6\u9891").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("--image <data-uri...>", "\u56FE\u751F\u89C6\u9891\u53C2\u8003\u56FE\uFF1B\u4EC5\u652F\u6301 base64 data URI\uFF0C\u53EF\u591A\u4E2A").option("--previous-interaction-id <id>", "\u4E0A\u4E00\u6B21\u4EA4\u4E92 ID\uFF0C\u7528\u4E8E\u8FDE\u7EED\u89C6\u9891\u7F16\u8F91").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF1A16:9 \u6216 9:16").option("--task <task>", "\u89C6\u9891\u4EFB\u52A1\uFF1Atext_to_video\u3001image_to_video\u3001reference_to_video \u6216 edit").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
|
|
1606
|
+
const g = cmd.optsWithGlobals();
|
|
1607
|
+
const auth = resolveAuth(g);
|
|
1608
|
+
if (opts.aspectRatio && !["16:9", "9:16"].includes(opts.aspectRatio)) {
|
|
1609
|
+
throw new ApiError("invalid_request", "--aspect-ratio must be 16:9 or 9:16");
|
|
1610
|
+
}
|
|
1611
|
+
if (opts.task && !["text_to_video", "image_to_video", "reference_to_video", "edit"].includes(opts.task)) {
|
|
1612
|
+
throw new ApiError("invalid_request", "--task must be text_to_video, image_to_video, reference_to_video, or edit");
|
|
1613
|
+
}
|
|
1614
|
+
const prompt = promptParts.join(" ");
|
|
1615
|
+
const imageInputs = (opts.image ?? []).map(geminiOmniImageInput);
|
|
1616
|
+
const input = imageInputs.length === 0 ? prompt : [...imageInputs, { type: "text", text: prompt }];
|
|
1617
|
+
const res = await withProgress("\u6B63\u5728\u751F\u6210 Gemini Omni \u89C6\u9891", () => request({
|
|
1618
|
+
baseUrl: auth.baseUrl,
|
|
1619
|
+
path: "/v1beta/interactions",
|
|
1620
|
+
apiKey: auth.apiKey,
|
|
1621
|
+
body: {
|
|
1622
|
+
model: "gemini-omni-flash-preview",
|
|
1623
|
+
input,
|
|
1624
|
+
...opts.previousInteractionId ? { previous_interaction_id: opts.previousInteractionId } : {},
|
|
1625
|
+
...opts.aspectRatio ? { response_format: { type: "video", aspect_ratio: opts.aspectRatio } } : {},
|
|
1626
|
+
...opts.task ? { generation_config: { video_config: { task: opts.task } } } : {}
|
|
1627
|
+
},
|
|
1628
|
+
timeoutMs: 6e5
|
|
1629
|
+
}));
|
|
1630
|
+
const video = extractGeminiOmniVideo(res);
|
|
1631
|
+
if (!video) {
|
|
1632
|
+
throw new ApiError("bad_response", "Gemini Omni \u54CD\u5E94\u4E2D\u672A\u627E\u5230\u89C6\u9891\u6570\u636E", { body: res });
|
|
1633
|
+
}
|
|
1634
|
+
const dir = resolve2(opts.out);
|
|
1635
|
+
await mkdir2(dir, { recursive: true });
|
|
1636
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1637
|
+
const extension = video.mimeType?.includes("webm") ? ".webm" : ".mp4";
|
|
1638
|
+
const file = join3(dir, `gemini-omni-${timestamp}${extension}`);
|
|
1639
|
+
await writeFile(file, Buffer.from(video.data, "base64"));
|
|
1640
|
+
if (g.json) {
|
|
1641
|
+
printJson({ interaction_id: res.id, file });
|
|
1642
|
+
} else {
|
|
1643
|
+
info(`\u2713 ${file}`);
|
|
1644
|
+
if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF1B\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "Grok \u89C6\u9891\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "Grok \u89C6\u9891\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u56FE\u751F\u89C6\u9891\u7684\u6E90\u56FE\u50CF URL\uFF0C\u53EF\u591A\u4E2A").option("--generate-audio <boolean>", "\u662F\u5426\u751F\u6210\u97F3\u9891\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "generate-audio")).option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--service-tier <tier>", "\u670D\u52A1\u5C42\u7EA7\uFF08Seedance 2.0 \u9ED8\u8BA4 default\uFF09").option("--priority <n>", "\u4EFB\u52A1\u4F18\u5148\u7EA7\uFF08\u4EC5 Seedance 2.0 \u7CFB\u5217\uFF09", (v) => Number.parseInt(v, 10)).option("--callback-url <url>", "\u4EFB\u52A1\u5B8C\u6210\u56DE\u8C03 URL").option("--return-last-frame <boolean>", "\u662F\u5426\u8FD4\u56DE\u6700\u540E\u4E00\u5E27\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "return-last-frame")).option("--execution-expires-after <seconds>", "\u4EFB\u52A1\u8FC7\u671F\u79D2\u6570\uFF083600\u2013259200\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-identifier <identifier>", "Seedance \u5B89\u5168\u6807\u8BC6\u7B26\uFF081\u201364 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09").option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u5B8C\u6210").option("--poll-interval <ms>", "\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--timeout <minutes>", "\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--content <json>", "Ark-compatible content JSON array; overrides prompt/image facade fields").action(
|
|
900
1648
|
async (promptParts, opts, cmd) => {
|
|
901
1649
|
const g = cmd.optsWithGlobals();
|
|
902
1650
|
const auth = resolveAuth(g);
|
|
903
|
-
const
|
|
1651
|
+
const model = opts.model ?? (await resolveCreativeModel(auth, "video")).model.id;
|
|
1652
|
+
if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u89C6\u9891\u6A21\u578B\uFF1A${model}`);
|
|
1653
|
+
const body = { model, prompt: promptParts.join(" ") };
|
|
1654
|
+
const metadata = {};
|
|
904
1655
|
if (opts.seconds !== void 0) {
|
|
905
|
-
|
|
1656
|
+
const seconds = clampInt(opts.seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds");
|
|
1657
|
+
body.duration = seconds;
|
|
906
1658
|
}
|
|
907
1659
|
if (opts.size) body.size = opts.size;
|
|
908
|
-
|
|
1660
|
+
if (opts.image) body.images = opts.image;
|
|
1661
|
+
if (opts.resolution) metadata.resolution = opts.resolution.toLowerCase();
|
|
1662
|
+
if (opts.ratio) metadata.ratio = opts.ratio;
|
|
1663
|
+
if (opts.aspectRatio) metadata.ratio = opts.aspectRatio;
|
|
1664
|
+
if (opts.seed !== void 0) metadata.seed = opts.seed;
|
|
1665
|
+
if (opts.generateAudio !== void 0) metadata.generate_audio = opts.generateAudio;
|
|
1666
|
+
if (opts.watermark !== void 0) metadata.watermark = opts.watermark;
|
|
1667
|
+
if (opts.serviceTier) metadata.service_tier = opts.serviceTier;
|
|
1668
|
+
if (opts.priority !== void 0) metadata.priority = opts.priority;
|
|
1669
|
+
if (opts.callbackUrl) metadata.callback_url = opts.callbackUrl;
|
|
1670
|
+
if (opts.returnLastFrame !== void 0) metadata.return_last_frame = opts.returnLastFrame;
|
|
1671
|
+
if (opts.executionExpiresAfter !== void 0) metadata.execution_expires_after = opts.executionExpiresAfter;
|
|
1672
|
+
if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
|
|
1673
|
+
if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
|
|
1674
|
+
validateVideoGeneration(model, {
|
|
1675
|
+
seconds: opts.seconds,
|
|
1676
|
+
resolution: opts.resolution,
|
|
1677
|
+
ratio: opts.ratio,
|
|
1678
|
+
aspectRatio: opts.aspectRatio,
|
|
1679
|
+
seed: opts.seed,
|
|
1680
|
+
serviceTier: opts.serviceTier,
|
|
1681
|
+
priority: opts.priority,
|
|
1682
|
+
executionExpiresAfter: opts.executionExpiresAfter,
|
|
1683
|
+
safetyIdentifier: opts.safetyIdentifier
|
|
1684
|
+
});
|
|
1685
|
+
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
1686
|
+
const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
|
|
909
1687
|
baseUrl: auth.baseUrl,
|
|
910
1688
|
path: "/v1/video/generations",
|
|
911
1689
|
apiKey: auth.apiKey,
|
|
912
1690
|
body,
|
|
913
1691
|
timeoutMs: 12e4
|
|
914
|
-
});
|
|
1692
|
+
}));
|
|
915
1693
|
const taskId = extractTaskId(created);
|
|
916
1694
|
if (!taskId) {
|
|
917
1695
|
throw new ApiError("bad_response", "\u89C6\u9891\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: created });
|
|
918
1696
|
}
|
|
919
1697
|
if (opts.wait === false) {
|
|
920
1698
|
if (g.json) {
|
|
921
|
-
printJson({ task_id: taskId, submitted: true });
|
|
1699
|
+
printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
922
1700
|
} else {
|
|
923
1701
|
process.stdout.write(taskId + "\n");
|
|
924
1702
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}`);
|
|
@@ -937,7 +1715,7 @@ function registerGen(program) {
|
|
|
937
1715
|
});
|
|
938
1716
|
const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
939
1717
|
if (g.json) {
|
|
940
|
-
printJson({ task_id: taskId, status: final.status, file: filePath });
|
|
1718
|
+
printJson({ model, task_id: taskId, status: final.status, file: filePath });
|
|
941
1719
|
} else {
|
|
942
1720
|
info(`\u2713 ${filePath}`);
|
|
943
1721
|
}
|
|
@@ -980,63 +1758,6 @@ function registerTask(program) {
|
|
|
980
1758
|
});
|
|
981
1759
|
}
|
|
982
1760
|
|
|
983
|
-
// src/commands/search.ts
|
|
984
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
985
|
-
function extractResults(raw) {
|
|
986
|
-
if (!raw || typeof raw !== "object") return [];
|
|
987
|
-
const obj = raw;
|
|
988
|
-
const candidates = [obj.results, obj.data, obj.items, obj.output];
|
|
989
|
-
for (const c of candidates) {
|
|
990
|
-
if (!Array.isArray(c)) continue;
|
|
991
|
-
const out = [];
|
|
992
|
-
for (const item of c) {
|
|
993
|
-
if (!item || typeof item !== "object") continue;
|
|
994
|
-
const it = item;
|
|
995
|
-
const url = typeof it.url === "string" ? it.url : typeof it.link === "string" ? it.link : "";
|
|
996
|
-
const title = typeof it.title === "string" ? it.title : url;
|
|
997
|
-
const snippet = typeof it.snippet === "string" ? it.snippet : typeof it.content === "string" ? it.content.slice(0, 200) : void 0;
|
|
998
|
-
if (url || title) out.push({ title, url, ...snippet ? { snippet } : {} });
|
|
999
|
-
}
|
|
1000
|
-
if (out.length > 0) return out;
|
|
1001
|
-
}
|
|
1002
|
-
return [];
|
|
1003
|
-
}
|
|
1004
|
-
function registerSearch(program) {
|
|
1005
|
-
program.command("search").description("\u8054\u7F51\u641C\u7D22\uFF08/v1/alpha/search\uFF0Calpha \u7EA7\u63A5\u53E3\uFF09").argument("<query...>", "\u641C\u7D22\u5185\u5BB9").requiredOption("-m, --model <model>", "\u641C\u7D22\u6A21\u578B ID\uFF08focalapi models list --filter search \u67E5\u770B\uFF09").option("--raw <json|@file>", "\u5B8C\u6574\u81EA\u5B9A\u4E49\u8BF7\u6C42\u4F53\uFF08JSON \u5B57\u7B26\u4E32\u6216 @\u6587\u4EF6\uFF09\uFF0C\u4E0E query \u5408\u5E76").action(async (queryParts, opts, cmd) => {
|
|
1006
|
-
const g = cmd.optsWithGlobals();
|
|
1007
|
-
const auth = resolveAuth(g);
|
|
1008
|
-
let body = { model: opts.model, query: queryParts.join(" ") };
|
|
1009
|
-
if (opts.raw) {
|
|
1010
|
-
const text = opts.raw.startsWith("@") ? readFileSync3(opts.raw.slice(1), "utf-8") : opts.raw;
|
|
1011
|
-
try {
|
|
1012
|
-
body = { ...JSON.parse(text), model: opts.model };
|
|
1013
|
-
} catch {
|
|
1014
|
-
throw new ApiError("invalid_request", "--raw \u4E0D\u662F\u5408\u6CD5 JSON");
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
const res = await request({
|
|
1018
|
-
baseUrl: auth.baseUrl,
|
|
1019
|
-
path: "/v1/alpha/search",
|
|
1020
|
-
apiKey: auth.apiKey,
|
|
1021
|
-
body,
|
|
1022
|
-
timeoutMs: 12e4
|
|
1023
|
-
});
|
|
1024
|
-
if (g.json) {
|
|
1025
|
-
printJson(res);
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
const results = extractResults(res);
|
|
1029
|
-
if (results.length === 0) {
|
|
1030
|
-
printJson(res);
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
printTable(
|
|
1034
|
-
["#", "\u6807\u9898", "\u94FE\u63A5"],
|
|
1035
|
-
results.map((r, i) => [String(i + 1), r.title.slice(0, 60), r.url])
|
|
1036
|
-
);
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
1761
|
// src/commands/audio.ts
|
|
1041
1762
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
1042
1763
|
import { resolve as resolve3 } from "path";
|
|
@@ -1087,77 +1808,15 @@ function registerAudio(program) {
|
|
|
1087
1808
|
});
|
|
1088
1809
|
}
|
|
1089
1810
|
|
|
1090
|
-
// src/commands/embed.ts
|
|
1091
|
-
function registerEmbed(program) {
|
|
1092
|
-
program.command("embed").description("\u6587\u672C\u5411\u91CF\u5316\uFF08/v1/embeddings\uFF09").argument("[text...]", "\u6587\u672C\uFF1B\u7701\u7565\u4E14 stdin \u4E3A\u7BA1\u9053\u65F6\u4ECE stdin \u8BFB\u53D6").requiredOption("-m, --model <model>", "\u5411\u91CF\u6A21\u578B ID").option("--input <file>", "\u4ECE\u6587\u4EF6\u8BFB\u53D6\u6587\u672C\uFF08@ \u524D\u7F00\u53EF\u9009\uFF09").action(async (textParts, opts, cmd) => {
|
|
1093
|
-
const g = cmd.optsWithGlobals();
|
|
1094
|
-
const auth = resolveAuth(g);
|
|
1095
|
-
let text = textParts.join(" ").trim();
|
|
1096
|
-
if (opts.input) {
|
|
1097
|
-
text = readInputFile(opts.input.replace(/^@/, "")).data.toString("utf-8");
|
|
1098
|
-
} else if (!text && !process.stdin.isTTY) {
|
|
1099
|
-
text = await readStdin();
|
|
1100
|
-
}
|
|
1101
|
-
if (!text) {
|
|
1102
|
-
throw new ApiError("invalid_request", "\u7F3A\u5C11\u5F85\u5411\u91CF\u5316\u6587\u672C", {
|
|
1103
|
-
hint: 'focalapi embed "\u6587\u672C" -m <model>\uFF0C\u6216 focalapi embed -m <model> --input @file.txt\u3002'
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
const res = await request({
|
|
1107
|
-
baseUrl: auth.baseUrl,
|
|
1108
|
-
path: "/v1/embeddings",
|
|
1109
|
-
apiKey: auth.apiKey,
|
|
1110
|
-
body: { model: opts.model, input: text },
|
|
1111
|
-
timeoutMs: 12e4
|
|
1112
|
-
});
|
|
1113
|
-
if (g.json) {
|
|
1114
|
-
printJson(res);
|
|
1115
|
-
} else {
|
|
1116
|
-
const vec = res.data?.[0]?.embedding ?? [];
|
|
1117
|
-
info(`\u7EF4\u5EA6\uFF1A${vec.length}\uFF1Btokens\uFF1A${res.usage?.total_tokens ?? "-"}`);
|
|
1118
|
-
info("\u5B8C\u6574\u5411\u91CF\u8BF7\u7528 --json \u8F93\u51FA\u3002");
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// src/commands/rerank.ts
|
|
1124
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1125
|
-
function registerRerank(program) {
|
|
1126
|
-
program.command("rerank").description("\u6309\u67E5\u8BE2\u5BF9\u6587\u6863\u91CD\u6392\u5E8F\uFF08/v1/rerank\uFF09").requiredOption("-m, --model <model>", "rerank \u6A21\u578B ID").requiredOption("--query <text>", "\u67E5\u8BE2").requiredOption("--docs <json|@file>", "\u6587\u6863\u6570\u7EC4\uFF08JSON \u5B57\u7B26\u4E32\u6216 @file.json\uFF09").option("--top-n <n>", "\u53EA\u8FD4\u56DE\u524D N \u6761", (v) => Number.parseInt(v, 10)).action(async (opts, cmd) => {
|
|
1127
|
-
const g = cmd.optsWithGlobals();
|
|
1128
|
-
const auth = resolveAuth(g);
|
|
1129
|
-
const text = opts.docs.startsWith("@") ? readFileSync4(opts.docs.slice(1), "utf-8") : opts.docs;
|
|
1130
|
-
let documents;
|
|
1131
|
-
try {
|
|
1132
|
-
documents = JSON.parse(text);
|
|
1133
|
-
} catch {
|
|
1134
|
-
throw new ApiError("invalid_request", "--docs \u4E0D\u662F\u5408\u6CD5 JSON \u6570\u7EC4");
|
|
1135
|
-
}
|
|
1136
|
-
if (!Array.isArray(documents) || documents.length === 0) {
|
|
1137
|
-
throw new ApiError("invalid_request", "--docs \u5FC5\u987B\u662F\u975E\u7A7A JSON \u6570\u7EC4");
|
|
1138
|
-
}
|
|
1139
|
-
const body = { model: opts.model, query: opts.query, documents };
|
|
1140
|
-
if (opts.topN !== void 0) body.top_n = opts.topN;
|
|
1141
|
-
const res = await request({
|
|
1142
|
-
baseUrl: auth.baseUrl,
|
|
1143
|
-
path: "/v1/rerank",
|
|
1144
|
-
apiKey: auth.apiKey,
|
|
1145
|
-
body,
|
|
1146
|
-
timeoutMs: 12e4
|
|
1147
|
-
});
|
|
1148
|
-
if (g.json) {
|
|
1149
|
-
printJson(res);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
const rows = (res.results ?? []).map((r) => {
|
|
1153
|
-
const doc = typeof r.document === "string" ? r.document : r.document?.text ?? "";
|
|
1154
|
-
return [String(r.index ?? "-"), String(r.relevance_score ?? "-"), doc.slice(0, 60)];
|
|
1155
|
-
});
|
|
1156
|
-
printTable(["\u539F\u6587\u6863\u5E8F\u53F7", "\u76F8\u5173\u5EA6", "\u6587\u6863\u9884\u89C8"], rows);
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
1811
|
// src/commands/usage.ts
|
|
1812
|
+
function formatBillingUsage(billing) {
|
|
1813
|
+
const value = billing.total_usage;
|
|
1814
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1815
|
+
return value.toLocaleString("zh-CN", { maximumFractionDigits: 4 });
|
|
1816
|
+
}
|
|
1817
|
+
if (typeof value === "string" && value.trim() !== "") return value;
|
|
1818
|
+
return "-";
|
|
1819
|
+
}
|
|
1161
1820
|
function defaultStartDate() {
|
|
1162
1821
|
const now = /* @__PURE__ */ new Date();
|
|
1163
1822
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
@@ -1191,226 +1850,441 @@ function registerUsage(program) {
|
|
|
1191
1850
|
["\u5DF2\u7528", String(token.total_used)],
|
|
1192
1851
|
["\u5269\u4F59", token.unlimited_quota ? "\u65E0\u9650" : String(token.total_available)],
|
|
1193
1852
|
["\u8FC7\u671F\u65F6\u95F4", token.expires_at > 0 ? new Date(token.expires_at * 1e3).toLocaleString() : "\u6C38\u4E0D\u8FC7\u671F"],
|
|
1194
|
-
[`\u5468\u671F\u7528\u91CF\uFF08${start} ~ ${end}\uFF09`,
|
|
1853
|
+
[`\u5468\u671F\u7528\u91CF\uFF08${start} ~ ${end}\uFF09`, formatBillingUsage(billing)],
|
|
1854
|
+
["\u8D26\u5355\u5BF9\u8C61", typeof billing.object === "string" ? billing.object : "-"]
|
|
1195
1855
|
]
|
|
1196
1856
|
);
|
|
1197
1857
|
});
|
|
1198
1858
|
}
|
|
1199
1859
|
|
|
1200
1860
|
// src/commands/connect.ts
|
|
1201
|
-
import {
|
|
1861
|
+
import { createHash, randomUUID } from "crypto";
|
|
1862
|
+
import {
|
|
1863
|
+
cpSync,
|
|
1864
|
+
existsSync as existsSync2,
|
|
1865
|
+
lstatSync,
|
|
1866
|
+
mkdirSync as mkdirSync2,
|
|
1867
|
+
readFileSync as readFileSync3,
|
|
1868
|
+
readdirSync,
|
|
1869
|
+
readlinkSync,
|
|
1870
|
+
renameSync,
|
|
1871
|
+
rmSync,
|
|
1872
|
+
writeFileSync as writeFileSync2
|
|
1873
|
+
} from "fs";
|
|
1202
1874
|
import { homedir as homedir2, platform } from "os";
|
|
1203
|
-
import { dirname, join as join4 } from "path";
|
|
1875
|
+
import { dirname, join as join4, relative, resolve as resolve4 } from "path";
|
|
1204
1876
|
import { fileURLToPath } from "url";
|
|
1205
|
-
var MANIFEST_NAME = ".focalapi-
|
|
1877
|
+
var MANIFEST_NAME = ".focalapi-managed-skills.json";
|
|
1878
|
+
var LEGACY_MANIFEST_NAME = ".focalapi-connect-manifest.json";
|
|
1879
|
+
var AGENTS = [
|
|
1880
|
+
{ id: "adal", name: "ADAL", skillsPath: [".adal", "skills"] },
|
|
1881
|
+
{ id: "amp", name: "Amp", skillsPath: [".config", "agents", "skills"], detectPaths: [[".amp"], [".config", "amp"]] },
|
|
1882
|
+
{ id: "antigravity", name: "Antigravity", skillsPath: [".gemini", "antigravity", "skills"] },
|
|
1883
|
+
{ id: "augment", name: "Augment", skillsPath: [".augment", "skills"] },
|
|
1884
|
+
{ id: "bob", name: "Bob", skillsPath: [".bob", "skills"] },
|
|
1885
|
+
{ id: "claude-code", name: "Claude Code", skillsPath: [".claude", "skills"] },
|
|
1886
|
+
{ id: "cline", name: "Cline", skillsPath: [".agents", "skills"], detectPaths: [[".cline"]] },
|
|
1887
|
+
{ id: "codebuddy", name: "CodeBuddy", skillsPath: [".codebuddy", "skills"] },
|
|
1888
|
+
{ id: "codex", name: "Codex", skillsPath: [".agents", "skills"], detectPaths: [[".codex"]] },
|
|
1889
|
+
{ id: "command-code", name: "Command Code", skillsPath: [".commandcode", "skills"] },
|
|
1890
|
+
{ id: "continue", name: "Continue", skillsPath: [".continue", "skills"] },
|
|
1891
|
+
{ id: "cortex", name: "Snowflake Cortex", skillsPath: [".snowflake", "cortex", "skills"] },
|
|
1892
|
+
{ id: "crush", name: "Crush", skillsPath: [".config", "crush", "skills"] },
|
|
1893
|
+
{ id: "cursor", name: "Cursor", skillsPath: [".cursor", "skills"] },
|
|
1894
|
+
{ id: "deepagents", name: "Deep Agents", skillsPath: [".deepagents", "agent", "skills"] },
|
|
1895
|
+
{ id: "droid", name: "Factory Droid", skillsPath: [".factory", "skills"] },
|
|
1896
|
+
{ id: "firebender", name: "Firebender", skillsPath: [".firebender", "skills"] },
|
|
1897
|
+
{ id: "gemini-cli", name: "Gemini CLI", skillsPath: [".gemini", "skills"] },
|
|
1898
|
+
{ id: "github-copilot", name: "GitHub Copilot", skillsPath: [".copilot", "skills"] },
|
|
1899
|
+
{ id: "goose", name: "Goose", skillsPath: [".config", "goose", "skills"] },
|
|
1900
|
+
{ id: "iflow-cli", name: "iFlow CLI", skillsPath: [".iflow", "skills"] },
|
|
1901
|
+
{ id: "junie", name: "Junie", skillsPath: [".junie", "skills"] },
|
|
1902
|
+
{ id: "kilo", name: "Kilo Code", skillsPath: [".kilocode", "skills"] },
|
|
1903
|
+
{ id: "kimi-cli", name: "Kimi CLI", skillsPath: [".config", "agents", "skills"], detectPaths: [[".kimi"], [".config", "kimi"]] },
|
|
1904
|
+
{ id: "kiro-cli", name: "Kiro CLI", skillsPath: [".kiro", "skills"] },
|
|
1905
|
+
{ id: "kode", name: "Kode", skillsPath: [".kode", "skills"] },
|
|
1906
|
+
{ id: "mcpjam", name: "MCPJam", skillsPath: [".mcpjam", "skills"] },
|
|
1907
|
+
{ id: "mistral-vibe", name: "Mistral Vibe", skillsPath: [".vibe", "skills"] },
|
|
1908
|
+
{ id: "mux", name: "Mux", skillsPath: [".mux", "skills"] },
|
|
1909
|
+
{ id: "neovate", name: "Neovate", skillsPath: [".neovate", "skills"] },
|
|
1910
|
+
{ id: "openclaw", name: "OpenClaw", skillsPath: [".openclaw", "skills"] },
|
|
1911
|
+
{ id: "opencode", name: "OpenCode", skillsPath: [".config", "opencode", "skills"] },
|
|
1912
|
+
{ id: "openhands", name: "OpenHands", skillsPath: [".openhands", "skills"] },
|
|
1913
|
+
{ id: "pi", name: "Pi", skillsPath: [".agents", "skills"], detectPaths: [[".pi", "agent"]] },
|
|
1914
|
+
{ id: "pochi", name: "Pochi", skillsPath: [".pochi", "skills"] },
|
|
1915
|
+
{ id: "qoder", name: "Qoder", skillsPath: [".qoder", "skills"] },
|
|
1916
|
+
{ id: "qwen-code", name: "Qwen Code", skillsPath: [".qwen", "skills"] },
|
|
1917
|
+
{ id: "roo", name: "Roo Code", skillsPath: [".roo", "skills"] },
|
|
1918
|
+
{ id: "trae", name: "Trae", skillsPath: [".trae", "skills"] },
|
|
1919
|
+
{ id: "trae-cn", name: "Trae CN", skillsPath: [".trae-cn", "skills"] },
|
|
1920
|
+
{ id: "warp", name: "Warp", skillsPath: [".agents", "skills"], detectPaths: [[".warp"]] },
|
|
1921
|
+
{ id: "windsurf", name: "Windsurf", skillsPath: [".codeium", "windsurf", "skills"] },
|
|
1922
|
+
{ id: "zencoder", name: "Zencoder", skillsPath: [".zencoder", "skills"] }
|
|
1923
|
+
];
|
|
1206
1924
|
function homeDir() {
|
|
1207
1925
|
return normalizeHomePath(process.env.FOCALAPI_HOME ?? homedir2());
|
|
1208
1926
|
}
|
|
1209
1927
|
function getTargets() {
|
|
1210
1928
|
const home = homeDir();
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
].join("\n")
|
|
1228
|
-
},
|
|
1229
|
-
{
|
|
1230
|
-
id: "codex",
|
|
1231
|
-
name: "Codex",
|
|
1232
|
-
skillsDir: join4(codexRoot, "skills"),
|
|
1233
|
-
detected: existsSync2(codexRoot),
|
|
1234
|
-
providerHint: [
|
|
1235
|
-
"Codex provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
|
|
1236
|
-
" \u5728 ~/.codex/config.toml \u52A0\u5165\uFF1A",
|
|
1237
|
-
" [model_providers.focalapi]",
|
|
1238
|
-
' name = "focalapi"',
|
|
1239
|
-
' base_url = "https://api.focalapi.com/v1"',
|
|
1240
|
-
' env_key = "FOCALAPI_API_KEY"',
|
|
1241
|
-
" \uFF08focalapi \u5DF2\u517C\u5BB9 /v1/responses \u4E0E /v1/chat/completions\uFF09"
|
|
1242
|
-
].join("\n")
|
|
1243
|
-
},
|
|
1244
|
-
{
|
|
1245
|
-
id: "opencode",
|
|
1246
|
-
name: "OpenCode",
|
|
1247
|
-
skillsDir: join4(opencodeRoot, "skills"),
|
|
1248
|
-
detected: existsSync2(opencodeRoot),
|
|
1249
|
-
providerHint: [
|
|
1250
|
-
"OpenCode provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
|
|
1251
|
-
" \u5728 opencode.json \u7684 provider \u6BB5\u52A0\u5165 openai-compatible \u63D0\u4F9B\u5546\uFF0C",
|
|
1252
|
-
' baseURL = "https://api.focalapi.com/v1"\uFF0CapiKey \u6307\u5411\u4F60\u7684 sk- key\u3002'
|
|
1253
|
-
].join("\n")
|
|
1254
|
-
},
|
|
1255
|
-
{
|
|
1256
|
-
id: "hermes",
|
|
1257
|
-
name: "Hermes",
|
|
1258
|
-
skillsDir: join4(hermesRoot, "skills"),
|
|
1259
|
-
detected: existsSync2(hermesRoot),
|
|
1260
|
-
providerHint: [
|
|
1261
|
-
"Hermes \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
|
|
1262
|
-
" \u6280\u80FD\u5DF2\u88C5\u5165\u9ED8\u8BA4 profile \u7684 skills \u76EE\u5F55\uFF1B\u975E\u9ED8\u8BA4 profile \u8BF7\u628A\u6280\u80FD\u76EE\u5F55\u590D\u5236\u5230",
|
|
1263
|
-
" hermes/profiles/<name>/skills/\u3002provider \u5728 config.yaml \u52A0 openai-compatible",
|
|
1264
|
-
' \u63D0\u4F9B\u5546\uFF0Cbase_url = "https://api.focalapi.com/v1"\u3002'
|
|
1265
|
-
].join("\n")
|
|
1266
|
-
}
|
|
1267
|
-
];
|
|
1929
|
+
const definitions = [...AGENTS];
|
|
1930
|
+
definitions.push({
|
|
1931
|
+
id: "hermes",
|
|
1932
|
+
name: "Hermes",
|
|
1933
|
+
skillsPath: platform() === "win32" ? ["AppData", "Local", "hermes", "skills"] : [".config", "hermes", "skills"]
|
|
1934
|
+
});
|
|
1935
|
+
return definitions.map((agent) => {
|
|
1936
|
+
const skillsDir = join4(home, ...agent.skillsPath);
|
|
1937
|
+
const detectPaths = agent.detectPaths ?? [agent.skillsPath.slice(0, -1)];
|
|
1938
|
+
return {
|
|
1939
|
+
id: agent.id,
|
|
1940
|
+
name: agent.name,
|
|
1941
|
+
skillsDir,
|
|
1942
|
+
detected: detectPaths.some((parts) => existsSync2(join4(home, ...parts)))
|
|
1943
|
+
};
|
|
1944
|
+
});
|
|
1268
1945
|
}
|
|
1269
1946
|
function bundledSkillsDir() {
|
|
1270
|
-
if (process.env.FOCALAPI_SKILLS_DIR)
|
|
1271
|
-
return process.env.FOCALAPI_SKILLS_DIR;
|
|
1272
|
-
}
|
|
1947
|
+
if (process.env.FOCALAPI_SKILLS_DIR) return normalizeHomePath(process.env.FOCALAPI_SKILLS_DIR);
|
|
1273
1948
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1274
1949
|
const candidates = [join4(here, "..", "skills"), join4(here, "..", "..", "skills")];
|
|
1275
1950
|
for (const dir of candidates) {
|
|
1276
|
-
if (existsSync2(join4(dir, "focalapi", "SKILL.md")))
|
|
1277
|
-
return dir;
|
|
1278
|
-
}
|
|
1951
|
+
if (existsSync2(join4(dir, "focalapi", "SKILL.md"))) return dir;
|
|
1279
1952
|
}
|
|
1280
|
-
throw new ApiError("internal_error", "\u672A\u627E\u5230\u5185\u7F6E
|
|
1953
|
+
throw new ApiError("internal_error", "\u672A\u627E\u5230\u5185\u7F6E Skills\uFF08focalapi/SKILL.md \u7F3A\u5931\uFF09");
|
|
1281
1954
|
}
|
|
1282
1955
|
function listBundledSkills(srcDir) {
|
|
1283
1956
|
const dir = srcDir ?? bundledSkillsDir();
|
|
1284
|
-
return readdirSync(dir, { withFileTypes: true }).filter((
|
|
1957
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("focalapi") && existsSync2(join4(dir, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
|
|
1285
1958
|
}
|
|
1286
1959
|
function manifestPath(skillsDir) {
|
|
1287
1960
|
return join4(skillsDir, MANIFEST_NAME);
|
|
1288
1961
|
}
|
|
1289
1962
|
function readManifest(skillsDir) {
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1963
|
+
for (const name of [MANIFEST_NAME, LEGACY_MANIFEST_NAME]) {
|
|
1964
|
+
const path = join4(skillsDir, name);
|
|
1965
|
+
if (!existsSync2(path)) continue;
|
|
1966
|
+
try {
|
|
1967
|
+
const manifest = JSON.parse(readFileSync3(path, "utf8"));
|
|
1968
|
+
if (manifest.tool === "focalapi-cli" && Array.isArray(manifest.skills)) return manifest;
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return void 0;
|
|
1973
|
+
}
|
|
1974
|
+
function digestDirectory(root) {
|
|
1975
|
+
const hash = createHash("sha256");
|
|
1976
|
+
const walk = (dir) => {
|
|
1977
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1978
|
+
for (const entry of entries) {
|
|
1979
|
+
const path = join4(dir, entry.name);
|
|
1980
|
+
const name = relative(root, path).replaceAll("\\", "/");
|
|
1981
|
+
const stat = lstatSync(path);
|
|
1982
|
+
if (stat.isSymbolicLink()) {
|
|
1983
|
+
hash.update(`link\0${name}\0${readlinkSync(path)}\0`);
|
|
1984
|
+
} else if (stat.isDirectory()) {
|
|
1985
|
+
hash.update(`dir\0${name}\0`);
|
|
1986
|
+
walk(path);
|
|
1987
|
+
} else if (stat.isFile()) {
|
|
1988
|
+
hash.update(`file\0${name}\0`);
|
|
1989
|
+
hash.update(readFileSync3(path));
|
|
1990
|
+
hash.update("\0");
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
walk(root);
|
|
1995
|
+
return hash.digest("hex");
|
|
1996
|
+
}
|
|
1997
|
+
function isAuthConfigured() {
|
|
1998
|
+
if (process.env.FOCALAPI_API_KEY) return true;
|
|
1292
1999
|
try {
|
|
1293
|
-
return
|
|
2000
|
+
return Object.values(loadConfig().profiles).some((profile) => Boolean(profile.apiKey));
|
|
1294
2001
|
} catch {
|
|
1295
|
-
return
|
|
2002
|
+
return false;
|
|
1296
2003
|
}
|
|
1297
2004
|
}
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
for (const
|
|
1301
|
-
|
|
2005
|
+
function groupTargets(targets) {
|
|
2006
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2007
|
+
for (const target of targets) {
|
|
2008
|
+
const key = resolve4(target.skillsDir).toLowerCase();
|
|
2009
|
+
grouped.set(key, [...grouped.get(key) ?? [], target]);
|
|
1302
2010
|
}
|
|
1303
|
-
|
|
1304
|
-
tool: "focalapi-cli",
|
|
1305
|
-
version: VERSION,
|
|
1306
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1307
|
-
skills
|
|
1308
|
-
};
|
|
1309
|
-
writeFileSync2(manifestPath(target.skillsDir), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
1310
|
-
return manifest;
|
|
2011
|
+
return [...grouped.values()].map((agents) => ({ agents, skillsDir: agents[0].skillsDir }));
|
|
1311
2012
|
}
|
|
1312
|
-
function
|
|
1313
|
-
const
|
|
1314
|
-
if (
|
|
1315
|
-
|
|
2013
|
+
function installTo(skillsDir, agents, skills, srcDir) {
|
|
2014
|
+
const sourceRoot = resolve4(srcDir).toLowerCase();
|
|
2015
|
+
if (resolve4(skillsDir).toLowerCase() === sourceRoot) {
|
|
2016
|
+
throw new ApiError("invalid_request", "\u5B89\u88C5\u76EE\u6807\u4E0D\u80FD\u662F focalapi-cli \u81EA\u5E26 Skills \u6E90\u76EE\u5F55");
|
|
1316
2017
|
}
|
|
2018
|
+
mkdirSync2(skillsDir, { recursive: true });
|
|
2019
|
+
const oldManifest = readManifest(skillsDir);
|
|
2020
|
+
const transactionRoot = join4(skillsDir, `.focalapi-install-${randomUUID()}`);
|
|
2021
|
+
const stageRoot = join4(transactionRoot, "stage");
|
|
2022
|
+
const backupRoot = join4(transactionRoot, "backup");
|
|
2023
|
+
mkdirSync2(stageRoot, { recursive: true });
|
|
2024
|
+
mkdirSync2(backupRoot, { recursive: true });
|
|
2025
|
+
const digests = {};
|
|
2026
|
+
const touched = [];
|
|
2027
|
+
let oldManifestBackup;
|
|
2028
|
+
let oldLegacyManifestBackup;
|
|
2029
|
+
try {
|
|
2030
|
+
for (const skill of skills) {
|
|
2031
|
+
const staged = join4(stageRoot, skill);
|
|
2032
|
+
cpSync(join4(srcDir, skill), staged, { recursive: true, force: true });
|
|
2033
|
+
digests[skill] = digestDirectory(staged);
|
|
2034
|
+
}
|
|
2035
|
+
const currentManifest = manifestPath(skillsDir);
|
|
2036
|
+
if (existsSync2(currentManifest)) {
|
|
2037
|
+
oldManifestBackup = join4(backupRoot, MANIFEST_NAME);
|
|
2038
|
+
renameSync(currentManifest, oldManifestBackup);
|
|
2039
|
+
}
|
|
2040
|
+
const legacyManifest = join4(skillsDir, LEGACY_MANIFEST_NAME);
|
|
2041
|
+
if (existsSync2(legacyManifest)) {
|
|
2042
|
+
oldLegacyManifestBackup = join4(backupRoot, LEGACY_MANIFEST_NAME);
|
|
2043
|
+
renameSync(legacyManifest, oldLegacyManifestBackup);
|
|
2044
|
+
}
|
|
2045
|
+
for (const skill of skills) {
|
|
2046
|
+
const destination = join4(skillsDir, skill);
|
|
2047
|
+
const backup = join4(backupRoot, skill);
|
|
2048
|
+
if (existsSync2(destination)) {
|
|
2049
|
+
renameSync(destination, backup);
|
|
2050
|
+
touched.push({ destination, backup });
|
|
2051
|
+
} else {
|
|
2052
|
+
touched.push({ destination });
|
|
2053
|
+
}
|
|
2054
|
+
renameSync(join4(stageRoot, skill), destination);
|
|
2055
|
+
}
|
|
2056
|
+
for (const retired of oldManifest?.skills ?? []) {
|
|
2057
|
+
if (skills.includes(retired) || !retired.startsWith("focalapi")) continue;
|
|
2058
|
+
const destination = join4(skillsDir, retired);
|
|
2059
|
+
const expected = oldManifest?.digests?.[retired];
|
|
2060
|
+
if (!expected || !existsSync2(destination) || digestDirectory(destination) !== expected) continue;
|
|
2061
|
+
const backup = join4(backupRoot, `retired-${retired}`);
|
|
2062
|
+
renameSync(destination, backup);
|
|
2063
|
+
touched.push({ destination, backup });
|
|
2064
|
+
}
|
|
2065
|
+
const manifest = {
|
|
2066
|
+
tool: "focalapi-cli",
|
|
2067
|
+
schema: 2,
|
|
2068
|
+
version: VERSION,
|
|
2069
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2070
|
+
agents,
|
|
2071
|
+
skills,
|
|
2072
|
+
digests
|
|
2073
|
+
};
|
|
2074
|
+
const stagedManifest = join4(transactionRoot, MANIFEST_NAME);
|
|
2075
|
+
writeFileSync2(stagedManifest, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
2076
|
+
renameSync(stagedManifest, currentManifest);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
rmSync(manifestPath(skillsDir), { force: true });
|
|
2079
|
+
for (const item of touched.reverse()) {
|
|
2080
|
+
rmSync(item.destination, { recursive: true, force: true });
|
|
2081
|
+
if (item.backup && existsSync2(item.backup)) renameSync(item.backup, item.destination);
|
|
2082
|
+
}
|
|
2083
|
+
if (oldManifestBackup && existsSync2(oldManifestBackup)) renameSync(oldManifestBackup, manifestPath(skillsDir));
|
|
2084
|
+
if (oldLegacyManifestBackup && existsSync2(oldLegacyManifestBackup)) {
|
|
2085
|
+
renameSync(oldLegacyManifestBackup, join4(skillsDir, LEGACY_MANIFEST_NAME));
|
|
2086
|
+
}
|
|
2087
|
+
throw error;
|
|
2088
|
+
} finally {
|
|
2089
|
+
rmSync(transactionRoot, { recursive: true, force: true });
|
|
2090
|
+
}
|
|
2091
|
+
return { agents, skillsDir, skills, version: VERSION };
|
|
2092
|
+
}
|
|
2093
|
+
function uninstallFrom(skillsDir, srcDir) {
|
|
2094
|
+
const manifest = readManifest(skillsDir);
|
|
2095
|
+
if (!manifest) return { removed: [], preserved: [], hadManifest: false };
|
|
1317
2096
|
const removed = [];
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (skill.startsWith("focalapi")
|
|
1321
|
-
|
|
1322
|
-
|
|
2097
|
+
const preserved = [];
|
|
2098
|
+
for (const skill of manifest.skills ?? []) {
|
|
2099
|
+
if (!skill.startsWith("focalapi")) continue;
|
|
2100
|
+
const dir = join4(skillsDir, skill);
|
|
2101
|
+
if (!existsSync2(dir)) continue;
|
|
2102
|
+
const expected = manifest.digests?.[skill] ?? (existsSync2(join4(srcDir, skill)) ? digestDirectory(join4(srcDir, skill)) : void 0);
|
|
2103
|
+
if (!expected || digestDirectory(dir) !== expected) {
|
|
2104
|
+
preserved.push(skill);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2108
|
+
removed.push(skill);
|
|
2109
|
+
}
|
|
2110
|
+
rmSync(manifestPath(skillsDir), { force: true });
|
|
2111
|
+
rmSync(join4(skillsDir, LEGACY_MANIFEST_NAME), { force: true });
|
|
2112
|
+
return { removed, preserved, hadManifest: true };
|
|
2113
|
+
}
|
|
2114
|
+
function verifyTarget(skillsDir) {
|
|
2115
|
+
const manifest = readManifest(skillsDir);
|
|
2116
|
+
if (!manifest) return { installed: false, valid: false, missing: [], modified: [] };
|
|
2117
|
+
const missing = [];
|
|
2118
|
+
const modified = [];
|
|
2119
|
+
for (const skill of manifest.skills ?? []) {
|
|
2120
|
+
const dir = join4(skillsDir, skill);
|
|
2121
|
+
if (!existsSync2(dir)) {
|
|
2122
|
+
missing.push(skill);
|
|
2123
|
+
continue;
|
|
1323
2124
|
}
|
|
2125
|
+
const expected = manifest.digests?.[skill];
|
|
2126
|
+
if (!expected || digestDirectory(dir) !== expected) modified.push(skill);
|
|
2127
|
+
}
|
|
2128
|
+
return {
|
|
2129
|
+
installed: true,
|
|
2130
|
+
valid: missing.length === 0 && modified.length === 0,
|
|
2131
|
+
version: manifest.version,
|
|
2132
|
+
missing,
|
|
2133
|
+
modified
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
function customTarget(path) {
|
|
2137
|
+
const skillsDir = resolve4(normalizeHomePath(path));
|
|
2138
|
+
if (dirname(skillsDir) === skillsDir || skillsDir.toLowerCase() === resolve4(homeDir()).toLowerCase()) {
|
|
2139
|
+
throw new ApiError("invalid_request", "--path \u5FC5\u987B\u6307\u5411\u5177\u4F53\u7684 Skills \u76EE\u5F55\uFF0C\u4E0D\u80FD\u662F\u78C1\u76D8\u6839\u76EE\u5F55\u6216\u7528\u6237\u4E3B\u76EE\u5F55");
|
|
1324
2140
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
2141
|
+
return {
|
|
2142
|
+
id: "custom",
|
|
2143
|
+
name: "Custom Skills Directory",
|
|
2144
|
+
skillsDir,
|
|
2145
|
+
detected: true
|
|
2146
|
+
};
|
|
1327
2147
|
}
|
|
1328
|
-
function resolveTargets(targetIds, g) {
|
|
2148
|
+
function resolveTargets(targetIds, path, g) {
|
|
2149
|
+
if (path) {
|
|
2150
|
+
if (targetIds.length > 0) {
|
|
2151
|
+
throw new ApiError("invalid_request", "--path \u4E0E Agent ID \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
|
|
2152
|
+
}
|
|
2153
|
+
return [customTarget(path)];
|
|
2154
|
+
}
|
|
1329
2155
|
const all = getTargets();
|
|
1330
|
-
if (targetIds
|
|
1331
|
-
const unknown = targetIds.filter((id) => !all.some((
|
|
2156
|
+
if (targetIds.length > 0) {
|
|
2157
|
+
const unknown = targetIds.filter((id) => !all.some((target) => target.id === id));
|
|
1332
2158
|
if (unknown.length > 0) {
|
|
1333
2159
|
throw new ApiError("invalid_request", `\u672A\u77E5 Agent\uFF1A${unknown.join(", ")}`, {
|
|
1334
|
-
hint: `\u53EF\u9009\uFF1A${all.map((
|
|
2160
|
+
hint: `\u53EF\u9009\uFF1A${all.map((target) => target.id).join(" | ")}\uFF1B\u672A\u77E5 Agent \u53EF\u4F7F\u7528 --path <skills-dir>\u3002`
|
|
1335
2161
|
});
|
|
1336
2162
|
}
|
|
1337
|
-
return all.filter((
|
|
2163
|
+
return all.filter((target) => targetIds.includes(target.id));
|
|
1338
2164
|
}
|
|
1339
|
-
const detected = all.filter((
|
|
2165
|
+
const detected = all.filter((target) => target.detected);
|
|
1340
2166
|
if (detected.length === 0) {
|
|
1341
|
-
throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\
|
|
1342
|
-
hint:
|
|
2167
|
+
throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\u672C\u673A Agent", {
|
|
2168
|
+
hint: "\u8FD0\u884C focalapi connect list \u67E5\u770B\u652F\u6301\u5217\u8868\uFF0C\u6216\u4F7F\u7528 focalapi connect install --path <skills-dir>\u3002"
|
|
1343
2169
|
});
|
|
1344
2170
|
}
|
|
1345
|
-
if (!g.json) {
|
|
1346
|
-
info(`\u68C0\u6D4B\u5230 ${detected.length} \u4E2A Agent\uFF1A${detected.map((t) => t.name).join("\u3001")}`);
|
|
1347
|
-
}
|
|
2171
|
+
if (!g.json) info(`\u68C0\u6D4B\u5230 ${detected.length} \u4E2A Agent\uFF0C\u5C06\u6309 ${groupTargets(detected).length} \u4E2A\u6280\u80FD\u76EE\u5F55\u5B89\u88C5\u3002`);
|
|
1348
2172
|
return detected;
|
|
1349
2173
|
}
|
|
2174
|
+
function installCommand(targetIds, path, g) {
|
|
2175
|
+
const targets = resolveTargets(targetIds, path, g);
|
|
2176
|
+
const srcDir = bundledSkillsDir();
|
|
2177
|
+
const skills = listBundledSkills(srcDir);
|
|
2178
|
+
const results = groupTargets(targets).map((group) => installTo(
|
|
2179
|
+
group.skillsDir,
|
|
2180
|
+
group.agents.map((agent) => agent.id),
|
|
2181
|
+
skills,
|
|
2182
|
+
srcDir
|
|
2183
|
+
));
|
|
2184
|
+
const authConfigured = isAuthConfigured();
|
|
2185
|
+
const nextSteps = [
|
|
2186
|
+
...!authConfigured ? ["focalapi auth login --key <sk-...>"] : [],
|
|
2187
|
+
"\u91CD\u542F Agent \u4F1A\u8BDD",
|
|
2188
|
+
"\u76F4\u63A5\u63CF\u8FF0\u521B\u4F5C\u4EFB\u52A1\uFF0C\u4F8B\u5982\u201C\u751F\u6210\u4E00\u5F20\u4EA7\u54C1\u4E3B\u89C6\u89C9\u201D"
|
|
2189
|
+
];
|
|
2190
|
+
if (g.json) {
|
|
2191
|
+
printJson({
|
|
2192
|
+
installed: results,
|
|
2193
|
+
auth_configured: authConfigured,
|
|
2194
|
+
ready: authConfigured,
|
|
2195
|
+
restart_required: true,
|
|
2196
|
+
next_steps: nextSteps
|
|
2197
|
+
});
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
for (const result of results) {
|
|
2201
|
+
info(`\u2713 ${result.agents.join(" / ")}\uFF1A${result.skills.length} \u4E2A Skills \u5DF2\u88C5\u5165 ${result.skillsDir}`);
|
|
2202
|
+
}
|
|
2203
|
+
info(authConfigured ? "\u63A5\u5165\u5DF2\u95ED\u73AF\uFF1A\u91CD\u542F Agent \u540E\uFF0C\u76F4\u63A5\u63CF\u8FF0\u56FE\u7247\u6216\u89C6\u9891\u4EFB\u52A1\u5373\u53EF\uFF1B\u65E0\u9700\u70B9\u540D focalapi \u6216\u5148\u8BD5\u6A21\u578B\u3002" : "Skills \u5DF2\u5B89\u88C5\uFF1B\u8FD8\u9700\u8FD0\u884C focalapi auth login --key <sk-...>\uFF0C\u7136\u540E\u91CD\u542F Agent\u3002");
|
|
2204
|
+
}
|
|
1350
2205
|
function registerConnect(program) {
|
|
1351
|
-
const connect = program.command("connect").description("\
|
|
1352
|
-
|
|
2206
|
+
const connect = program.command("connect").description("\u8BA9\u672C\u673A AI Agent \u81EA\u52A8\u8C03\u7528 focalapi \u521B\u4F5C\u6A21\u578B\uFF08\u4E0D\u6539 Agent \u7684\u4E3B\u6A21\u578B/provider\uFF09").option("--path <skills-dir>", "\u5B89\u88C5\u5230\u6307\u5B9A Skills \u76EE\u5F55\uFF0C\u4E0D\u626B\u63CF\u5168\u5C40 Agent").action(async (opts, cmd) => {
|
|
2207
|
+
installCommand([], opts.path, cmd.optsWithGlobals());
|
|
2208
|
+
});
|
|
2209
|
+
connect.command("list").description("\u5217\u51FA\u652F\u6301\u7684 Agent \u53CA\u68C0\u6D4B/\u5B89\u88C5\u72B6\u6001\uFF08\u53EA\u8BFB\uFF09").action(async (_opts, cmd) => {
|
|
1353
2210
|
const g = cmd.optsWithGlobals();
|
|
1354
|
-
const rows = getTargets().map((
|
|
1355
|
-
const
|
|
2211
|
+
const rows = getTargets().map((target) => {
|
|
2212
|
+
const status = verifyTarget(target.skillsDir);
|
|
1356
2213
|
return {
|
|
1357
|
-
id:
|
|
1358
|
-
name:
|
|
1359
|
-
detected:
|
|
1360
|
-
|
|
1361
|
-
installed:
|
|
2214
|
+
id: target.id,
|
|
2215
|
+
name: target.name,
|
|
2216
|
+
detected: target.detected,
|
|
2217
|
+
skills_dir: target.skillsDir,
|
|
2218
|
+
installed: status.installed,
|
|
2219
|
+
valid: status.valid,
|
|
2220
|
+
version: status.version
|
|
1362
2221
|
};
|
|
1363
2222
|
});
|
|
1364
2223
|
if (g.json) {
|
|
1365
|
-
printJson({ agents: rows });
|
|
2224
|
+
printJson({ supported: rows.length, agents: rows });
|
|
1366
2225
|
} else {
|
|
1367
2226
|
printTable(
|
|
1368
|
-
["Agent", "ID", "\u68C0\u6D4B\u5230", "\
|
|
1369
|
-
rows.map((
|
|
2227
|
+
["Agent", "ID", "\u68C0\u6D4B\u5230", "\u5B89\u88C5\u72B6\u6001", "\u6280\u80FD\u76EE\u5F55"],
|
|
2228
|
+
rows.map((row) => [
|
|
2229
|
+
row.name,
|
|
2230
|
+
row.id,
|
|
2231
|
+
row.detected ? "\u2713" : "-",
|
|
2232
|
+
row.valid ? `\u2713 v${row.version}` : row.installed ? "\u9700\u4FEE\u590D" : "-",
|
|
2233
|
+
row.skills_dir
|
|
2234
|
+
])
|
|
1370
2235
|
);
|
|
1371
2236
|
}
|
|
1372
2237
|
});
|
|
1373
|
-
connect.command("install").description("\
|
|
2238
|
+
connect.command("install").description("\u4E8B\u52A1\u5F0F\u5B89\u88C5/\u4FEE\u590D Skills\uFF1B\u7701\u7565 Agent ID \u65F6\u5904\u7406\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684 Agent").argument("[targets...]", "Agent ID\uFF0C\u5982 claude-code codex").option("--path <skills-dir>", "\u5B89\u88C5\u5230\u6307\u5B9A Skills \u76EE\u5F55\uFF0C\u4E0D\u626B\u63CF\u5168\u5C40 Agent").action(async (targetIds, opts, cmd) => {
|
|
2239
|
+
installCommand(targetIds, opts.path ?? cmd.parent?.opts().path, cmd.optsWithGlobals());
|
|
2240
|
+
});
|
|
2241
|
+
connect.command("verify").description("\u9A8C\u8BC1 Skills \u5B8C\u6574\u6027\u3001\u8BA4\u8BC1\u5C31\u7EEA\u72B6\u6001\u4E0E\u9700\u8981\u91CD\u542F\u7684 Agent").argument("[targets...]", "Agent ID\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684").option("--path <skills-dir>", "\u9A8C\u8BC1\u6307\u5B9A Skills \u76EE\u5F55").action(async (targetIds, opts, cmd) => {
|
|
1374
2242
|
const g = cmd.optsWithGlobals();
|
|
1375
|
-
const targets = resolveTargets(targetIds, g);
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
2243
|
+
const targets = resolveTargets(targetIds, opts.path ?? cmd.parent?.opts().path, g);
|
|
2244
|
+
const rows = groupTargets(targets).map((group) => ({
|
|
2245
|
+
agents: group.agents.map((agent) => agent.id),
|
|
2246
|
+
skills_dir: group.skillsDir,
|
|
2247
|
+
...verifyTarget(group.skillsDir)
|
|
2248
|
+
}));
|
|
2249
|
+
const authConfigured = isAuthConfigured();
|
|
2250
|
+
const valid = rows.every((row) => row.valid);
|
|
2251
|
+
const result = {
|
|
2252
|
+
valid,
|
|
2253
|
+
auth_configured: authConfigured,
|
|
2254
|
+
ready: valid && authConfigured,
|
|
2255
|
+
targets: rows,
|
|
2256
|
+
next_steps: [
|
|
2257
|
+
...!valid ? ["focalapi connect install"] : [],
|
|
2258
|
+
...!authConfigured ? ["focalapi auth login --key <sk-...>"] : [],
|
|
2259
|
+
...valid && authConfigured ? ["\u91CD\u542F Agent \u4F1A\u8BDD\u540E\u76F4\u63A5\u63CF\u8FF0\u521B\u4F5C\u4EFB\u52A1"] : []
|
|
2260
|
+
]
|
|
2261
|
+
};
|
|
1379
2262
|
if (g.json) {
|
|
1380
|
-
printJson(
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
info("");
|
|
1389
|
-
info("\u6280\u80FD\u5DF2\u5C31\u7EEA\u3002\u8981\u8BA9 Agent \u76F4\u63A5\u4EE5 focalapi \u4E3A\u6A21\u578B\u540E\u7AEF\uFF0C\u8FD8\u9700\u914D\u7F6E provider\uFF08v1 \u8BF7\u624B\u52A8\uFF09\uFF1A");
|
|
1390
|
-
for (const { target } of results) {
|
|
1391
|
-
info("");
|
|
1392
|
-
info(target.providerHint);
|
|
2263
|
+
printJson(result);
|
|
2264
|
+
} else {
|
|
2265
|
+
printTable(
|
|
2266
|
+
["Agent", "Skills", "\u8BA4\u8BC1", "\u5C31\u7EEA"],
|
|
2267
|
+
rows.map((row) => [row.agents.join(" / "), row.valid ? "\u2713" : "\u9700\u4FEE\u590D", authConfigured ? "\u2713" : "\u672A\u914D\u7F6E", row.valid && authConfigured ? "\u2713" : "-"])
|
|
2268
|
+
);
|
|
2269
|
+
for (const step of result.next_steps) info(`\u4E0B\u4E00\u6B65\uFF1A${step}`);
|
|
1393
2270
|
}
|
|
1394
|
-
|
|
1395
|
-
info("\u5B8C\u6210\u540E\u91CD\u542F Agent \u4F1A\u8BDD\uFF0C\u5373\u53EF\u7528\u81EA\u7136\u8BED\u8A00\u8BA9\u5B83\u8C03\u7528 focalapi\uFF08\u5982\u300C\u7528 focalapi \u753B\u4E00\u5F20\u2026\u2026\u300D\uFF09\u3002");
|
|
1396
|
-
info("\u5378\u8F7D\uFF1Afocalapi connect uninstall");
|
|
2271
|
+
if (!result.ready) process.exitCode = 1;
|
|
1397
2272
|
});
|
|
1398
|
-
connect.command("uninstall").description("\
|
|
2273
|
+
connect.command("uninstall").description("\u5378\u8F7D\u672A\u88AB\u7528\u6237\u4FEE\u6539\u7684\u6258\u7BA1 Skills\uFF1B\u5176\u4ED6\u6587\u4EF6\u4E00\u5F8B\u4FDD\u7559").argument("[targets...]", "Agent ID\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684").option("--path <skills-dir>", "\u4ECE\u6307\u5B9A Skills \u76EE\u5F55\u5378\u8F7D").action(async (targetIds, opts, cmd) => {
|
|
1399
2274
|
const g = cmd.optsWithGlobals();
|
|
1400
|
-
const targets = resolveTargets(targetIds, g);
|
|
1401
|
-
const
|
|
2275
|
+
const targets = resolveTargets(targetIds, opts.path ?? cmd.parent?.opts().path, g);
|
|
2276
|
+
const srcDir = bundledSkillsDir();
|
|
2277
|
+
const results = groupTargets(targets).map((group) => ({
|
|
2278
|
+
agents: group.agents.map((agent) => agent.id),
|
|
2279
|
+
skills_dir: group.skillsDir,
|
|
2280
|
+
...uninstallFrom(group.skillsDir, srcDir)
|
|
2281
|
+
}));
|
|
1402
2282
|
if (g.json) {
|
|
1403
|
-
printJson({
|
|
1404
|
-
uninstalled: results.map((r) => ({ agent: r.target.id, removed: r.removed, hadManifest: r.hadManifest }))
|
|
1405
|
-
});
|
|
2283
|
+
printJson({ uninstalled: results });
|
|
1406
2284
|
return;
|
|
1407
2285
|
}
|
|
1408
|
-
for (const
|
|
1409
|
-
|
|
1410
|
-
info(`- ${r.target.name}\uFF1A\u65E0 focalapi \u5B89\u88C5\u8BB0\u5F55\uFF0C\u8DF3\u8FC7`);
|
|
1411
|
-
} else {
|
|
1412
|
-
info(`\u2713 ${r.target.name}\uFF1A\u5DF2\u79FB\u9664 ${r.removed.length} \u4E2A\u6280\u80FD`);
|
|
1413
|
-
}
|
|
2286
|
+
for (const result of results) {
|
|
2287
|
+
info(`\u2713 ${result.agents.join(" / ")}\uFF1A\u79FB\u9664 ${result.removed.length} \u4E2A\uFF0C\u4FDD\u7559\u7528\u6237\u4FEE\u6539 ${result.preserved.length} \u4E2A`);
|
|
1414
2288
|
}
|
|
1415
2289
|
});
|
|
1416
2290
|
}
|
|
@@ -1513,16 +2387,13 @@ function registerRequest(program) {
|
|
|
1513
2387
|
// src/cli.ts
|
|
1514
2388
|
function buildProgram() {
|
|
1515
2389
|
const program = new Command();
|
|
1516
|
-
program.name("focalapi").description("\
|
|
2390
|
+
program.name("focalapi").description("\u8BA9 AI Agent \u76F4\u63A5\u8C03\u7528 focalapi \u521B\u4F5C\u6A21\u578B\uFF1A\u81EA\u52A8\u9009\u6A21\u3001\u751F\u6210\u3001\u4EFB\u52A1\u7EED\u53D6\u4E0E\u7528\u91CF\u8BCA\u65AD").version(VERSION, "-v, --version", "\u663E\u793A\u7248\u672C\u53F7").option("--json", "\u4EE5 JSON \u8F93\u51FA\uFF08\u9762\u5411 Agent \u4E0E\u811A\u672C\uFF0Cstdout \u7EAF\u51C0\uFF09").option("--base-url <url>", "\u8986\u76D6 API \u5730\u5740\uFF08\u9ED8\u8BA4 https://api.focalapi.com\uFF0C\u53EF\u7528 FOCALAPI_BASE_URL\uFF09").option("--key <key>", "\u8986\u76D6 API Key\uFF08\u53EF\u7528 FOCALAPI_API_KEY\uFF09").option("--profile <name>", "\u4F7F\u7528\u6307\u5B9A\u914D\u7F6E\u6863\u6848");
|
|
1517
2391
|
registerAuth(program);
|
|
1518
2392
|
registerModels(program);
|
|
1519
2393
|
registerChat(program);
|
|
1520
2394
|
registerGen(program);
|
|
1521
2395
|
registerTask(program);
|
|
1522
|
-
registerSearch(program);
|
|
1523
2396
|
registerAudio(program);
|
|
1524
|
-
registerEmbed(program);
|
|
1525
|
-
registerRerank(program);
|
|
1526
2397
|
registerUsage(program);
|
|
1527
2398
|
registerDoctor(program);
|
|
1528
2399
|
registerConnect(program);
|
|
@@ -1531,10 +2402,11 @@ function buildProgram() {
|
|
|
1531
2402
|
return program;
|
|
1532
2403
|
}
|
|
1533
2404
|
async function main(argv = process.argv) {
|
|
2405
|
+
process.exitCode = 0;
|
|
1534
2406
|
const program = buildProgram();
|
|
1535
2407
|
try {
|
|
1536
2408
|
await program.parseAsync(argv);
|
|
1537
|
-
return 0;
|
|
2409
|
+
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
1538
2410
|
} catch (err) {
|
|
1539
2411
|
let json = false;
|
|
1540
2412
|
try {
|