focalapi-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cli.js +166 -29
- package/package.json +1 -1
- package/skills/focalapi/SKILL.md +4 -4
- package/skills/focalapi-gen/SKILL.md +2 -0
- package/skills/focalapi-task/SKILL.md +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
- Added `task status --wait [--download]`: built-in bounded polling with elapsed-time reporting and an optional auto-download, so agents never need to write their own poller (the audited agent session's custom script crashed on the Windows `.cmd` shim and stalled blind).
|
|
6
|
+
- Added `task list [--status] [--action] [--limit] [--offset]` backed by `GET /v1/tasks`, so a caller that lost a submission output can reconcile recent tasks instead of resubmitting and double-charging.
|
|
7
|
+
- Added `--idempotency-key` to `gen video` and async `gen image`: same-key retries replay the original task server-side without a second charge. Keys are auto-generated when omitted, breadcrumbed on stderr (`idempotency_key=...`), and the server's `idempotent_replay` marker is surfaced in both JSON output and stderr.
|
|
8
|
+
- Aligned `task download --json` output with `gen image` by exposing `files[]` alongside the legacy `file`.
|
|
9
|
+
- Added a stderr warning for double-encoded reference URLs (`%25XX`) before submission — the top cause of 403 `invalid_reference_url` from presigned URLs.
|
|
10
|
+
|
|
11
|
+
## 0.3.1 - 2026-08-18
|
|
12
|
+
|
|
13
|
+
- Added `--duration` as an alias of `--seconds` on `gen video` so the API contract field name works directly; conflicting values are rejected locally.
|
|
14
|
+
- Emitted a stderr breadcrumb (`task_id=...`) for every `--no-wait` submission so callers that fail to parse stdout JSON can recover the task instead of resubmitting and double-charging.
|
|
15
|
+
- Added case-sensitive ID transcription guidance to `task status`/`task cancel` 404 errors (`l`/`1`/`I`, `O`/`0`) with an explicit warning against blind resubmission.
|
|
16
|
+
- Updated the bundled Agent Skills with resubmission discipline, the Windows `.cmd` shim invocation requirement, and exact task-ID copying guidance.
|
|
17
|
+
|
|
3
18
|
## 0.3.0 - 2026-08-18
|
|
4
19
|
|
|
5
20
|
- Added `task cancel` for queued tasks through `DELETE /v1/video/generations/{task_id}`, including the 409 `task_already_running` / `task_already_finished` and 502 `task_cancel_failed` contract with actionable hints.
|
package/dist/cli.js
CHANGED
|
@@ -176,7 +176,7 @@ function displayWidth(s) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/lib/version.ts
|
|
179
|
-
var VERSION = true ? "0.
|
|
179
|
+
var VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
180
180
|
|
|
181
181
|
// src/commands/auth.ts
|
|
182
182
|
import { createInterface } from "readline/promises";
|
|
@@ -916,6 +916,7 @@ import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
|
916
916
|
import { join as join3, resolve as resolve2 } from "path";
|
|
917
917
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
918
918
|
import { Readable as Readable2 } from "stream";
|
|
919
|
+
import { randomUUID } from "crypto";
|
|
919
920
|
|
|
920
921
|
// src/lib/model-capabilities.ts
|
|
921
922
|
var GEMINI_IMAGE_MAX_SEED = 9007199254740991;
|
|
@@ -1526,6 +1527,19 @@ function extractProgress(body) {
|
|
|
1526
1527
|
}
|
|
1527
1528
|
return void 0;
|
|
1528
1529
|
}
|
|
1530
|
+
function extractCreatedAt(raw) {
|
|
1531
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1532
|
+
const obj = raw;
|
|
1533
|
+
const candidates = [obj.created_at, obj.data?.created_at];
|
|
1534
|
+
for (const candidate of candidates) {
|
|
1535
|
+
if (typeof candidate === "number" && candidate > 0) return candidate;
|
|
1536
|
+
if (typeof candidate === "string") {
|
|
1537
|
+
const parsed = Number.parseInt(candidate, 10);
|
|
1538
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return void 0;
|
|
1542
|
+
}
|
|
1529
1543
|
async function cancelTask(baseUrl, apiKey, taskId) {
|
|
1530
1544
|
try {
|
|
1531
1545
|
await request({
|
|
@@ -1549,6 +1563,12 @@ async function cancelTask(baseUrl, apiKey, taskId) {
|
|
|
1549
1563
|
hint: "\u8FD0\u884C focalapi task status " + taskId + " \u67E5\u770B\u7ED3\u679C\uFF1B\u6210\u529F\u540E\u53EF\u4E0B\u8F7D\u4EA7\u7269\u3002"
|
|
1550
1564
|
});
|
|
1551
1565
|
}
|
|
1566
|
+
if (code === "task_not_found") {
|
|
1567
|
+
throw new ApiError("task_not_found", `\u4EFB\u52A1 ${taskId} \u4E0D\u5B58\u5728\u6216\u4E0D\u5C5E\u4E8E\u5F53\u524D Key`, {
|
|
1568
|
+
status: err.status,
|
|
1569
|
+
hint: "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\uFF1B\u8BF7\u56DE\u67E5\u63D0\u4EA4\u8F93\u51FA\u540E\u518D\u8BD5\u3002"
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1552
1572
|
if (code === "task_cancel_incomplete") {
|
|
1553
1573
|
throw new ApiError("task_cancel_incomplete", `\u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88\u4F46\u6E05\u7406\u672A\u5B8C\u6210`, {
|
|
1554
1574
|
status: err.status,
|
|
@@ -1582,9 +1602,24 @@ async function fetchTask(baseUrl, apiKey, taskId) {
|
|
|
1582
1602
|
status: normalizeTaskStatus(rawStatus),
|
|
1583
1603
|
rawStatus,
|
|
1584
1604
|
progress: extractProgress(raw),
|
|
1605
|
+
createdAt: extractCreatedAt(raw),
|
|
1585
1606
|
raw
|
|
1586
1607
|
};
|
|
1587
1608
|
}
|
|
1609
|
+
async function listTasks(baseUrl, apiKey, opts) {
|
|
1610
|
+
const raw = await request({
|
|
1611
|
+
baseUrl,
|
|
1612
|
+
path: "/v1/tasks",
|
|
1613
|
+
apiKey,
|
|
1614
|
+
query: {
|
|
1615
|
+
status: opts?.status,
|
|
1616
|
+
action: opts?.action,
|
|
1617
|
+
limit: opts?.limit,
|
|
1618
|
+
offset: opts?.offset
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
return raw.data ?? [];
|
|
1622
|
+
}
|
|
1588
1623
|
async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
1589
1624
|
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
1590
1625
|
const timeoutMs = opts?.timeoutMs ?? 30 * 6e4;
|
|
@@ -1609,7 +1644,7 @@ async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
|
1609
1644
|
}
|
|
1610
1645
|
if (Date.now() > deadline) {
|
|
1611
1646
|
throw new ApiError("timeout", `\u4EFB\u52A1 ${taskId} \u7B49\u5F85\u8D85\u65F6\uFF08${Math.round(timeoutMs / 6e4)} \u5206\u949F\uFF09`, {
|
|
1612
|
-
hint: `\u53EF\
|
|
1647
|
+
hint: `\u53EF\u8FD0\u884C focalapi task status ${taskId} --wait \u7EE7\u7EED\u7B49\u5F85\uFF0C\u6216 focalapi task status ${taskId} \u67E5\u770B\u5F53\u524D\u72B6\u6001\u3002`
|
|
1613
1648
|
});
|
|
1614
1649
|
}
|
|
1615
1650
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
@@ -1647,6 +1682,20 @@ async function downloadTaskContent(baseUrl, apiKey, taskId, outDir, filenameBase
|
|
|
1647
1682
|
var MAX_IMAGE_N = 128;
|
|
1648
1683
|
var MAX_TASK_DURATION_SECONDS = 3600;
|
|
1649
1684
|
var DEFAULT_OUT_DIR = "focalapi-out";
|
|
1685
|
+
function resolveIdempotencyKey(provided) {
|
|
1686
|
+
const key = provided?.trim() || randomUUID();
|
|
1687
|
+
if (!/^[\x21-\x7e]{8,128}$/.test(key)) {
|
|
1688
|
+
throw new ApiError("invalid_request", "--idempotency-key \u5FC5\u987B\u662F 8\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF08\u6536\u5230\uFF1A" + provided + "\uFF09");
|
|
1689
|
+
}
|
|
1690
|
+
return key;
|
|
1691
|
+
}
|
|
1692
|
+
function warnDoubleEncodedReferenceURLs(label, urls) {
|
|
1693
|
+
for (const url of urls) {
|
|
1694
|
+
if (url && /%25[0-9a-f]{2}/i.test(url)) {
|
|
1695
|
+
info(`\u8B66\u544A\uFF1A${label} \u7684 URL \u7591\u4F3C\u88AB\u4E8C\u6B21\u7F16\u7801\uFF08\u5305\u542B %25XX\uFF09\uFF1A${url.slice(0, 100)} \u2014\u2014 \u9884\u7B7E\u540D URL \u5FC5\u987B\u539F\u6837\u4F20\u9012\uFF0C\u5426\u5219\u4E0A\u6E38\u4F1A\u8FD4\u56DE 403 invalid_reference_url`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1650
1699
|
function clampInt(value, min, max, name) {
|
|
1651
1700
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
1652
1701
|
throw new ApiError("invalid_request", `${name} \u5FC5\u987B\u662F ${min}\u2013${max} \u7684\u6574\u6570\uFF08\u6536\u5230\uFF1A${value}\uFF09`);
|
|
@@ -1763,7 +1812,7 @@ function extractGeminiImageItems(response) {
|
|
|
1763
1812
|
}
|
|
1764
1813
|
function registerGen(program) {
|
|
1765
1814
|
const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
|
|
1766
|
-
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>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u6A21\u578B\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\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("--negative-prompt <text>", "\u8D1F\u9762\u63D0\u793A\u8BCD\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--creativity <level>", "\u63D0\u793A\u8BCD\u6269\u5C55\u5F3A\u5EA6\uFF0C\u5982 raw\u3001low\u3001medium\u3001high").option("--prompt-extend <boolean>", "\u662F\u5426\u6269\u5C55\u63D0\u793A\u8BCD\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "prompt-extend")).option("--style-references <json>", "Krea image_style_references JSON \u6570\u7EC4").option("--moodboards <json>", "Krea moodboards JSON \u6570\u7EC4").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\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) => {
|
|
1815
|
+
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>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u6A21\u578B\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\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("--negative-prompt <text>", "\u8D1F\u9762\u63D0\u793A\u8BCD\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--creativity <level>", "\u63D0\u793A\u8BCD\u6269\u5C55\u5F3A\u5EA6\uFF0C\u5982 raw\u3001low\u3001medium\u3001high").option("--prompt-extend <boolean>", "\u662F\u5426\u6269\u5C55\u63D0\u793A\u8BCD\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "prompt-extend")).option("--style-references <json>", "Krea image_style_references JSON \u6570\u7EC4").option("--moodboards <json>", "Krea moodboards JSON \u6570\u7EC4").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\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("--idempotency-key <key>", "\u5E42\u7B49\u952E\uFF088\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09\u3002\u540C\u4E00 key \u7684\u91CD\u590D\u63D0\u4EA4\u62FF\u56DE\u539F\u4EFB\u52A1\u800C\u4E0D\u91CD\u590D\u8BA1\u8D39\uFF1B\u7701\u7565\u65F6\u81EA\u52A8\u751F\u6210\u3002\u91CD\u8BD5\u4E0D\u786E\u5B9A\u7684\u63D0\u4EA4\u65F6\u52A1\u5FC5\u590D\u7528\u539F key").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
|
|
1767
1816
|
const g = cmd.optsWithGlobals();
|
|
1768
1817
|
const auth = resolveAuth(g);
|
|
1769
1818
|
const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
|
|
@@ -1812,12 +1861,17 @@ function registerGen(program) {
|
|
|
1812
1861
|
if (opts.image) body.image = opts.image;
|
|
1813
1862
|
if (opts.mask) body.mask = opts.mask;
|
|
1814
1863
|
if (opts.responseFormat) body.response_format = opts.responseFormat;
|
|
1864
|
+
const idempotencyKey = opts.wait === false ? resolveIdempotencyKey(opts.idempotencyKey) : void 0;
|
|
1865
|
+
warnDoubleEncodedReferenceURLs("--image", opts.image ?? []);
|
|
1815
1866
|
const res = await withProgress(opts.wait === false ? "\u6B63\u5728\u63D0\u4EA4\u56FE\u50CF\u4EFB\u52A1" : "\u6B63\u5728\u751F\u6210\u56FE\u50CF", () => request({
|
|
1816
1867
|
baseUrl: auth.baseUrl,
|
|
1817
1868
|
path: "/v1/images/generations",
|
|
1818
1869
|
apiKey: auth.apiKey,
|
|
1819
1870
|
body,
|
|
1820
|
-
headers:
|
|
1871
|
+
headers: {
|
|
1872
|
+
...opts.wait === false ? { Prefer: "respond-async" } : {},
|
|
1873
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
1874
|
+
},
|
|
1821
1875
|
timeoutMs: 6e5
|
|
1822
1876
|
}));
|
|
1823
1877
|
if (opts.wait === false) {
|
|
@@ -1825,8 +1879,10 @@ function registerGen(program) {
|
|
|
1825
1879
|
if (!taskId) {
|
|
1826
1880
|
throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
|
|
1827
1881
|
}
|
|
1882
|
+
info(`task_id=${taskId}`);
|
|
1883
|
+
if (idempotencyKey) info(`idempotency_key=${idempotencyKey}`);
|
|
1828
1884
|
if (g.json) {
|
|
1829
|
-
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
1885
|
+
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, ...res.idempotent_replay ? { idempotent_replay: true } : {}, next_command: `focalapi task status ${taskId} --json` });
|
|
1830
1886
|
} else {
|
|
1831
1887
|
process.stdout.write(taskId + "\n");
|
|
1832
1888
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u67E5\u8BE2\uFF1Afocalapi task status ${taskId}`);
|
|
@@ -1951,17 +2007,21 @@ function registerGen(program) {
|
|
|
1951
2007
|
if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
|
|
1952
2008
|
}
|
|
1953
2009
|
});
|
|
1954
|
-
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>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL\uFF08Grok \u89C6\u9891\u4E3A reference-to-video \u6A21\u5F0F\uFF0C\u53EF\u591A\u4E2A\uFF09").option("--first-frame <url>", "\u56FE\u751F\u89C6\u9891\u9996\u5E27\u56FE URL\uFF08image-to-video \u6A21\u5F0F\uFF1B\u4E0E --image \u4E92\u65A5\uFF09").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(
|
|
2010
|
+
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--duration \u4E3A\u540C\u4E49\u522B\u540D\u3002\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--duration <n>", "\u65F6\u957F\u79D2\u6570\uFF08--seconds \u7684\u522B\u540D\uFF0C\u4E0E API \u5951\u7EA6\u5B57\u6BB5\u540C\u540D\uFF09", (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>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL\uFF08Grok \u89C6\u9891\u4E3A reference-to-video \u6A21\u5F0F\uFF0C\u53EF\u591A\u4E2A\uFF09").option("--first-frame <url>", "\u56FE\u751F\u89C6\u9891\u9996\u5E27\u56FE URL\uFF08image-to-video \u6A21\u5F0F\uFF1B\u4E0E --image \u4E92\u65A5\uFF09").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("--idempotency-key <key>", "\u5E42\u7B49\u952E\uFF088\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09\u3002\u540C\u4E00 key \u7684\u91CD\u590D\u63D0\u4EA4\u62FF\u56DE\u539F\u4EFB\u52A1\u800C\u4E0D\u91CD\u590D\u8BA1\u8D39\uFF1B\u7701\u7565\u65F6\u81EA\u52A8\u751F\u6210\u3002\u91CD\u8BD5\u4E0D\u786E\u5B9A\u7684\u63D0\u4EA4\u65F6\u52A1\u5FC5\u590D\u7528\u539F key").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(
|
|
1955
2011
|
async (promptParts, opts, cmd) => {
|
|
1956
2012
|
const g = cmd.optsWithGlobals();
|
|
1957
2013
|
const auth = resolveAuth(g);
|
|
1958
2014
|
const model = opts.model ?? (await resolveCreativeModel(auth, "video")).model.id;
|
|
1959
2015
|
if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u89C6\u9891\u6A21\u578B\uFF1A${model}`);
|
|
2016
|
+
if (opts.seconds !== void 0 && opts.duration !== void 0 && opts.seconds !== opts.duration) {
|
|
2017
|
+
throw new ApiError("invalid_request", `--seconds \u4E0E --duration \u662F\u540C\u4E00\u53C2\u6570\uFF0C\u53EA\u9700\u4F20\u4E00\u4E2A\uFF08\u6536\u5230 ${opts.seconds} \u548C ${opts.duration}\uFF09`);
|
|
2018
|
+
}
|
|
2019
|
+
const seconds = opts.seconds ?? opts.duration;
|
|
1960
2020
|
const body = { model, prompt: promptParts.join(" ") };
|
|
1961
2021
|
const metadata = {};
|
|
1962
|
-
if (
|
|
1963
|
-
const
|
|
1964
|
-
body.duration =
|
|
2022
|
+
if (seconds !== void 0) {
|
|
2023
|
+
const secondsValue = clampInt(seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds");
|
|
2024
|
+
body.duration = secondsValue;
|
|
1965
2025
|
}
|
|
1966
2026
|
if (opts.size) body.size = opts.size;
|
|
1967
2027
|
if (opts.image) body.images = opts.image;
|
|
@@ -1982,7 +2042,7 @@ function registerGen(program) {
|
|
|
1982
2042
|
if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
|
|
1983
2043
|
if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
|
|
1984
2044
|
validateVideoGeneration(model, {
|
|
1985
|
-
seconds
|
|
2045
|
+
seconds,
|
|
1986
2046
|
resolution: opts.resolution,
|
|
1987
2047
|
ratio: opts.ratio,
|
|
1988
2048
|
aspectRatio: opts.aspectRatio,
|
|
@@ -1999,11 +2059,14 @@ function registerGen(program) {
|
|
|
1999
2059
|
watermark: opts.watermark
|
|
2000
2060
|
});
|
|
2001
2061
|
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
2062
|
+
const idempotencyKey = resolveIdempotencyKey(opts.idempotencyKey);
|
|
2063
|
+
warnDoubleEncodedReferenceURLs("--image/--first-frame", [...opts.image ?? [], opts.firstFrame]);
|
|
2002
2064
|
const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
|
|
2003
2065
|
baseUrl: auth.baseUrl,
|
|
2004
2066
|
path: "/v1/video/generations",
|
|
2005
2067
|
apiKey: auth.apiKey,
|
|
2006
2068
|
body,
|
|
2069
|
+
headers: { "Idempotency-Key": idempotencyKey },
|
|
2007
2070
|
timeoutMs: 12e4
|
|
2008
2071
|
}));
|
|
2009
2072
|
const taskId = extractTaskId(created);
|
|
@@ -2011,8 +2074,11 @@ function registerGen(program) {
|
|
|
2011
2074
|
throw new ApiError("bad_response", "\u89C6\u9891\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: created });
|
|
2012
2075
|
}
|
|
2013
2076
|
if (opts.wait === false) {
|
|
2077
|
+
info(`task_id=${taskId}`);
|
|
2078
|
+
info(`idempotency_key=${idempotencyKey}`);
|
|
2079
|
+
if (created.idempotent_replay) info("\u5DF2\u56DE\u653E\u539F\u4EFB\u52A1\uFF08idempotent_replay\uFF0C\u672A\u91CD\u590D\u8BA1\u8D39\uFF09");
|
|
2014
2080
|
if (g.json) {
|
|
2015
|
-
printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
2081
|
+
printJson({ model, task_id: taskId, submitted: true, ...created.idempotent_replay ? { idempotent_replay: true } : {}, next_command: `focalapi task status ${taskId} --json` });
|
|
2016
2082
|
} else {
|
|
2017
2083
|
process.stdout.write(taskId + "\n");
|
|
2018
2084
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
@@ -2040,29 +2106,100 @@ function registerGen(program) {
|
|
|
2040
2106
|
}
|
|
2041
2107
|
|
|
2042
2108
|
// src/commands/task.ts
|
|
2109
|
+
var TASK_ID_HINT = "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF08O \u4E0E 0 \u540C\u7406\uFF09\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\u3002\u82E5\u63D0\u4EA4\u65F6\u7684\u8F93\u51FA\u5DF2\u4E22\u5931\uFF0C\u5148\u8FD0\u884C focalapi task list \u627E\u56DE\u6700\u8FD1\u7684\u4EFB\u52A1\uFF0C\u4E0D\u8981\u76F2\u76EE\u91CD\u65B0\u63D0\u4EA4\u2014\u2014\u91CD\u590D\u63D0\u4EA4\u4F1A\u91CD\u590D\u6263\u8D39\u3002";
|
|
2110
|
+
function withTaskIdHint(err) {
|
|
2111
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
2112
|
+
return new ApiError(err.code, err.message, { status: err.status, hint: TASK_ID_HINT, body: err.body, upstreamCode: err.upstreamCode, requestId: err.requestId });
|
|
2113
|
+
}
|
|
2114
|
+
return err;
|
|
2115
|
+
}
|
|
2116
|
+
function formatElapsed(seconds) {
|
|
2117
|
+
if (!seconds || seconds < 0) return "-";
|
|
2118
|
+
if (seconds < 90) return `${seconds} \u79D2`;
|
|
2119
|
+
return `${Math.floor(seconds / 60)} \u5206 ${seconds % 60} \u79D2`;
|
|
2120
|
+
}
|
|
2121
|
+
function taskAgeSeconds(createdAt) {
|
|
2122
|
+
if (!createdAt) return void 0;
|
|
2123
|
+
return Math.max(0, Math.floor(Date.now() / 1e3 - createdAt));
|
|
2124
|
+
}
|
|
2043
2125
|
function registerTask(program) {
|
|
2044
2126
|
const task = program.command("task").description("\u4EFB\u52A1\u67E5\u8BE2\u3001\u53D6\u6D88\u4E0E\u4EA7\u7269\u4E0B\u8F7D\uFF08\u89C6\u9891\u7B49\u4EFB\u52A1\u5236\u80FD\u529B\uFF09");
|
|
2045
|
-
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId,
|
|
2127
|
+
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001\uFF1B--wait \u5185\u7F6E\u8F6E\u8BE2\u7B49\u5F85\u7EC8\u6001\uFF0C\u65E0\u9700\u81EA\u5199\u8F6E\u8BE2\u811A\u672C").argument("<task_id>", "\u4EFB\u52A1 ID").option("--wait", "\u7B49\u5F85\u4EFB\u52A1\u5230\u8FBE\u7EC8\u6001\uFF08\u6210\u529F / \u5931\u8D25 / \u53D6\u6D88\uFF09\u540E\u518D\u8FD4\u56DE").option("--timeout <minutes>", "--wait \u7684\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("--poll-interval <ms>", "--wait \u7684\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--download", "--wait \u6210\u529F\u540E\u81EA\u52A8\u4E0B\u8F7D\u4EA7\u7269\uFF08\u7B49\u4EF7\u4E8E\u63A5\u7740\u6267\u884C task download\uFF09").option("-o, --out <dir>", "--download \u7684\u8F93\u51FA\u76EE\u5F55", "focalapi-out").action(async (taskId, opts, cmd) => {
|
|
2128
|
+
const g = cmd.optsWithGlobals();
|
|
2129
|
+
const auth = resolveAuth(g);
|
|
2130
|
+
try {
|
|
2131
|
+
let info_ = await fetchTask(auth.baseUrl, auth.apiKey, taskId);
|
|
2132
|
+
if (opts.wait && info_.status !== "success" && info_.status !== "failed" && info_.status !== "cancelled") {
|
|
2133
|
+
info_ = await pollTask(auth.baseUrl, auth.apiKey, taskId, {
|
|
2134
|
+
intervalMs: opts.pollInterval,
|
|
2135
|
+
timeoutMs: opts.timeout * 6e4,
|
|
2136
|
+
onUpdate: (t) => {
|
|
2137
|
+
if (!g.json) {
|
|
2138
|
+
const elapsed2 = formatElapsed(taskAgeSeconds(t.createdAt));
|
|
2139
|
+
info(` \u72B6\u6001\uFF1A${t.rawStatus || t.status}${t.progress !== void 0 ? `\uFF08${t.progress}%\uFF09` : ""}\uFF0C\u5DF2\u8017\u65F6 ${elapsed2}`);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
const elapsed = taskAgeSeconds(info_.createdAt);
|
|
2145
|
+
let file;
|
|
2146
|
+
if (opts.download && info_.status === "success") {
|
|
2147
|
+
file = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
2148
|
+
}
|
|
2149
|
+
if (g.json) {
|
|
2150
|
+
printJson({
|
|
2151
|
+
task_id: taskId,
|
|
2152
|
+
status: info_.status,
|
|
2153
|
+
raw_status: info_.rawStatus,
|
|
2154
|
+
progress: info_.progress,
|
|
2155
|
+
...elapsed !== void 0 ? { elapsed_seconds: elapsed } : {},
|
|
2156
|
+
...file ? { file } : {},
|
|
2157
|
+
raw: info_.raw
|
|
2158
|
+
});
|
|
2159
|
+
} else {
|
|
2160
|
+
printTable(
|
|
2161
|
+
["\u5B57\u6BB5", "\u503C"],
|
|
2162
|
+
[
|
|
2163
|
+
["\u4EFB\u52A1 ID", taskId],
|
|
2164
|
+
["\u72B6\u6001", `${info_.status}${info_.rawStatus && info_.rawStatus !== info_.status ? `\uFF08\u4E0A\u6E38\uFF1A${info_.rawStatus}\uFF09` : ""}`],
|
|
2165
|
+
["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"],
|
|
2166
|
+
["\u5DF2\u8017\u65F6", formatElapsed(elapsed)]
|
|
2167
|
+
]
|
|
2168
|
+
);
|
|
2169
|
+
if (file) {
|
|
2170
|
+
info(`\u2713 ${file}`);
|
|
2171
|
+
} else if (info_.status === "success") {
|
|
2172
|
+
info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
|
|
2173
|
+
}
|
|
2174
|
+
if (info_.status === "pending" || info_.status === "running") {
|
|
2175
|
+
info(`\u7B49\u5F85\u5B8C\u6210\uFF1Afocalapi task status ${taskId} --wait\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
} catch (err) {
|
|
2179
|
+
throw withTaskIdHint(err);
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
task.command("list").description("\u5217\u51FA\u5F53\u524D Key \u7684\u8FD1\u671F\u4EFB\u52A1\uFF08\u63D0\u4EA4\u8F93\u51FA\u4E22\u5931\u65F6\u7528\u5B83\u627E\u56DE task_id\uFF09").option("--status <status>", "\u6309\u72B6\u6001\u8FC7\u6EE4\uFF1Aqueued\u3001in_progress\u3001completed\u3001failed\u3001cancelled").option("--action <action>", "\u6309\u4EFB\u52A1\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 generate\u3001image_generation").option("--limit <n>", "\u6BCF\u9875\u6570\u91CF\uFF081\u2013100\uFF09", (v) => Number.parseInt(v, 10), 20).option("--offset <n>", "\u504F\u79FB\u91CF", (v) => Number.parseInt(v, 10), 0).action(async (opts, cmd) => {
|
|
2046
2183
|
const g = cmd.optsWithGlobals();
|
|
2047
2184
|
const auth = resolveAuth(g);
|
|
2048
|
-
const
|
|
2185
|
+
const items = await listTasks(auth.baseUrl, auth.apiKey, opts);
|
|
2049
2186
|
if (g.json) {
|
|
2050
|
-
printJson({
|
|
2187
|
+
printJson({ object: "list", data: items });
|
|
2188
|
+
} else if (items.length === 0) {
|
|
2189
|
+
info("\u5F53\u524D Key \u6682\u65E0\u4EFB\u52A1\u8BB0\u5F55\u3002");
|
|
2051
2190
|
} else {
|
|
2052
2191
|
printTable(
|
|
2053
|
-
["\
|
|
2054
|
-
[
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2192
|
+
["\u4EFB\u52A1 ID", "\u6A21\u578B", "\u72B6\u6001", "\u8FDB\u5EA6", "\u5DF2\u8017\u65F6", "\u989D\u5EA6"],
|
|
2193
|
+
items.map((item) => [
|
|
2194
|
+
item.task_id,
|
|
2195
|
+
item.model ?? "-",
|
|
2196
|
+
item.status ?? "-",
|
|
2197
|
+
item.progress !== void 0 ? `${item.progress}%` : "-",
|
|
2198
|
+
formatElapsed(item.created_at ? Math.max(0, Math.floor(Date.now() / 1e3 - item.created_at)) : void 0),
|
|
2199
|
+
item.quota !== void 0 ? String(item.quota) : "-"
|
|
2200
|
+
])
|
|
2059
2201
|
);
|
|
2060
|
-
|
|
2061
|
-
info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
|
|
2062
|
-
}
|
|
2063
|
-
if (info_.status === "pending" || info_.status === "running") {
|
|
2064
|
-
info(`\u5982\u9700\u505C\u6B62\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF1Afocalapi task cancel ${taskId}`);
|
|
2065
|
-
}
|
|
2202
|
+
info("\u7EED\u53D6\uFF1Afocalapi task status <task_id> --wait");
|
|
2066
2203
|
}
|
|
2067
2204
|
});
|
|
2068
2205
|
task.command("cancel").description("\u53D6\u6D88\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF08\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4E0D\u53EF\u53D6\u6D88\uFF1B\u53D6\u6D88\u540E\u8D39\u7528\u81EA\u52A8\u9000\u8FD8\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
|
|
@@ -2080,7 +2217,7 @@ function registerTask(program) {
|
|
|
2080
2217
|
const auth = resolveAuth(g);
|
|
2081
2218
|
const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
2082
2219
|
if (g.json) {
|
|
2083
|
-
printJson({ task_id: taskId, file: filePath });
|
|
2220
|
+
printJson({ task_id: taskId, file: filePath, files: [filePath] });
|
|
2084
2221
|
} else {
|
|
2085
2222
|
info(`\u2713 ${filePath}`);
|
|
2086
2223
|
}
|
|
@@ -2187,7 +2324,7 @@ function registerUsage(program) {
|
|
|
2187
2324
|
}
|
|
2188
2325
|
|
|
2189
2326
|
// src/commands/connect.ts
|
|
2190
|
-
import { createHash, randomUUID } from "crypto";
|
|
2327
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2191
2328
|
import {
|
|
2192
2329
|
cpSync,
|
|
2193
2330
|
existsSync as existsSync2,
|
|
@@ -2346,7 +2483,7 @@ function installTo(skillsDir, agents, skills, srcDir) {
|
|
|
2346
2483
|
}
|
|
2347
2484
|
mkdirSync2(skillsDir, { recursive: true });
|
|
2348
2485
|
const oldManifest = readManifest(skillsDir);
|
|
2349
|
-
const transactionRoot = join4(skillsDir, `.focalapi-install-${
|
|
2486
|
+
const transactionRoot = join4(skillsDir, `.focalapi-install-${randomUUID2()}`);
|
|
2350
2487
|
const stageRoot = join4(transactionRoot, "stage");
|
|
2351
2488
|
const backupRoot = join4(transactionRoot, "backup");
|
|
2352
2489
|
mkdirSync2(stageRoot, { recursive: true });
|
package/package.json
CHANGED
package/skills/focalapi/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ FocalAPI is a creative-model gateway for the current Agent to call. It is not th
|
|
|
17
17
|
1. If the user does not specify a model, omit `--model`. The CLI selects a FocalAPI default from the live model pool and detailed contracts available to the current key. Do not generate a test sample first.
|
|
18
18
|
2. If the user specifies a model or provider, run `focalapi models get <model-id> --json` first. If the model ID is incomplete, run `models search` once to find the exact ID, then read its details.
|
|
19
19
|
3. Use only parameters and values listed in the detailed `supported_params`. Never infer support from a similar model.
|
|
20
|
-
4. Add `--json` for Agent and script calls. stdout is the only machine-readable result; diagnostics go to stderr.
|
|
20
|
+
4. Add `--json` for Agent and script calls. stdout is the only machine-readable result; diagnostics go to stderr. Do not merge the two streams (`2>&1`) before parsing. Programmatic callers on Windows must invoke the CLI through a shell (`cmd /c focalapi ...` or `shell=True`) because npm installs a `.cmd` shim.
|
|
21
21
|
5. After successful generation, return local absolute file paths to the user. If a video response contains `task_id`, follow `next_command`; never submit the same task again.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
@@ -26,8 +26,8 @@ focalapi gen image "<user prompt>" -o ./focalapi-out --json
|
|
|
26
26
|
focalapi gen video "<user prompt>" --no-wait -o ./focalapi-out --json
|
|
27
27
|
|
|
28
28
|
# Continue an asynchronous video task.
|
|
29
|
-
focalapi task status <task-id> --json
|
|
30
|
-
focalapi task
|
|
29
|
+
focalapi task status <task-id> --wait --json # built-in polling; never write your own poll script
|
|
30
|
+
focalapi task list --json # recover recent task IDs after a lost output
|
|
31
31
|
focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are refunded
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -38,7 +38,7 @@ focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are
|
|
|
38
38
|
| Generate or edit images; create from reference images | `focalapi gen image` | focalapi-gen |
|
|
39
39
|
| Generate video; animate images or reference media | `focalapi gen video` | focalapi-gen |
|
|
40
40
|
| Select, compare, or inspect model parameters | `focalapi models resolve/get/search` | focalapi-models |
|
|
41
|
-
| Inspect progress or failures; cancel queued tasks; download results | `focalapi task status/cancel/download` | focalapi-task |
|
|
41
|
+
| Inspect progress or failures; wait, list, cancel queued tasks; download results | `focalapi task status/wait/list/cancel/download` | focalapi-task |
|
|
42
42
|
| Resolve key, sign-in, or 401 issues | `focalapi auth status/login` | focalapi-auth |
|
|
43
43
|
| Inspect quota, usage, or service failures | `focalapi usage/doctor` | focalapi-usage |
|
|
44
44
|
| Provide text assistance explicitly requested by the user | `focalapi chat` | focalapi-chat |
|
|
@@ -50,6 +50,8 @@ focalapi task download <task-id> -o ./focalapi-out --json
|
|
|
50
50
|
|
|
51
51
|
`pending` and `running` are not failures. Keep checking the same `task_id` and never resubmit generation. On failure, read the structured `error.code` and `hint`, and fix only the explicit problem instead of rotating models blindly.
|
|
52
52
|
|
|
53
|
+
Never resubmit after an output-parsing failure on your side. If your own pipeline fails to parse a submission command's stdout JSON, the task was almost certainly created and charged: recover the `task_id` from the command's stderr breadcrumb line (`task_id=...`) and continue with `task status`. Resubmitting a request whose outcome is unknown duplicates the charge.
|
|
54
|
+
|
|
53
55
|
Two transient outcomes need no parameter changes:
|
|
54
56
|
|
|
55
57
|
- `capacity_exhausted` (HTTP 503): the platform queue is full. Retry the same command after roughly 10 seconds; do not switch models or shrink the request.
|
|
@@ -13,8 +13,10 @@ metadata:
|
|
|
13
13
|
After a generation command returns `task_id`, prefer the response's `next_command`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
focalapi task status <task-id> --json
|
|
17
|
-
focalapi task
|
|
16
|
+
focalapi task status <task-id> --json # one-shot status check
|
|
17
|
+
focalapi task status <task-id> --wait # built-in polling until a terminal state — do not write your own poller
|
|
18
|
+
focalapi task status <task-id> --wait --download -o ./focalapi-out --json
|
|
19
|
+
focalapi task list --status in_progress --json # reconcile recent tasks after losing a submission output
|
|
18
20
|
focalapi task cancel <task-id> --json
|
|
19
21
|
```
|
|
20
22
|
|
|
@@ -24,6 +26,10 @@ focalapi task cancel <task-id> --json
|
|
|
24
26
|
- `cancelled`: the task was stopped; a cancelled queued task is refunded. Do not download or resubmit unless the user asks for a new attempt.
|
|
25
27
|
- `unknown`: preserve the raw response and run `focalapi doctor --json`; never fabricate a success state.
|
|
26
28
|
|
|
29
|
+
Task IDs are case-sensitive and mix `l`/`1`/`I` and `O`/`0`. Copy them exactly from the submission output — stdout JSON or the stderr `task_id=` breadcrumb — instead of retyping. If a submission output is lost entirely, run `task list` to recover recent task IDs before considering any resubmission.
|
|
30
|
+
|
|
31
|
+
Generation commands print the submission's `idempotency_key=` on stderr. When retrying a submission whose outcome is uncertain (timeout, crash, lost output), reuse that same key via `--idempotency-key` so the retry replays the original task instead of double-charging; a fresh key means a deliberate new generation.
|
|
32
|
+
|
|
27
33
|
`task cancel` only works while a task is still queued (`pending`). A 409 `task_already_running` means generation already started and cannot be stopped — keep tracking with `task status`. Cancellation failures return explicit codes (`task_already_finished`, `task_cancel_failed`); follow `error.hint` instead of retrying blindly.
|
|
28
34
|
|
|
29
35
|
Polling must be bounded. When the user does not ask for blocking wait behavior, report the current status and `task_id`.
|