focalapi-cli 0.1.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/LICENSE +202 -0
- package/README.md +190 -0
- package/dist/cli.js +1557 -0
- package/package.json +50 -0
- package/skills/focalapi/SKILL.md +43 -0
- package/skills/focalapi-auth/SKILL.md +50 -0
- package/skills/focalapi-chat/SKILL.md +51 -0
- package/skills/focalapi-gen/SKILL.md +43 -0
- package/skills/focalapi-search/SKILL.md +31 -0
- package/skills/focalapi-usage/SKILL.md +36 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/lib/errors.ts
|
|
7
|
+
var ApiError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
status;
|
|
10
|
+
hint;
|
|
11
|
+
/** 上游原始响应体(已截断),仅调试用途,打印前需脱敏。 */
|
|
12
|
+
body;
|
|
13
|
+
constructor(code, message, opts) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ApiError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.status = opts?.status;
|
|
18
|
+
this.hint = opts?.hint;
|
|
19
|
+
this.body = opts?.body;
|
|
20
|
+
}
|
|
21
|
+
toJSON() {
|
|
22
|
+
return {
|
|
23
|
+
error: {
|
|
24
|
+
code: this.code,
|
|
25
|
+
message: this.message,
|
|
26
|
+
...this.hint ? { hint: this.hint } : {},
|
|
27
|
+
...this.status !== void 0 ? { status: this.status } : {}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function refineErrorCode(status, message) {
|
|
33
|
+
const m = message.toLowerCase();
|
|
34
|
+
if (m.includes("quota") || m.includes("\u989D\u5EA6") || m.includes("insufficient")) {
|
|
35
|
+
return "insufficient_quota";
|
|
36
|
+
}
|
|
37
|
+
if (m.includes("model") && (m.includes("not") || m.includes("\u4E0D\u5B58\u5728") || m.includes("\u65E0"))) {
|
|
38
|
+
return "model_not_found";
|
|
39
|
+
}
|
|
40
|
+
if (m.includes("key") || m.includes("token") || m.includes("auth")) {
|
|
41
|
+
return status === 401 || status === 403 ? "invalid_api_key" : "invalid_request";
|
|
42
|
+
}
|
|
43
|
+
switch (status) {
|
|
44
|
+
case 400:
|
|
45
|
+
return "invalid_request";
|
|
46
|
+
case 401:
|
|
47
|
+
case 403:
|
|
48
|
+
return "invalid_api_key";
|
|
49
|
+
case 404:
|
|
50
|
+
return "model_not_found";
|
|
51
|
+
case 429:
|
|
52
|
+
return "rate_limited";
|
|
53
|
+
default:
|
|
54
|
+
return status >= 500 ? "server_error" : "invalid_request";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/lib/output.ts
|
|
59
|
+
function isInteractive() {
|
|
60
|
+
return Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
61
|
+
}
|
|
62
|
+
var KEY_PATTERN = /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}/g;
|
|
63
|
+
function maskKey(key) {
|
|
64
|
+
if (key.length <= 8) {
|
|
65
|
+
return "***";
|
|
66
|
+
}
|
|
67
|
+
return `${key.slice(0, 5)}***${key.slice(-4)}`;
|
|
68
|
+
}
|
|
69
|
+
function sanitize(value) {
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
return value.replace(KEY_PATTERN, (m) => maskKey(m));
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return value.map(sanitize);
|
|
75
|
+
}
|
|
76
|
+
if (value !== null && typeof value === "object") {
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [k, v] of Object.entries(value)) {
|
|
79
|
+
if (/api[-_]?key|authorization|token/i.test(k) && typeof v === "string") {
|
|
80
|
+
const replaced = v.replace(KEY_PATTERN, (m) => maskKey(m));
|
|
81
|
+
out[k] = replaced !== v ? replaced : maskKey(v);
|
|
82
|
+
} else {
|
|
83
|
+
out[k] = sanitize(v);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function printJson(data) {
|
|
91
|
+
process.stdout.write(JSON.stringify(sanitize(data), null, 2) + "\n");
|
|
92
|
+
}
|
|
93
|
+
function info(message) {
|
|
94
|
+
process.stderr.write(message + "\n");
|
|
95
|
+
}
|
|
96
|
+
function printError(err, opts) {
|
|
97
|
+
if (err instanceof ApiError) {
|
|
98
|
+
if (opts?.json) {
|
|
99
|
+
printJson(err.toJSON());
|
|
100
|
+
} else {
|
|
101
|
+
process.stderr.write(`\u9519\u8BEF [${err.code}]\uFF1A${err.message}
|
|
102
|
+
`);
|
|
103
|
+
const hint = err.hint;
|
|
104
|
+
if (hint) {
|
|
105
|
+
process.stderr.write(`\u63D0\u793A\uFF1A${hint}
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
+
if (opts?.json) {
|
|
113
|
+
printJson({ error: { code: "internal_error", message } });
|
|
114
|
+
} else {
|
|
115
|
+
process.stderr.write(`\u5185\u90E8\u9519\u8BEF\uFF1A${message}
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function printTable(headers, rows) {
|
|
120
|
+
const widths = headers.map(
|
|
121
|
+
(h, i) => Math.max(displayWidth(h), ...rows.map((r) => displayWidth(r[i] ?? "")))
|
|
122
|
+
);
|
|
123
|
+
const line = (cells) => cells.map((c, i) => c + " ".repeat(Math.max(0, (widths[i] ?? 0) - displayWidth(c)))).join(" ");
|
|
124
|
+
process.stdout.write(line(headers) + "\n");
|
|
125
|
+
process.stdout.write(widths.map((w) => "-".repeat(w)).join(" ") + "\n");
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
process.stdout.write(line(row) + "\n");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function displayWidth(s) {
|
|
131
|
+
let width = 0;
|
|
132
|
+
for (const ch of s) {
|
|
133
|
+
width += /[⺀-鿿豈-︰-﹏-¢£-₩]/.test(ch) ? 2 : 1;
|
|
134
|
+
}
|
|
135
|
+
return width;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/lib/version.ts
|
|
139
|
+
var VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
140
|
+
|
|
141
|
+
// src/commands/auth.ts
|
|
142
|
+
import { createInterface } from "readline/promises";
|
|
143
|
+
|
|
144
|
+
// src/lib/config.ts
|
|
145
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
146
|
+
import { homedir } from "os";
|
|
147
|
+
import { join } from "path";
|
|
148
|
+
var DEFAULT_BASE_URL = "https://api.focalapi.com";
|
|
149
|
+
var DEFAULT_PROFILE = "default";
|
|
150
|
+
function normalizeHomePath(p) {
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
const m = /^\/([a-zA-Z])\/(.*)$/.exec(p);
|
|
153
|
+
if (m) {
|
|
154
|
+
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return p;
|
|
158
|
+
}
|
|
159
|
+
function configDir() {
|
|
160
|
+
const dir = process.env.FOCALAPI_CONFIG_DIR ?? join(homedir(), ".focalapi");
|
|
161
|
+
return normalizeHomePath(dir);
|
|
162
|
+
}
|
|
163
|
+
function configPath() {
|
|
164
|
+
return join(configDir(), "config.json");
|
|
165
|
+
}
|
|
166
|
+
function loadConfig() {
|
|
167
|
+
const path = configPath();
|
|
168
|
+
if (!existsSync(path)) {
|
|
169
|
+
return { currentProfile: DEFAULT_PROFILE, profiles: {} };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
173
|
+
return {
|
|
174
|
+
currentProfile: raw.currentProfile ?? DEFAULT_PROFILE,
|
|
175
|
+
profiles: raw.profiles ?? {}
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
throw new ApiError("config_corrupted", `\u914D\u7F6E\u6587\u4EF6\u635F\u574F\uFF1A${path}`, {
|
|
179
|
+
hint: "\u4FEE\u590D\u6216\u5220\u9664\u8BE5\u6587\u4EF6\u540E\u91CD\u65B0\u8FD0\u884C focalapi auth login\u3002"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function saveConfig(config) {
|
|
184
|
+
const dir = configDir();
|
|
185
|
+
mkdirSync(dir, { recursive: true });
|
|
186
|
+
const path = configPath();
|
|
187
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
188
|
+
try {
|
|
189
|
+
chmodSync(path, 384);
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function getProfile(name) {
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
const profileName = name ?? config.currentProfile;
|
|
196
|
+
return config.profiles[profileName] ?? {};
|
|
197
|
+
}
|
|
198
|
+
function clearProfile(name) {
|
|
199
|
+
const config = loadConfig();
|
|
200
|
+
delete config.profiles[name];
|
|
201
|
+
if (config.currentProfile === name) {
|
|
202
|
+
config.currentProfile = DEFAULT_PROFILE;
|
|
203
|
+
}
|
|
204
|
+
saveConfig(config);
|
|
205
|
+
return config;
|
|
206
|
+
}
|
|
207
|
+
function resolveBaseUrl(explicit, profileName) {
|
|
208
|
+
const fromEnv = process.env.FOCALAPI_BASE_URL;
|
|
209
|
+
const fromProfile = getProfile(profileName).baseUrl;
|
|
210
|
+
return (explicit ?? fromEnv ?? fromProfile ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
211
|
+
}
|
|
212
|
+
function resolveAuth(opts) {
|
|
213
|
+
const baseUrl = resolveBaseUrl(opts?.baseUrl, opts?.profile);
|
|
214
|
+
if (opts?.key) {
|
|
215
|
+
return { apiKey: opts.key, baseUrl, keySource: "flag" };
|
|
216
|
+
}
|
|
217
|
+
const fromEnv = process.env.FOCALAPI_API_KEY;
|
|
218
|
+
if (fromEnv) {
|
|
219
|
+
return { apiKey: fromEnv, baseUrl, keySource: "env" };
|
|
220
|
+
}
|
|
221
|
+
const fromProfile = getProfile(opts?.profile).apiKey;
|
|
222
|
+
if (fromProfile) {
|
|
223
|
+
return { apiKey: fromProfile, baseUrl, keySource: "config" };
|
|
224
|
+
}
|
|
225
|
+
throw new ApiError("missing_api_key", "\u672A\u627E\u5230 API Key", {
|
|
226
|
+
hint: "\u8FD0\u884C focalapi auth login --key <sk-...>\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF FOCALAPI_API_KEY\u3002Key \u5728 https://focalapi.com/console/token \u521B\u5EFA\u3002"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/lib/http.ts
|
|
231
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
232
|
+
function buildUrl(baseUrl, path, query) {
|
|
233
|
+
const url = new URL(path.replace(/^\/+/, ""), `${baseUrl}/`);
|
|
234
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
235
|
+
if (value !== void 0) {
|
|
236
|
+
url.searchParams.set(key, String(value));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return url.toString();
|
|
240
|
+
}
|
|
241
|
+
function extractErrorMessage(raw) {
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(raw);
|
|
245
|
+
} catch {
|
|
246
|
+
return { message: raw.slice(0, 500) || "\u672A\u77E5\u9519\u8BEF", body: void 0 };
|
|
247
|
+
}
|
|
248
|
+
const obj = parsed;
|
|
249
|
+
const errObj = obj?.error;
|
|
250
|
+
const message = typeof errObj?.message === "string" && errObj.message || typeof obj?.message === "string" && obj.message || JSON.stringify(parsed).slice(0, 500);
|
|
251
|
+
return { message, body: parsed };
|
|
252
|
+
}
|
|
253
|
+
async function request(opts) {
|
|
254
|
+
const res = await rawRequest(opts);
|
|
255
|
+
const text = await res.text();
|
|
256
|
+
if (text.length === 0) {
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(text);
|
|
261
|
+
} catch {
|
|
262
|
+
throw new ApiError("bad_response", `\u54CD\u5E94\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${text.slice(0, 200)}`, {
|
|
263
|
+
status: res.status
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function rawRequest(opts) {
|
|
268
|
+
const url = buildUrl(opts.baseUrl, opts.path, opts.query);
|
|
269
|
+
const headers = { ...opts.headers };
|
|
270
|
+
if (opts.apiKey) {
|
|
271
|
+
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
272
|
+
}
|
|
273
|
+
let payload;
|
|
274
|
+
if (opts.formData) {
|
|
275
|
+
payload = opts.formData;
|
|
276
|
+
} else if (opts.body !== void 0) {
|
|
277
|
+
headers["Content-Type"] = "application/json";
|
|
278
|
+
payload = JSON.stringify(opts.body);
|
|
279
|
+
}
|
|
280
|
+
let res;
|
|
281
|
+
try {
|
|
282
|
+
res = await fetch(url, {
|
|
283
|
+
method: opts.method ?? (payload !== void 0 ? "POST" : "GET"),
|
|
284
|
+
headers,
|
|
285
|
+
body: payload,
|
|
286
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const name = err?.name ?? "";
|
|
290
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
291
|
+
throw new ApiError("timeout", `\u8BF7\u6C42\u8D85\u65F6\uFF08${opts.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms\uFF09\uFF1A${url}`, {
|
|
292
|
+
hint: "\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u8FD0\u884C focalapi doctor \u68C0\u67E5\u94FE\u8DEF\u8D28\u91CF\u3002"
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
throw new ApiError("network_error", `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF1A${err?.message ?? err}`, {
|
|
296
|
+
hint: "\u68C0\u67E5\u7F51\u7EDC\u4EE3\u7406\u4E0E FOCALAPI_BASE_URL \u914D\u7F6E\uFF1B\u53EF\u8FD0\u884C focalapi doctor \u505A\u94FE\u8DEF\u8BCA\u65AD\u3002"
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
const text = await res.text().catch(() => "");
|
|
301
|
+
const { message, body } = extractErrorMessage(text);
|
|
302
|
+
const code = refineErrorCode(res.status, message);
|
|
303
|
+
throw new ApiError(code, message, { status: res.status, body });
|
|
304
|
+
}
|
|
305
|
+
return res;
|
|
306
|
+
}
|
|
307
|
+
async function* sseEvents(res) {
|
|
308
|
+
if (!res.body) {
|
|
309
|
+
throw new ApiError("bad_response", "\u6D41\u5F0F\u54CD\u5E94\u7F3A\u5C11 body");
|
|
310
|
+
}
|
|
311
|
+
const reader = res.body.getReader();
|
|
312
|
+
const decoder = new TextDecoder();
|
|
313
|
+
let buffer = "";
|
|
314
|
+
try {
|
|
315
|
+
for (; ; ) {
|
|
316
|
+
const { done, value } = await reader.read();
|
|
317
|
+
if (done) break;
|
|
318
|
+
buffer += decoder.decode(value, { stream: true });
|
|
319
|
+
let newlineIndex;
|
|
320
|
+
while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
|
|
321
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
322
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
323
|
+
if (!line.startsWith("data:")) continue;
|
|
324
|
+
const data = line.slice(5).trim();
|
|
325
|
+
if (data === "[DONE]") return;
|
|
326
|
+
if (data) yield data;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const tail = buffer.trim();
|
|
330
|
+
if (tail.startsWith("data:")) {
|
|
331
|
+
const data = tail.slice(5).trim();
|
|
332
|
+
if (data && data !== "[DONE]") yield data;
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
reader.releaseLock();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/commands/auth.ts
|
|
340
|
+
async function fetchTokenUsage(baseUrl, apiKey) {
|
|
341
|
+
const res = await request({
|
|
342
|
+
baseUrl,
|
|
343
|
+
path: "/api/usage/token/",
|
|
344
|
+
apiKey,
|
|
345
|
+
timeoutMs: 15e3
|
|
346
|
+
});
|
|
347
|
+
if (!res.data) {
|
|
348
|
+
throw new ApiError("bad_response", "\u7528\u91CF\u63A5\u53E3\u54CD\u5E94\u7F3A\u5C11 data \u5B57\u6BB5");
|
|
349
|
+
}
|
|
350
|
+
return res.data;
|
|
351
|
+
}
|
|
352
|
+
async function promptForKey() {
|
|
353
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
354
|
+
try {
|
|
355
|
+
const answer = await rl.question("\u8BF7\u7C98\u8D34 API Key\uFF08sk-...\uFF09\uFF1A");
|
|
356
|
+
return answer.trim();
|
|
357
|
+
} finally {
|
|
358
|
+
rl.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function registerAuth(program) {
|
|
362
|
+
const auth = program.command("auth").description("API Key \u767B\u5F55\u4E0E\u72B6\u6001");
|
|
363
|
+
auth.command("login").description("\u9A8C\u8BC1\u5E76\u4FDD\u5B58 API Key\uFF08Key \u5728 https://focalapi.com/console/token \u521B\u5EFA\uFF09").option("--key <key>", "API Key\uFF08sk-...\uFF09\uFF1B\u4E0D\u4F20\u4E14\u4E3A\u7EC8\u7AEF\u73AF\u5883\u65F6\u8FDB\u5165\u4EA4\u4E92\u7C98\u8D34").action(async (opts, cmd) => {
|
|
364
|
+
const g = cmd.optsWithGlobals();
|
|
365
|
+
let key = opts.key ?? g.key ?? process.env.FOCALAPI_API_KEY;
|
|
366
|
+
if (!key) {
|
|
367
|
+
if (!isInteractive()) {
|
|
368
|
+
throw new ApiError("missing_api_key", "\u975E\u4EA4\u4E92\u73AF\u5883\u5FC5\u987B\u901A\u8FC7 --key \u6216 FOCALAPI_API_KEY \u63D0\u4F9B Key", {
|
|
369
|
+
hint: "focalapi auth login --key <sk-...>"
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
info(`\u63A7\u5236\u53F0\u4EE4\u724C\u9875\uFF1Ahttps://focalapi.com/console/token`);
|
|
373
|
+
key = await promptForKey();
|
|
374
|
+
}
|
|
375
|
+
if (!key.startsWith("sk-")) {
|
|
376
|
+
throw new ApiError("invalid_request", "Key \u683C\u5F0F\u4E0D\u6B63\u786E\uFF1A\u5E94\u4EE5 sk- \u5F00\u5934");
|
|
377
|
+
}
|
|
378
|
+
const baseUrl = resolveBaseUrl(g.baseUrl, g.profile);
|
|
379
|
+
const usage = await fetchTokenUsage(baseUrl, key);
|
|
380
|
+
const profileName = g.profile ?? loadConfig().currentProfile ?? DEFAULT_PROFILE;
|
|
381
|
+
const config = loadConfig();
|
|
382
|
+
config.profiles[profileName] = {
|
|
383
|
+
...config.profiles[profileName],
|
|
384
|
+
apiKey: key,
|
|
385
|
+
baseUrl: g.baseUrl ?? process.env.FOCALAPI_BASE_URL ?? config.profiles[profileName]?.baseUrl
|
|
386
|
+
};
|
|
387
|
+
config.currentProfile = profileName;
|
|
388
|
+
saveConfig(config);
|
|
389
|
+
if (g.json) {
|
|
390
|
+
printJson({
|
|
391
|
+
success: true,
|
|
392
|
+
profile: profileName,
|
|
393
|
+
baseUrl,
|
|
394
|
+
key: maskKey(key),
|
|
395
|
+
token: usage
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
info(`\u2713 \u767B\u5F55\u6210\u529F\uFF08profile: ${profileName}\uFF09`);
|
|
399
|
+
info(` Key\uFF1A${maskKey(key)}`);
|
|
400
|
+
info(` \u4EE4\u724C\u540D\uFF1A${usage.name}`);
|
|
401
|
+
info(` \u5269\u4F59\u989D\u5EA6\uFF1A${usage.unlimited_quota ? "\u65E0\u9650" : usage.total_available}`);
|
|
402
|
+
info(` API \u5730\u5740\uFF1A${baseUrl}`);
|
|
403
|
+
info(`\u63A5\u4E0B\u6765\u53EF\u8FD0\u884C\uFF1Afocalapi doctor \u505A\u7AEF\u5230\u7AEF\u81EA\u68C0\uFF08\u4F7F\u7528\u514D\u8D39\u6F14\u7EC3\u6A21\u578B\uFF0C\u4E0D\u6D88\u8017\u989D\u5EA6\uFF09`);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
auth.command("status").description("\u67E5\u770B\u5F53\u524D Key \u7684\u6709\u6548\u6027\u3001\u989D\u5EA6\u4E0E\u6765\u6E90").action(async (_opts, cmd) => {
|
|
407
|
+
const g = cmd.optsWithGlobals();
|
|
408
|
+
const auth2 = resolveAuth(g);
|
|
409
|
+
const usage = await fetchTokenUsage(auth2.baseUrl, auth2.apiKey);
|
|
410
|
+
if (g.json) {
|
|
411
|
+
printJson({
|
|
412
|
+
valid: true,
|
|
413
|
+
key: maskKey(auth2.apiKey),
|
|
414
|
+
keySource: auth2.keySource,
|
|
415
|
+
baseUrl: auth2.baseUrl,
|
|
416
|
+
token: usage
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
printTable(
|
|
420
|
+
["\u9879\u76EE", "\u503C"],
|
|
421
|
+
[
|
|
422
|
+
["Key", maskKey(auth2.apiKey)],
|
|
423
|
+
["\u6765\u6E90", auth2.keySource],
|
|
424
|
+
["API \u5730\u5740", auth2.baseUrl],
|
|
425
|
+
["\u4EE4\u724C\u540D", usage.name],
|
|
426
|
+
["\u5269\u4F59\u989D\u5EA6", usage.unlimited_quota ? "\u65E0\u9650" : String(usage.total_available)],
|
|
427
|
+
["\u5DF2\u7528\u989D\u5EA6", String(usage.total_used)],
|
|
428
|
+
["\u8FC7\u671F\u65F6\u95F4", usage.expires_at > 0 ? new Date(usage.expires_at * 1e3).toLocaleString() : "\u6C38\u4E0D\u8FC7\u671F"]
|
|
429
|
+
]
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
auth.command("logout").description("\u5220\u9664\u672C\u5730\u4FDD\u5B58\u7684 API Key").action(async (_opts, cmd) => {
|
|
434
|
+
const g = cmd.optsWithGlobals();
|
|
435
|
+
const profileName = g.profile ?? loadConfig().currentProfile ?? DEFAULT_PROFILE;
|
|
436
|
+
const profile = getProfile(profileName);
|
|
437
|
+
clearProfile(profileName);
|
|
438
|
+
if (g.json) {
|
|
439
|
+
printJson({ success: true, profile: profileName, hadKey: Boolean(profile.apiKey) });
|
|
440
|
+
} else {
|
|
441
|
+
info(profile.apiKey ? `\u2713 \u5DF2\u5220\u9664 profile\u300C${profileName}\u300D\u7684\u672C\u5730 Key` : `profile\u300C${profileName}\u300D\u672C\u5C31\u6CA1\u6709\u4FDD\u5B58 Key`);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/commands/models.ts
|
|
447
|
+
function registerModels(program) {
|
|
448
|
+
const models = program.command("models").description("\u53EF\u7528\u6A21\u578B\u67E5\u8BE2");
|
|
449
|
+
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").action(async (opts, cmd) => {
|
|
450
|
+
const g = cmd.optsWithGlobals();
|
|
451
|
+
const auth = resolveAuth(g);
|
|
452
|
+
const res = await request({ baseUrl: auth.baseUrl, path: "/v1/models", apiKey: auth.apiKey });
|
|
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
|
+
}
|
|
458
|
+
if (g.json) {
|
|
459
|
+
printJson({ data: list });
|
|
460
|
+
} else {
|
|
461
|
+
printTable(
|
|
462
|
+
["\u6A21\u578B ID", "\u63D0\u4F9B\u65B9"],
|
|
463
|
+
list.map((m) => [m.id, m.owned_by ?? "-"])
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
models.command("get").description("\u67E5\u770B\u5355\u4E2A\u6A21\u578B\u8BE6\u60C5").argument("<model>", "\u6A21\u578B ID").action(async (model, _opts, cmd) => {
|
|
468
|
+
const g = cmd.optsWithGlobals();
|
|
469
|
+
const auth = resolveAuth(g);
|
|
470
|
+
const res = await request({
|
|
471
|
+
baseUrl: auth.baseUrl,
|
|
472
|
+
path: `/v1/models/${encodeURIComponent(model)}`,
|
|
473
|
+
apiKey: auth.apiKey
|
|
474
|
+
});
|
|
475
|
+
if (g.json) {
|
|
476
|
+
printJson(res);
|
|
477
|
+
} else {
|
|
478
|
+
printTable(
|
|
479
|
+
["\u5B57\u6BB5", "\u503C"],
|
|
480
|
+
Object.entries(res).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/lib/fileinput.ts
|
|
487
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
488
|
+
import { basename, extname } from "path";
|
|
489
|
+
var MIME_BY_EXT = {
|
|
490
|
+
".png": "image/png",
|
|
491
|
+
".jpg": "image/jpeg",
|
|
492
|
+
".jpeg": "image/jpeg",
|
|
493
|
+
".webp": "image/webp",
|
|
494
|
+
".gif": "image/gif",
|
|
495
|
+
".mp3": "audio/mpeg",
|
|
496
|
+
".wav": "audio/wav",
|
|
497
|
+
".m4a": "audio/mp4",
|
|
498
|
+
".mp4": "video/mp4",
|
|
499
|
+
".txt": "text/plain",
|
|
500
|
+
".md": "text/markdown",
|
|
501
|
+
".json": "application/json"
|
|
502
|
+
};
|
|
503
|
+
function isImageMime(mime) {
|
|
504
|
+
return mime.startsWith("image/");
|
|
505
|
+
}
|
|
506
|
+
function readInputFile(path) {
|
|
507
|
+
let stat;
|
|
508
|
+
try {
|
|
509
|
+
stat = statSync(path);
|
|
510
|
+
} catch {
|
|
511
|
+
throw new ApiError("invalid_request", `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${path}`);
|
|
512
|
+
}
|
|
513
|
+
if (!stat.isFile()) {
|
|
514
|
+
throw new ApiError("invalid_request", `\u4E0D\u662F\u6587\u4EF6\uFF1A${path}`);
|
|
515
|
+
}
|
|
516
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
517
|
+
throw new ApiError("invalid_request", `\u6587\u4EF6\u8D85\u8FC7 50MB \u4E0A\u9650\uFF1A${path}`, {
|
|
518
|
+
hint: "\u5927\u6587\u4EF6\u8BF7\u5148\u538B\u7F29\u6216\u5206\u6BB5\u5904\u7406\u3002"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const ext = extname(path).toLowerCase();
|
|
522
|
+
const mime = MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
523
|
+
return { path, name: basename(path), mime, size: stat.size, data: readFileSync2(path) };
|
|
524
|
+
}
|
|
525
|
+
function toDataUrl(file) {
|
|
526
|
+
return `data:${file.mime};base64,${file.data.toString("base64")}`;
|
|
527
|
+
}
|
|
528
|
+
async function readStdin() {
|
|
529
|
+
const chunks = [];
|
|
530
|
+
for await (const chunk of process.stdin) {
|
|
531
|
+
chunks.push(Buffer.from(chunk));
|
|
532
|
+
}
|
|
533
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/commands/chat.ts
|
|
537
|
+
function resolveModel(flag) {
|
|
538
|
+
const model = flag ?? process.env.FOCALAPI_MODEL;
|
|
539
|
+
if (!model) {
|
|
540
|
+
throw new ApiError("invalid_request", "\u7F3A\u5C11\u6A21\u578B\u53C2\u6570", {
|
|
541
|
+
hint: "\u7528 -m <model> \u6307\u5B9A\u6A21\u578B\uFF0C\u6216\u8BBE\u7F6E FOCALAPI_MODEL\uFF1B\u53EF\u7528\u6A21\u578B\u89C1 focalapi models list\u3002\u514D\u8D39\u6F14\u7EC3\u6A21\u578B\uFF1Afocal-rehearsal-chat\u3002"
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return model;
|
|
545
|
+
}
|
|
546
|
+
function extractText(content) {
|
|
547
|
+
if (typeof content === "string") return content;
|
|
548
|
+
if (Array.isArray(content)) {
|
|
549
|
+
return content.map((part) => part && typeof part === "object" && "text" in part ? String(part.text ?? "") : "").join("");
|
|
550
|
+
}
|
|
551
|
+
return "";
|
|
552
|
+
}
|
|
553
|
+
function registerChat(program) {
|
|
554
|
+
program.command("chat").description("\u5BF9\u8BDD\u4E0E\u591A\u6A21\u6001\u63A8\u7406\uFF08/v1/chat/completions\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
|
+
async (promptParts, opts, cmd) => {
|
|
556
|
+
const g = cmd.optsWithGlobals();
|
|
557
|
+
const auth = resolveAuth(g);
|
|
558
|
+
const model = resolveModel(opts.model);
|
|
559
|
+
let prompt = promptParts.join(" ").trim();
|
|
560
|
+
if (!prompt && !process.stdin.isTTY) {
|
|
561
|
+
prompt = await readStdin();
|
|
562
|
+
}
|
|
563
|
+
const inputs = (opts.input ?? []).map((p) => readInputFile(p.replace(/^@/, "")));
|
|
564
|
+
if (!prompt && inputs.length === 0) {
|
|
565
|
+
throw new ApiError("invalid_request", "\u7F3A\u5C11\u63D0\u793A\u8BCD", {
|
|
566
|
+
hint: 'focalapi chat "\u4F60\u7684\u95EE\u9898" -m <model>\uFF0C\u6216 echo "\u95EE\u9898" | focalapi chat -m <model>\u3002'
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
let userContent = prompt;
|
|
570
|
+
if (inputs.length > 0) {
|
|
571
|
+
const parts = [];
|
|
572
|
+
if (prompt) parts.push({ type: "text", text: prompt });
|
|
573
|
+
for (const file of inputs) {
|
|
574
|
+
if (isImageMime(file.mime)) {
|
|
575
|
+
parts.push({ type: "image_url", image_url: { url: toDataUrl(file) } });
|
|
576
|
+
} else if (file.mime.startsWith("text/") || file.mime === "application/json") {
|
|
577
|
+
parts.push({ type: "text", text: `
|
|
578
|
+
|
|
579
|
+
[\u6587\u4EF6 ${file.name}]
|
|
580
|
+
${file.data.toString("utf-8")}` });
|
|
581
|
+
} else {
|
|
582
|
+
throw new ApiError("invalid_request", `chat \u6682\u4E0D\u652F\u6301\u8BE5\u6587\u4EF6\u7C7B\u578B\uFF1A${file.name}\uFF08${file.mime}\uFF09`, {
|
|
583
|
+
hint: "\u56FE\u7247\u53EF\u76F4\u63A5\u4F20\u5165\uFF1B\u97F3\u89C6\u9891\u8BF7\u7528 focalapi audio \u7CFB\u5217\u547D\u4EE4\u3002"
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
userContent = parts;
|
|
588
|
+
}
|
|
589
|
+
const messages = [];
|
|
590
|
+
if (opts.system) messages.push({ role: "system", content: opts.system });
|
|
591
|
+
messages.push({ role: "user", content: userContent });
|
|
592
|
+
const body = { model, messages };
|
|
593
|
+
if (opts.maxTokens !== void 0) body.max_tokens = opts.maxTokens;
|
|
594
|
+
const stream = opts.stream ?? (isInteractive() && !g.json);
|
|
595
|
+
body.stream = stream;
|
|
596
|
+
if (!stream) {
|
|
597
|
+
const res2 = await request({
|
|
598
|
+
baseUrl: auth.baseUrl,
|
|
599
|
+
path: "/v1/chat/completions",
|
|
600
|
+
apiKey: auth.apiKey,
|
|
601
|
+
body,
|
|
602
|
+
timeoutMs: 3e5
|
|
603
|
+
});
|
|
604
|
+
if (g.json) {
|
|
605
|
+
printJson(res2);
|
|
606
|
+
} else {
|
|
607
|
+
const text = extractText(res2.choices?.[0]?.message?.content);
|
|
608
|
+
process.stdout.write(text + "\n");
|
|
609
|
+
if (res2.usage) {
|
|
610
|
+
info(`\uFF08tokens: prompt=${res2.usage.prompt_tokens ?? "-"} completion=${res2.usage.completion_tokens ?? "-"}\uFF09`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const res = await rawRequest({
|
|
616
|
+
baseUrl: auth.baseUrl,
|
|
617
|
+
path: "/v1/chat/completions",
|
|
618
|
+
apiKey: auth.apiKey,
|
|
619
|
+
body,
|
|
620
|
+
timeoutMs: 3e5
|
|
621
|
+
});
|
|
622
|
+
const collected = [];
|
|
623
|
+
for await (const data of sseEvents(res)) {
|
|
624
|
+
let chunk;
|
|
625
|
+
try {
|
|
626
|
+
chunk = JSON.parse(data);
|
|
627
|
+
} catch {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const text = extractText(chunk.choices?.[0]?.delta?.content);
|
|
631
|
+
if (text) {
|
|
632
|
+
collected.push(text);
|
|
633
|
+
process.stdout.write(text);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
process.stdout.write("\n");
|
|
637
|
+
if (g.json) {
|
|
638
|
+
printJson({ model, content: collected.join("") });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/commands/doctor.ts
|
|
645
|
+
var REHEARSAL_MODEL = "focal-rehearsal-chat";
|
|
646
|
+
var CHECK_TIMEOUT_MS = 15e3;
|
|
647
|
+
async function runCheck(name, fn) {
|
|
648
|
+
try {
|
|
649
|
+
return { name, ok: true, detail: await fn() };
|
|
650
|
+
} catch (err) {
|
|
651
|
+
if (err instanceof ApiError) {
|
|
652
|
+
return { name, ok: false, detail: `[${err.code}] ${err.message}`, hint: err.hint };
|
|
653
|
+
}
|
|
654
|
+
return { name, ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function registerDoctor(program) {
|
|
658
|
+
program.command("doctor").description("\u53EA\u8BFB\u8BCA\u65AD\uFF1A\u7F51\u7EDC\u3001\u9274\u6743\u3001\u6F14\u7EC3\u6A21\u578B\u7AEF\u5230\u7AEF\u3001\u989D\u5EA6\uFF08\u4E0D\u4FEE\u6539\u4EFB\u4F55\u8D44\u6E90\uFF09").action(async (_opts, cmd) => {
|
|
659
|
+
const g = cmd.optsWithGlobals();
|
|
660
|
+
const results = [];
|
|
661
|
+
let auth;
|
|
662
|
+
results.push(
|
|
663
|
+
await runCheck("API Key \u89E3\u6790", async () => {
|
|
664
|
+
auth = resolveAuth(g);
|
|
665
|
+
return `${maskKey(auth.apiKey)}\uFF08\u6765\u6E90\uFF1A${auth.keySource}\uFF09`;
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
if (auth) {
|
|
669
|
+
const a = auth;
|
|
670
|
+
results.push(
|
|
671
|
+
await runCheck("\u7F51\u7EDC\u4E0E\u9274\u6743\uFF08GET /v1/models\uFF09", async () => {
|
|
672
|
+
const res = await request({
|
|
673
|
+
baseUrl: a.baseUrl,
|
|
674
|
+
path: "/v1/models",
|
|
675
|
+
apiKey: a.apiKey,
|
|
676
|
+
timeoutMs: CHECK_TIMEOUT_MS
|
|
677
|
+
});
|
|
678
|
+
return `${a.baseUrl} \u53EF\u8FBE\uFF0C${res.data?.length ?? 0} \u4E2A\u53EF\u7528\u6A21\u578B`;
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
results.push(
|
|
682
|
+
await runCheck(`\u7AEF\u5230\u7AEF\u63A8\u7406\uFF08${REHEARSAL_MODEL}\uFF0C\u514D\u8D39\u6F14\u7EC3\u6A21\u578B\uFF09`, async () => {
|
|
683
|
+
const res = await request({
|
|
684
|
+
baseUrl: a.baseUrl,
|
|
685
|
+
path: "/v1/chat/completions",
|
|
686
|
+
apiKey: a.apiKey,
|
|
687
|
+
body: { model: REHEARSAL_MODEL, messages: [{ role: "user", content: "ping" }] },
|
|
688
|
+
timeoutMs: CHECK_TIMEOUT_MS
|
|
689
|
+
});
|
|
690
|
+
const ok = (res.choices?.length ?? 0) > 0;
|
|
691
|
+
if (!ok) throw new ApiError("bad_response", "\u6F14\u7EC3\u6A21\u578B\u54CD\u5E94\u7F3A\u5C11 choices");
|
|
692
|
+
return "\u6F14\u7EC3\u6A21\u578B\u5F80\u8FD4\u6210\u529F";
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
results.push(
|
|
696
|
+
await runCheck("\u989D\u5EA6\uFF08GET /api/usage/token/\uFF09", async () => {
|
|
697
|
+
const usage = await fetchTokenUsage(a.baseUrl, a.apiKey);
|
|
698
|
+
const quota = usage.unlimited_quota ? "\u65E0\u9650" : `\u5269\u4F59 ${usage.total_available}`;
|
|
699
|
+
const expiry = usage.expires_at > 0 ? `\uFF0C${new Date(usage.expires_at * 1e3).toLocaleDateString()} \u8FC7\u671F` : "";
|
|
700
|
+
return `${quota}${expiry}`;
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const allOk = results.every((r) => r.ok);
|
|
705
|
+
if (g.json) {
|
|
706
|
+
printJson({ ok: allOk, checks: results });
|
|
707
|
+
} else {
|
|
708
|
+
printTable(
|
|
709
|
+
["\u68C0\u67E5\u9879", "\u7ED3\u679C", "\u8BE6\u60C5"],
|
|
710
|
+
results.map((r) => [r.name, r.ok ? "\u2713" : "\u2717", r.detail + (r.hint ? `
|
|
711
|
+
\u63D0\u793A\uFF1A${r.hint}` : "")])
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (!allOk) {
|
|
715
|
+
process.exitCode = 1;
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/commands/gen.ts
|
|
721
|
+
import { createWriteStream as createWriteStream2 } from "fs";
|
|
722
|
+
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
723
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
724
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
725
|
+
import { Readable as Readable2 } from "stream";
|
|
726
|
+
|
|
727
|
+
// src/lib/tasks.ts
|
|
728
|
+
import { createWriteStream } from "fs";
|
|
729
|
+
import { mkdir } from "fs/promises";
|
|
730
|
+
import { join as join2, resolve } from "path";
|
|
731
|
+
import { pipeline } from "stream/promises";
|
|
732
|
+
import { Readable } from "stream";
|
|
733
|
+
var SUCCESS_STATES = /* @__PURE__ */ new Set(["success", "succeeded", "completed", "done", "finish", "finished"]);
|
|
734
|
+
var FAILED_STATES = /* @__PURE__ */ new Set(["failed", "failure", "error", "cancelled", "canceled"]);
|
|
735
|
+
var RUNNING_STATES = /* @__PURE__ */ new Set(["running", "processing", "in_progress", "generating"]);
|
|
736
|
+
var PENDING_STATES = /* @__PURE__ */ new Set(["pending", "queued", "submitted", "waiting", "not_start"]);
|
|
737
|
+
function normalizeTaskStatus(raw) {
|
|
738
|
+
const s = String(raw ?? "").toLowerCase();
|
|
739
|
+
if (SUCCESS_STATES.has(s)) return "success";
|
|
740
|
+
if (FAILED_STATES.has(s)) return "failed";
|
|
741
|
+
if (RUNNING_STATES.has(s)) return "running";
|
|
742
|
+
if (PENDING_STATES.has(s)) return "pending";
|
|
743
|
+
return "unknown";
|
|
744
|
+
}
|
|
745
|
+
function extractTaskId(body) {
|
|
746
|
+
if (!body || typeof body !== "object") return void 0;
|
|
747
|
+
const obj = body;
|
|
748
|
+
const direct = obj.task_id ?? obj.id ?? obj.taskId;
|
|
749
|
+
if (typeof direct === "string" && direct) return direct;
|
|
750
|
+
if (typeof direct === "number") return String(direct);
|
|
751
|
+
const data = obj.data;
|
|
752
|
+
const nested = data?.task_id ?? data?.id;
|
|
753
|
+
if (typeof nested === "string" && nested) return nested;
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
function extractProgress(body) {
|
|
757
|
+
if (!body || typeof body !== "object") return void 0;
|
|
758
|
+
const obj = body;
|
|
759
|
+
for (const key of ["progress", "percent"]) {
|
|
760
|
+
const v = obj[key] ?? obj.data?.[key];
|
|
761
|
+
if (typeof v === "number") return v <= 1 ? Math.round(v * 100) : Math.round(v);
|
|
762
|
+
if (typeof v === "string") {
|
|
763
|
+
const n = Number.parseFloat(v.replace("%", ""));
|
|
764
|
+
if (!Number.isNaN(n)) return n <= 1 ? Math.round(n * 100) : Math.round(n);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return void 0;
|
|
768
|
+
}
|
|
769
|
+
async function fetchTask(baseUrl, apiKey, taskId) {
|
|
770
|
+
const raw = await request({
|
|
771
|
+
baseUrl,
|
|
772
|
+
path: `/v1/video/generations/${encodeURIComponent(taskId)}`,
|
|
773
|
+
apiKey
|
|
774
|
+
});
|
|
775
|
+
const obj = raw;
|
|
776
|
+
const rawStatus = String(obj?.status ?? obj?.data?.status ?? "");
|
|
777
|
+
return {
|
|
778
|
+
taskId,
|
|
779
|
+
status: normalizeTaskStatus(rawStatus),
|
|
780
|
+
rawStatus,
|
|
781
|
+
progress: extractProgress(raw),
|
|
782
|
+
raw
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
786
|
+
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
787
|
+
const timeoutMs = opts?.timeoutMs ?? 30 * 6e4;
|
|
788
|
+
const deadline = Date.now() + timeoutMs;
|
|
789
|
+
let last;
|
|
790
|
+
for (; ; ) {
|
|
791
|
+
last = await fetchTask(baseUrl, apiKey, taskId);
|
|
792
|
+
opts?.onUpdate?.(last);
|
|
793
|
+
if (last.status === "success") return last;
|
|
794
|
+
if (last.status === "failed") {
|
|
795
|
+
throw new ApiError("task_failed", `\u4EFB\u52A1 ${taskId} \u5931\u8D25\uFF08\u4E0A\u6E38\u72B6\u6001\uFF1A${last.rawStatus || "unknown"}\uFF09`, {
|
|
796
|
+
body: last.raw,
|
|
797
|
+
hint: "\u8FD0\u884C focalapi task status " + taskId + " --json \u67E5\u770B\u4E0A\u6E38\u8FD4\u56DE\u8BE6\u60C5\uFF1B\u82E5\u662F\u63D0\u793A\u8BCD\u6216\u53C2\u6570\u95EE\u9898\u8BF7\u8C03\u6574\u540E\u91CD\u8BD5\u3002"
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (Date.now() > deadline) {
|
|
801
|
+
throw new ApiError("timeout", `\u4EFB\u52A1 ${taskId} \u7B49\u5F85\u8D85\u65F6\uFF08${Math.round(timeoutMs / 6e4)} \u5206\u949F\uFF09`, {
|
|
802
|
+
hint: `\u53EF\u7A0D\u540E\u8FD0\u884C focalapi task status ${taskId} \u67E5\u770B\uFF0C\u6216 focalapi task download ${taskId} \u7EED\u53D6\u4EA7\u7269\u3002`
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
var EXT_BY_CONTENT_TYPE = {
|
|
809
|
+
"video/mp4": ".mp4",
|
|
810
|
+
"video/webm": ".webm",
|
|
811
|
+
"image/png": ".png",
|
|
812
|
+
"image/jpeg": ".jpg",
|
|
813
|
+
"image/webp": ".webp",
|
|
814
|
+
"audio/mpeg": ".mp3",
|
|
815
|
+
"audio/wav": ".wav"
|
|
816
|
+
};
|
|
817
|
+
async function downloadTaskContent(baseUrl, apiKey, taskId, outDir, filenameBase) {
|
|
818
|
+
const res = await rawRequest({
|
|
819
|
+
baseUrl,
|
|
820
|
+
path: `/v1/videos/${encodeURIComponent(taskId)}/content`,
|
|
821
|
+
apiKey,
|
|
822
|
+
timeoutMs: 6e5
|
|
823
|
+
});
|
|
824
|
+
const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
|
|
825
|
+
const ext = EXT_BY_CONTENT_TYPE[contentType] ?? ".bin";
|
|
826
|
+
const dir = resolve(outDir);
|
|
827
|
+
await mkdir(dir, { recursive: true });
|
|
828
|
+
const filePath = join2(dir, `${filenameBase ?? `task-${taskId}`}${ext}`);
|
|
829
|
+
if (!res.body) {
|
|
830
|
+
throw new ApiError("bad_response", "\u4E0B\u8F7D\u54CD\u5E94\u7F3A\u5C11 body");
|
|
831
|
+
}
|
|
832
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(filePath));
|
|
833
|
+
return filePath;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/commands/gen.ts
|
|
837
|
+
var MAX_IMAGE_N = 128;
|
|
838
|
+
var MAX_TASK_DURATION_SECONDS = 3600;
|
|
839
|
+
var DEFAULT_OUT_DIR = "focalapi-out";
|
|
840
|
+
function clampInt(value, min, max, name) {
|
|
841
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
842
|
+
throw new ApiError("invalid_request", `${name} \u5FC5\u987B\u662F ${min}\u2013${max} \u7684\u6574\u6570\uFF08\u6536\u5230\uFF1A${value}\uFF09`);
|
|
843
|
+
}
|
|
844
|
+
return value;
|
|
845
|
+
}
|
|
846
|
+
async function saveImageItem(item, dir, base, apiKey) {
|
|
847
|
+
if (item.b64_json) {
|
|
848
|
+
const filePath = join3(dir, `${base}.png`);
|
|
849
|
+
await writeFile(filePath, Buffer.from(item.b64_json, "base64"));
|
|
850
|
+
return filePath;
|
|
851
|
+
}
|
|
852
|
+
if (item.url) {
|
|
853
|
+
const res = await fetch(item.url, {
|
|
854
|
+
headers: item.url.includes("focalapi") ? { Authorization: `Bearer ${apiKey}` } : void 0,
|
|
855
|
+
signal: AbortSignal.timeout(3e5)
|
|
856
|
+
});
|
|
857
|
+
if (!res.ok || !res.body) {
|
|
858
|
+
throw new ApiError("bad_response", `\u56FE\u50CF\u4E0B\u8F7D\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A${item.url.slice(0, 120)}`);
|
|
859
|
+
}
|
|
860
|
+
const ext = res.headers.get("content-type")?.includes("jpeg") ? ".jpg" : ".png";
|
|
861
|
+
const filePath = join3(dir, `${base}${ext}`);
|
|
862
|
+
await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(filePath));
|
|
863
|
+
return filePath;
|
|
864
|
+
}
|
|
865
|
+
throw new ApiError("bad_response", "\u56FE\u50CF\u7ED3\u679C\u65E2\u6CA1\u6709 url \u4E5F\u6CA1\u6709 b64_json");
|
|
866
|
+
}
|
|
867
|
+
function registerGen(program) {
|
|
868
|
+
const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
|
|
869
|
+
gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u540C\u6B65\u8FD4\u56DE\uFF0C\u4EA7\u7269\u81EA\u52A8\u4E0B\u8F7D\u5230\u672C\u5730\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "\u56FE\u50CF\u6A21\u578B ID\uFF08focalapi models list \u67E5\u770B\uFF09").option("--size <size>", "\u5C3A\u5BF8\uFF0C\u5982 1024x1024").option("--n <count>", "\u5F20\u6570\uFF081\u2013128\uFF09", (v) => Number.parseInt(v, 10), 1).option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
|
|
870
|
+
const g = cmd.optsWithGlobals();
|
|
871
|
+
const auth = resolveAuth(g);
|
|
872
|
+
const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
|
|
873
|
+
const body = { model: opts.model, prompt: promptParts.join(" "), n };
|
|
874
|
+
if (opts.size) body.size = opts.size;
|
|
875
|
+
const res = await request({
|
|
876
|
+
baseUrl: auth.baseUrl,
|
|
877
|
+
path: "/v1/images/generations",
|
|
878
|
+
apiKey: auth.apiKey,
|
|
879
|
+
body,
|
|
880
|
+
timeoutMs: 6e5
|
|
881
|
+
});
|
|
882
|
+
const items = res.data ?? [];
|
|
883
|
+
if (items.length === 0) {
|
|
884
|
+
throw new ApiError("bad_response", "\u56FE\u50CF\u751F\u6210\u54CD\u5E94\u4E3A\u7A7A", { body: res });
|
|
885
|
+
}
|
|
886
|
+
const dir = resolve2(opts.out);
|
|
887
|
+
await mkdir2(dir, { recursive: true });
|
|
888
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
889
|
+
const files = [];
|
|
890
|
+
for (const [i, item] of items.entries()) {
|
|
891
|
+
files.push(await saveImageItem(item, dir, `image-${ts}-${i + 1}`, auth.apiKey));
|
|
892
|
+
}
|
|
893
|
+
if (g.json) {
|
|
894
|
+
printJson({ files, count: files.length });
|
|
895
|
+
} else {
|
|
896
|
+
for (const f of files) info(`\u2713 ${f}`);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u4EFB\u52A1\u5236\uFF1A\u9ED8\u8BA4\u8F6E\u8BE2\u81F3\u5B8C\u6210\u5E76\u4E0B\u8F7D\uFF1B--no-wait \u53EA\u53D6 task_id\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF08focalapi models list \u67E5\u770B\uFF09").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF081\u20133600\uFF09", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").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).action(
|
|
900
|
+
async (promptParts, opts, cmd) => {
|
|
901
|
+
const g = cmd.optsWithGlobals();
|
|
902
|
+
const auth = resolveAuth(g);
|
|
903
|
+
const body = { model: opts.model, prompt: promptParts.join(" ") };
|
|
904
|
+
if (opts.seconds !== void 0) {
|
|
905
|
+
body.seconds = String(clampInt(opts.seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds"));
|
|
906
|
+
}
|
|
907
|
+
if (opts.size) body.size = opts.size;
|
|
908
|
+
const created = await request({
|
|
909
|
+
baseUrl: auth.baseUrl,
|
|
910
|
+
path: "/v1/video/generations",
|
|
911
|
+
apiKey: auth.apiKey,
|
|
912
|
+
body,
|
|
913
|
+
timeoutMs: 12e4
|
|
914
|
+
});
|
|
915
|
+
const taskId = extractTaskId(created);
|
|
916
|
+
if (!taskId) {
|
|
917
|
+
throw new ApiError("bad_response", "\u89C6\u9891\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: created });
|
|
918
|
+
}
|
|
919
|
+
if (opts.wait === false) {
|
|
920
|
+
if (g.json) {
|
|
921
|
+
printJson({ task_id: taskId, submitted: true });
|
|
922
|
+
} else {
|
|
923
|
+
process.stdout.write(taskId + "\n");
|
|
924
|
+
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}`);
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
info(`\u4EFB\u52A1 ${taskId} \u5DF2\u63D0\u4EA4\uFF0C\u7B49\u5F85\u5B8C\u6210\u2026\u2026`);
|
|
929
|
+
const final = await pollTask(auth.baseUrl, auth.apiKey, taskId, {
|
|
930
|
+
intervalMs: opts.pollInterval,
|
|
931
|
+
timeoutMs: opts.timeout * 6e4,
|
|
932
|
+
onUpdate: (t) => {
|
|
933
|
+
if (!g.json) {
|
|
934
|
+
info(` \u72B6\u6001\uFF1A${t.rawStatus || t.status}${t.progress !== void 0 ? `\uFF08${t.progress}%\uFF09` : ""}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
939
|
+
if (g.json) {
|
|
940
|
+
printJson({ task_id: taskId, status: final.status, file: filePath });
|
|
941
|
+
} else {
|
|
942
|
+
info(`\u2713 ${filePath}`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/commands/task.ts
|
|
949
|
+
function registerTask(program) {
|
|
950
|
+
const task = program.command("task").description("\u4EFB\u52A1\u67E5\u8BE2\u4E0E\u4EA7\u7269\u4E0B\u8F7D\uFF08\u89C6\u9891\u7B49\u4EFB\u52A1\u5236\u80FD\u529B\uFF09");
|
|
951
|
+
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
|
|
952
|
+
const g = cmd.optsWithGlobals();
|
|
953
|
+
const auth = resolveAuth(g);
|
|
954
|
+
const info_ = await fetchTask(auth.baseUrl, auth.apiKey, taskId);
|
|
955
|
+
if (g.json) {
|
|
956
|
+
printJson({ task_id: taskId, status: info_.status, raw_status: info_.rawStatus, progress: info_.progress, raw: info_.raw });
|
|
957
|
+
} else {
|
|
958
|
+
printTable(
|
|
959
|
+
["\u5B57\u6BB5", "\u503C"],
|
|
960
|
+
[
|
|
961
|
+
["\u4EFB\u52A1 ID", taskId],
|
|
962
|
+
["\u72B6\u6001", `${info_.status}${info_.rawStatus && info_.rawStatus !== info_.status ? `\uFF08\u4E0A\u6E38\uFF1A${info_.rawStatus}\uFF09` : ""}`],
|
|
963
|
+
["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"]
|
|
964
|
+
]
|
|
965
|
+
);
|
|
966
|
+
if (info_.status === "success") {
|
|
967
|
+
info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
task.command("download").description("\u4E0B\u8F7D\u4EFB\u52A1\u4EA7\u7269\uFF08\u7ECF focalapi \u5185\u5BB9\u4EE3\u7406\uFF0C\u65E0\u9700\u4E0A\u6E38\u7B7E\u540D URL\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", "focalapi-out").action(async (taskId, opts, cmd) => {
|
|
972
|
+
const g = cmd.optsWithGlobals();
|
|
973
|
+
const auth = resolveAuth(g);
|
|
974
|
+
const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
975
|
+
if (g.json) {
|
|
976
|
+
printJson({ task_id: taskId, file: filePath });
|
|
977
|
+
} else {
|
|
978
|
+
info(`\u2713 ${filePath}`);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
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
|
+
// src/commands/audio.ts
|
|
1041
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
1042
|
+
import { resolve as resolve3 } from "path";
|
|
1043
|
+
function registerAudio(program) {
|
|
1044
|
+
const audio = program.command("audio").description("\u97F3\u9891\uFF1A\u8F6C\u5199\u4E0E\u5408\u6210");
|
|
1045
|
+
audio.command("transcribe").description("\u8BED\u97F3\u8F6C\u6587\u5B57").argument("<file>", "\u97F3\u9891\u6587\u4EF6\u8DEF\u5F84").requiredOption("-m, --model <model>", "\u8F6C\u5199\u6A21\u578B ID").option("--language <lang>", "\u8BED\u8A00\u4EE3\u7801\uFF08\u5982 zh\u3001en\uFF09").action(async (file, opts, cmd) => {
|
|
1046
|
+
const g = cmd.optsWithGlobals();
|
|
1047
|
+
const auth = resolveAuth(g);
|
|
1048
|
+
const input = readInputFile(file);
|
|
1049
|
+
const form = new FormData();
|
|
1050
|
+
form.append("file", new Blob([input.data], { type: input.mime }), input.name);
|
|
1051
|
+
form.append("model", opts.model);
|
|
1052
|
+
if (opts.language) form.append("language", opts.language);
|
|
1053
|
+
const res = await request({
|
|
1054
|
+
baseUrl: auth.baseUrl,
|
|
1055
|
+
path: "/v1/audio/transcriptions",
|
|
1056
|
+
apiKey: auth.apiKey,
|
|
1057
|
+
formData: form,
|
|
1058
|
+
timeoutMs: 3e5
|
|
1059
|
+
});
|
|
1060
|
+
if (g.json) {
|
|
1061
|
+
printJson(res);
|
|
1062
|
+
} else {
|
|
1063
|
+
process.stdout.write((res.text ?? JSON.stringify(res)) + "\n");
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
audio.command("speech").description("\u6587\u5B57\u8F6C\u8BED\u97F3\uFF0C\u4EA7\u7269\u4FDD\u5B58\u4E3A\u97F3\u9891\u6587\u4EF6").argument("<text...>", "\u8981\u5408\u6210\u7684\u6587\u672C").requiredOption("-m, --model <model>", "TTS \u6A21\u578B ID").option("--voice <voice>", "\u97F3\u8272", "alloy").option("--format <fmt>", "\u97F3\u9891\u683C\u5F0F\uFF08mp3/wav/...\uFF09", "mp3").option("-o, --out <file>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (textParts, opts, cmd) => {
|
|
1067
|
+
const g = cmd.optsWithGlobals();
|
|
1068
|
+
const auth = resolveAuth(g);
|
|
1069
|
+
const res = await rawRequest({
|
|
1070
|
+
baseUrl: auth.baseUrl,
|
|
1071
|
+
path: "/v1/audio/speech",
|
|
1072
|
+
apiKey: auth.apiKey,
|
|
1073
|
+
body: { model: opts.model, input: textParts.join(" "), voice: opts.voice, response_format: opts.format },
|
|
1074
|
+
timeoutMs: 3e5
|
|
1075
|
+
});
|
|
1076
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1077
|
+
if (buf.length === 0) {
|
|
1078
|
+
throw new ApiError("bad_response", "\u8BED\u97F3\u5408\u6210\u8FD4\u56DE\u7A7A\u5185\u5BB9");
|
|
1079
|
+
}
|
|
1080
|
+
const outPath = resolve3(opts.out ?? `focalapi-out/speech-${Date.now()}.${opts.format}`);
|
|
1081
|
+
await writeFile2(outPath, buf);
|
|
1082
|
+
if (g.json) {
|
|
1083
|
+
printJson({ file: outPath, bytes: buf.length });
|
|
1084
|
+
} else {
|
|
1085
|
+
info(`\u2713 ${outPath}`);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
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
|
+
// src/commands/usage.ts
|
|
1161
|
+
function defaultStartDate() {
|
|
1162
|
+
const now = /* @__PURE__ */ new Date();
|
|
1163
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
1164
|
+
}
|
|
1165
|
+
function todayDate() {
|
|
1166
|
+
const now = /* @__PURE__ */ new Date();
|
|
1167
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
1168
|
+
}
|
|
1169
|
+
function registerUsage(program) {
|
|
1170
|
+
program.command("usage").description("\u67E5\u770B\u5F53\u524D Key \u7684\u989D\u5EA6\u4E0E\u5468\u671F\u7528\u91CF").option("--start <date>", "\u7528\u91CF\u7EDF\u8BA1\u8D77\u59CB\u65E5\uFF08YYYY-MM-DD\uFF0C\u9ED8\u8BA4\u5F53\u6708 1 \u65E5\uFF09").option("--end <date>", "\u7528\u91CF\u7EDF\u8BA1\u622A\u6B62\u65E5\uFF08YYYY-MM-DD\uFF0C\u9ED8\u8BA4\u4ECA\u5929\uFF09").action(async (opts, cmd) => {
|
|
1171
|
+
const g = cmd.optsWithGlobals();
|
|
1172
|
+
const auth = resolveAuth(g);
|
|
1173
|
+
const token = await fetchTokenUsage(auth.baseUrl, auth.apiKey);
|
|
1174
|
+
const start = opts.start ?? defaultStartDate();
|
|
1175
|
+
const end = opts.end ?? todayDate();
|
|
1176
|
+
const billing = await request({
|
|
1177
|
+
baseUrl: auth.baseUrl,
|
|
1178
|
+
path: "/v1/dashboard/billing/usage",
|
|
1179
|
+
query: { start_date: start, end_date: end },
|
|
1180
|
+
apiKey: auth.apiKey
|
|
1181
|
+
});
|
|
1182
|
+
if (g.json) {
|
|
1183
|
+
printJson({ token, period: { start, end }, billing });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
printTable(
|
|
1187
|
+
["\u9879\u76EE", "\u503C"],
|
|
1188
|
+
[
|
|
1189
|
+
["\u4EE4\u724C\u540D", token.name],
|
|
1190
|
+
["\u603B\u989D\u5EA6", token.unlimited_quota ? "\u65E0\u9650" : String(token.total_granted)],
|
|
1191
|
+
["\u5DF2\u7528", String(token.total_used)],
|
|
1192
|
+
["\u5269\u4F59", token.unlimited_quota ? "\u65E0\u9650" : String(token.total_available)],
|
|
1193
|
+
["\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`, JSON.stringify(billing)]
|
|
1195
|
+
]
|
|
1196
|
+
);
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/commands/connect.ts
|
|
1201
|
+
import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1202
|
+
import { homedir as homedir2, platform } from "os";
|
|
1203
|
+
import { dirname, join as join4 } from "path";
|
|
1204
|
+
import { fileURLToPath } from "url";
|
|
1205
|
+
var MANIFEST_NAME = ".focalapi-connect-manifest.json";
|
|
1206
|
+
function homeDir() {
|
|
1207
|
+
return normalizeHomePath(process.env.FOCALAPI_HOME ?? homedir2());
|
|
1208
|
+
}
|
|
1209
|
+
function getTargets() {
|
|
1210
|
+
const home = homeDir();
|
|
1211
|
+
const claudeRoot = join4(home, ".claude");
|
|
1212
|
+
const codexRoot = join4(home, ".codex");
|
|
1213
|
+
const opencodeRoot = join4(home, ".config", "opencode");
|
|
1214
|
+
const hermesRoot = platform() === "win32" ? join4(home, "AppData", "Local", "hermes") : join4(home, ".config", "hermes");
|
|
1215
|
+
return [
|
|
1216
|
+
{
|
|
1217
|
+
id: "claude-code",
|
|
1218
|
+
name: "Claude Code",
|
|
1219
|
+
skillsDir: join4(claudeRoot, "skills"),
|
|
1220
|
+
detected: existsSync2(claudeRoot),
|
|
1221
|
+
providerHint: [
|
|
1222
|
+
"Claude Code provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
|
|
1223
|
+
" \u5728 ~/.claude/settings.json \u7684 env \u6BB5\u52A0\u5165\uFF1A",
|
|
1224
|
+
' "ANTHROPIC_BASE_URL": "https://api.focalapi.com",',
|
|
1225
|
+
' "ANTHROPIC_AUTH_TOKEN": "<\u4F60\u7684 sk- key>"',
|
|
1226
|
+
" \uFF08focalapi \u5DF2\u517C\u5BB9 /v1/messages \u534F\u8BAE\uFF09"
|
|
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
|
+
];
|
|
1268
|
+
}
|
|
1269
|
+
function bundledSkillsDir() {
|
|
1270
|
+
if (process.env.FOCALAPI_SKILLS_DIR) {
|
|
1271
|
+
return process.env.FOCALAPI_SKILLS_DIR;
|
|
1272
|
+
}
|
|
1273
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1274
|
+
const candidates = [join4(here, "..", "skills"), join4(here, "..", "..", "skills")];
|
|
1275
|
+
for (const dir of candidates) {
|
|
1276
|
+
if (existsSync2(join4(dir, "focalapi", "SKILL.md"))) {
|
|
1277
|
+
return dir;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
throw new ApiError("internal_error", "\u672A\u627E\u5230\u5185\u7F6E skills \u76EE\u5F55\uFF08focalapi/SKILL.md \u7F3A\u5931\uFF09");
|
|
1281
|
+
}
|
|
1282
|
+
function listBundledSkills(srcDir) {
|
|
1283
|
+
const dir = srcDir ?? bundledSkillsDir();
|
|
1284
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("focalapi") && existsSync2(join4(dir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
1285
|
+
}
|
|
1286
|
+
function manifestPath(skillsDir) {
|
|
1287
|
+
return join4(skillsDir, MANIFEST_NAME);
|
|
1288
|
+
}
|
|
1289
|
+
function readManifest(skillsDir) {
|
|
1290
|
+
const path = manifestPath(skillsDir);
|
|
1291
|
+
if (!existsSync2(path)) return void 0;
|
|
1292
|
+
try {
|
|
1293
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
1294
|
+
} catch {
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function installTo(target, skills, srcDir) {
|
|
1299
|
+
mkdirSync2(target.skillsDir, { recursive: true });
|
|
1300
|
+
for (const skill of skills) {
|
|
1301
|
+
cpSync(join4(srcDir, skill), join4(target.skillsDir, skill), { recursive: true });
|
|
1302
|
+
}
|
|
1303
|
+
const manifest = {
|
|
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;
|
|
1311
|
+
}
|
|
1312
|
+
function uninstallFrom(target) {
|
|
1313
|
+
const manifest = readManifest(target.skillsDir);
|
|
1314
|
+
if (!manifest) {
|
|
1315
|
+
return { removed: [], hadManifest: false };
|
|
1316
|
+
}
|
|
1317
|
+
const removed = [];
|
|
1318
|
+
for (const skill of manifest.skills) {
|
|
1319
|
+
const dir = join4(target.skillsDir, skill);
|
|
1320
|
+
if (skill.startsWith("focalapi") && existsSync2(dir)) {
|
|
1321
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1322
|
+
removed.push(skill);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
rmSync(manifestPath(target.skillsDir), { force: true });
|
|
1326
|
+
return { removed, hadManifest: true };
|
|
1327
|
+
}
|
|
1328
|
+
function resolveTargets(targetIds, g) {
|
|
1329
|
+
const all = getTargets();
|
|
1330
|
+
if (targetIds && targetIds.length > 0) {
|
|
1331
|
+
const unknown = targetIds.filter((id) => !all.some((t) => t.id === id));
|
|
1332
|
+
if (unknown.length > 0) {
|
|
1333
|
+
throw new ApiError("invalid_request", `\u672A\u77E5 Agent\uFF1A${unknown.join(", ")}`, {
|
|
1334
|
+
hint: `\u53EF\u9009\uFF1A${all.map((t) => t.id).join(" | ")}\u3002`
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
return all.filter((t) => targetIds.includes(t.id));
|
|
1338
|
+
}
|
|
1339
|
+
const detected = all.filter((t) => t.detected);
|
|
1340
|
+
if (detected.length === 0) {
|
|
1341
|
+
throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\u4EFB\u4F55\u672C\u673A Agent", {
|
|
1342
|
+
hint: `\u652F\u6301\uFF1A${all.map((t) => `${t.id}\uFF08${t.name}\uFF09`).join("\u3001")}\u3002\u53EF\u5148\u663E\u5F0F\u6307\u5B9A\uFF1Afocalapi connect install <agent>\u3002`
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (!g.json) {
|
|
1346
|
+
info(`\u68C0\u6D4B\u5230 ${detected.length} \u4E2A Agent\uFF1A${detected.map((t) => t.name).join("\u3001")}`);
|
|
1347
|
+
}
|
|
1348
|
+
return detected;
|
|
1349
|
+
}
|
|
1350
|
+
function registerConnect(program) {
|
|
1351
|
+
const connect = program.command("connect").description("\u628A focalapi \u80FD\u529B\u6CE8\u5165\u672C\u673A AI Agent\uFF08skills \u5B89\u88C5/\u5378\u8F7D\uFF09");
|
|
1352
|
+
connect.command("list").description("\u5217\u51FA\u652F\u6301\u7684 Agent \u53CA\u68C0\u6D4B/\u5B89\u88C5\u72B6\u6001").action(async (_opts, cmd) => {
|
|
1353
|
+
const g = cmd.optsWithGlobals();
|
|
1354
|
+
const rows = getTargets().map((t) => {
|
|
1355
|
+
const manifest = readManifest(t.skillsDir);
|
|
1356
|
+
return {
|
|
1357
|
+
id: t.id,
|
|
1358
|
+
name: t.name,
|
|
1359
|
+
detected: t.detected,
|
|
1360
|
+
skillsDir: t.skillsDir,
|
|
1361
|
+
installed: manifest ? `${manifest.skills.length} \u4E2A\u6280\u80FD\uFF08v${manifest.version}\uFF09` : ""
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1364
|
+
if (g.json) {
|
|
1365
|
+
printJson({ agents: rows });
|
|
1366
|
+
} else {
|
|
1367
|
+
printTable(
|
|
1368
|
+
["Agent", "ID", "\u68C0\u6D4B\u5230", "\u5DF2\u5B89\u88C5", "\u6280\u80FD\u76EE\u5F55"],
|
|
1369
|
+
rows.map((r) => [r.name, r.id, r.detected ? "\u2713" : "-", r.installed || "-", r.skillsDir])
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
connect.command("install").description("\u5411\u6307\u5B9A\uFF08\u6216\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684\uFF09Agent \u5B89\u88C5 focalapi \u6280\u80FD\u5305").argument("[targets...]", `Agent ID\uFF0C\u5982 claude-code codex\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684`).action(async (targetIds, _opts, cmd) => {
|
|
1374
|
+
const g = cmd.optsWithGlobals();
|
|
1375
|
+
const targets = resolveTargets(targetIds, g);
|
|
1376
|
+
const srcDir = bundledSkillsDir();
|
|
1377
|
+
const skills = listBundledSkills(srcDir);
|
|
1378
|
+
const results = targets.map((t) => ({ target: t, manifest: installTo(t, skills, srcDir) }));
|
|
1379
|
+
if (g.json) {
|
|
1380
|
+
printJson({
|
|
1381
|
+
installed: results.map((r) => ({ agent: r.target.id, skillsDir: r.target.skillsDir, skills: r.manifest.skills }))
|
|
1382
|
+
});
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
for (const { target, manifest } of results) {
|
|
1386
|
+
info(`\u2713 ${target.name}\uFF1A${manifest.skills.length} \u4E2A\u6280\u80FD\u5DF2\u88C5\u5165 ${target.skillsDir}`);
|
|
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);
|
|
1393
|
+
}
|
|
1394
|
+
info("");
|
|
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");
|
|
1397
|
+
});
|
|
1398
|
+
connect.command("uninstall").description("\u6309 manifest \u7CBE\u786E\u5378\u8F7D\u6CE8\u5165\u7684\u6280\u80FD\uFF08\u4E0D\u78B0\u5176\u4ED6\u6587\u4EF6\uFF09").argument("[targets...]", "Agent ID\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684").action(async (targetIds, _opts, cmd) => {
|
|
1399
|
+
const g = cmd.optsWithGlobals();
|
|
1400
|
+
const targets = resolveTargets(targetIds, g);
|
|
1401
|
+
const results = targets.map((t) => ({ target: t, ...uninstallFrom(t) }));
|
|
1402
|
+
if (g.json) {
|
|
1403
|
+
printJson({
|
|
1404
|
+
uninstalled: results.map((r) => ({ agent: r.target.id, removed: r.removed, hadManifest: r.hadManifest }))
|
|
1405
|
+
});
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
for (const r of results) {
|
|
1409
|
+
if (!r.hadManifest) {
|
|
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
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/commands/update.ts
|
|
1419
|
+
var REGISTRY_URL = "https://registry.npmjs.org/focalapi-cli/latest";
|
|
1420
|
+
function compareVersions(a, b) {
|
|
1421
|
+
const pa = a.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
1422
|
+
const pb = b.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
1423
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1424
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1425
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
1426
|
+
}
|
|
1427
|
+
return 0;
|
|
1428
|
+
}
|
|
1429
|
+
function registerUpdate(program) {
|
|
1430
|
+
program.command("update").description("\u68C0\u67E5\u662F\u5426\u6709\u65B0\u7248\u672C\uFF08\u53EA\u8BFB\uFF0C\u4E0D\u81EA\u52A8\u5347\u7EA7\uFF09").action(async (_opts, cmd) => {
|
|
1431
|
+
const g = cmd.optsWithGlobals();
|
|
1432
|
+
let latest;
|
|
1433
|
+
try {
|
|
1434
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(8e3) });
|
|
1435
|
+
if (res.status === 404) {
|
|
1436
|
+
if (g.json) {
|
|
1437
|
+
printJson({ current: VERSION, published: false, updateAvailable: false });
|
|
1438
|
+
} else {
|
|
1439
|
+
info(`\u5F53\u524D\u7248\u672C v${VERSION}\uFF1Bfocalapi-cli \u5C1A\u672A\u53D1\u5E03\u5230 npm\uFF0C\u65E0\u9700\u66F4\u65B0\u3002`);
|
|
1440
|
+
}
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!res.ok) {
|
|
1444
|
+
throw new ApiError("network_error", `npm registry \u8FD4\u56DE HTTP ${res.status}`);
|
|
1445
|
+
}
|
|
1446
|
+
const data = await res.json();
|
|
1447
|
+
latest = data.version ?? "0.0.0";
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
if (err instanceof ApiError) throw err;
|
|
1450
|
+
throw new ApiError("network_error", `\u65E0\u6CD5\u8FDE\u63A5 npm registry\uFF1A${err?.message ?? err}`, {
|
|
1451
|
+
hint: "\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u8BE5\u547D\u4EE4\u53EA\u505A\u7248\u672C\u68C0\u67E5\uFF0C\u4E0D\u5F71\u54CD\u672C\u5730\u4F7F\u7528\u3002"
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
const updateAvailable = compareVersions(latest, VERSION) > 0;
|
|
1455
|
+
if (g.json) {
|
|
1456
|
+
printJson({ current: VERSION, latest, published: true, updateAvailable });
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (updateAvailable) {
|
|
1460
|
+
info(`\u53D1\u73B0\u65B0\u7248\u672C\uFF1Av${latest}\uFF08\u5F53\u524D v${VERSION}\uFF09`);
|
|
1461
|
+
info("\u5347\u7EA7\uFF1Anpm i -g focalapi-cli@latest");
|
|
1462
|
+
} else {
|
|
1463
|
+
info(`\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${VERSION}\uFF09\u3002`);
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/commands/request.ts
|
|
1469
|
+
function normalizeReadPath(path) {
|
|
1470
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
1471
|
+
throw new ApiError("invalid_request", "\u8BF7\u6C42\u8DEF\u5F84\u5FC5\u987B\u662F\u4EE5 / \u5F00\u5934\u7684\u7AD9\u5185\u8DEF\u5F84\uFF0C\u4F8B\u5982 /v1/models");
|
|
1472
|
+
}
|
|
1473
|
+
const parsed = new URL(path, "https://focalapi.invalid");
|
|
1474
|
+
if (parsed.origin !== "https://focalapi.invalid") {
|
|
1475
|
+
throw new ApiError("invalid_request", "\u8BF7\u6C42\u8DEF\u5F84\u4E0D\u80FD\u5305\u542B\u5916\u90E8\u57DF\u540D");
|
|
1476
|
+
}
|
|
1477
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
1478
|
+
}
|
|
1479
|
+
function registerRequest(program) {
|
|
1480
|
+
program.command("request").description("\u539F\u59CB\u53EA\u8BFB API \u8BF7\u6C42\uFF08\u4EC5 GET/HEAD\uFF1B\u7528\u4E8E\u5C1A\u672A\u5C01\u88C5\u7684\u7AEF\u70B9\uFF09").argument("<method>", "HTTP \u65B9\u6CD5\uFF1AGET \u6216 HEAD").argument("<path>", "\u7AD9\u5185 API \u8DEF\u5F84\uFF0C\u4F8B\u5982 /v1/models").action(async (methodArg, pathArg, _opts, cmd) => {
|
|
1481
|
+
const method = methodArg.toUpperCase();
|
|
1482
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1483
|
+
throw new ApiError("invalid_request", "request \u4EC5\u5141\u8BB8 GET \u6216 HEAD\uFF1B\u5199\u64CD\u4F5C\u8BF7\u4F7F\u7528\u660E\u786E\u7684\u9AD8\u9636\u547D\u4EE4");
|
|
1484
|
+
}
|
|
1485
|
+
const g = cmd.optsWithGlobals();
|
|
1486
|
+
const auth = resolveAuth(g);
|
|
1487
|
+
const path = normalizeReadPath(pathArg);
|
|
1488
|
+
const res = await rawRequest({
|
|
1489
|
+
baseUrl: auth.baseUrl,
|
|
1490
|
+
path,
|
|
1491
|
+
method,
|
|
1492
|
+
apiKey: auth.apiKey
|
|
1493
|
+
});
|
|
1494
|
+
const text = await res.text();
|
|
1495
|
+
let data = null;
|
|
1496
|
+
if (text) {
|
|
1497
|
+
try {
|
|
1498
|
+
data = JSON.parse(text);
|
|
1499
|
+
} catch {
|
|
1500
|
+
data = text;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
printJson({
|
|
1504
|
+
method,
|
|
1505
|
+
path,
|
|
1506
|
+
status: res.status,
|
|
1507
|
+
content_type: res.headers.get("content-type") ?? null,
|
|
1508
|
+
data
|
|
1509
|
+
});
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/cli.ts
|
|
1514
|
+
function buildProgram() {
|
|
1515
|
+
const program = new Command();
|
|
1516
|
+
program.name("focalapi").description("\u8FDE\u63A5 focalapi \u4E0E AI Agent \u7684\u547D\u4EE4\u884C\u5DE5\u5177\uFF1A\u5BF9\u8BDD\u3001\u56FE\u50CF\u3001\u89C6\u9891\u3001\u641C\u7D22\u3001\u97F3\u9891\u3001\u7528\u91CF\uFF0C\u4E00\u6761\u547D\u4EE4\u76F4\u8FBE").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
|
+
registerAuth(program);
|
|
1518
|
+
registerModels(program);
|
|
1519
|
+
registerChat(program);
|
|
1520
|
+
registerGen(program);
|
|
1521
|
+
registerTask(program);
|
|
1522
|
+
registerSearch(program);
|
|
1523
|
+
registerAudio(program);
|
|
1524
|
+
registerEmbed(program);
|
|
1525
|
+
registerRerank(program);
|
|
1526
|
+
registerUsage(program);
|
|
1527
|
+
registerDoctor(program);
|
|
1528
|
+
registerConnect(program);
|
|
1529
|
+
registerUpdate(program);
|
|
1530
|
+
registerRequest(program);
|
|
1531
|
+
return program;
|
|
1532
|
+
}
|
|
1533
|
+
async function main(argv = process.argv) {
|
|
1534
|
+
const program = buildProgram();
|
|
1535
|
+
try {
|
|
1536
|
+
await program.parseAsync(argv);
|
|
1537
|
+
return 0;
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
let json = false;
|
|
1540
|
+
try {
|
|
1541
|
+
json = Boolean(program.opts().json);
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
printError(err, { json });
|
|
1545
|
+
return 1;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
var invokedDirectly = typeof process.argv[1] === "string" && (process.argv[1].endsWith("cli.ts") || process.argv[1].endsWith("cli.js"));
|
|
1549
|
+
if (invokedDirectly) {
|
|
1550
|
+
main().then((code) => {
|
|
1551
|
+
process.exitCode = code;
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
export {
|
|
1555
|
+
buildProgram,
|
|
1556
|
+
main
|
|
1557
|
+
};
|